概述
etcd 提供了线性一致性。在线性一致性的根底上。etcd 提供了
- 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 的问题,即避免出现脑裂。
Campaign
代码
见 https://github.com/etcd-io/et… 8ee1dd9e23bce4d9770816edf5816b13767ac51d
流程简述
- put value to prefix/lease_id (排队)
-
waitDeletes (期待 prefix 下所有早于本人的 value,即本人排到第一)
Campaign 代码含正文
type Election struct { session *Session keyPrefix string leaderKey string leaderRev int64 leaderSession *Session hdr *pb.ResponseHeader }
election.go: 59
// 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 }
一个典型的选主过程
@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。选举过程被封装,用户只须要实现上面的事件:
- become_leader 这个时候能够开启 leader 服务。
- leader_change 这个时候要更新 leader node。
- get_out_of_leadership 这个时候要敞开 leader 服务。(不论是 ETCD 分区,还是和 ETCD 网络中断,参选人 process 因为程序 BUG 退出,都意味着本人不是 leader 了,这个时候要敞开 leader 服务)