经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Kubernetes » 查看文章
client-go实战之八:更新资源时的冲突错误处理
来源:cnblogs  作者:程序员欣宸  时间:2023/10/17 11:02:19  对本文有异议

欢迎访问我的GitHub

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

本篇概览

  • 本文是《client-go实战》系列的第七篇,来了解一个常见的错误:版本冲突,以及client-go官方推荐的处理方式
  • 本篇由以下部分组成
  1. 什么是版本冲突(from kubernetes官方)
  2. 编码,复现版本冲突
  3. 版本冲突的解决思路(from kubernetes官方)
  4. 版本冲突的实际解决手段(from client-go官方)
  5. 编码,演示如何解决版本冲突
  6. 自定义入参,对抗更高的并发

什么是版本冲突(from kubernetes官方)

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

编码,复现版本冲突

  • 接下来,咱们将上述冲突用代码复现出来,具体的功能如下
  1. 创建一个deployment资源,该资源带有一个label,名为biz-version,值为101
  2. 启动5个协程,每个协程都做同样的事情:读取deployment,得到label的值后,加一,再提交保存
  3. 正常情况下,label的值被累加了5次,那么最终的值应该等于101+5=106
  4. 等5个协程都执行完毕后,再读读取一次deployment,看label值是都等于106
  1. $ tree client-go-tutorials
  2. client-go-tutorials
  3. ├── action
  4. ├── action.go
  5. ├── conflict.go
  6. └── list_pod.go
  7. ├── client-go-tutorials
  8. ├── go.mod
  9. ├── go.sum
  10. └── main.go
  • 接下来的代码都写在conflict.go中
  • 首先是新增两个常量
  1. const (
  2. // deployment的名称
  3. DP_NAME string = "demo-deployment"
  4. // 用于更新的标签的名字
  5. LABEL_CUSTOMIZE string = "biz-version"
  6. )
  • 然后是辅助方法,返回32位整型的指针,后面会用到
  1. func int32Ptr(i int32) *int32 { return &i }
  • 创建deployment的方法,要注意的是增加了一个label,名为LABEL_CUSTOMIZE,其值为101
  1. // 创建deployment
  2. func create(clientset *kubernetes.Clientset) error {
  3. deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)
  4. deployment := &appsv1.Deployment{
  5. ObjectMeta: metav1.ObjectMeta{
  6. Name: DP_NAME,
  7. Labels: map[string]string{LABEL_CUSTOMIZE: "101"},
  8. },
  9. Spec: appsv1.DeploymentSpec{
  10. Replicas: int32Ptr(1),
  11. Selector: &metav1.LabelSelector{
  12. MatchLabels: map[string]string{
  13. "app": "demo",
  14. },
  15. },
  16. Template: apiv1.PodTemplateSpec{
  17. ObjectMeta: metav1.ObjectMeta{
  18. Labels: map[string]string{
  19. "app": "demo",
  20. },
  21. },
  22. Spec: apiv1.PodSpec{
  23. Containers: []apiv1.Container{
  24. {
  25. Name: "web",
  26. Image: "nginx:1.12",
  27. Ports: []apiv1.ContainerPort{
  28. {
  29. Name: "http",
  30. Protocol: apiv1.ProtocolTCP,
  31. ContainerPort: 80,
  32. },
  33. },
  34. },
  35. },
  36. },
  37. },
  38. },
  39. }
  40. // Create Deployment
  41. fmt.Println("Creating deployment...")
  42. result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
  43. if err != nil {
  44. return err
  45. }
  46. fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())
  47. return nil
  48. }
  • 按照名称删除deployment的方法,实战的最后会调用,将deployment清理掉
  1. // 按照名称删除
  2. func delete(clientset *kubernetes.Clientset, name string) error {
  3. deletePolicy := metav1.DeletePropagationBackground
  4. err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Delete(context.TODO(), name, metav1.DeleteOptions{PropagationPolicy: &deletePolicy})
  5. if err != nil {
  6. return err
  7. }
  8. return nil
  9. }
  • 再封装一个get方法,用于所有更新操作完成后,获取最新的deployment,检查其label值是否符合预期
  1. // 按照名称查找deployment
  2. func get(clientset *kubernetes.Clientset, name string) (*v1.Deployment, error) {
  3. deployment, err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Get(context.TODO(), name, metav1.GetOptions{})
  4. if err != nil {
  5. return nil, err
  6. }
  7. return deployment, nil
  8. }
  • 接下来是最重要的更新方法,这里用的是常见的先查询再更新的方式,查询deployment,取得标签值之后加一再提交保存
  1. // 查询指定名称的deployment对象,得到其名为biz-version的label,加一后保存
  2. func updateByGetAndUpdate(clientset *kubernetes.Clientset, name string) error {
  3. deployment, err := clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Get(context.TODO(), name, metav1.GetOptions{})
  4. if err != nil {
  5. return err
  6. }
  7. // 取出当前值
  8. currentVal, ok := deployment.Labels[LABEL_CUSTOMIZE]
  9. if !ok {
  10. return errors.New("未取得自定义标签")
  11. }
  12. // 将字符串类型转为int型
  13. val, err := strconv.Atoi(currentVal)
  14. if err != nil {
  15. fmt.Println("取得了无效的标签,重新赋初值")
  16. currentVal = "101"
  17. }
  18. // 将int型的label加一,再转为字符串
  19. deployment.Labels[LABEL_CUSTOMIZE] = strconv.Itoa(val + 1)
  20. _, err = clientset.AppsV1().Deployments(apiv1.NamespaceDefault).Update(context.TODO(), deployment, metav1.UpdateOptions{})
  21. return err
  22. }
  • 最后,是主流程代码,为了能在现有工程框架下运行,这里新增一个struct,并实现了action接口的DoAction方法,这个DoAction方法中就是主流程
  1. type Confilct struct{}
  2. func (conflict Confilct) DoAction(clientset *kubernetes.Clientset) error {
  3. fmt.Println("开始创建deployment")
  4. // 开始创建deployment
  5. err := create(clientset)
  6. if err != nil {
  7. return err
  8. }
  9. // 如果不延时,就会导致下面的更新过早,会报错
  10. <-time.NewTimer(1 * time.Second).C
  11. // 一旦创建成功,就一定到删除再返回
  12. defer delete(clientset, DP_NAME)
  13. testNum := 5
  14. waitGroup := sync.WaitGroup{}
  15. waitGroup.Add(testNum)
  16. fmt.Println("在协程中并发更新自定义标签")
  17. startTime := time.Now().UnixMilli()
  18. for i := 0; i < testNum; i++ {
  19. go func(clientsetA *kubernetes.Clientset, index int) {
  20. // 避免进程卡死
  21. defer waitGroup.Done()
  22. err := updateByGetAndUpdate(clientsetA, DP_NAME)
  23. // var retryParam = wait.Backoff{
  24. // Steps: 5,
  25. // Duration: 10 * time.Millisecond,
  26. // Factor: 1.0,
  27. // Jitter: 0.1,
  28. // }
  29. // err := retry.RetryOnConflict(retryParam, func() error {
  30. // return updateByGetAndUpdate(clientset, DP_NAME)
  31. // })
  32. if err != nil {
  33. fmt.Printf("err: %v\n", err)
  34. }
  35. }(clientset, i)
  36. }
  37. // 等待协程完成全部操作
  38. waitGroup.Wait()
  39. // 再查一下,自定义标签的最终值
  40. deployment, err := get(clientset, DP_NAME)
  41. if err != nil {
  42. fmt.Printf("查询deployment发生异常: %v\n", err)
  43. return err
  44. }
  45. fmt.Printf("自定义标签的最终值为: %v,耗时%v毫秒\n", deployment.Labels[LABEL_CUSTOMIZE], time.Now().UnixMilli()-startTime)
  46. return nil
  47. }
  • 最后还要修改main.go,增加一个action的处理,新增的内容如下
    在这里插入图片描述
  • 这里给出完整main.go
  1. package main
  2. import (
  3. "client-go-tutorials/action"
  4. "flag"
  5. "fmt"
  6. "path/filepath"
  7. "k8s.io/client-go/kubernetes"
  8. "k8s.io/client-go/tools/clientcmd"
  9. "k8s.io/client-go/util/homedir"
  10. )
  11. func main() {
  12. var kubeconfig *string
  13. var actionFlag *string
  14. // 试图取到当前账号的家目录
  15. if home := homedir.HomeDir(); home != "" {
  16. // 如果能取到,就把家目录下的.kube/config作为默认配置文件
  17. kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
  18. } else {
  19. // 如果取不到,就没有默认配置文件,必须通过kubeconfig参数来指定
  20. kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
  21. }
  22. actionFlag = flag.String("action", "list-pod", "指定实际操作功能")
  23. flag.Parse()
  24. fmt.Println("解析命令完毕,开始加载配置文件")
  25. // 加载配置文件
  26. config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
  27. if err != nil {
  28. panic(err.Error())
  29. }
  30. // 用clientset类来执行后续的查询操作
  31. clientset, err := kubernetes.NewForConfig(config)
  32. if err != nil {
  33. panic(err.Error())
  34. }
  35. fmt.Printf("加载配置文件完毕,即将执行业务 [%v]\n", *actionFlag)
  36. var actionInterface action.Action
  37. // 注意,如果有新的功能类实现,就在这里添加对应的处理
  38. switch *actionFlag {
  39. case "list-pod":
  40. listPod := action.ListPod{}
  41. actionInterface = &listPod
  42. case "conflict":
  43. conflict := action.Confilct{}
  44. actionInterface = &conflict
  45. }
  46. err = actionInterface.DoAction(clientset)
  47. if err != nil {
  48. fmt.Printf("err: %v\n", err)
  49. } else {
  50. fmt.Println("执行完成")
  51. }
  52. }
  • 最后,如果您用的是vscode,可以修改launch.json,调整输入参数
  1. {
  2. "version": "0.2.0",
  3. "configurations": [
  4. {
  5. "name": "Launch Package",
  6. "type": "go",
  7. "request": "launch",
  8. "mode": "auto",
  9. "program": "${workspaceFolder}",
  10. "args": ["-action=conflict"]
  11. }
  12. ]
  13. }
  • 回顾上面的代码,您会发现是5个协程并行执行先查询再修改提交的逻辑,理论上会出现前面提到的冲突问题,5个协程并发更新,会出现并发冲突,因此最终标签的值是小于101+5=106的,咱们来运行代码试试

  • 果然,经过更新后,lable的最终值等于102,也就是说过5个协程同时提交,只成功了一个
    在这里插入图片描述

  • 至此,咱们通过代码证明了资源版本冲突问题确实存在,接下来就要想办法解决此问题了

