使用etcd選舉sdk實踐master/slave故障轉移

部落格猿馬甲哥發表於2022-04-19

本次將記錄[利用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 --endpoints=127.0.0.1:2379 get /merc --prefix --sort-by=CREATE --order=ASCEND --limit=1

--sort-by :以x標準(建立時間)檢索資料
-- order : 以升降序對已檢出的資料排序
-- limit: 從已檢出的資料中取x條資料顯示

相關文章