etcd は 1 台落ちても平気なのに、なぜ過半数を割ると書き込みが止まるのか?
Kubernetes のコントロールプレーンで障害訓練をしたことがあるなら、etcd を 1 台落としても kubectl は何事もなく動き続けたのに、もう 1 台落とした瞬間に kubectl apply がタイムアウトし始めた、という経験があるかもしれない。同じ「1 台減る」でも結果が全く違うのはなぜか。答えは etcd が使う Raft という合意アルゴリズムの、過半数だけを信じるという設計にある。
- Raft では Leader が全ての書き込みを受け付け、Follower に複製する。役割は Leader・Follower・Candidate(選挙中)の 3 つしかない
- 書き込みは「クラスタの過半数(クォーラム)に複製が届いた」時点で初めて確定(コミット)する。過半数を割ると新しい書き込みを確定できず、etcd は応答を止める
- 3 台構成なら 1 台の故障まで、5 台構成なら 2 台の故障まで許容できる。ノードを増やすほど頑丈になるとは限らず、過半数の計算に必要な台数が増えるだけの場合もある
複製するだけでは足りない
非同期のストリーミングレプリケーションでレプリカを増やす話ならすでに馴染みがあるかもしれない。しかしそれは「最新のデータが少し遅れて届く」ことを許容する設計であり、read-your-writes が崩れる代償と引き換えに書き込みの速さを守っている。etcd が保存しているのは Kubernetes の「どの Pod がどのノードで動いているべきか」というクラスタの唯一の正解であり、ここで古いデータを返すと kube-scheduler が既に削除された Pod を再スケジュールするような矛盾が起こりかねない。
では単純に「Leader が生きている限りその内容を信じる」設計にすればよいかというと、それも危険だ。ネットワーク分断が起きて旧 Leader が孤立しても、旧 Leader 自身は自分がまだ Leader だと思い込んだままクライアントの書き込みを受け付け続けてしまう。分断の向こう側で別の Leader が選ばれていれば、クラスタに 2 つの「正解」が同時に存在する split-brain 状態になる。Raft が過半数の合意を要求するのは、この矛盾を構造的に起こさせないためだ。
Leader・Follower・Candidate という三つの役割
Raft のノードは常にこの 3 つの役割のどれかを取る。Leader はクライアントからの全ての書き込みを受け付け、自分のログを Follower に複製する唯一の存在だ。Follower は普段 Leader の複製を受け取るだけの受動的な役割で、Leader は自分の生存を示すため一定間隔でハートビートを送る。etcd の既定値ではこの間隔は 100ms で、Follower は election timeout(既定 1000ms)の間ハートビートが届かなければ「Leader が死んだ」と判断し、自ら Candidate(立候補者)となって他のノードに投票を依頼する。
Node A(Leader) │ heartbeat(既定 100ms 毎) ├──▶ Node B(Follower) └──▶ Node C(Follower) Node A がクラッシュ → heartbeat 途絶 │ Node B: election timeout(既定 1000ms)超過 │ RequestVote を B→C へ送信 └──▶ Node C が Yes 投票 = 2/3 票(過半数)→ Node B が新 Leader
ここで election timeout を全ノードが同じ値に固定すると、複数の Follower が同時に立候補してしまい票が割れる(split vote)リスクが上がる。Raft の原論文はこの対策として、各ノードが election timeout をランダムな幅からずらして選ぶ手法を提案している。誰か 1 人が先に立候補すればその時点で他のノードの待機がリセットされるため、たいていは 1 回の選挙で決着する。
Raft のクォーラムは、株主総会の決議に近い。議長(Leader)が生きて議事を進めている間は誰も異議を挟まないが、議長からの連絡が一定時間途絶えると出席者の誰かが「次の議長を決めましょう」と挙手する。ただし新しい議長を選ぶにも、決議を通すにも、出席者の過半数の賛成が要る。会場が 2 つに分かれて連絡が取れなくなっても、過半数を確保できた側の会場でしか正式な決議は成立しない——だからこそ、片方の会場だけが暴走して矛盾した決定を下すことがない。
過半数(クォーラム)が全てを決める
n 台構成のクラスタでクォーラムは (n/2)+1 で計算される。3 台なら 2 台、5 台なら 3 台だ。etcd 公式ドキュメントの FAQ は、3 台構成なら 1 台の故障まで、5 台構成なら 2 台の故障まで許容できると説明している。ここで直感に反するのが、ノードを増やせば増やすほど頑丈になるとは限らないという点だ。4 台構成のクォーラムは 3 台であり、3 台構成(クォーラム 2)と同じ 1 台の故障しか許容できない。それでいて故障しうるノードは 1 台多いので、故障確率は 3 台構成より高くなる。だから公式ドキュメントは偶数台構成を避け、奇数台を推奨している。
クォーラムを割った状態、つまり過半数のノードが同時に落ちた状態になると、残ったノードだけでは新しい Leader を選ぶための票も、書き込みを確定させるための複製先も確保できなくなる。この状態の etcd は読み取り専用にすらならず、リーダーが不在のまま新規の書き込み・強い整合性を要する読み取りの両方がタイムアウトするまで応答を返さない。kubectl apply が固まって見えるのは、ここで API サーバーが etcd からの応答を待ち続けているからだ。
書き込みが「確定」するまで
Leader はクライアントから書き込みを受け取ると、まず自分のログに追記し、それを全 Follower に複製する。ここで Leader がクライアントに成功を返すのは、全 Follower からの ACK を待った後ではなく、過半数の Follower(クォーラムを構成するのに十分な数)からログを受け取った旨のACKが返ってきた時点だ。この「過半数に届いた」状態をコミットと呼び、コミット済みのログだけがクラスタの正式な履歴として扱われる。
1 台の Follower が一時的にネットワーク遅延で応答が遅くても、他の過半数が先に応答していればコミットは進む。つまり Leader は最も遅い 1 台を待つ必要がなく、応答の速い過半数だけを見ていればよい。裏を返せば、この「過半数」という基準こそが可用性と一貫性のトレードオフの境界線であり、境界を割った瞬間にクラスタは新しい書き込みを一切確定できなくなる。
マネージド Kubernetes(EKS/GKE/AKS)を使っていると etcd は普段見えないが、コントロールプレーンの API サーバーが突然応答不能になったり、大規模クラスタで kubectl のレイテンシが急に悪化したりするインシデントの裏で etcd のクォーラム喪失が起きていることがある。自前で etcd や、同じ Raft ベースの Kafka の KRaft コントローラ、CockroachDB、Consul などを運用する場合は、ノード数を奇数に保つこと、そしてクラスタを跨いだ増設・入れ替え作業中に一時的にクォーラムを割らないよう手順を組むことが直接の運用課題になる。
ローカルで単一ノードの etcd を起動し、etcdctl で Raft の状態(Leader かどうか・Raft term)を確認してみる。単一ノードでもクォーラムは 1(自分自身が過半数)なので、起動直後から自分が Leader になる。
docker run -d --name etcd-demo --rm \ -p 2379:2379 \ gcr.io/etcd-development/etcd:v3.6.0 \ /usr/local/bin/etcd \ --listen-client-urls http://0.0.0.0:2379 \ --advertise-client-urls http://127.0.0.1:2379 docker exec etcd-demo etcdctl \ --endpoints=http://127.0.0.1:2379 \ endpoint status -w table docker stop etcd-demo
出力の IS LEADER 列が true、RAFT TERM 列に選挙が行われた回数(起動直後なら通常 2)が表示されるはずだ。これがまさに Leader が自分自身に投票して過半数(1/1)を得て就任した結果であり、複数ノード構成でもこの列を見ればどのノードが現在の Leader かを外から判別できる。
- ノードを増やせば増やすほど耐障害性が上がる — 4 台構成のクォーラムは 3 台であり、3 台構成と同じ「1 台の故障」しか許容できないのに故障しうるノード数は増えている。奇数台構成でのみノードを増やす意味がある。
- Raft は複数ノードにデータをコピーする仕組みにすぎない — 単なる複製と違い、「過半数に届くまで確定しない」という制約があるからこそ、Leader が孤立しても矛盾した書き込みが 2 系統同時に成立しない。
- 過半数を割っても、少なくとも読み取りはできる — 強い整合性を保証する読み取りには現在の Leader からの応答が必要であり、クォーラムを割った etcd は新規の Leader を選出できないため読み取りも書き込みもタイムアウトする。
- Raft
- 分散システムのノード群が一貫したログの順序に合意するための合意アルゴリズム。
- クォーラム(過半数)
- n 台構成で (n/2)+1 台。書き込みの確定・Leader 選出の両方に必要な最低出席数。
- Leader / Follower / Candidate
- Raft におけるノードの 3 つの役割。書き込みを受け付けるのは Leader のみ。
- ハートビート
- Leader が自分の生存を Follower に伝える定期的な信号。etcd の既定は 100ms 間隔。
- election timeout
- Follower がハートビートを受け取れず Leader 不在と判断するまでの待機時間。etcd の既定は 1000ms。
- コミット
- ログエントリが過半数のノードに複製され、クラスタの正式な履歴として確定した状態。
- In Search of an Understandable Consensus Algorithm (Extended Version) — Ongaro と Ousterhout による Raft の原論文。Leader 選出とログ複製の安全性証明まで読める
- Frequently Asked Questions (FAQ) — etcd — なぜ奇数台構成を推奨するのか、クラスタサイズと耐障害性の対応表
- Configuration options — etcd — heartbeat-interval・election-timeout など Raft タイミングの設定項目と既定値