版本冲突的解决思路(from kubernetes官方)

  • 来看看kubernetes的官方对于处理此问题是如何建议的,下面是官方原话
  1. In the case of a conflict, the correct client action at this point is to GET the resource again, apply the changes afresh, and try submitting again
  • 很明显,在更新因为版本冲突而失败的时候,官方建议重新获取最新版本的资源,然后再次修改并提交
  • 听起来很像CAS
  • 在前面复现失败的场景,如果是5个协程并发提交,总有一个会失败多次,那岂不是要反复重试,把代码变得更复杂?
  • 还好,client-go帮我们解决了这个问题,按照kubernetes官方的指导方向,将重试逻辑进行了封装,让使用者可以很方便的实现完成失败重试

版本冲突的实际解决手段(from client-go官方)

  • client-go提供的是方法,下面是该方法的源码
  1. func RetryOnConflict(backoff wait.Backoff, fn func() error) error {
  2. return OnError(backoff, errors.IsConflict, fn)
  3. }
  • 从上述方法有两个入参,backoff用于控制重试相关的细节,如重试次数、间隔时间等,fn则是常规的先查询再更新的自定义方法,由调用方根据自己的业务自行实现,总之,只要fn返回错误,并且该错误是可以通过重试来解决的,RetryOnConflict方法就会按照backoff的配置进行等待和重试
  • 可见经过client-go的封装,对应普通开发者来说已经无需关注重试的实现了,只要调用RetryOnConflict即可确保版本冲突问题会被解决
  • 接下来咱们改造前面有问题的代码,看看能否解决并发冲突的问题

