Kubernetes 中 Pod 的選舉過程


為什么需要 Pod 之間的 Leader Election

一般來說,由 Deployment 創建的 1 個或多個 Pod 都是對等關系,彼此之間提供一樣的服務。但是在某些場合,多個 Pod 之間需要有一個 Leader 的角色,即:

  • Pod 之間有且只有一個 Leader;

  • Leader 在一定周期不可用時,其他 Pod 會再選出一個 Leader;

  • 由處於 Leader 身份的 Pod 來完成某些特殊的業務邏輯(通常是寫操作);

比如,當多個 Pod 之間只需要一個寫者時,如果不采用 Leader Election,那么就必須在 Pod 啟動之初人為地配置一個 Leader。如果配置的 Leader 在后續的服務中失效且沒有對應機制來生成新的 Leader,那么對應 Pod 服務就可能處於不可用狀態,違背高可用原則。

典型地,Kubernetes 的核心組件 kube-controller-manager 就需要一個需要 Leader 的場景。當 kube-controller-manager 的啟動參數設置 --leader-elect=true 時,對應節點的 kube-controller-manager 在啟動時會執行選主操作。當選出一個 Leader 之后,由 Leader 來啟動所有的控制器。如果 Leader Pod 不可用,將會自動選出新的 Leader Pod,從而保障控制器仍處於運行狀態。

一個簡單的 Leader Election 的例子

備注:該例子取自項目文檔

啟動一個 leader-elector 的 Pod

  1. 創建一個 leader-elector 的 Deployment,其中的 Pod 會進行 Leader Election 的過程

    1
    
     $ kubectl run leader-elector --image=k8s.gcr.io/leader-elector:0.5 --replicas=3 -- --election=example --http=0.0.0.0:4040

    副本數為 3,即將生成 3 個 Pod,如果運行成功,可觀察到:

    1
    2 3 4 5 
     $ kubectl get po
     NAME                              READY   STATUS    RESTARTS   AGE
     leader-elector-68dcb58d55-7dhdz   1/1     Running   0          2m36s
     leader-elector-68dcb58d55-g5zp8   1/1     Running   0          2m36s
     leader-elector-68dcb58d55-q45pd   1/1     Running   0          2m36s
  2. 查看哪個 Pod 成為 Leader;

    可以逐個查看 Pod 的日志:

    1
    
     $ kubectl logs -f ${pod_name}

    如果是 Leader 的話,將會有如下的日志:

    1
    2 3 4 5 6 7 
     $ kubectl logs leader-elector-68dcb58d55-g5zp8
     leader-elector-9577494c7-l64lp is the leader
     I0122 03:24:31.779331       8 leaderelection.go:296] lock is held by leader-elector-9577494c7-l64lp and has not yet expired
     I0122 03:24:36.101800       8 leaderelection.go:296] lock is held by leader-elector-9577494c7-l64lp and has not yet expired
     I0122 03:24:41.426387       8 leaderelection.go:296] lock is held by leader-elector-9577494c7-l64lp and has not yet expired
     I0122 03:24:45.947321       8 leaderelection.go:215] sucessfully acquired lease default/example
    leader-elector-68dcb58d55-g5zp8 is the leader

    更通用的方式是查看資源鎖的身份標識信息:

    1
    
     $ kubectl get ep example -o yaml

    過查看 annotations 中的 control-plane.alpha.kubernetes.io/leader 字段來獲得 Leader 的信息;

  3. 使用 leader-elector 的 HTTP 接口查看 Leader;

    leader-elector 實現了一個簡單的 HTTP 接口(:4040)來查看當前 Leader:

    1
    2 
     curl http://localhost:8001/api/v1/namespaces/default/pods/leader-elector-5d77ccc44d-gwsgg:4040/proxy/
     {"name":"leader-elector-5d77ccc44d-7tmgm"}

用 Sidecar 模式使用 leader-elector

如果自己的項目中需要用到 Leader Election 的邏輯,可以有兩種方式:

  • 將調用 leaderelection 庫的邏輯內嵌到自己項目中;

  • 使用 Sidecar 的方式將 leader-elector 容器組合在 Pod 中,通過調用 HTTP 接口來始終獲得 Leader 的信息;

文檔中以 Node.js 的方式舉了一個簡單例子,大家可以參考,此處不展開了。

Leader Election 的實現

