使用etcd選主

概述

etcd提供了線性一致性。在線性一致性的基礎上。etcd提供了node

  • Campaign 進入等待隊列,當前面全部其它候選人的value都delete後返回。
  • Proclaim 不丟失leadership的狀況下,從新申明一個新的值。
  • Resign 放棄leadership
  • Leader 得到當前的的leader value
  • Observe 開始watch leader value 的變動

這篇blog討論了etcd選舉機制的實現細節,以及應該如何利用etcd選舉來避免腦裂。
若是僅僅是須要知道本身是否主節點,那麼只須要Campaign指令,當Campaign返回時,本身便成爲主節點。
在一個分佈式系統中,是沒有同時這個概念的,假設Campaign指令的返回tcp包因丟包屢次重試,晚了1分鐘纔到達Campaigner,那麼Campaign返回的同時,這個結果就已經失效。
因此,關鍵是:心跳間隔必定要短於value的ttl,且心跳失敗的時間不能長於ttl,心跳失敗數次時就應從新參加選舉。這個時候,其它節點因value的ttl沒過不能成爲leader,而leader會因心跳失敗放棄成爲leader,從而規避Campaign指令滯後問題,或其它緣由致使的,leader campaigner的value過時後,該campaigner還認爲本身是leader的問題,即避免出現腦裂。git

Campaign

代碼

https://github.com/etcd-io/et... 8ee1dd9e23bce4d9770816edf5816b13767ac51dgithub

流程簡述

  1. put value to prefix/lease_id (排隊)
  2. waitDeletes (等待prefix下全部早於本身的value,即本身排到第一)網絡

    Campaign 代碼含註釋

    type Election struct {
     session *Session
    
     keyPrefix string
    
     leaderKey     string
     leaderRev     int64
     leaderSession *Session
     hdr           *pb.ResponseHeader
    }

    election.go: 59session

    // Campaign puts a value as eligible for the election on the prefix
    // key.
    // Multiple sessions can participate in the election for the
    // same prefix, but only one can be the leader at a time.
    //
    // If the context is 'context.TODO()/context.Background()', the Campaign
    // will continue to be blocked for other keys to be deleted, unless server
    // returns a non-recoverable error (e.g. ErrCompacted).
    // Otherwise, until the context is not cancelled or timed-out, Campaign will
    // continue to be blocked until it becomes the leader.
    func (e *Election) Campaign(ctx context.Context, val string) error {
     s := e.session
     client := e.session.Client()
    
     k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
     // put 一個 key value,通常而言,不會有衝突。
     // 若是同一個session 重複put,致使key的 revision 不爲0,txn纔會失敗。
     txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
     txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
     txn = txn.Else(v3.OpGet(k))
     resp, err := txn.Commit()
     if err != nil {
         return err
     }
     e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
     // 若是 put 時發現 key 的revision 不爲 0
     if !resp.Succeeded {
         kv := resp.Responses[0].GetResponseRange().Kvs[0]
         // 更新 leaderRev
         e.leaderRev = kv.CreateRevision
         if string(kv.Value) != val {
             // value 不相等,更新value
             if err = e.Proclaim(ctx, val); err != nil {
                 // 失敗則經過刪除本身的key (辭職)
                 // 從新開始選舉, 返回錯誤
                 // 若是從新開始選舉錯誤?有心跳超時。
                 e.Resign(ctx)
                 return err
             }
         }
     }
    
     _, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
     if err != nil {
         // clean up in case of context cancel
         select {
         case <-ctx.Done():
             // 發生錯誤,刪除本身的key,避免被誤認爲leader.
             e.Resign(client.Ctx())
         default:
             e.leaderSession = nil
         }
         return err
     }
     // 成爲leader
     e.hdr = resp.Header
    
     return nil
    }

    一個典型的選主過程

    image.png

    @startuml
    
    autonumber
    
    participant "actor1" as actor1
    participant "actor2" as actor2
    participant "etcd" as etcd
    
    activate actor1
    activate actor2
    activate etcd
    
    actor1 -> etcd: put "prefix/lease_id_a" value1
    actor2 -> etcd: put "prefix/lease_id_b" value2
    etcd -> actor2: key_revision:1 total_revision: 10000
    etcd -> actor1: key_revision:1 total_revision: 10002
    actor1 -> etcd: wait for "prefix" delete with revision 10001
    actor2 -> actor2: i'm leader
    note right: 當前隊列的狀態是[actor2_lease_id_b, actor1_lease_id_a], actor2 排在最前面,因此返回actor2,actor2成爲leader,actor1須要等待
    deactivate actor2
    actor2 -> etcd: put "prefix/lease_id_c" value2
    etcd -> actor1: 10000 delete
    actor1 -> actor1: i'm leader
    note right: 當前隊列的狀態是[actor1_lease_id_a, actor2_lease_id_c], actor1 排在最前面,因此返回actor1,actor1成爲leader, actor2須要等待
    etcd -> actor2: key_revision:1 total_revision: 10003
    actor2 -> etcd: wait for "prefix" delete with revision 10002
    
    @enduml

    選舉封裝

    抽象

    能夠建立campaigner,即選舉人去參選,選舉人本身負責參選,維持心跳。前面分析過,系統裏絕對不會同時存在兩個節點認爲本身是leader。選舉過程被封裝,用戶只須要實現下面的事件:less

  3. become_leader 這個時候能夠開啓leader服務。
  4. leader_change 這個時候要更新leader node。
  5. get_out_of_leadership 這個時候要關閉leader服務。(無論是ETCD分區,仍是和ETCD網絡中斷,參選人process由於程序BUG退出,都意味着本身不是leader了,這個時候要關閉leader服務)
相關文章
相關標籤/搜索