etcd 通过选举单一领导让写入有序
etcd 是一份被复制的日志,Raft 就是让各副本保持一致的算法。每个任期里只有一名成员是领导者,也只有它可以提议新条目;当多数成员都拥有该条目时,它才算提交。领导者选举决定谁可以写,日志复制决定什么才算持久——这是两套机制,故障模式也各不相同。 两者都被同一个数字支配。仲裁数决定了每次写入要付多少确认、你能承受几台成员失效,以及发生网络分区时会怎样。成员数选错,你为同样的韧性付得更多;丢掉仲裁,集群就停止写入,却仍会回答一部分读请求。 每个任期只有一个领导者 成员在选举超时内没听到领导者的消息就会变成候选人,超时时间是随机的,以免所有人同时竞选。它递增任期、投自己一票,然后向其他成员拉票;一个成员在每个任期最多投一票,并且会拒绝任期更低的候选人。拿到多数票的候选人成为领导者。如果两个成员同时超时,票可能被瓜分——四成员集群里可能变成 2 比 2,够不到 3 这个仲裁数——这一轮就白费了,代价是再等一个选举超时。 选举期间集群无法处理写入:etcd 会把请求排队,等领导者产生后再处理。已经发给旧领导者但尚未提交的写入可能丢失,因为新领导者有权覆盖未提交的条目;已提交的写入永远不会丢。任期就是让这一切安全的时钟,也正是为什么一个任期偏高的孤立成员是隐患而不是趣闻。 仲裁数的算术,以及为什么奇数更好 仲裁数就是简单多数:floor(N/2) + 1,因此集群能承受的失效数是 N - quorum。三个成员承受一次失效、需要两个确认;四个成员同样只承受一次失效,却需要三个确认。五个成员承受两次失效、需要三个确认;六个成员同样承受两次,却需要四个。偶数成员数买不到额外韧性,只会让每次写入多付一个确认。 成员变更会立刻改变仲裁数,偶数集群的危险就在这里。文档里的例子是一个单成员集群:加一个成员会把仲裁数从 1 抬到 2,原来的领导者发现自己不再拥有活跃的仲裁,于是触发一次选举。把三成员扩到四成员会把仲裁数从 2 抬到 3,于是一次 2 对 2 的分区让两侧都选不出领导者——而三成员集群在这种情况下,多数一侧仍然完好。 learner 成员,以及两套机制的分工 learner 会接收领导者的日志但不投票,这正适合刚加入、还在追赶的节点:给三成员集群加一个 learner,仲裁数仍是 2,加入动作本身不会触发选举。learner 拒绝客户端的读写,领导者身份不能转移给它,它也永远不会自我提升。只有当它的日志追平之后,运维才能提升它;也只有到那时,它才开始计入仲裁数。 把选举与复制分开看,能解释大部分令人困惑的现象。领导者失效会让追加停止,直到选出新的领导者,哪怕每个成员本身都健康。一个健康的领导者如果联系不上跟随者,同样无法提交。而一个跟随者可以落后任意多,集群却照样提交,因为只需要多数拥有该条目——落后是复制问题,不是可用性问题,直到多数本身消失为止。 坏掉的样子 最经典的故障是丢掉仲裁。写入停止:多数成员失效的集群无法接受更多写入,只有多数重新可用它才能恢复,否则唯一的路是从备份做灾难恢复。陷阱在于读可能仍然能用。线性一致读是一次共识操作,会像写入一样失败;而串行化读由成员自己的状态直接提供,可能返回集群早已越过的数据——所以「它还能应答」并不是健康的证据。 第二种故障是被分区的少数派,它更安静。少数派选不出领导者,因此既没有仲裁也没有写入;由于成员变更同样需要多数,这里不会出现脑裂。在 Raft 层面,那些被隔离的成员会不断超时并抬高自己的任期,而多数一侧的任期原地不动;分区恢复时,更高的任期迫使领导者下台,于是重新跑一次选举。也就是说,分区恢复本身会带来一次短暂的写入中断。 ...