Kubernetes 併發控制與數據一致性的實現原理

在大型分佈式系統中,定會存在大量併發寫入的場景。在這種場景下如何進行更好的併發控制,即在多個任務同時存取數據時保證數據的一致性,成爲分佈式系統必須解決的問題。git

悲觀併發控制和樂觀併發控制是併發控制中採用的主要技術手段,對於不一樣的業務場景,應該選擇不一樣的控制方法。json

悲觀鎖

悲觀併發控制(又名「悲觀鎖」,Pessimistic Concurrency Control,縮寫「PCC」)是一種併發控制的方法。它能夠阻止一個事務以影響其餘用戶的方式來修改數據。若是一個事務執行的操做讀某行數據應用了鎖,那只有當這個事務把鎖釋放,其餘事務纔可以執行與該鎖衝突的操做。api

在悲觀鎖的場景下,假設用戶 A 和 B 要修改同一個文件,A 在鎖定文件而且修改的過程當中,B 是沒法修改這個文件的,只有等到 A 修改完成,而且釋放鎖之後,B 才能夠獲取鎖,而後修改文件。由此能夠看出,悲觀鎖對併發的控制持悲觀態度,它在進行任何修改前,首先會爲其加鎖,確保整個修改過程當中不會出現衝突,從而有效的保證數據一致性。但這樣的機制同時下降了系統的併發性,尤爲是兩個同時修改的對象自己不存在衝突的狀況。同時也可能在競爭鎖的時候出現死鎖,因此如今不少的系統例如 Kubernetes 採用了樂觀併發的控制方法。bash

樂觀鎖

樂觀併發控制(又名「樂觀鎖」,Optimistic Concurrency Control,縮寫「OCC」)是一種併發控制的方法。它假設多用戶併發的事務在處理時不會彼此影響,各事務可以在不請求鎖的狀況下處理各自的數據。在提交數據更新以前,每一個事務會先檢查在該事務讀取數據後,有沒有其餘事務又修改了該數據。若是其餘事務有更新的話,正在提交的事務會進行回滾。服務器

相對於悲觀鎖對鎖的提早控制,樂觀鎖相信請求之間出現衝突的機率是比較小的,在讀取及更改的過程當中都是不加鎖的,只有在最後提交更新時纔會檢測衝突,所以在高併發量的系統中佔有絕對優點。一樣假設用戶A和B要修改同一個文件,A和B會先將文件獲取到本地,而後進行修改。若是A已經修改好而且將數據提交,此時B再提交,服務器端會告知B文件已經被修改,返回衝突錯誤。此時衝突必須由B來解決,能夠將文件從新獲取回來,再一次修改後提交。併發

樂觀鎖一般經過增長一個資源版本字段,來判斷請求是否衝突。初始化時指定一個版本值,每次讀取數據時將版本號一同讀出,每次更新數據,同時也對版本號進行更新。當服務器端收到數據時,將數據中的版本號與服務器端的作對比,若是不一致,則說明數據已經被修改,返回衝突錯誤。app

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 } 複製代碼

基本流程爲:

  1. 獲取當前更新請求中 obj 對象的 ResourceVersion 值,及服務器端最新 obj 對象 (existing) 的 ResourceVersion 值

  2. 若是當前更新請求中 bj 對象的 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"}]}}'
複製代碼

服務器端只收到以上的 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 中數據的連續性與完整性。

相關文章
相關標籤/搜索