分散式鎖?選主?
分散式鎖可以保證當有多臺例項同時競爭一把鎖時,只有一個人會成功,其他的都是失敗。諸如共享資源修改、冪等、頻控等場景都可以通過分散式鎖來實現。
還有一種場景,也可以通過分散式鎖來實現,那就是選主,為了保證服務的可用性,我們都會以一主多從的方式去部署,特別是提供儲存能力的服務。Leader服務來接收資料的寫入,然後將資料同步給Follower服務。當Leader服務掛掉時,我們需要從Follower服務中重新選舉一個服務來當Leader,複雜的方式是通過Raft協議去協商,簡單點,可以通過分散式鎖的思路來做:
- 所有的Follower服務去競爭同一把鎖,並給這個鎖設定一個過期時間
- 只會有一個Follower服務取到鎖,這把鎖的值就為它的標識,他就變成了Leader服務
- 其他Follower服務競爭失敗後,去獲取鎖得到的當前的Leader服務標識,與之通訊
- Leader服務需要在鎖過期之前不斷的續期,證明自己是健康的
- 所有Follower服務監控這把鎖是否還被Leader服務持有,如果沒有,就跳到了第1步
通過 Redis、Zookeeper 都可以實現,不過這次,我們使用 Etcd 來實現。
Etcd 簡單介紹
Etcd:A highly-available key value store for shared configuration and service discovery。
Etcd 是一個K/V儲存,和 Redis 功能類似,這是我對它的直觀印象,和實現Master選舉好像八竿子打不著。隨著對 Etcd 瞭解的加深,我才開始對官網介紹那句話有了一定理解,Redis K/V 儲存是用來做純粹的快取功能,高併發讀寫是核心,而 Etcd 這個基於 Raft 的分散式 K/V 儲存,強一致性的 K/V 讀寫是核心,基本這點誕生了很多有想象力的使用場景:服務發現、分散式鎖、Master 選舉等等。
基於 Etcd 以下特性,我們可以實現自動選主:
- MVCC,key存在版本屬性,沒被建立時版本號為0
- CAS操作,結合MVCC,可以實現競選邏輯,if(version == 0) set(key,value),通過原子操作,確保只有一臺機器能set成功;
- Lease租約,可以對key繫結一個租約,租約到期時沒預約,這個key就會被回收;
- Watch監聽,監聽key的變化事件,如果key被刪除,則重新發起競選。
準備工作
啟動 Etcd
我們使用 Docker 安裝,簡單方便:
> docker run -d --name Etcd-server \
--publish 2379:2379 \
--publish 2380:2380 \
--env ALLOW_NONE_AUTHENTICATION=yes \
--env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 \
bitnami/etcd:latest
最好是使用最新般本
Go 依賴庫安裝
Etcd 提供開箱即用的選主工作庫,我們直接使用就行
> go get go.etcd.io/etcd/client/v3
這一步看似簡單,如果放在以前,少不了一頓百度,原因是因為它依賴的 grpc 和 bbolt 庫的版本不能是最新的,需要在 go.mod 中去寫死版本。所幸趕上了好時代,官方終於出手整改了,現在只要一行命令列。
選主Demo
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
var (
serverName = flag.String("name", "", "")
)
func main() {
flag.Parse()
// Etcd 伺服器地址
endpoints := []string{"127.0.0.1:2379"}
clientConfig := clientv3.Config{
Endpoints: endpoints,
DialTimeout: 2 * time.Second,
}
cli, err := clientv3.New(clientConfig)
if err != nil {
panic(err)
}
s1, err := concurrency.NewSession(cli)
if err != nil {
panic(err)
}
fmt.Println("session lessId is ", s1.Lease())
e1 := concurrency.NewElection(s1, "my-election")
go func() {
// 參與選舉,如果選舉成功,會定時續期
if err := e1.Campaign(context.Background(), *serverName); err != nil {
fmt.Println(err)
}
}()
masterName := ""
go func() {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
timer := time.NewTicker(time.Second)
for range timer.C {
timer.Reset(time.Second)
select {
case resp := <-e1.Observe(ctx):
if len(resp.Kvs) > 0 {
// 檢視當前誰是 master
masterName = string(resp.Kvs[0].Value)
fmt.Println("get master with:", masterName)
}
}
}
}()
go func() {
timer := time.NewTicker(5 * time.Second)
for range timer.C {
// 判斷自己是 master 還是 slave
if masterName == *serverName {
fmt.Println("oh, i'm master")
} else {
fmt.Println("slave!")
}
}
}()
c := make(chan os.Signal, 1)
// 接收 Ctrl C 中斷
signal.Notify(c, os.Interrupt, os.Kill)
s := <-c
fmt.Println("Got signal:", s)
e1.Resign(context.TODO())
}
我們在兩個終端分別執行下面兩個命令,模擬兩個服務去競爭:
> go run main.go -name A
session lessId is 7587863771971134868
get master with: A
get master with: A
get master with: A
get master with: A
oh, i'm master
> go run main.go -name B
session lessId is 7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!
當我們使用 Ctrl C 中斷,此時 B 就成為了 master
> go run main.go -name A
session lessId is 7587863771971134868
get master with: A
get master with: A
get master with: A
get master with: A
oh, i'm master
^CGot signal: interrupt
> go run main.go -name B
session lessId is 7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!
get master with: B
get master with: B
get master with: B
get master with: B
oh, i'm master
原理
當我們啟動 A 和 B 兩個服務時,他們後會在公共字首 "my-election/" 下建立自己的 key,這個 key 的構成為 "my-election/" + 十六進位制(LessId)。這個LessId 是在服務啟動時,從 Etcd 服務端取到的客戶端唯一標識。比如上面程式執行的兩個服務建立的 key 分別是:
- A 服務建立的 key 是 "my-election/694d81e5fc652594",值是 "A"
- B 服務建立的 key 是 "my-election/694d81e5fc65259c",值是 "B"
因為是通過事務的方式去建立 key,可以保證如果這個 key 已經建立了,不去建立了。並且這個 key 是有過期時間,兩個服務 A 和 B 會啟動一個協程定期去重新整理過期時間,通過這個方式證明自己的健康的。
現在兩個服務都建立了 key, 那麼那個才是 master 呢?我們選取最早建立的那個 key 的擁有者作為 master。
Etcd 服務的查詢介面支援根據字首查詢和按照建立時間排序,所以我們可以輕鬆的拿到第一個建立成功的 key,這個 key 對應的值就是 master 了,也就是 A 服務。
當現在 master 服務掛掉了,因為它的 key 沒有在過期之前續期,就會被刪除的,此時當初第二個建立的 key 就會變成第一個,那個 master 就變成了 B 服務。
我們是通過e1.Campaign(context.Background(), *serverName)
行程式碼是參加去參加選舉的,裡面有一個細節:如果競爭失敗,這個函式會阻塞,直到它選舉成功或者服務中斷。也就是說:如果 B 服務建立的 key
不是最早的一個,那它會一直等待,直到服務 A 的 key 被刪除後,函式才會有返回。