我可以不吃啊 发表于 2023-11-4 08:00:19

client-go实战之八:更新资源时的冲突错误处理

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览


[*]本文是《client-go实战》系列的第七篇,来了解一个常见的错误:版本冲突,以及client-go官方推荐的处理方式
[*]本篇由以下部分组成

[*]什么是版本冲突(from kubernetes官方)
[*]编码,复现版本冲突
[*]版本冲突的解决思路(from kubernetes官方)
[*]版本冲突的实际解决手段(from client-go官方)
[*]编码,演示如何解决版本冲突
[*]自定义入参,对抗更高的并发
什么是版本冲突(from kubernetes官方)


[*]简单的说,就是同时出现多个修改请求,针对同一个kubernetes资源的时候,会出现一个请求成功其余请求都失败的情况
[*]这里有kubernetes官方对版本冲突的描述:https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
[*]以下是个人的理解

[*]首先,在逻辑上来说,提交冲突是肯定存在的,多人同时获取到同一个资源的信息(例如同一个pod),然后各自在本地修改后提交,就有可能出现A的提交把B的提交覆盖的情况,这一个点就不展开了,数据库的乐观锁和悲观锁都可以用来处理并发冲突
[*]kubernetes应对提交冲突的方式是资源版本号,属于乐观锁类型(Kubernetes leverages the concept of resource versions to achieve optimistic concurrency)
[*]基于版本实现并发控制是常见套路,放在kubernetes也是一样,基本原理如下图所示,按照序号看一遍即可理解:左右两人从后台拿到的资源都是1.0版本,然而右侧提交的1.1的时候,服务器上已经被左侧更新到1.1了,于是服务器不接受右侧提交
https://img2023.cnblogs.com/blog/485422/202309/485422-20230923163723436-776770053.png
编码,复现版本冲突


[*]接下来,咱们将上述冲突用代码复现出来,具体的功能如下

[*]创建一个deployment资源,该资源带有一个label,名为biz-version,值为101
[*]启动5个协程,每个协程都做同样的事情:读取deployment,得到label的值后,加一,再提交保存
[*]正常情况下,label的值被累加了5次,那么最终的值应该等于101+5=106
[*]等5个协程都执行完毕后,再读读取一次deployment,看label值是都等于106


[*]接下来就写代码实现上述功能
[*]为了后续文章的实战代码能统一管理,这里继续使用前文《client-go实战之七:准备一个工程管理后续实战的代码
》创建的client-go-tutorials工程,将代码写在这个工程中
[*]在client-go-tutorials工程中新增名为的conflict.go的文件,整个工程结构如下图所示
$ tree client-go-tutorials
client-go-tutorials
├── action
│   ├── action.go
│   ├── conflict.go
│   └── list_pod.go
├── client-go-tutorials
├── go.mod
├── go.sum
└── main.go

[*]接下来的代码都写在conflict.go中
[*]首先是新增两个常量
const (
        // deployment的名称
        DP_NAME string = "demo-deployment"
        // 用于更新的标签的名字
        LABEL_CUSTOMIZE string = "biz-version"
)

[*]然后是辅助方法,返回32位整型的指针,后面会用到
func int32Ptr(i int32) *int32 { return &i }

[*]创建deployment的方法,要注意的是增加了一个label,名为LABEL_CUSTOMIZE,其值为101
// 创建deployment
func create(clientset *kubernetes.Clientset) error {
        deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

        deployment := &appsv1.Deployment{
                ObjectMeta: metav1.ObjectMeta{
                        Name:   DP_NAME,
                        Labels: mapstring{LABEL_CUSTOMIZE: "101"},
                },
                Spec: appsv1.DeploymentSpec{
                        Replicas: int32Ptr(1),
                        Selector: &metav1.LabelSelector{
                                MatchLabels: mapstring{
                                        "app": "demo",
                                },
                        },
                        Template: apiv1.PodTemplateSpec{
                                ObjectMeta: metav1.ObjectMeta{
                                        Labels: mapstring{
                                                "app": "demo",
                                        },
                                },
                                Spec: apiv1.PodSpec{
                                        Containers: []apiv1.Container{
                                                {
                                                        Name:"web",
                                                        Image: "nginx:1.12",
                                                        Ports: []apiv1.ContainerPort{
                                                                {
                                                                        Name:          "http",
                                                                        Protocol:      apiv1.ProtocolTCP,
                                                                        ContainerPort: 80,
                                                                },
                                                        },
                                                },
                                        },
                                },
                        },
                },
        }

        // Create Deployment
        fmt.Println("Creating deployment...")
        result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
        if err != nil {
                return err
        }

        fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())

        return nil
}

[*]按照名称删除deployment的方法,实战的最后会调用,将deployment清理掉
// 按照名称删除
func delete(clientset *kubernetes.Clientset, name string) error {
        deletePolicy := metav1.DeletePropagationBackground

        err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Delete(context.TODO(), name, metav1.DeleteOptions{PropagationPolicy: &deletePolicy})

        if err != nil {
                return err
        }

        return nil
}

[*]再封装一个get方法,用于所有更新操作完成后,获取最新的deployment,检查其label值是否符合预期
// 按照名称查找deployment
func get(clientset *kubernetes.Clientset, name string) (*v1.Deployment, error) {
        deployment, err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Get(context.TODO(), name, metav1.GetOptions{})
        if err != nil {
                return nil, err
        }

        return deployment, nil
}

[*]接下来是最重要的更新方法,这里用的是常见的先查询再更新的方式,查询deployment,取得标签值之后加一再提交保存
// 查询指定名称的deployment对象,得到其名为biz-version的label,加一后保存
func updateByGetAndUpdate(clientset *kubernetes.Clientset, name string) error {

        deployment, err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Get(context.TODO(), name, metav1.GetOptions{})

        if err != nil {
                return err
        }

        // 取出当前值
        currentVal, ok := deployment.Labels

        if !ok {
                return errors.New("未取得自定义标签")
        }

        // 将字符串类型转为int型
        val, err := strconv.Atoi(currentVal)

        if err != nil {
                fmt.Println("取得了无效的标签,重新赋初值")
                currentVal = "101"
        }

        // 将int型的label加一,再转为字符串
        deployment.Labels = strconv.Itoa(val + 1)

        _, err = clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Update(context.TODO(), deployment, metav1.UpdateOptions{})
        return err
}

[*]最后,是主流程代码,为了能在现有工程框架下运行,这里新增一个struct,并实现了action接口的DoAction方法,这个DoAction方法中就是主流程
type Confilct struct{}func (conflict Confilct) DoAction(clientset *kubernetes.Clientset) error {        fmt.Println("开始创建deployment")        // 开始创建deployment        err := create(clientset)        if err != nil {                return err        }        // 如果不延时,就会导致下面的更新过早,会报错
页: [1]
查看完整版本: client-go实战之八:更新资源时的冲突错误处理