您好,登錄后才能下訂單哦!
這篇文章主要講解了“Kubernetes中鎖機制的設計與實現方法是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Kubernetes中鎖機制的設計與實現方法是什么”吧!
面向終態的鎖基礎篇
在分布式系統中通常由各種各樣的鎖,我們先來看下,主流的鎖里面有哪些共性,以及是如何進行設計的。
分布式系統中的鎖
在分布式系統中鎖有很多種實現方式: 基于CP模型的、基于AP模型的 ,但是這些鎖機制都有一些通用的設計原則,接下來我們先看下這部分。
1. 鎖憑證
鎖憑證主要來證明誰持有鎖,不同系統里面的實現各不相同,比如在zookeeper中是臨時順序節點,而在redission中則是通過uuid+threadID組成,而K8s中則是LeaderElectionRecord, 通過該憑證來識別當前是哪個客戶端加的鎖。
2. 鎖超時
當有leader節點持有鎖之后,其余的節點就需要嘗試競爭鎖,在CP系統中通常會由服務端進行維護,即如果發現對應的節點沒有心跳,則會進行節點的踢出,并且通過watch這種機制進行回調,而在AP系統中則需要客戶端自己維護,比如redission里面的時間戳。
3. 時鐘
在分布式系統中通常我們無法保證各個節點的物理時鐘完全一致,通常就會有一個邏輯時鐘的概念,在很多系統中比如raft和zab中其實就是一個遞增的全局計數器,但是在redission中則是通過物理時鐘,即需要保證大家的物理時鐘盡可能同步,不能超過鎖超時的時間。
網絡分區問題
無論是CP還是AP,在分布式系統中通常我們都要保證P即分區可用性,那如果持有鎖的Leader節點發生網絡分區的情況,則需要一種保護機制,即Leader節點需要主動退出。
在zookeeper中因為leader節點需要通過session來進行心跳的維護,如果說對應的leader節點發生分區,則session就無法進行心跳的發生,就會退出,就需要通知我們的主流程來進行退出清理工作。
資源鎖的實現機制
資源鎖其實就是可以通過操作一個資源(順序一致性),借助前面說的鎖的思想來實現分布式鎖,其首先核心流程如下:
通過資源對象來存儲鎖憑證信息
即將標識當前Leader節點的信息放入到對應的憑證里面,并嘗試進行鎖競爭,進行鎖的獲取的嘗試。
鎖超時
K8s的鎖超時的機制比較有趣,即他并不關心你的邏輯時鐘,而是以本地時鐘為準,即每個節點會存儲觀測到leader節點變更的時間,然后根據本地的鎖超時時間來檢測,是否重新發起leader的競爭。
核心源碼剖析
因為篇幅原因這里只介紹基于configMap的resourceLock, 其他的都大同小異。
LeaderElectionRecord
在我的理解上這個數結構的設計,才是真正的那把鎖(就好像生活中我們可以隨便買把鎖,鎖各種門)。通過這個鎖屏蔽底層的各種鎖實現系統的實現細節,但注意這把鎖并不是嚴格的分布式互斥鎖。
數據結構
在鎖的實現中,數據主要分為三類:身份憑證、時間戳、全局計數器,然后我們依次來看猜下對應的設計思路。
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
身份憑證主要是用于標識一個節點信息,在一些分布式協調系統中通常都是系統自帶的機制,比如zookeeper中的session, 在此處資源鎖的場景下,主要是為了用于后續流程里驗證當前節點是否獲取到鎖。
時間戳:LeaseDurationSeconds、AcquireTime、RenewTime
因為之前說的時間同步的問題,這里的時間相關的主要是用于leader節點觸發節點變更來使用(Lease類型也在使用),非Leader節點則根據當前記錄是否變更來檢測leader節點是否存活。
LeaderTransitions
計數器主要就是通過計數來記錄leader節點切換的次數。
ConfigMapLock
所謂的資源鎖其實就是通過創建一個ConfigMap實例來保存我們的鎖信息,并通過這個實例信息的維護,來實現鎖的競爭和釋放。
1. 創建鎖
通過利用etcd的冪等性操作,可以保證同時只會有一個leader節點進行鎖創建成功,并且通過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的核心流程分為三部分:競爭鎖、超時檢測、心跳維護,首先所有節點都會進行資源鎖的競爭,但是最終只會有一個節點成為Leader節點, 然后核心流程就會按照角色分成兩個主流程, 讓我們一起來看下其實現。
1. 核心流程
如果節點沒有acquire成功則會一直進行嘗試,直至取消或者競選成功,而leader節點則會執行成為 leader節點的回調(補充基于leader的zookeeper的實現機制)
func (le *LeaderElector) Run(ctx context.Context) { defer func() { runtime.HandleCrash() le.config.Callbacks.OnStoppedLeading() }() if !le.acquire(ctx) { // 精選鎖 return // ctx signalled done } // 如果鎖競選成功,則leader節點會執行剩余流程,而非leader節點則繼續嘗試acquire ctx, cancel := context.WithCancel(ctx) defer cancel() go le.config.Callbacks.OnStartedLeading(ctx) le.renew(ctx) }
2. 鎖的續約
如果競選為leader節點,則就需要進行鎖的續約操作,就是通過調用上面提到的更新鎖的操作來,周期性的更新鎖記錄信息即LeaderElectionRecord,從而達到續約的目標。
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) // 鎖的續約 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)創建鎖 3)檢測鎖 4)更新鎖,下面來依次看下對應的實現。
獲取鎖
首先會嘗試獲取對應的鎖,在獲取鎖中會檢測對應的annotations中是否存在,如果不存在則oldLeaderElectionRecord就為空,即當前資源鎖沒有被人持有。
oldLeaderElectionRecord, oldLeaderElectionRawRecord, err := le.config.Lock.Get()
創建鎖
如果檢測到對應的鎖不存在,則就會直接進行鎖的創建,如果創建成功則表明當前節點獲取鎖,則就成為leader,執行leader的回調邏輯。
if err != nil { if !errors.IsNotFound(err) { klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err) return false } // 創建鎖 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內來更新對應的鎖憑證信息,則當前節點就會嘗試成為leader。
同時這里還會保障最終的一致性鎖,因為后續的renew其實也是走的這個邏輯,如果說當前節點最開始持有鎖,但是被別的節點搶占,則當前節點會主動讓出鎖。
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中我們可以同時有多個節點都走到這里,但是因為更新etcd是一個原子的操作,最終只會有一個節點更新成功,那如何保證最終的鎖的語義呢,其實就要配合上面的檢測鎖,這樣就可以實現一個面向終態的最終的鎖機制。
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
疑問
回過來看鎖是因為最近在做系統設計的時候,想到的一個問題。在PAAS系統中通常會有N多的Operator,那在一些沖突的場景該如何解決呢?比如擴縮容、發布、容災這幾個控制器,如果要操作同一個app下面的pod該如何被調度呢?
其實我理解這個流程中是無法做到各種完美cover各種異常沖突的,但是我們可以玩另外一種有意思的事情,比如我們可以加一個保護狀態,因為對生產穩定壓倒一起。即對應的控制器,關注當前的狀態是否處于穩定狀態,如果是非穩定狀態,則就應該自身凍結,等當前應用處于非保護狀態再進行操作,保證SLA的同時也不影響各種好玩的操作。
感謝各位的閱讀,以上就是“Kubernetes中鎖機制的設計與實現方法是什么”的內容了,經過本文的學習后,相信大家對Kubernetes中鎖機制的設計與實現方法是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。