编码,演示如何解决版本冲突

  • 改成client-go提供的自动重试代码,整体改动很小,如下图所示,原来是直接调用updateByGetAndUpdate方法,现在注释掉,改为调用RetryOnConflict,并且将updateByGetAndUpdate作为入参使用
    在这里插入图片描述
  • 再次运行代码,如下图,这次五个协程都更新成功了,不过耗时也更长,毕竟是靠着重试来实现最终提交成功的
    在这里插入图片描述

自定义入参,对抗更高的并发

  • 前面的验证过程中,并发数被设置为5,现在加大一些试试,改成10,如下图红色箭头位置
    在这里插入图片描述
  • 执行结果如下图所示,10个并发请求,只成功了5个,其余5个就算重试也还是失败了
    在这里插入图片描述
  • 出现这样的问题,原因很明显:下面是咱们调用方法时的入参,每个并发请求最多重试5次,显然即便是重试5次,也只能确保每一次有个协程提交成功,所以5次过后没有重试机会,导致只成功了5个
  1. var retryParam = wait.Backoff{
  2. Steps: 5,
  3. Duration: 10 * time.Millisecond,
  4. Factor: 1.0,
  5. Jitter: 0.1,
  6. }
  • 找到了原因就好处理了,把上面的Steps参数调大,改为10,再试试
    在这里插入图片描述
  • 如下图,这一次结果符合预期,不过耗时更长了
    在这里插入图片描述
  • 最后留下一个问题:Steps参数到底该设置成多少呢?这个当然没有固定值了,5是client-go官方推荐的值,结果在并发为10的时候依然不够用,所以具体该设置成多少还是要依照您的实际情况来决定,需要大于最大的瞬间并发数,才能保证所有并发冲突都能通过重试解决,当然了,实际场景中,大量并发同时修改同一个资源对象的情况并不多见,所以大多数时候可以直接使用client-go官方的推荐值
  • 至此,kubernetes资源更新时的版本冲突问题,经过实战咱们都已经了解了,并且掌握了解决方法,基本的增删改查算是没问题了,接下来的文章,咱们要聚焦的是client-go另一个极其重要的能力:List&Watch
  • 敬请期待,欣宸原创必不会辜负您

源码下载

名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在tutorials/client-go-tutorials文件夹下,如下图红框所示:
    在这里插入图片描述

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

原文链接:https://www.cnblogs.com/bolingcavalry/p/17724756.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号