etcdとMVCC・watchの内部
なぜetcdの容量が推奨8GiBで頭打ちになり、watchは取りこぼさないのか。リビジョンによるMVCCとwatchストリーム、compactionの原理を押さえれば、Kubernetesの真実の源としての制約と運用勘所が腑に落ちる。
- 1.etcdはRaftで複製されるKVS。書き込みはまずRaftログに合意され、確定後に各ノードのローカルストア(boltdbのB+ツリー)へ適用される。読み取りは線形化可能性を得るためにReadIndexでリーダーの確定点を確認する。
- 2.MVCCの中心はクラスタ単位で単調増加する64ビットのrevision。各書き込みが新リビジョンを刻み、キーは(key, revision)で版管理される。watchは指定リビジョン以降のイベントを順序通り・取りこぼしなく配信でき、これがコントローラの再同期を支える。
- 3.古い版はcompactionで回収しないと無限に肥大化する。compaction済みリビジョンより前を要求したwatchはErrCompactedで失敗し、再listが必要。etcdのDB容量は既定2GiB・推奨上限8GiBで、Kubernetesの単一の真実の源としての規模制約を生む。
なぜetcdの内部を原理から見るのか
etcdは「Kubernetesの設定を入れる箱」として語られがちですが、実体はRaftで複製される強整合なKey-Valueストアであり、その上にリビジョン番号によるMVCCとwatchストリームという2つの仕組みが乗っています。この3層——合意層・ストレージ層・監視層——を分けて理解すると、「なぜwatchはイベントを取りこぼさないのか」「なぜcompactionを止めるとクラスタが壊れるのか」「なぜ大規模クラスタでetcdが先に音を上げるのか」が、運用の経験則ではなく原理として説明できます。
本記事は (1) Raftによる複製と線形化可能読み取り、(2) revisionを軸にしたMVCCストレージ、(3) watchとcompaction、の順に積み上げます。各層は独立しており、上の層は下の層が提供する保証(確定順序・版の不変性)に依存します。
第1層:Raftで複製されるKVS
etcdの各書き込みは、いきなりディスクのKey-Valueには書かれません。まずRaftログのエントリとして提案され、過半数(quorum)のノードが永続化して初めて「確定(commit)」します。確定したエントリは各ノードの状態機械——ローカルのMVCCストア——へ順番に適用(apply)されます。
クライアント書き込み (Put)
│
▼
リーダーが Raft ログに提案 ──► フォロワへ複製
│ │
│ 過半数が fsync で永続化(確定 commit)
▼ ▼
各ノードがログを順に apply ──► ローカル MVCC ストアへ反映
ここで重要なのは、Raftログの順序がそのまま全ノードの適用順序になる点です。全ノードが同じ列を同じ順で適用するので、状態が一意に揃います。Raft自体の系譜と「強いリーダー+任期+過半数確定」の骨格は合意アルゴリズムの系統と派生で扱った通りで、etcdはそのCFT・部分同期・リーダーありの典型実装です。
線形化可能読み取りとReadIndex
書き込みが過半数確定で線形化可能なのは直感的ですが、読み取りも油断できません。古いリーダーがネットワーク分断で孤立し、まだ自分がリーダーだと思い込んでいる場合、そのノードへの読み取りは確定前の古い値を返しかねません。これはリーダー選出とスプリットブレインで言うところの「二重リーダー」由来の危険です。
etcdは既定で線形化可能(linearizable)読み取りを提供し、これを安価に実現するのがReadIndexです。
# ReadIndex 読み取りの流れ
1. 現在の commit index を記録(= readIndex)
2. リーダーはハートビートを送り、過半数から応答を得て
「今もリーダーである」ことを確認(分断なら確認できず失敗)
3. applied index が readIndex に追いつくまで待つ
4. 追いついたら、その時点のローカルストアから読む
ログを丸ごと書き込まずに「今リーダーか」をハートビート1往復で確認するため、フルRaftラウンドより軽い。なおSerializableオプションを使うと過半数確認を省いてローカルから即読みできますが、その代わり古い値を読む可能性を受け入れることになります。整合性と速度のこのトレードオフの位置づけは整合性モデルの線形化可能性と直列化可能性の議論に対応します。
第2層:revisionで動くMVCC
ここがetcdの心臓部です。etcdは値を上書きで潰さず、多版型同時実行制御(MVCC, Multi-Version Concurrency Control)で版を積み上げます。鍵となるのがrevisionという64ビットの単調増加カウンタです。
| 概念 | 意味 | 性質 |
|---|---|---|
| revision | クラスタ全体で1つの論理時計。書き込みトランザクションごとに +1 | クラスタ単位で単調増加。キーに依存しない全順序 |
| create_revision | そのキーが作成された時点のrevision | 削除→再作成で変わる。存在判定に使える |
| mod_revision | そのキーが最後に更新されたrevision | 楽観ロック(CAS)の比較対象 |
| version | そのキー個別の更新回数カウンタ | キーごとに1から増える。revisionとは別物 |
revisionはクラスタで唯一の論理クロックです。複数キーへの書き込みであっても、各書き込みトランザクションが1つのrevisionを刻むため、全書き込みに全順序が付きます。これは論理クロックのLamportクロックに近い役割で、「どちらが先に起きたか」をクラスタ全体で曖昧さなく決めます。
キーは(key, revision)で版管理される
物理ストレージはboltdb(B+ツリー)で、論理キーをそのままキーにはしません。(main revision, sub revision)を符号化したバイト列を物理キーとし、値に「ユーザのキー名・値・各種revision・lease情報」を詰めます。
# 概念的なレイアウト(boltdb 内)
物理キー: encode(main_rev, sub_rev) → 値: {user_key, value, create_rev, mod_rev, version, lease}
# 別途、インメモリの索引(treeIndex, B-tree)
user_key "foo" → [rev1, rev3, rev7, ...] # そのキーが書かれた revision 列
読み取りの流れはこうです。Get("foo")は、まずインメモリのtreeIndexでfooの最新(または指定)revisionを引き、得られた物理キーでboltdbを引きます。Get("foo", rev=5)のような過去の版の読み取りも、treeIndexで「revision 5以前の最大revision」を選べば同じ仕組みで取れます。これがスナップショット読み取りを可能にし、トランザクション(Txn)の比較条件(mod_revision == Xなら更新、といったCAS)の基盤にもなります。
上書き・削除をしても古い物理キーは即座には消えません。削除は「tombstone(墓標)」という特殊な版を新revisionとして追加するだけです。つまり放置すれば(key, revision)エントリは単調に増え続け、boltdbのファイルは肥大化します。これを刈り取るのが次に述べるcompactionです。
第3層:watchとcompaction
watchはなぜ取りこぼさないのか
watchは「あるキー(または範囲)に、あるrevision以降に起きた変更を、起きた順に配信せよ」という要求です。revisionが全順序の論理時計だからこそ、これが厳密に定義できます。
# watch の本質
Watch(key="/registry/pods/", start_revision=R)
→ revision R 以降にこの範囲で起きた Put/Delete を
revision 昇順・取りこぼしなしで配信し続ける
クライアント(たとえばKubernetesのコントローラ)は、start_revisionを指定して切断地点から再開できます。listで全件と現在のrevision Rを取得し、watch(start_revision=R+1)で続きを購読する——この「list-then-watch」がコントローラの基本動作です。revisionが連続した整数なので、配信に穴がないことをクライアント側で検証でき、KubernetesのresourceVersionはこのetcd revisionに対応します。
複数ノードを跨いでも順序が壊れないのは、第1層のRaftが全ノードで同一の適用順序を保証するからです。watchストリームの順序保証は、突き詰めればRaftログの全順序に由来します。
compaction:版の墓場を片づける
放置すれば版は無限に積もるので、compactionで「指定revisionより前の歴史」を物理的に回収します。
| 操作 | 対象 | 効果 |
|---|---|---|
| compaction | 指定revision未満の古い版・tombstone | 歴史を削除。現在値は残すが過去スナップショットは失われる |
| defrag(デフラグ) | boltdbファイルの空き領域 | compaction後に空いた領域をOSへ返し、ファイルを縮小 |
ここにwatchとの緊張関係があります。compactionでrevision C未満を消した後に、watch(start_revision=R)(ただしRがC未満)を要求すると、その歴史はもう存在しません。etcdはErrCompactedを返してwatchを失敗させます。
# よくある事故と回復
compact(rev=1000) 実行 → revision 1000 未満は消滅
その後 watch(start_revision=800) → ErrCompacted(800 の歴史は無い)
回復: 再 list で現在の revision を取り直し、そこから watch し直す(再同期)
Kubernetesでコントローラが長時間切断されたあと"too old resource version"で再listを強いられるのは、まさにこのErrCompactedです。compaction間隔が短すぎると切断耐性が下がり、長すぎる(あるいは止める)とDBが肥大化する——この調整がetcd運用の勘所になります。
defragはboltdbを一時的にロックするため、その間そのノードは読み書きに応答できません。クラスタ全体で同時に実行するとquorumを失います。1ノードずつ順番に行うのが鉄則です。compactionは歴史を消すだけでファイルは縮まないため、容量を実際に取り戻すにはdefragまでが必要です。
Kubernetesの「単一の真実の源」としての制約
Kubernetesはすべての宣言的状態(Pod・Service・Secret等)をetcdに置き、etcd 1クラスタが文字どおり真実の源です。この設計は強整合と取りこぼしなきwatchという利点を生む一方、明確な上限を課します。
| 制約 | 原因(原理) | 実務上の帰結 |
|---|---|---|
| DB容量 既定2GiB/推奨上限8GiB | quota-backend-bytesの既定は2GiB。boltdbのmmapと安定運用上、8GiB超は非推奨 | オブジェクト数・履歴の上限。超過でクラスタが読み取り専用化(NOSPACE alarm) |
| 書き込みスループット | 全書き込みがRaftの過半数fsyncを通る | ディスク(特にfsyncレイテンシ)とネットワークが律速。SSD/低RTT必須 |
| watchファンアウト | 全APIサーバ・コントローラがwatchを張る | 大量watcherへの配信がCPU・帯域を消費。大規模ほど効く |
| compaction依存 | MVCCで版が無限に積もる | compaction/defrag運用を怠ると肥大化→quota到達→停止 |
つまりKubernetesのスケールは、しばしばetcdが先に頭打ちになります。etcdに大きなオブジェクトや高頻度更新を大量に流す設計(巨大なConfigMap、頻繁に書き換わるアノテーション、Event濫造など)は、RaftのfsyncパスとMVCCの版蓄積を直撃します。これはマイクロサービスで各サービスがKubernetes APIへ無頓着に書き込むと、共有etcdがボトルネック化する典型でもあります。
・etcdはRaftで複製されるKVS。書き込みは過半数のfsync確定→ローカルapply。線形化可能読み取りはReadIndexで実現。
・MVCCの軸はクラスタ単位で単調増加するrevision。キーは(key, revision)で版管理され、過去スナップショットを読める。
・watchはstart_revision以降を順序通り・取りこぼしなく配信。KubernetesのresourceVersion=etcd revision。
・compactionで古い版を回収しないと肥大化。compaction済み未満のwatchはErrCompacted→再list。DB容量は既定2GiB・推奨上限8GiB。
まとめ
- etcdは3層構造。Raft合意層が全順序の確定を、MVCCストレージ層がrevisionによる版管理を、watch層が順序保証付きの変更通知を担う。上の層は下の層の保証に依存する。
- 書き込みは過半数のfsyncで確定してからローカル適用され、線形化可能読み取りはReadIndexでリーダー性を確認する。
Serializableは速いが古い値を読みうる。 - revisionはクラスタ唯一の論理時計であり、(key, revision)の版管理・スナップショット読み取り・CAS・watchの順序保証のすべてがこの単調増加カウンタに乗る。
- 版は放置すれば無限に積もる。compactionで歴史を回収し、defragでファイルを縮める。compaction済み未満のwatchは
ErrCompactedで失敗し、再listによる再同期を要する。 - Kubernetesはetcdを単一の真実の源とするため、DB容量(既定2GiB・推奨上限8GiB)・fsync律速の書き込み・watchファンアウトがスケール上限を作る。大きなオブジェクトや高頻度更新はetcdを直撃する。
DevOps/インフラ Article
etcdとMVCC・watchの内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
etcd
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
MVCCの中心はクラスタ単位で単調増加する64ビットのrevision。各書き込みが新リビジョンを刻み、キーは(key, revision)で版管理される。watchは指定リビジョン以降のイベントを順序通り・取りこぼしなく配信でき、これがコントローラの再同期を支える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「etcd / Kubernetes」に近いか確認する。
- 強みである「etcdはRaftで複製されるKVS。書き込みはまずRaftログに合意され、確定後に各ノードのローカルストア(boltdbのB+ツリー)へ適用される。読み取りは線形化可能性を得るためにReadIndexでリーダーの確定点を確認する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。