Go:使用 consul 實現領導者選舉
在分佈式計算中,leader 選舉是指通過選舉產生一個領導者節點,該領導者節點能將任務分佈到其他不同節點。起初,集羣各節點是不感知哪個是”leader“節點的。在領導者選舉算法運行後,集羣中的每個節點識別一個特定的、唯一的節點作爲 leader,其他節點作爲 follower 節點。
Leader:作爲集羣特殊節點執行一些特殊操作。這些操作包括分配任務,修改一些數據的能力,甚至負責所有系統請求。
Follower:集羣中執行分配任務節點。
例如:Kubernetes 集羣
-
在控制平面運行多個 Kube-scheduler 實例
-
只有選舉產生的 leader 節點的實例負責將 pod 調度到各節點。
領導者選舉
通常 leader 選舉使用分佈式鎖來實現,所有的節點競爭同一個資源上的鎖,獲取到鎖的節點成爲 leader。要實現分佈式鎖,需要一個強一致性系統來決定哪個節點持有鎖,因爲這個過程必須是原子操作。典型的一致性協議例如 Paxos、Raft 協議都用於這個目的。然而要正確地實現這些算法是非常困難的,因爲必須經過廣泛的測試和嚴格驗證纔行。領導者選舉也可以使用第三方工具,如 zookeeper, etcd, consul 來實現,但這會增加保持高可用系統的開銷。
因爲我們在 Metro【一個開源項目】中使用 Consul 作爲數據庫註冊中心,所以我們決定在 leader 選舉實現中使用相同的方法。讓我們看看如何使用 Consul 實現分佈式鎖。
實現分佈式鎖
提供分佈式鎖需要的基本要求:
1、互斥:在任何時候只有一個節點持有鎖。
2、無死鎖:最終,即使鎖定資源的節點發生故障,其他節點也可以獲得鎖。
3、容錯:只要大多數節點還正常工作,鎖的獲取和釋放就可以完成。
consul
consul 是一個分佈式高可用的 Key-value 存儲系統(類似 etcd 和 zookeeper)。數據在 consul 中以 KV 格式存儲。
讓我們來看看 Consul 是如何滿足分佈式鎖定需求的。
互斥性
-
Consul 要求客戶端使用 session API 創建會話。
-
在使用 API 獲取 KV 資源時需要 session id。
-
確保只有 1 一個節點能成功
無死鎖
- session 有 TTL(存活時間),如果 session 過期,所有拿到的 key 就會釋放 / 刪除,因此客戶端需要定時更新 session 避免過期。
容錯
- consul 運行在高可用模式。
基於 consul 使用 Go 來實現 Leader 選舉
選舉可以通過以下 4 個步驟來實現:
1、創建具有存活時間 TTL 的 session
sessionID, _, err := client.Session().Create(&api.SessionEntry{
Name: "my-service-lock",
Behavior: "delete",
TTL: "30s",
LockDelay : 2 * time.Second
}, nil)
if err != nil {
panic(err)
}
-
TTL 的選擇是根據應用來決定的。TTL 太大的話,在 leader 故障時需等到過期纔會有新的 leader 選舉產生。如果太小會頻繁的更新 session 增加系統的負載。
-
LockDelay 防止 KV 在釋放後立即被獲取,這個延遲間隔可以被領導者用來執行退出相關的動作。
-
2、更新 session
使用 Renew / RenewPeriodic API 定期更新會話。在 TTL 過期前,會話將按照 TTL 持續時間進行更新。
client.Session().RenewPeriodic(
"30s",
sessionID,
nil,
doneChan,
)
這是一個阻塞調用,因此應該在單獨的 goroutine 中運行。通常,doneCh 是當前上下文的 Done 通道。
根據 Key 獲取鎖
isAcquired, _, err := client.KV().Acquire(&api.KVPair{
Key: "path/to/leader/key",
Value: []byte("any value"),
Session: sessionID,
}, nil)
if err != nil {
// 錯誤處理
}
// 如果沒有錯誤,isAquired爲true就是領導者節點
能夠獲得鎖的節點可以定爲 leader,其他節點成爲 follower。
- “Value” 可以用來在 leader 和 follower 之間傳遞集羣狀態。例如,如果 follower 想要連接 leader,leader 可以在 Value 中包含它的標識符。
觀察領導者 key
params := map[string]interface{}{}
params["type"] = "key"
params["key"] = "path/to/leader/key"
plan, err := watch.Parse(params)
if err != nil {
// 錯誤處理
}
// 當所監視的key發生任何更改時調用該handler函數
plan.Handler = handler
// 阻塞調用,因此這段代碼應該在goroutine中運行
plan.RunWithClientAndHclog(client, nil)
handler 函數
func handler(index uint64, result interface{}) {
log.Printf("watch data: %s", result)
// 檢查返回的鍵是否有sessionID
// 如果session ID存在,則key被某個節點獲取
// 如果沒有,目前沒有領導者,嘗試獲取key
}
使用 watch 功能,可以觀察 key 的變化。如果領導者 key 發送變化,Consul 會通知的,因此 session 會被刪除 / 自動過期,領導者 key 釋放被其他節點獲取,導致 KV 的更新。所有 watch 這個 key 的節點都會被通知有變化。在收到通知後,所有節點可以檢查 key 的狀態,並運行新的選舉。
從概念上講很簡單,但在編寫生產系統時,需要考慮許多邊界情況,例如:
-
部署時會發生什麼——在部署時,leader 節點將關閉,爲了優雅地處理,leader 應該退出,讓其他節點立即成爲 leader。可以通過釋放會話鎖或刪除會話本身來退讓。
-
如果 leader/worker 節點意外宕機怎麼辦?如果該節點意外退出,而 leader 沒有退讓,那麼其他節點仍然會認爲 leader 存在,直到會話過期,所以根據應用程序需求選擇一個好的 TTL 很重要。
代碼
你可以參考 Metro 的領導者選舉實現【https://github.com/razorpay/metro/blob/5eb8881adbf5da6d387d1f4659916c83028dfb06/pkg/leaderelection/candidate.go#L56】。
參考文獻
-
https://learn.hashicorp.com/tutorials/consul/application-leader-elections
-
https://clivern.com/leader-election-with-consul-and-golang/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bMSW2nRhM7Gyn9WFAw2ejg