频道澳门葡京手机版网址
登录注册
澳门葡京手机版网址 > 网络 > 云计算 > 正文

Kubernetes并发控制与数据一致性的实现原理

2018-11-27 14:28:54           
收藏   我要投稿

在大型分布式系统中,定会存在大量并发写入的场景。在这种场景下如何进行更好的并发控制,即在多个任务同时存取数据时保证数据的一致性,成为分布式系统必须解决的问题。

悲观并发控制和乐观并发控制是并发控制中采用的主要技术手段,对于不同的业务场景,应该选择不同的控制方法。

悲观锁

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

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

乐观锁

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

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

乐观锁通常通过增加一个资源版本字段,来判断请求是否冲突。初始化时指定一个版本值,每次读取数据时将版本号一同读出,每次更新数据,同时也对版本号进行更新。当服务器端收到数据时,将数据中的版本号与服务器端的做对比,如果不一致,则说明数据已经被修改,返回冲突错误。

Kubernetes中的并发控制

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

type ObjectMeta struct {

......

// An opaque value that represents the internal version of this object that can

// be used by clients to determine when objects have changed. May be used for optimistic

// concurrency, change detection, and the watch operation on a resource or set of resources.

// Clients must treat these values as opaque and passed unmodified back to the server.

// They may only be valid for a particular resource or set of resources.

//

// Populated by the system.

// Read-only.

// Value must be treated as opaque by clients and .

// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#concurrency-control-and-consistency

// +optional

ResourceVersion string

......

}

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

Client #1: GET Foo

Client #2: GET Foo

Client #1: Set Foo.Bar = "one"

Client #1: PUT Foo

Client #2: Set Foo.Baz = "two"

Client #2: PUT Foo

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

Client #1: GET Foo //初始Foo.ResourceVersion=1

Client #2: GET Foo //初始Foo.ResourceVersion=1

Client #1: Set Foo.Bar = "one"

Client #1: PUT Foo //更新Foo.ResourceVersion=2

Client #2: Set Foo.Baz = "two"

Client #2: PUT Foo //返回409冲突

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

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

Kubernetes中的Update和Patch

Kubernetes实现了Update和Patch两个对象更新的方法,两者提供不同的更新操作方式,但冲突判断机制是相同的。

Update

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

e.Storage.GuaranteedUpdate(ctx, key...) (runtime.Object, *uint64, error) {

// If AllowUnconditionalUpdate() is true and the object specified by

// the user does not have a resource version, then we populate it with

// the latest version. Else, we check that the version specified by

// the user matches the version of latest storage object.

resourceVersion, err := e.Storage.Versioner().ObjectResourceVersion(obj)

if err != nil {

return nil, nil, err

}

version, err := e.Storage.Versioner().ObjectResourceVersion(existing)

doUnconditionalUpdate := resourceVersion == 0 && e.UpdateStrategy.AllowUnconditionalUpdate()

if doUnconditionalUpdate {

// Update the object's resource version to match the latest

// storage object's resource version.

err = e.Storage.Versioner().UpdateObject(obj, res.ResourceVersion)

if err != nil {

return nil, nil, err

}

} else {

// Check if the object's resource version matches the latest

// resource version.

......

if resourceVersion != version {

return nil, nil, kubeerr.NewConflict(qualifiedResource, name, fmt.Errorf(OptimisticLockErrorMsg))

}

}

......

return out, creating, nil

}

基本流程为:

获取当前更新请求中obj对象的ResourceVersion值,及服务器端最新obj对象(existing)的ResourceVersion值 如果当前更新请求中obj对象的ResourceVersion值等于0,即客户端未设置该值,则判断是否要硬改写(AllowUnconditionalUpdate),如配置为硬改写策略,将直接更新obj对象 如果当前更新请求中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"}]}}'

服务器端只收到以上的patch信息,然后通过如下代码将该patch更新到Etcd中。

func (p *patcher) patchResource(ctx context.Context) (runtime.Object, error) {

p.namespace = request.NamespaceValue(ctx)

switch p.patchType {

case types.JSONPatchType, types.MergePatchType:

p.mechanism = &jsonPatcher{patcher: p}

case types.StrategicMergePatchType:

schemaReferenceObj, err := p.unsafeConvertor.ConvertToVersion(p.restPatcher.New(), p.kind.GroupVersion())

if err != nil {

return nil, err

}

p.mechanism = &smpPatcher{patcher: p, schemaReferenceObj: schemaReferenceObj}

default:

return nil, fmt.Errorf("%v: unimplemented patch type", p.patchType)

}

p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission)

return finishRequest(p.timeout, func() (runtime.Object, error) {

updateObject, _, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, false, p.options)

return updateObject, updateErr

})

}

基本流程为:

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

2.利用DefaultUpdatedObjectInfo方法将applyPatch(应用Patch的方法)添加到admission chain的头部

3.最终还是调用上述Update方法实行更新操作

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

相比Update,Patch的主要优势在于客户端不必提供全量的obj对象信息。客户端只需以patch的方式提交要修改的字段信息,服务器端会将该patch数据应用到最新获取的obj中。省略了Client端获取、修改再提交全量obj的步骤,降低了数据被修改的风险,更大大减小了冲突概率。 由于Patch方法在传输效率及冲突概率上都占有绝对优势,目前Kubernetes中几乎所有更新操作都采用了Patch方法,大家在编写代码时也应该注意使用Patch方法。

附:

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

上一篇:在mysql使用binlog日志
下一篇:初识SAP解决方案及其上云优势
相关文章
图文推荐

关于大家 | 联系大家 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 澳门葡京手机版网址_澳门新莆京娱乐_www.88807.com - 点此进入--致力于做实用的IT技术学习网站

XML 地图 | Sitemap 地图