Leader Election 的過程本質上就是一個競爭分布式鎖的過程。在 Kubernetes 中,這個分布式鎖是以創建 Endpoint 或者 ConfigMap 資源的形式進行:誰先創建了某種資源,誰就獲得鎖。

按照我們以往的慣例,帶着問題去看源碼。有這么幾個問題:

  • Leader Election 如何競選 ?

  • Leader 不可用之后如何競選新的 Leader ?

不同於 Raft 算法的一致性算法的 Leader 競選,Pod 之間的 Leader Election 是無狀態的,也就是說現在的 Leader 無需同步上一個 Leader 的數據信息,這就把競選的過程變得非常簡單:先到先得。

這部分代碼在 kubernetes/staging/src/k8s.io/client-go/tools/leaderelection 中,取 1.9.2 版本來分析。

資源鎖的實現

Kubernetes 實現了兩種資源鎖(resourcelock):Endpoint 和 ConfigMap。如果是基於 Endpoint 的資源鎖,獲取到鎖的 Pod 將會在對應 Namespace 下創建對應的 Endpoint 對象,並在其 Annotations 上記錄 Pod 的信息。

比如 kube-controller-manager:

 1
 2  3  4  5  6  7  8  9 10 11 
$ kubectl get ep -n kube-system | grep kube-controller-manager
kube-controller-manager   <none>   41d

