新聞中心
資源鎖是通過一個資源的CRUD操作,然后配合分布式鎖的一些機制來完成,分布式環(huán)境中Leader節(jié)點的選舉,今天我們來臆測下K8s里面是如何基于configMap來實現(xiàn)的吧。

面向終態(tài)的鎖基礎篇
在分布式系統(tǒng)中通常由各種各樣的鎖,我們先來看下,主流的鎖里面有哪些共性,以及是如何進行設計的。
分布式系統(tǒng)中的鎖
在分布式系統(tǒng)中鎖有很多種實現(xiàn)方式: 基于CP模型的、基于AP模型的 ,但是這些鎖機制都有一些通用的設計原則,接下來我們先看下這部分。
1. 鎖憑證
鎖憑證主要來證明誰持有鎖,不同系統(tǒng)里面的實現(xiàn)各不相同,比如在zookeeper中是臨時順序節(jié)點,而在redission中則是通過uuid+threadID組成,而K8s中則是LeaderElectionRecord, 通過該憑證來識別當前是哪個客戶端加的鎖。
2. 鎖超時
當有l(wèi)eader節(jié)點持有鎖之后,其余的節(jié)點就需要嘗試競爭鎖,在CP系統(tǒng)中通常會由服務端進行維護,即如果發(fā)現(xiàn)對應的節(jié)點沒有心跳,則會進行節(jié)點的踢出,并且通過watch這種機制進行回調,而在AP系統(tǒng)中則需要客戶端自己維護,比如redission里面的時間戳。
3. 時鐘
在分布式系統(tǒng)中通常我們無法保證各個節(jié)點的物理時鐘完全一致,通常就會有一個邏輯時鐘的概念,在很多系統(tǒng)中比如raft和zab中其實就是一個遞增的全局計數(shù)器,但是在redission中則是通過物理時鐘,即需要保證大家的物理時鐘盡可能同步,不能超過鎖超時的時間。
網(wǎng)絡分區(qū)問題
無論是CP還是AP,在分布式系統(tǒng)中通常我們都要保證P即分區(qū)可用性,那如果持有鎖的Leader節(jié)點發(fā)生網(wǎng)絡分區(qū)的情況,則需要一種保護機制,即Leader節(jié)點需要主動退出。
在zookeeper中因為leader節(jié)點需要通過session來進行心跳的維護,如果說對應的leader節(jié)點發(fā)生分區(qū),則session就無法進行心跳的發(fā)生,就會退出,就需要通知我們的主流程來進行退出清理工作。
資源鎖的實現(xiàn)機制
資源鎖其實就是可以通過操作一個資源(順序一致性),借助前面說的鎖的思想來實現(xiàn)分布式鎖,其首先核心流程如下:
通過資源對象來存儲鎖憑證信息
即將標識當前Leader節(jié)點的信息放入到對應的憑證里面,并嘗試進行鎖競爭,進行鎖的獲取的嘗試。
鎖超時
K8s的鎖超時的機制比較有趣,即他并不關心你的邏輯時鐘,而是以本地時鐘為準,即每個節(jié)點會存儲觀測到leader節(jié)點變更的時間,然后根據(jù)本地的鎖超時時間來檢測,是否重新發(fā)起leader的競爭。
核心源碼剖析
因為篇幅原因這里只介紹基于configMap的resourceLock, 其他的都大同小異。
LeaderElectionRecord
在我的理解上這個數(shù)結構的設計,才是真正的那把鎖(就好像生活中我們可以隨便買把鎖,鎖各種門)。通過這個鎖屏蔽底層的各種鎖實現(xiàn)系統(tǒng)的實現(xiàn)細節(jié),但注意這把鎖并不是嚴格的分布式互斥鎖。
數(shù)據(jù)結構
在鎖的實現(xiàn)中,數(shù)據(jù)主要分為三類:身份憑證、時間戳、全局計數(shù)器,然后我們依次來看猜下對應的設計思路。
- type LeaderElectionRecord struct {
- HolderIdentity string `json:"holderIdentity"`
- LeaseDurationSeconds int `json:"leaseDurationSeconds"`
- AcquireTime metav1.Time `json:"acquireTime"`
- RenewTime metav1.Time `json:"renewTime"`
- LeaderTransitions int `json:"leaderTransitions"`
- }
身份憑證:HolderIdentity
身份憑證主要是用于標識一個節(jié)點信息,在一些分布式協(xié)調系統(tǒng)中通常都是系統(tǒng)自帶的機制,比如zookeeper中的session, 在此處資源鎖的場景下,主要是為了用于后續(xù)流程里驗證當前節(jié)點是否獲取到鎖。
時間戳:LeaseDurationSeconds、AcquireTime、RenewTime
因為之前說的時間同步的問題,這里的時間相關的主要是用于leader節(jié)點觸發(fā)節(jié)點變更來使用(Lease類型也在使用),非Leader節(jié)點則根據(jù)當前記錄是否變更來檢測leader節(jié)點是否存活。
- LeaderTransitions
計數(shù)器主要就是通過計數(shù)來記錄leader節(jié)點切換的次數(shù)。
ConfigMapLock
所謂的資源鎖其實就是通過創(chuàng)建一個ConfigMap實例來保存我們的鎖信息,并通過這個實例信息的維護,來實現(xiàn)鎖的競爭和釋放。
1. 創(chuàng)建鎖
通過利用etcd的冪等性操作,可以保證同時只會有一個leader節(jié)點進行鎖創(chuàng)建成功,并且通過Annotations來提交上面說的LeaderElectionRecord來進行鎖的提交。
- func (cml *ConfigMapLock) Create(ler LeaderElectionRecord) error {
- cml.cm, err = cml.Client.ConfigMaps(cml.ConfigMapMeta.Namespace).Create(&v1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{
- Name: cml.ConfigMapMeta.Name,
- Namespace: cml.ConfigMapMeta.Namespace,
- Annotations: map[string]string{
- LeaderElectionRecordAnnotationKey: string(recordBytes),
- },
- },
- })
- return err
- }
2. 獲取鎖
- func (cml *ConfigMapLock) Get() (*LeaderElectionRecord, []byte, error) {
- cml.cm, err = cml.Client.ConfigMaps(cml.ConfigMapMeta.Namespace).Get(cml.ConfigMapMeta.Name, metav1.GetOptions{})
- recordBytes, found := cml.cm.Annotations[LeaderElectionRecordAnnotationKey]
- if found {
- if err := json.Unmarshal([]byte(recordBytes), &record); err != nil {
- return nil, nil, err
- }
- }
- return &record, []byte(recordBytes), nil
- }
3. 更新鎖
- func (cml *ConfigMapLock) Update(ler LeaderElectionRecord) error {
- cml.cm.Annotations[LeaderElectionRecordAnnotationKey] = string(recordBytes)
- cml.cm, err = cml.Client.ConfigMaps(cml.ConfigMapMeta.Namespace).Update(cml.cm)
- return err
- }
LeaderElector
LeaderElector的核心流程分為三部分:競爭鎖、超時檢測、心跳維護,首先所有節(jié)點都會進行資源鎖的競爭,但是最終只會有一個節(jié)點成為Leader節(jié)點, 然后核心流程就會按照角色分成兩個主流程, 讓我們一起來看下其實現(xiàn)。
1. 核心流程
如果節(jié)點沒有acquire成功則會一直進行嘗試,直至取消或者競選成功,而leader節(jié)點則會執(zhí)行成為 leader節(jié)點的回調(補充基于leader的zookeeper的實現(xiàn)機制)
- func (le *LeaderElector) Run(ctx context.Context) {
- defer func() {
- runtime.HandleCrash()
- le.config.Callbacks.OnStoppedLeading()
- }()
- if !le.acquire(ctx) { // 精選鎖
- return // ctx signalled done
- }
- // 如果鎖競選成功,則leader節(jié)點會執(zhí)行剩余流程,而非leader節(jié)點則繼續(xù)嘗試acquire
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
- go le.config.Callbacks.OnStartedLeading(ctx)
- le.renew(ctx)
- }
2. 鎖的續(xù)約
如果競選為leader節(jié)點,則就需要進行鎖的續(xù)約操作,就是通過調用上面提到的更新鎖的操作來,周期性的更新鎖記錄信息即LeaderElectionRecord,從而達到續(xù)約的目標。
- func (le *LeaderElector) renew(ctx context.Context) {
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
- wait.Until(func() {
- timeoutCtx, timeoutCancel := context.WithTimeout(ctx, le.config.RenewDeadline)
- defer timeoutCancel()
- err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
- done := make(chan bool, 1)
- go func() {
- defer close(done)
- // 鎖的續(xù)約
- done <- le.tryAcquireOrRenew()
- }()
- select {
- case <-timeoutCtx.Done():
- return false, fmt.Errorf("failed to tryAcquireOrRenew %s", timeoutCtx.Err())
- case result := <-done:
- return result, nil
- }
- }, timeoutCtx.Done())
- cancel()
- }, le.config.RetryPeriod, ctx.Done())
- // if we hold the lease, give it up
- if le.config.ReleaseOnCancel {
- // 釋放鎖
- le.release()
- }
- }
3. 鎖的釋放
鎖的釋放則比較好玩,就是更新對應的資源,去掉annotations里面的信息,這樣在獲取鎖的時候,因為檢測到當前資源沒有被任何憑證信息,就會嘗試進行競選。
- func (le *LeaderElector) release() bool {
- if !le.IsLeader() {
- return true
- }
- leaderElectionRecord := rl.LeaderElectionRecord{
- LeaderTransitions: le.observedRecord.LeaderTransitions,
- }
- if err := le.config.Lock.Update(leaderElectionRecord); err != nil {
- klog.Errorf("Failed to release lock: %v", err)
- return false
- }
- le.observedRecord = leaderElectionRecord
- le.observedTime = le.clock.Now()
- return true
- }
4. 鎖的競爭
鎖的競爭整體分為四個部分: 1)獲取鎖 2)創(chuàng)建鎖 3)檢測鎖 4)更新鎖,下面來依次看下對應的實現(xiàn)。
獲取鎖
首先會嘗試獲取對應的鎖,在獲取鎖中會檢測對應的annotations中是否存在,如果不存在則oldLeaderElectionRecord就為空,即當前資源鎖沒有被人持有。
- oldLeaderElectionRecord, oldLeaderElectionRawRecord, err := le.config.Lock.Get()
創(chuàng)建鎖
如果檢測到對應的鎖不存在,則就會直接進行鎖的創(chuàng)建,如果創(chuàng)建成功則表明當前節(jié)點獲取鎖,則就成為leader,執(zhí)行l(wèi)eader的回調邏輯。
- if err != nil {
- if !errors.IsNotFound(err) {
- klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err)
- return false
- }
- // 創(chuàng)建鎖
- if err = le.config.Lock.Create(leaderElectionRecord); err != nil {
- klog.Errorf("error initially creating leader election record: %v", err)
- return false
- }
- // 記錄當前的選舉記錄,還有時鐘
- le.observedRecord = leaderElectionRecord
- le.observedTime = le.clock.Now()
- return true
- }
檢查鎖
在K8s里面并沒有使用邏輯時鐘而是使用本地時間,通過對比每次鎖憑證是否更新,來進行本地observedTime的更新,如果leader沒有在LeaseDuration內來更新對應的鎖憑證信息,則當前節(jié)點就會嘗試成為leader。
同時這里還會保障最終的一致性鎖,因為后續(xù)的renew其實也是走的這個邏輯,如果說當前節(jié)點最開始持有鎖,但是被別的節(jié)點搶占,則當前節(jié)點會主動讓出鎖。
- if !bytes.Equal(le.observedRawRecord, oldLeaderElectionRawRecord) {
- le.observedRecord = *oldLeaderElectionRecord
- le.observedRawRecord = oldLeaderElectionRawRecord
- le.observedTime = le.clock.Now() // 此處更新的是本地的時鐘
- }
- if len(oldLeaderElectionRecord.HolderIdentity) > 0 &&
- le.observedTime.Add(le.config.LeaseDuration).After(now.Time) &&
- !le.IsLeader() {
- // 如果當前Leader任期沒有超時,則當前競選鎖失敗
- klog.V(4).Infof("lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity)
- return false
- }
更新鎖
核心邏輯其實就是Lock.Update這個地方,設計的比較有意思,不同于強一致性的鎖,在K8s中我們可以同時有多個節(jié)點都走到這里,但是因為更新etcd是一個原子的操作,最終只會有一個節(jié)點更新成功,那如何保證最終的鎖的語義呢,其實就要配合上面的檢測鎖,這樣就可以實現(xiàn)一個面向終態(tài)的最終的鎖機制。
- if le.IsLeader() {
- leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
- leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
- } else {
- leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
- }
- // update the lock itself
- if err = le.config.Lock.Update(leaderElectionRecord); err != nil {
- klog.Errorf("Failed to update lock: %v", err)
- return false
- }
- le.observedRecord = leaderElectionRecord
- le.observedTime = le.clock.Now()
- return true
疑問
回過來看鎖是因為最近在做系統(tǒng)設計的時候,想到的一個問題。在PAAS系統(tǒng)中通常會有N多的Operator,那在一些沖突的場景該如何解決呢?比如擴縮容、發(fā)布、容災這幾個控制器,如果要操作同一個app下面的pod該如何被調度呢?
其實我理解這個流程中是無法做到各種完美cover各種異常沖突的,但是我們可以玩另外一種有意思的事情,比如我們可以加一個保護狀態(tài),因為對生產(chǎn)穩(wěn)定壓倒一起。即對應的控制器,關注當前的狀態(tài)是否處于穩(wěn)定狀態(tài),如果是非穩(wěn)定狀態(tài),則就應該自身凍結,等當前應用處于非保護狀態(tài)再進行操作,保證SLA的同時也不影響各種好玩的操作。
當前標題:Kubernetes中鎖機制的設計與實現(xiàn)
路徑分享:http://www.5511xx.com/article/coophdg.html


咨詢
建站咨詢
