本次將記錄[利用etcd選主sdk實踐master/slave高可用], 並利用etcdctl原生指令碼驗證選主sdk的工作原理。
master/slave高可用叢集
本文目標
在異地多機房部署節點,slave作為備用例項啟動,但不接受業務流量, 監測到master當機,slave節點自動提升為master並接管業務流量。
基本思路
各節點向etcd註冊帶租約的節點資訊, 並各自維持心跳保活,選主sdk根據目前存活的、最早建立的節點資訊鍵值對 來判斷leader, 並通過watch機制通知業務程式碼leader變更。
講道理,每個節點只需要知道兩個資訊就能各司其職
- 誰是leader > 當前節點是什麼角色=> 當前節點該做什麼事情
- 感知叢集leader變更的能力 ===》當前節點現在要不要改變行為
除了官方etcd客戶端go.etcd.io/etcd/client/v3, 還依賴go.etcd.io/etcd/client/v3/concurrency
package:實現了基於etcd的分散式鎖、屏障、選舉
選主過程 | 實質 | api |
---|---|---|
競選前先查詢leader瞭解現場 | 查詢當前存活的,最早建立的kv值 | *concurrency.Election.Leader() |
初始化時,各節點向etcd阻塞式競選 | 各節點向etcd註冊帶租約的鍵值對 | *concurrency.Election.compaign |
建立master/slave叢集,還能及時收到變更通知 | 通過chan傳遞最新的leader value | *concurrency.Election.Observe() |
重點解讀
1.初始化etcd go客戶端
注意:etcd客戶端和服務端是通過grpc來通訊,目前新版本的etcd客戶端預設使用非阻塞式連線, 也就是說v3.New
函式僅表示從指定配置建立etcd客戶端。
為快速確定etcd選舉的可用性,本實踐使用阻塞式建立客戶端:
cli, err := v3.New(v3.Config{
Endpoints: addr,
DialTimeout: time.Second * 5,
DialOptions: []grpc.DialOption{grpc.WithBlock()},
})
if err != nil {
log.WithField("instance", Id).Errorln(err)
return nil, err
}
2. 競選
使用阻塞式命令compaign
競選之前,應先查詢當前leader
// 將id:ip:port作為競選時寫入etcd的value
func (c *Client) Election(id string, notify chan<- bool) error {
//競選前先試圖去了解情況
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := c.election.Leader(ctx)
if err != nil {
if err != concurrency.ErrElectionNoLeader {
return err
}
} else { // 已經有leader了
c.Leader = string(resp.Kvs[0].Value)
notify <- (c.Leader == id)
}
if err = c.election.Campaign(context.TODO(), id); err != nil {
log.WithError(err).WithField("id", id).Error("Campaign error")
return err
} else {
log.Infoln("Campaign success!!!")
c.Leader = id
notify <- true
}
c.election.Key()
return nil
}
參選: 將持續重新整理的leaseID
作為key,將特定的客戶端標記(這裡使用ip:port)作為value,寫到etcd.
當選: 當前存活的、最早建立的key是leader , 也就是說master/slave故障轉移並不是隨機的。
3. watch leader變更
golang使用通道完成goroutine通訊,
本例宣告通道: notify = make(chan bool, 1)
一石二鳥:標記叢集leader是否發生變化;通道內傳值表示當前節點是否是leader
func (c *Client) Watchloop(id string, notify chan<- bool) error {
ch := c.election.Observe(context.TODO()) // 觀察leader變更
tick := time.NewTicker(c.askTime)
defer tick.Stop()
for {
var leader string
select {
case _ = <-c.sessionCh:
log.Warning("Recv session event")
return fmt.Errorf("session Done") // 一次續約不穩,立馬退出程式
case e := <-ch:
log.WithField("event", e).Info("watch leader event")
leader = string(e.Kvs[0].Value)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := c.election.Leader(ctx)
if err != nil {
if err != concurrency.ErrElectionNoLeader {
return err
} else { // 目前沒leader,開始競選了
if err = c.election.Campaign(context.TODO(), id); err != nil {
log.WithError(err).WithField("id", id).Error("Campaign error")
return err
} else { // 競選成功
leader = id
}
}
} else {
leader = string(resp.Kvs[0].Value)
}
}
if leader != c.Leader {
log.WithField("before", c.Leader).WithField("after", leader == id).Info("leader changed")
notify <- (leader == id)
}
c.Leader = leader
}
}
c.election.Observe(context.TODO()) 返回最新的leader資訊,配合select case控制結構
能夠及時拿到leader變更資訊。
如題:通過Leader欄位和chan <- bool, 掌控了整個選舉叢集的狀態, 可根據這兩個資訊去完成業務上的master/slave故障轉移。
使用etcdctl確定leader
election.Leader的原始碼證明了[當前存活的,最早建立的kv為leader]
// Leader returns the leader value for the current election.
func (e *Election) Leader(ctx context.Context) (*v3.GetResponse, error) {
client := e.session.Client()
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
if err != nil {
return nil, err
} else if len(resp.Kvs) == 0 {
// no leader currently elected
return nil, ErrElectionNoLeader
}
return resp, nil
}
等價於./etcdctl get /merc --prefix --sort-by=CREATE --order=ASCEND --limit=1
--sort-by :以x標準(建立時間)檢索資料
-- order : 以升降序對已檢出的資料排序
-- limit: 從已檢出的資料中取x條資料顯示