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

本次將記錄[利用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條數據顯示

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章