為什么需要 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
-
創建一個
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
-
查看哪個 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 的信息; -
使用
leader-elector的 HTTP 接口查看 Leader;leader-elector實現了一個簡單的 HTTP 接口(:4040)來查看當前 Leader:1 2curl 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())通過相應的參數來控制具體實例化的過程:
|
|
其中 Get()、Create() 和 Update() 本質上就是對 LeaderElectionRecord 的讀寫操作。LeaderElectionRecord 定義如下:
|
|
理論上,LeaderElectionRecord 是保存在資源鎖的 Annotations 中,可以是任意的字符串,此處是將 JSON 序列化為字符串來進行存儲。
在 leaderelection/resourcelock/configmaplock.go 和 leaderelection/resourcelock/endpointslock.go 分別是基於 Endpoint 和 ConfigMap 對上面接口的實現。拿 endpointslock.go 來看,對這幾個接口的實現實際上就是對 Endpoint 資源中 Annotations 的增刪查改罷了,比較簡單,就不詳細展開。
競爭鎖的過程
完整的 Leader Election 過程在 leaderelection/leaderelection.go 中。
整個過程可以簡單描述為:
-
每個 Pod 在啟動的時候都會創建
LeaderElector對象,然后執行LeaderElector.Run()循環; -
在循環中,Pod 會定期(
RetryPeriod)去不斷嘗試創建資源,如果創建成功,就在對應資源的字段中記錄 Pod 相關的 Id(比如節點的 hostname); -
在循環周期中,Leader 會不斷 Update 資源鎖的對應時間信息,從節點則會不斷檢查資源鎖是否過期,如果過期則嘗試更新資源,標記資源所有權。這樣一來,一旦 Leader 不可用,則對應的資源鎖將得不到更新,過期之后其他從節點會再次創建新的資源鎖成為 Leader;
其中,LeaderElector.Run() 的源碼為:
|
|
acquire() 會周期性地創建鎖或探查鎖有沒有過期:
|
|
執行的周期為 RetryPeriod。
我們重點關注 tryAcquireOrRenew() 的邏輯:
|
|
由上可以看出,tryAcquireOrRenew() 就是一個不斷嘗試 Update 操作的過程。
如果執行邏輯從 le.acquire() 跳出,往下執行 le.renew(),這說明當前 Pod 已經成功搶到資源鎖成為 Leader,必須定期續租:
|
|
如何使用 leaderelection 庫
讓我們來關注一下 election 的實現。
主要的邏輯位於 election/lib/election.go:
|
|
主體邏輯很簡單,就是不斷執行 Run()。而 Run() 的實現就是上文中 leaderelection 的 Run() 。
上層應用只需要創建(NewElection())創建 LeaderElector 對象,然后在一個 loop 中調用 Run() 即可。
綜上所述,Kubernetes 中 Pod 的選舉過程本質上還是為了服務的高可用。希望大家研究得愉快!
