1. 程式人生 > 其它 >Kubernetes 中 Pod 的選舉過程

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.goleaderelection/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()的實現就是上文中leaderelectionRun()

上層應用只需要建立(NewElection())建立LeaderElector物件,然後在一個 loop 中呼叫Run()即可。

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

參考文件

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