$ kubectl describe ep kube-controller-manager -n kube-system
Name:         kube-controller-manager
Namespace:    kube-system
Labels:       <none>
Annotations:  control-plane.alpha.kubernetes.io/leader:
                {"holderIdentity":"szdc-k8sm-0-5","leaseDurationSeconds":15,"acquireTime":"2018-12-11T0...
Subsets:
Events:  <none>

發現在 kube-system 中創建了同名的 Endpoint(kube-controller-manager),並在 Annotations 中以設置了 key 為 control-plane.alpha.kubernetes.io/leader,value 為對應 Leader 信息的 JSON 數據。同理,如果采用 ConfigMap 作為資源鎖也是類似的實現模式。

resourcelock 是以 interface 的形式對外暴露,在創建過程(New())通過相應的參數來控制具體實例化的過程:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 
// leaderelection/resourcelock/interface.go type Interface interface { // Get returns the LeaderElectionRecord  Get() (*LeaderElectionRecord, error) // Create attempts to create a LeaderElectionRecord  Create(ler LeaderElectionRecord) error // Update will update and existing LeaderElectionRecord  Update(ler LeaderElectionRecord) error // RecordEvent is used to record events  RecordEvent(string) // Identity will return the locks Identity  Identity() string // Describe is used to convert details on current resource lock  // into a string  Describe() string }

其中 Get()Create() 和 Update() 本質上就是對 LeaderElectionRecord 的讀寫操作。LeaderElectionRecord 定義如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 
type LeaderElectionRecord struct { // 標示當前資源鎖的所有權的信息  HolderIdentity string `json:"holderIdentity"` // 資源鎖租約時間是多長  LeaseDurationSeconds int `json:"leaseDurationSeconds"` // 鎖獲得的時間  AcquireTime metav1.Time `json:"acquireTime"` // 續租的時間  RenewTime metav1.Time `json:"renewTime"` // Leader 進行切換的次數  LeaderTransitions int `json:"leaderTransitions"` }

理論上,LeaderElectionRecord 是保存在資源鎖的 Annotations 中,可以是任意的字符串,此處是將 JSON 序列化為字符串來進行存儲。

在 leaderelection/resourcelock/configmaplock.go 和 leaderelection/resourcelock/endpointslock.go 分別是基於 Endpoint 和 ConfigMap 對上面接口的實現。拿 endpointslock.go 來看,對這幾個接口的實現實際上就是對 Endpoint 資源中 Annotations 的增刪查改罷了,比較簡單,就不詳細展開。

競爭鎖的過程

完整的 Leader Election 過程在 leaderelection/leaderelection.go 中。

整個過程可以簡單描述為:

  1. 每個 Pod 在啟動的時候都會創建 LeaderElector 對象,然后執行 LeaderElector.Run() 循環;

  2. 在循環中,Pod 會定期(RetryPeriod)去不斷嘗試創建資源,如果創建成功,就在對應資源的字段中記錄 Pod 相關的 Id(比如節點的 hostname);

  3. 在循環周期中,Leader 會不斷 Update 資源鎖的對應時間信息,從節點則會不斷檢查資源鎖是否過期,如果過期則嘗試更新資源,標記資源所有權。這樣一來,一旦 Leader 不可用,則對應的資源鎖將得不到更新,過期之后其他從節點會再次創建新的資源鎖成為 Leader;

其中,LeaderElector.Run() 的源碼為:

1 2 3 4 5 6 7 8 
func (le *LeaderElector) Run() { ... // 嘗試創建鎖  le.acquire() // Leader 更新資源鎖的租約  le.renew() ... }

acquire() 會周期性地創建鎖或探查鎖有沒有過期:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 
func (le *LeaderElector) acquire() { ... wait.JitterUntil(func() { // 嘗試創建或者續約資源鎖  succeeded := le.tryAcquireOrRenew() // leader 可能發生了改變,執行相應的 OnNewLeader() 回調函數  le.maybeReportTransition() // 不成功說明創建資源失敗,當前 Leader 是其他 Pod  if !succeeded { ... return } ... }, le.config.RetryPeriod, JitterFactor, true, stop) }

執行的周期為 RetryPeriod

我們重點關注 tryAcquireOrRenew() 的邏輯:

 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 29 30 31 32 33 34 35 36 37 38 39 40 41 
func (le *leaderElector) tryAcquireOrRenew() bool { now := metav1.Now() leaderElectionRecord := rl.LeaderElectionRecord{ HolderIdentity: le.config.Lock.Identity(), LeaseDurationSeconds: int(le.config.LeaseDuration) / time.Second), // 將租約改成 now  RenewTime: now, AcquireTime: now, } // 獲取當前的資源鎖  oldLeaderElectionRecord, err := le.config.Lock.Get() if err != nil { ... // 執行到這里說明找不到資源鎖,執行資源鎖的創建動作  // 由於資源鎖對應的底層 Kubernetes 資源 Endpoint 或 ConfigMap 是不可重復創建的,所以此處創建是安全的  if err = le.config.Lock.Create(leaderElectionRecord); err != nil { ... } ... } // 如果當前已經有 Leader,進行 Update 操作  // 如果當前是 Leader:Update 操作就是續租動作,即將對應字段的時間改成當前時間  // 如果是非 Leader 節點且可運行 Update 操作,則是一個搶奪資源鎖的過程,誰先更新成功誰就搶到資源  ... // 如果還沒有過期且當前不是 Leader,直接返回  // 只有 Leader 才進行續租操作且此時其他節點無須搶奪資源鎖  if le.observedTime.Add(le.config.LeaseDuration).After(now.Time) && oldLeaderElectionRecord.HolderIdentity != le.config.Lock.Identity() { ... return false } ... // 更新資源  // 對於 Leader 來說,這是一個續租的過程  // 對於非 Leader 節點(僅在上一個資源鎖已經過期),這是一個更新鎖所有權的過程  if err = le.config.Lock.Update(leaderElectionRecord); err != nil { ... } }

由上可以看出,tryAcquireOrRenew() 就是一個不斷嘗試 Update 操作的過程。

如果執行邏輯從 le.acquire() 跳出,往下執行 le.renew(),這說明當前 Pod 已經成功搶到資源鎖成為 Leader,必須定期續租:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 
func (le *LeaderElector) renew() { stop := make(chan struct{}) // period 為 0 說明會一直執行  wait.Until(func() { // 每間隔 RetryPeriod 就執行 tryAcquireOrRenew()  // 如果 tryAcquireOrRenew() 返回 false 跳出 Poll()  // tryAcquireOrRenew() 返回 false 說明續租失敗  err := wait.Poll(le.config.RetryPeriod, le.config.RenewDeadline, func() (bool, error) { return le.tryAcquireOrRenew(), nil }) // 續租失敗,說明已經不是 Leader  ... }, 0, stop) }

如何使用 leaderelection 庫

讓我們來關注一下 election 的實現。

主要的邏輯位於 election/lib/election.go

1 2 3 
func RunElection(e *leaderelection.LeaderElector) { wait.Forever(e.Run, 0) }

主體邏輯很簡單,就是不斷執行 Run()。而 Run() 的實現就是上文中 leaderelection 的 Run() 。

上層應用只需要創建(NewElection())創建 LeaderElector 對象,然后在一個 loop 中調用 Run() 即可。

綜上所述,Kubernetes 中 Pod 的選舉過程本質上還是為了服務的高可用。希望大家研究得愉快!

參考文檔

  1. kube-controller-manager
  2. Simple Leader Election with Kubernetes and Docker


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM