• 【博客550】k8s乐观锁机制:控制并发请求与数据一致性


    k8s乐观锁机制:控制并发请求与数据一致性

    1、乐观锁与悲观锁

    悲观锁

    悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

    在悲观锁的场景下,假设用户A和B要修改同一个文件,A在锁定文件并且修改的过程中,B是无法修改这个文件的,只有等到A修改完成,并且释放锁以后,B才可以获取锁,然后修改文件。由此可以看出,悲观锁对并发的控制持悲观态度,它在进行任何修改前,首先会为其加锁,确保整个修改过程中不会出现冲突,从而有效的保证数据一致性。但这样的机制同时降低了系统的并发性,尤其是两个同时修改的对象本身不存在冲突的情况。同时也可能在竞争锁的时候出现死锁,所以现在很多的系统例如Kubernetes采用了乐观并发的控制方法。

    乐观锁

    乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此影响,各事务能够在不请求锁的情况下处理各自的数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

    相对于悲观锁对锁的提前控制,乐观锁相信请求之间出现冲突的概率是比较小的,在读取及更改的过程中都是不加锁的,只有在最后提交更新时才会检测冲突,因此在高并发量的系统中占有绝对优势。同样假设用户A和B要修改同一个文件,A和B会先将文件获取到本地,然后进行修改。如果A已经修改好并且将数据提交,此时B再提交,服务器端会告知B文件已经被修改,返回冲突错误。此时冲突必须由B来解决,可以将文件重新获取回来,再一次修改后提交。
    乐观锁通常通过增加一个资源版本字段,来判断请求是否冲突。初始化时指定一个版本值,每次读取数据时将版本号一同读出,每次更新数据,同时也对版本号进行更新。当服务器端收到数据时,将数据中的版本号与服务器端的做对比,如果不一致,则说明数据已经被修改,返回冲突错误。

    2、k8s采用乐观锁机制

    在Kubernetes集群中,外部用户及内部组件频繁的数据更新操作,导致系统的数据并发读写量非常大。假设采用悲观并行的控制方法,将严重损耗集群性能,因此Kubernetes采用乐观并行的控制方法。Kubernetes通过定义资源版本字段实现了乐观并发控制,资源版本(ResourceVersion)字段包含在Kubernetes对象的元数据(Metadata)中。这个字符串格式的字段标识了对象的内部版本号,其取值来自etcd的modifiedindex,且当对象被修改时,该字段将随之被修改。值得注意的是该字段由服务端维护,不建议在客户端进行修改。

    3、k8s如何采用乐观锁机制控制并发请求与数据一致性

    Kube-Apiserver可以通过该字段判断对象是否已经被修改。当包含ResourceVersion的更新请求到达Apiserver,服务器端将对比请求数据与服务器中数据的资源版本号,如果不一致,则表明在本次更新提交时,服务端对象已被修改,此时Apiserver将返回冲突错误(409),客户端需重新获取服务端数据,重新修改后再次提交到服务器端。

    上述并行控制方法可防止如下的data race:

    Client1: GET Foo
    Client2: GET Foo
    
    Client1: Set Foo.Bar = "one"
    Client1: PUT Foo 
    
    Client2: Set Foo.Baz = "two"
    Client2: PUT Foo
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当未采用并发控制时,假设发生如上请求序列,两个客户端同时从服务端获取同一对象Foo(含有Bar、Baz两个字段),Client#1先将Bar字段置成one,其后Client#2对Baz字段赋值的更新请求到服务端时,将覆盖Client#1对Bar的修改。反之在对象中添加资源版本字段,同样的请求序列将如下:

    Client1: GET Foo  //初始Foo.ResourceVersion=1
    Client2: GET Foo  //初始Foo.ResourceVersion=1
    
    Client 1: Set Foo.Bar ="one"
    Client1: PUT Foo  //更新Foo.ResourceVersion=2
    
    Client2: Set Foo.Baz = "two"
    Client2: PUT Foo  //返回409冲突
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Client1更新对象后资源版本号将改变,Client#2在更新提交时将返回冲突错误(409),此时Client#2必须在本地重新获取数据,更新后再提交到服务端。

    假设更新请求的对象中未设置ResourceVersion值,Kubernetes将会根据硬改写策略(可配置)决定是否进行硬更新。如果配置为可硬改写,则数据将直接更新并存入Etcd,反之则返回错误,提示用户必须指定ResourceVersion。

    4、k8s的Update和Patch

    Update:

    对于Update,客户端更新请求中包含的是整个obj对象,服务器端将对比该请求中的obj对象和服务器端最新obj对象的ResourceVersion值。如果相等,则表明未发生冲突,将成功更新整个对象。反之若不相等则返回409冲突错误

    基本流程:

    1.获取当前更新请求中obj对象的ResourceVersion值,及服务器端最新obj对象(existing)的ResourceVersion值

    2.如果当前更新请求中obj对象的ResourceVersion值等于0,即客户端未设置该值,则判断是否要硬改写(AllowUnconditionalUpdate),如配置为硬改写策略,将直接更新obj对象。

    3.如果当前更新请求中obj对象的ResourceVersion值不等于0,则判断两个ResourceVersion值是否一致,不一致返回冲突错误(OptimisticLockErrorMsg)。

    Patch

    相比Update请求包含整个obj对象,Patch请求实现了更细粒度的对象更新操作,其请求中只包含需要更新的字段。例如要更新pod中container的镜像,可使用如下命令:
    kubectl patch pod my-pod -p ‘{“spec”:{“containers”:[{“name”:“my-container”,“image”:“new-image”}]}}’

    基本流程:

    1.首先判断patch类型,根据类型选择相应的mechanism

    2.将patch应用到最新获取的服务器端obj上,生成一个已更新的obj,再对该obj继续执行admission chain中的Admit与Validate。最终调用的还是update方法,因此冲突检测的机制与上述Update方法完全一致。

    3.调用Update方法执行更新操作。

    5、k8s的对象版本控制ResourceVersion和Generation

    etcd version机制

    ETCD共四种version:
    在这里插入图片描述

    ResourceVersion:

    基于底层etcd的revision机制,资源对象每次update时都会改变,且集群范围内唯一

    resourceversion存在哪里:

    更新对象时,Kubernetes会比较该resourceVersion和ETCD中对象的resourceVersion,在一致的情况下都会更新,一旦发生更新,该对象的resourceVersion值也会改变。所以,resourceVersion相当于一把锁。
    当然,Kubernetes在resourceVersion值的生成上,并没有实现自己的一套管理机制,而是直接使用了ETCD的index。
    在ETCD中,会维护一个全局的index,每发生一个操作,该index会加1。每个key都会维护一个modified index,表明该节点最近的一次更改index。所以Kubernetes就是借用了modified index。
    那和,既然从ETCD的节点中能获取到resourceVersion(即modified index),那就没必要把resourceVersion存储到ETCD中了。所以存储在ETCD中的对象并没有resourceVersion字段,而是在获取时动态添加resourceVersion字段。

    resourceVersion的维护其实是利用了底层存储etcd的Revision机制:

    // Get implements storage.Interface.Get.
    func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, out runtime.Object) error {
    	key = path.Join(s.pathPrefix, key)
    	startTime := time.Now()
    	getResp, err := s.client.KV.Get(ctx, key)
    	metrics.RecordEtcdRequestLatency("get", getTypeName(out), startTime)
    	if err != nil {
    		return err
    	}
    	if err = s.validateMinimumResourceVersion(opts.ResourceVersion, uint64(getResp.Header.Revision)); err != nil {
    		return err
    	}
    
    	if len(getResp.Kvs) == 0 {
    		if opts.IgnoreNotFound {
    			return runtime.SetZeroValue(out)
    		}
    		return storage.NewKeyNotFoundError(key, 0)
    	}
    	kv := getResp.Kvs[0]
    
    	data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(key))
    	if err != nil {
    		return storage.NewInternalError(err.Error())
    	}
    
    	return decode(s.codec, s.versioner, data, out, kv.ModRevision)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    kv.ModRevision看到从ETCD读取了key的ModRevision,继续看下decode函数

    // decode decodes value of bytes into object. It will also set the object resource version to rev.
    // On success, objPtr would be set to the object.
    func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objPtr runtime.Object, rev int64) error {
    	if _, err := conversion.EnforcePtr(objPtr); err != nil {
    		return fmt.Errorf("unable to convert output object to pointer: %v", err)
    	}
    	_, _, err := codec.Decode(value, nil, objPtr)
    	if err != nil {
    		return err
    	}
    	// being unable to set the version does not prevent the object from being extracted
    	if err := versioner.UpdateObject(objPtr, uint64(rev)); err != nil {
    		klog.Errorf("failed to update object version: %v", err)
    	}
    	return nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    到这里已经可出Kubernetes的resourceVersion是利用了底层ETCD kv版本机制。

    根据更新资源时是否带有resourceVersion分两种情况:

    • 未带resourceVersion:无条件更新,获得etcd中最新的数据然后再此基础上更新
    • 带有resourceVersion:和etcd中modRevision对比,不一样就提示版本冲突,说明数据已发生修改,当前要修改的版本已不是最新数据。
    Generation:

    初始值为1,随Spec内容的改变而自增

    func (deploymentStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
    	deployment := obj.(*apps.Deployment)
    	deployment.Status = apps.DeploymentStatus{}
    	deployment.Generation = 1
    
    	pod.DropDisabledTemplateFields(&deployment.Spec.Template, nil)
    }
    
    func (deploymentStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
    	newDeployment := obj.(*apps.Deployment)
    	oldDeployment := old.(*apps.Deployment)
    	newDeployment.Status = oldDeployment.Status
    
    	pod.DropDisabledTemplateFields(&newDeployment.Spec.Template, &oldDeployment.Spec.Template)
    
    	// Spec updates bump the generation so that we can distinguish between
    	// scaling events and template changes, annotation updates bump the generation
    	// because annotations are copied from deployments to their replica sets.
    	if !apiequality.Semantic.DeepEqual(newDeployment.Spec, oldDeployment.Spec) ||
    		!apiequality.Semantic.DeepEqual(newDeployment.Annotations, oldDeployment.Annotations) {
    		newDeployment.Generation = oldDeployment.Generation + 1
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6、ResourceVersion在list-watch机制中的使用

    ResourceVersion字段在Kubernetes中除了用在上述并发控制机制外,还用在Kubernetes的list-watch机制中。Client端的list-watch分为两个步骤,先list取回所有对象,再以增量的方式watch后续对象。Client端在list取回所有对象后,将会把最新对象的ResourceVersion作为下一步watch操作的起点参数,也即Kube-Apiserver以收到的ResourceVersion为起始点返回后续数据,保证了list-watch中数据的连续性与完整性。

  • 相关阅读:
    C++--Linux基础使用
    vue-ant design示例大全——按钮本地css/js资源
    Vue框架学习笔记——v-bind数据单向绑定和v-model数据双向绑定
    Webbench阅读
    Vitis HLS 加法器(整数)设计
    Vue使用脚手架出现问题 2
    TCP 报文各字段解析
    使用项目管理系统优化公众号文章排期
    [SDK]Unity接入Sign in with Apple
    外贸SEO外链类型有哪些?外链建设如何做?
  • 原文地址:https://blog.csdn.net/qq_43684922/article/details/128160129