<?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>Consensus · XB Field Notes</title><link>https://hk.xbcatm.com/tags/consensus/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/consensus/index.xml" rel="self" type="application/rss+xml"/><item><title>etcd elects a leader so writes happen in order</title><link>https://hk.xbcatm.com/posts/etcd-leader-election-raft/</link><pubDate>Thu, 09 Mar 2028 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/etcd-leader-election-raft/</guid><description>The Raft algorithm behind etcd picks one leader, and every write must pass through it for the cluster to agree.</description><content:encoded>&lt;p&gt;etcd uses Raft to order changes to its replicated state. Replicas can temporarily lag; consensus does not mean every member holds identical bytes at every instant. A leader coordinates replication, while the voting majority determines progress. Keep election, commitment, local application, and client acknowledgment separate when diagnosing whether an operation succeeded.&lt;/p&gt;
&lt;h2 id="one-leader-per-term-with-voting-restrictions"&gt;One leader per term, with voting restrictions&lt;/h2&gt;
&lt;p&gt;A candidate needs a majority of votes, and each voter grants at most one vote in a term. Log freshness also matters: a newer term alone does not make an outdated candidate eligible for every vote. Randomized election timeouts reduce repeated split votes. Terms order leadership attempts; they do not measure elapsed time or directly identify a network partition.&lt;/p&gt;
&lt;p&gt;For an entry from its current term, a leader can advance commitment after replication to a majority. Earlier-term entries become committed indirectly as that current-term entry commits; merely counting old copies is not the complete Raft rule. Within the protocol&amp;rsquo;s storage and failure assumptions, later leaders preserve committed entries. Simultaneous loss of persistent data or restoring an old backup is a different disaster-recovery problem.&lt;/p&gt;
&lt;p&gt;An election interrupts progress until a leader is established. Client requests can wait, time out, or fail according to their deadlines and connectivity. A timeout does not establish that a proposed operation never committed. Applications need an appropriate retry or transaction design rather than interpreting every failed response as permission to repeat a non-idempotent effect.&lt;/p&gt;
&lt;h2 id="count-voters-rather-than-processes"&gt;Count voters rather than processes&lt;/h2&gt;
&lt;p&gt;A voting configuration of N members needs floor(N/2)+1 for a majority. Three voters tolerate one unavailable member; four still tolerate one but need three participants. Five and six each tolerate two. An even count does not add another tolerated failure over the preceding odd count, although temporary even configurations can occur during controlled membership changes.&lt;/p&gt;
&lt;p&gt;A learner receives replication but does not vote. Adding it leaves the voting quorum unchanged; promotion changes that count after catch-up checks. This avoids immediately depending on an empty new replica for voting, but catch-up traffic still consumes resources and can affect availability. An unchanged quorum is not a guarantee that adding a learner cannot contribute to timeouts or an election.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Without a reachable majority, the cluster cannot make normal write progress. Serializable reads can use one member&amp;rsquo;s local state and may return stale values. Linearizable reads need coordination that establishes current state; they are not simply local reads with a nicer label. They also need not append a new log entry for every read, so calling all reads replicated writes obscures the implementation.&lt;/p&gt;
&lt;p&gt;The minority of a network partition cannot independently commit a competing history. Recovery behavior depends on the actual configuration. etcd 3.5 enables pre-vote by default to reduce disruption when a disconnected member rejoins. A healed partition therefore does not inevitably force a higher-term election and write outage. Inspect configuration, timing, and logs instead of asserting that every isolated member must have raised its term.&lt;/p&gt;
&lt;p&gt;Losing a follower may leave enough voters to continue, but lag still reduces redundancy and affects local reads. Do not dismiss it until the majority disappears. Likewise, a failed endpoint probe can reflect client routing, TLS, permissions, timeouts, or an active alarm rather than a dead voter. Client reachability and peer-to-peer quorum connectivity are different observations.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-etcd-leader-election-raft.detail.en.svg" alt="A schematic current-term entry replicated to two of three voters can advance commitment. A learner copies data but adds no vote. Commitment, local application and client acknowledgment differ; a timeout does not prove no commitment." width="560" height="2601"&gt;
&lt;figcaption&gt;Replication, then commitment. Source-based mechanism illustration. Model values and source-reported observations are not a new runtime test.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this Python 3 arithmetic fixture without contacting a cluster. It prints voting thresholds and checks the learner distinction; it does not implement election or replication:&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;The output gives quorums one, two, two, three, three, four for one through six voters. A two-plus-two split of four voters has no majority. Three voters plus one learner still require two votes. PASS establishes these calculations only, not the behavior of a live cluster under failure.&lt;/p&gt;
&lt;p&gt;The original operational example below is retained, not rerun. These private addresses and the &lt;code&gt;/registry/&lt;/code&gt; prefix are placeholders: adapt them only to an authorized test cluster, with its TLS/authentication settings and non-sensitive fixture keys. No authenticated cluster or partition test was executed in this review:&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;The first command queries only one endpoint, so its table cannot prove that the cluster has exactly one leader. Collect status from every member before comparing leader identity, terms, and applied-index progress; samples need not be simultaneous. The final query intentionally uses local serializable semantics. &lt;code&gt;head&lt;/code&gt; limits displayed lines, not the server-side range read. Success does not certify quorum health. Preserve probe errors and avoid changing membership merely to make a table look healthy.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/op-guide/failures/"&gt;etcd failure modes&lt;/a&gt;, leader loss, majority loss, and partitions, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/op-guide/runtime-configuration/"&gt;Runtime reconfiguration&lt;/a&gt;, voting membership and learner promotion, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/learning/design-learner/"&gt;etcd learner design&lt;/a&gt;, non-voting replication and catch-up load, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://etcd.io/docs/v3.5/learning/api_guarantees/"&gt;etcd API guarantees&lt;/a&gt;, linearizable and serializable operations, checked 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 configuration&lt;/a&gt;, pre-vote enabled by default, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raft.github.io/raft.pdf"&gt;Raft paper&lt;/a&gt;, sections 5.2 and 5.4 on voting and current-term commitment, checked 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 endpoint checks&lt;/a&gt;, health read and alarm checks, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>