Golang 通過 Consul 實現分佈式鎖
Consul 是什麼
Consul 是一個支持多數據中心分佈式高可用的服務發現和配置共享的服務軟件,由 HashiCorp 公司用 Go 語言開發, 基於 Mozilla Public License 2.0 的協議進行開源. Consul 支持健康檢查,並允許 HTTP 和 DNS 協議調用 API 存儲鍵值對. 命令行超級好用的虛擬機管理軟件 vgrant 也是 HashiCorp 公司開發的產品. 一致性協議採用 Raft 算法,用來保證服務的高可用. 使用 GOSSIP 協議管理成員和廣播消息, 並且支持 ACL 訪問控制.
Consul 的使用場景
- docker 實例的註冊與配置共享
- coreos 實例的註冊與配置共享
- vitess 集羣
- SaaS 應用的配置共享
- 與 confd 服務集成,動態生成 nginx 和 haproxy 配置文件
Consul 的優勢
- 使用 Raft 算法來保證一致性, 比複雜的 Paxos 算法更直接. 相比較而言, zookeeper 採用的是 Paxos, 而 etcd 使用的則是 Raft.
- 支持多數據中心,內外網的服務採用不同的端口進行監聽。 多數據中心集羣可以避免單數據中心的單點故障,而其部署則需要考慮網絡延遲, 分片等情況等. zookeeper 和 etcd 均不提供多數據中心功能的支持.
- 支持健康檢查. etcd 不提供此功能.
- 支持 http 和 dns 協議接口. zookeeper 的集成較爲複雜, etcd 只支持 http 協議.
- 官方提供web管理界面, etcd 無此功能. 綜合比較, Consul 作爲服務註冊和配置管理的新星, 比較值得關注和研究.
Consul 的角色
client: 客戶端, 無狀態, 將 HTTP 和 DNS 接口請求轉發給局域網內的服務端集羣. server: 服務端, 保存配置信息, 高可用集羣, 在局域網內與本地客戶端通訊, 通過廣域網與其他數據中心通訊. 每個數據中心的 server 數量推薦爲 3 個或是 5 個.
什麼是分佈式鎖
分佈式鎖,是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分佈式鎖。 目前幾乎很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴我們“任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的範圍內即可。 在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。 在這裏我們使用Consul來管理分佈式鎖。Consul內置了服務註冊與發現框 架、分佈一致性協議實現、健康檢查、Key/Value存儲、多數據中心方案,不再需要依賴其他工具(比如ZooKeeper等)。
Sessions
session是一個遠程進程和consul節點之間的鏈接,它由一個遠程進程和可以顯式無效或由於健康檢查機制。根據會話配置,創建與已失效會話鎖摧毀或釋放。
Health checks
Consul支持多種檢查 (如 HTTP、 TCP 等)。在session創建過程中可以定義的健康檢查列表。這些檢查將用於確定是否sessio需要使之失效。
TTL
除了健康檢查,會話也具有內置支持的 TTL。當 TTL 過期session被視爲無效。遠程進程負責更新session之前 TTL 過期。
Golang API
Consul API client 提供一個方便的抽象,session和 K/V 存儲。有是一個鎖結構與鎖定、 解鎖和破壞的方法。也有用於幫助創建鎖實例方法。API 客戶端還負責更新會話。
Creating the Consul client
client, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500"})
Creating Lock instance
type LockOptions struct {
Key string // Must be set and have write permissions
Value []byte // Optional, value to associate with the lock
Session string // Optional, created if not specified
SessionOpts *SessionEntry // Optional, options to use when creating a session
SessionName string // Optional, defaults to DefaultLockSessionName (ignored if SessionOpts is given)
SessionTTL string // Optional, defaults to DefaultLockSessionTTL (ignored if SessionOpts is given)
MonitorRetries int // Optional, defaults to 0 which means no retries
MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime
LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime
LockTryOnce bool // Optional, defaults to false which means try forever
}
LockOptions 是所有可能的選項的容器,可以用於設置鍵和值、 定製會話或設置TTL。
opts := &api.LockOptions{
Key: "webhook_receiver/1",
Value: []byte("set by sender 1"),
SessionTTL: "10s",
SessionOpts: &api.SessionEntry{
Checks: []string{"check1", "check2"},
Behavior: "release",
},
}
lock, err := client.LockOpts(opts)
另一種常用的方法是 LockKey,它創建一個鎖與所有選項設置爲默認條目名稱除外。
lock, err := client.LockKey("webhook_receiver/1")
Acquiring lock
stopCh := make(chan struct{})
lockCh, err := lock.Lock(stopCh)
if err != nil {
panic(err)
}
cancelCtx, cancelRequest := context.WithCancel(context.Background())
req, _ := http.NewRequest("GET", "https://example.com/webhook", nil)
req = req.WithContext(cancelCtx)
go func() {
http.DefaultClient.Do(req)
select {
case <-cancelCtx.Done():
log.Println("request cancelled")
default:
log.Println("request done")
err = lock.Unlock()
if err != nil {
log.Println("lock already unlocked")
}
}
}()
go func() {
<-lockCh
cancelRequest()
}()