基於etcd的選主功能實現的主備節點管理

遊俠souy發表於2020-11-20

etcd有多種使用場景,Master選舉是其中一種。本文主要講述如何使用etcd在多個service中選一個主服務。使用etcd選主和分散式鎖差不多,哪個service首先搶到某個值,誰就是主節點,選主介面是個同步介面,搶到主或者出現錯誤就返回,沒有搶到主的會一直阻塞。

pfx是字首,比如"/election/"

key類似於"/election/service1"

leaseID是一個64位的整數值,etcd v3引入了lease(租約)的概念,lease被封裝在session中,每一個客戶端都生成自己的lease,也就是說每個客戶端都有一個唯一的64位整形值。

val表示了當前主被誰獲得。獲取主的service依靠租約機制來維持主節點的有效性,租約預設有效期是60s,主servie不斷的續約就可以不斷的佔有主節點。

先來看clientv3/concurrency中自帶的選舉用例,閱讀這個用例之後我們大概就可以知道選舉的大概方法

func ExampleElection_Campaign() {
         //建立v3的client端,我們和etcd的所有介面都是通過該client實現
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    //建立兩個競爭的Session,這裡Session有效期使用預設值60s
    s1, err := concurrency.NewSession(cli)
    if err != nil {
        log.Fatal(err)
    }
defer s1.Close()
//pfx 是"/my-election/"
    e1 := concurrency.NewElection(s1, "/my-election/")

    s2, err := concurrency.NewSession(cli)
    if err != nil {
        log.Fatal(err)
    }
defer s2.Close()
//pfx 是"/my-election/"
    e2 := concurrency.NewElection(s2, "/my-election/")

    // create competing candidates, with e1 initially losing to e2
    var wg sync.WaitGroup
    wg.Add(2)
electc := make(chan *concurrency.Election, 2)
//啟動兩個goroutine,競爭主節點
    go func() {
        defer wg.Done()
        // delay candidacy so e2 wins first
        time.Sleep(3 * time.Second)
                //節點名"e1"
        if err := e1.Campaign(context.Background(), "e1"); err != nil {
            log.Fatal(err)
        }
        electc <- e1
    }()
    go func() {
        defer wg.Done()
        if err := e2.Campaign(context.Background(), "e2"); err != nil {
            log.Fatal(err)
        }
        electc <- e2
    }()

    cctx, cancel := context.WithCancel(context.TODO())
    defer cancel()
        // electc 返回說明有主選出來,沒選出來的goroutine會一直阻塞
    e := <-electc
    fmt.Println("completed first election with", string((<-e.Observe(cctx)).Kvs[0].Value))

    // 當前的主節點主動離職
    if err := e.Resign(context.TODO()); err != nil {
        log.Fatal(err)
    }
        //又有新節點當選主主節點
    e = <-electc
    fmt.Println("completed second election with", string((<-e.Observe(cctx)).Kvs[0].Value))

    wg.Wait()

    // Output:
    // completed first election with e2
    // completed second election with e1
}

根據上面的簡單示例程式碼,大概清楚了選主的主要過程。

該如何應該這個選主機制呢?看下更復雜的場景下如何應用該選主機制,假設我們有三個service,需通過選主機制選出一個主,沒當選的降為備份節點。並且要滿足主節點切換需求。

 

我們將每個service內的主節點管理分成兩部分,選舉部分和監聽部分。

選舉部分負責選舉,監聽部分負責主備節點切換。

邏輯關係圖如下:

直接看程式碼吧,為了方便大家能看的更直觀,程式碼經過簡化處理。

func (s *service) Campaign(resign chan int) {
    for {
        //建立用於選舉的Session,有效時間可以根據實際情況設定。
        session, err := createSession(s.Etcd.Client())
        if err != nil {
            //todo
            continue
        }
        e := concurrency.NewElection(session, prefix)
        s.election = e
        if err = e.Campaign(context.Background(), s.SelfName); err != nil {
            //todo
            continue
        }

        //執行至此說明該節點當選主節點
        s.IsMaster = true
        s.SetLeader(s.SelfName)           
        cancel()
        
        //超時監聽
        select {
        case <-session.Done():
            s.IsMaster = false
            s.ResetLeader()
             //todo
        case <-resign:
            //todo
        }
    }
}


func (s *service) Leader Monitor () {
    resign := make(chan int, 1)
    //啟動選主協程
    go s.Campaign(resign)
    //監聽主從切換
    for {
        cctx, cancel := context.WithCancel(context.TODO())
        ch := s.election.Observe(cctx)
        for {
            select {
            case resp := <-ch:
                if len(resp.Kvs) > 0 {
                    leader := string(resp.Kvs[0].Value)
                    if leader != s.SelfName {
                        if s.IsMaster {
                            s.IsMaster = false
                        }
                        s.SetLeader(leader)
                    }
                } else {
                    cancel()
                    time.Sleep(time.Duration(public.MasterIntervalTime) * time.Second)
                    cctx, cancel = context.WithCancel(context.TODO())
                    ch = s.election.Observe(cctx)
                }
            }
        }
    }
}

 

 

 

 

 

 

相關文章