<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Raft · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/raft/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/raft/index.xml" rel="self" type="application/rss+xml"/><item><title>etcd 通过选举单一领导让写入有序</title><link>https://www.xbcatm.com/zh/posts/etcd-leader-election-raft/</link><pubDate>Thu, 09 Mar 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/etcd-leader-election-raft/</guid><description>etcd 背后的 Raft 算法只选一名领导，所有写入都必须经它之手，集群才能达成一致。</description><content:encoded>&lt;p&gt;etcd 使用 Raft 为复制状态的变化确定顺序。副本可以暂时落后，共识不表示每个成员在每一瞬间都保存相同字节。领导者协调复制，投票多数决定能否继续推进。判断操作是否成功时，应区分选举、提交、本地应用与客户端收到确认，不能把这些阶段压缩成一个含义不明的“已经写入”。&lt;/p&gt;
&lt;h2 id="每个任期的领导者与投票限制"&gt;每个任期的领导者与投票限制&lt;/h2&gt;
&lt;p&gt;候选人需要多数票，每位投票者在同一任期最多投一票。日志新旧也参与判断，任期更大并不意味着日志落后的候选人就能取得所有选票。随机选举超时可以减少反复平票。任期排列领导权尝试，不衡量经过的时间，也不能单独证明某个成员曾经发生网络分区。&lt;/p&gt;
&lt;p&gt;对于当前任期的条目，领导者复制到多数后可以推进提交；旧任期条目会随着当前任期条目提交而间接提交，不能只数旧条目已有多少副本就完整解释 Raft 规则。在协议的存储与故障假设内，后续领导者保留已提交条目。多数持久数据同时丢失，或恢复到旧备份，是另一类灾难恢复问题，不能用共识安全性代替备份。&lt;/p&gt;
&lt;p&gt;选举期间，进展会暂停到领导者建立。客户端请求可能等待、超时或失败，取决于截止时间和连接情况。超时不能证明提议从未提交。应用需要合适的重试或事务设计，不能把每次失败响应都解释成可以重复非幂等业务效果的许可。诊断时还应区分服务端是否提交与客户端是否知道结果。&lt;/p&gt;
&lt;h2 id="数投票者不是数进程"&gt;数投票者，不是数进程&lt;/h2&gt;
&lt;p&gt;包含 N 位投票者的配置，需要 floor(N/2)+1 形成多数。三位允许一位不可用，四位仍只允许一位，却需要三位参与；五位和六位都容忍两位失效。与前一个奇数相比，偶数配置没有增加可容忍失效数，不过受控成员变更期间可能暂时经过偶数配置。这个算术描述稳定投票配置，不意味着成员可以随意删改。&lt;/p&gt;
&lt;p&gt;learner 接收复制但不投票，因此加入它不会改变投票仲裁数；追赶检查通过后的提升才改变计数。这样避免立即依赖一份刚加入的空副本投票，但追赶流量仍消耗资源，可能影响可用性。仲裁数不变，不保证加入 learner 绝不引起超时或促成选举；容量、网络与磁盘负载仍需观察。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;缺少可达多数时，集群无法正常推进写入。serializable 读取可以使用某个成员的本地状态，因此可能返回旧值；linearizable 读取需要建立当前状态的协调，不能理解成只是给本地读取换了名字。它也不要求每次读都追加一个日志条目，所以把所有线性一致读直接称为复制写入，会掩盖实际实现和性能差异。&lt;/p&gt;
&lt;p&gt;网络分区中的少数派不能独立提交另一段冲突历史。分区恢复的具体行为取决于配置。etcd 3.5 默认启用 pre-vote，以减少失联成员重新加入时的扰动，因此恢复分区并不必然引发更高任期选举和写入中断。应检查配置、时序与日志，不能断言每个孤立成员都会不断提升正式任期，也不能仅凭某个较大值推导全部故障经过。&lt;/p&gt;
&lt;p&gt;丢失一个跟随者时，剩余投票者可能仍足够继续服务，但落后会减少冗余，也会影响本地读取。不能等到多数消失才重视它。端点探测失败同样可能来自客户端路由、TLS、权限、超时或活动告警，而不等于投票者已死亡。客户端可达性与成员之间的仲裁通信，是两种需要分别检查的观察。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的 Python 3 算术用例不联系任何集群。它打印投票门槛，并检查 learner 不参与计数的区别，不实现领导者选举或日志复制。用这个小实验先验证配置算术，再把结果与真实成员清单对应，避免把进程总数直接当成投票者数量。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;voters&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;voters&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;voters=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, quorum=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, tolerated=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Neither side of a 2+2 partition has a majority.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# A learner replicates data but is not included in the voting count.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;learners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PASS: adding a learner leaves the voting quorum unchanged&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一至六名投票者的输出仲裁数依次是一、二、二、三、三、四。四位投票者分成二加二时，两侧都不构成多数；三位投票者再加一位 learner，仍需要两票。PASS 只证明这些计算，不证明真实集群在故障中的行为，更不替代日志持久化和网络分区测试。&lt;/p&gt;
&lt;p&gt;下面保留原有运维示例，本轮没有重跑。这些私有地址和 &lt;code&gt;/registry/&lt;/code&gt; 前缀只是占位示例；只能在另行获准的测试集群中替换为相应端点、TLS 和认证设置，以及不敏感的测试键。本轮没有运行经过认证的集群或分区实验，不应把它们写成已经通过的集成验证。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;etcdctl --endpoints=https://10.0.1.11:2379 endpoint status --write-out=table
etcdctl --endpoints=https://10.0.1.11:2379,https://10.0.1.12:2379,https://10.0.1.13:2379 endpoint health --write-out=table
etcdctl --endpoints=https://10.0.1.11:2379 member list --write-out=table
etcdctl --endpoints=https://10.0.1.11:2379 --consistency=s get /registry/ --prefix --keys-only | head
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第一条命令只查询一个端点，其表格不能证明整个集群恰好有一个领导者。应从所有成员采集状态，再比较领导者身份、任期和已应用索引进展；采样也不一定同时发生。最后的查询有意使用本地 serializable 语义，&lt;code&gt;head&lt;/code&gt; 只限制展示行数，不限制服务端读取范围，成功不能证明仲裁健康。保留探测错误，不要为了让表格变绿就更改成员配置，恢复动作必须依据完整故障证据。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/op-guide/failures/"&gt;etcd 故障模式&lt;/a&gt;，领导者、多数丢失与网络分区，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/op-guide/runtime-configuration/"&gt;运行时重新配置&lt;/a&gt;，投票成员与 learner 提升，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/learning/design-learner/"&gt;etcd learner 设计&lt;/a&gt;，不投票的复制状态与追赶负载，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/learning/api_guarantees/"&gt;etcd API 保证&lt;/a&gt;，线性一致与 serializable 操作，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/op-guide/configuration/"&gt;etcd 3.5 配置&lt;/a&gt;，默认启用 pre-vote，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raft.github.io/raft.pdf"&gt;Raft 论文&lt;/a&gt;，第 5.2 与 5.4 节的投票和当前任期提交规则，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/etcd-io/etcd/v3.5.21/etcdctl/ctlv3/command/ep_command.go"&gt;etcdctl 3.5.21 端点检查&lt;/a&gt;，健康读取与告警检查，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>