就來搞懂 K8s Leader Election 吧
前因後果
安安, 由於工作上有遇到一個情境會需要使用到分散式鎖的概念進行解題,雖然最後不是透過這篇文章要說明的方法解決的,但我覺得這方法也蠻好玩的,所以寫篇文章來做個紀錄。
好,因為使用情境比較特殊,我們就先來定義一下這篇文章實做的情境吧!
情境: 當一個 method 作為專案被部屬至 K8s 環境中,replica 為 3,其中只能有一個 pod 執行該 method。
什麼是 Kubernetes Leader Election?
在分散式系統中,Leader Election 是一種協調機制,簡單來說就是讓多個服務副本(Pod)中,只有一個被選為 Leader 來執行特定的任務。這樣做可以防止重複工作、確保資料一致性,在需要協調多個服務副本的場景下非常重要。
在 Kubernetes 中,Leader Election 通常用於以下情況
以我目前工作上遇到的案例來說,有以下幾種:
- 當有多個 replica 時,可能只有一個 replica 被允許處理寫入請求
- 如果有一個每隔一段時間需要執行的任務,Leader Election 可以確保只有一個 Pod 執行該任務
Leader Election 的工作原理
Kubernetes 的 Leader Election 通常依賴於以下機制:
Coordination Resource (協調資源)
Leader Election 需要一個共享的資源來進行協調。在 Kubernetes 中,這通常是:
- ConfigMap:這是最常見的方式。會用一個特定的 ConfigMap 來儲存當前 Leader 的資訊(例如 Leader 的 ID、租約到期時間等)
- Lease (租約) 對象:coordination.k8s.io/v1 API 引入了 Lease 對象,它是專為 Leader Election 設計的,比 ConfigMap 效率更高,而且有內建的租約過期機制
- Endpoints:在早期版本中也曾被使用,但現在比較少見了
競爭和續約 (Contention and Renewal)
所有參與 Leader Election 的 Pod 都會嘗試獲取(或更新)這個協調資源,把自己標記為 Leader,成功獲取或更新資源的那個就成為 Leader。
另外,Leader 會定期「續約」(更新資源),來表明它還活著,仍然是 active 的 Leader。如果 Leader 沒能續約(比如因為程式異常或 IO 問題),租約就會過期,其他 Pod 就會開始競爭成為新的 Leader。
Client-go
Kubernetes 應用程式通常會使用 client-go 庫中提供的 Leader Election 相關工具,像是 leaderelection.LeaderElectionConfig 和 leaderelection.LeaderElector,來簡化 Leader Election 的實作。
實做部份:使用 Golang (引用 client-go 庫)
實作部份我會直接用程式碼來呈現,語言選擇 Golang(因為這是我最近私下在玩的語言),相關的 library 會使用 client-go。
這是官方提供的 library,覺得挺不錯的,特別是在開發 Kubernetes 相關應用程式時很好用。
引入必要的 library
import (
"context"
"flag"
"fmt"
"os"
"time"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/klog/v2"
)
設定 Leader Election 相關參數
leaseLockName:用於 Leader Election 的 Lease 對象名稱leaseLockNamespace:Lease 對象所在的 namespaceid:當前參與者的唯一 ID,通常是 Pod 的名稱
var (
kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
leaseLockName = flag.String("lease-lock-name", "my-leader-election-lock", "the name of the lease lock resource")
leaseLockNamespace = flag.String("lease-lock-namespace", "default", "the namespace of the lease lock resource")
id = flag.String("id", "", "the id of this leader election participant")
)
建立 Kubernetes client
func buildConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig != "" {
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
return cfg, nil
}
cfg, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
return cfg, nil
}
啟動 Kubernet client
config, err := buildConfig(*kubeconfig)
if err != nil {
_ = fmt.Errorf("error building kubeconfig: %s", err.Error())
}
client := kubernetes.NewForConfigOrDie(config)
設定 Leader Election 相關函數
這些函數定義了當 Pod 成為 Leader 或失去 Leader 權限時該做什麼事情。
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 這裡是當你成為 Leader 時要執行的主要邏輯
run := func(ctx context.Context) {
klog.Infof("%s: I am the leader! Performing my duties...", *id)
// 在這裡執行只有 Leader 才應該執行的業務邏輯
// 例如:啟動控制器、處理特定任務等
for {
select {
case <-ctx.Done():
klog.Infof("%s: Leader duties stopped.", *id)
return
case <-time.After(5 * time.Second):
klog.Infof("%s: Still the leader, doing important work...", *id)
}
}
}
// 建立 LeaderElectionConfig
lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: *leaseLockName,
Namespace: *leaseLockNamespace,
},
Client: client.CoordinationV1(),
LockConfig: resourcelock.LockConfig{
Identity: *id,
},
}
leaderElectionConfig := leaderelection.LeaderElectionConfig{
Lock: lock,
// 這個 Pod 成為 Leader 後,執行 `run` 函數
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
run(ctx)
},
// 當 Pod 失去 Leader 身份時執行
OnStoppedLeading: func() {
klog.Infof("%s: Lost leadership, exiting.", *id)
cancel() // 終止上下文,退出程式
},
// 當有新的 Leader 被選出時執行
OnNewLeader: func(identity string) {
if identity == *id {
return
}
klog.Infof("%s: New leader elected: %s", *id, identity)
},
},
// 續約間隔:Leader 會每隔多久更新 Lease 資源
LeaseDuration: 15 * time.Second,
// 續約失敗後,多久會嘗試再次成為 Leader(非 Leader 才會競爭)
RenewDeadline: 10 * time.Second,
// 在失去領導權後,多久會釋放領導權
RetryPeriod: 2 * time.Second,
}
啟動 Leader Election
leaderElector, err := leaderelection.NewLeaderElector(leaderElectionConfig)
if err != nil {
klog.Fatalf("Error creating leader elector: %s", err.Error())
}
leaderElector.Run(ctx) // 啟動 Leader Election 循環
如何編譯和運行(在 Kubernetes 叢集外部進行測試)
編譯:
go build -o my-leader-app main.go
運行(提供 kubeconfig):
# 開啟第一個終端機
./my-leader-app --kubeconfig=$HOME/.kube/config --id=instance-1
# 開啟第二個終端機
./my-leader-app --kubeconfig=$HOME/.kube/config --id=instance-2
# 開啟第三個終端機
./my-leader-app --kubeconfig=$HOME/.kube/config --id=instance-3
[!NOTE] 由於整段程式碼我覺得比較長,所以我放在這邊:main.go
把整個專案跑起來後,我們會看到其中一個終端機被選舉為 Leader 並印出 “I am the leader!”,其他的則會印出 “New leader elected: instance-X”。如果我們把作為 Leader 的程式按 Ctrl+C 關閉,其他還在運行的程式就會開始競爭並選出新的 Leader。
我們也可以通過 kubectl 指令來觀察 Lease 的變化,指令如下:
kubectl get lease my-leader-election-lock
以下圖片是我自己的運行結果圖,讓我來簡單說明一下:我透過 tmux 切分出三個 terminal 視窗,模擬三個 Pod 來爭奪 Leader 的位置。
過程中可以看到,instance-1 首先取得了 Leader 的角色,同時 instance-2 和 instance-3 也會同步得知當前的 Leader 是 instance-1。
接著,我把作為 Leader 的 instance-1 結束,並透過指令將 K8s 的 Lease 資訊印出來,可以發現當前的 Leader 還是 instance-1,但過幾秒再看一次就會發現 Leader 變成了 instance-3,同時也能看到 instance-3 得知自己變成 Leader,instance-2 也同步得知 instance-3 變成 Leader。

結論
Kubernetes Leader Election 是一種分散式鎖的機制,用於在分散式應用程式中實現高可用性和協調性。通過 client-go 庫,開發者可以相對容易地將 Leader Election 功能集成到他們的程式中。 但有一點需要注意,就是當部署到 Kubernetes cluster 時,需要確保Pod 擁有正確的權限來操作 Leader Election 的資源 (例如 Lease 或 ConfigMap),並且每個 Pod 都使用唯一的 ID 參與整個 Election。
好啦,就是這樣啦。希望這篇文章可以幫助到正在觀看文章的你。