DDIA 第9章: 一貫性と合意(わかりやすく解説)
Tony Duong
5月 11, 2026 ・ 1 分
『データ指向アプリケーションデザイン』の第9章は、本書の中でも最難関として知られています。この章が扱うのは、一見シンプルに見える問い、「複数のコンピューターはどうやって何が真実かについて合意するのか?」 という問題です。この記事では、日常的な例えを使いながら、誰にでも理解できるようにこの章のアイデアを解説していきます。
大きな問題
会議でメモを取っている3人の友達を想像してみてください。お互いにメモを回し合うのですが、時々気が散ってしまったり、メモを落としてしまったり、同時に書き込んでしまうこともあります。そんな中で、会議の内容について全員が合意する1つの共有バージョンに、どうやってたどり着けるでしょうか?
これが分散システムの本質です。コンピューター(ノードと呼ばれます)は協力して動作しますが、ネットワークは遅く、メッセージは失われ、時計はずれていきます。何か問題が起きても合意できるようなルールが必要なのです。
一貫性 (Consistency): データはどれくらい「最新」であるべきか?
あるノードにデータを書き込んで、別のノードから読み取るとき、何が見えると期待しますか?保証の種類によって、答えは変わります。
結果整合性 (Eventual Consistency) — 最も弱い保証
「しばらく誰も書き込まなければ、いずれ全員が同じものを見るようになる。」
メッセージが同期するのに数秒かかるグループチャットを思い浮かべてください。1分間誰も何も入力しなければ、全員が追いつきます。しかし、その1分の間は、人によって違うものが見えているかもしれません。
これは高速でスケールしやすい一方で、混乱を招きます。何かを書き込んだ後にページを更新しても、自分の書き込みが見えないなんてこともあり得ます!
線形化可能性 (Linearizability) — 単一キーで最も強い保証
「システムは、データのコピーが1つしかなく、操作が一度に1つずつ起こっているかのように振る舞う。」
全員が交代で書き込む1冊のノートをイメージしてください。順序は明確で、何かを書き込めば、次に読む人は必ずそれを見ることができます。
これが欲しい理由: ロック、リーダー選出、ユーザー名のような「一意性」制約 — これらはすべて、線形化可能性がないと成り立ちません。
難しい理由: ノード間の協調が必要で、ネットワークに問題があると遅くて壊れやすくなります。
CAP定理(簡単に)
「Consistency(一貫性)、Availability(可用性)、Partition tolerance(分断耐性)の3つから2つを選べ」という話を聞いたことがあるかもしれません。DDIAではこの説明は誤解を招くと指摘されています。より正確な表現はこうです:
ネットワークが壊れたとき、選ばなければならない: 一貫性を保つ(一部のリクエストを拒否する)か、可用性を保つ(古いデータを返すリスクを負う)か。
それだけです。ネットワークは必ず壊れます。だから実際には、CP(分断時に一貫性を保つ)か、AP(分断時に可用性を保つ)かを選んでいるのです。
順序付け: 何が何よりも先に起きたか?
厳密な一貫性がなくても、順序は気になることがよくあります。アリスがメッセージを送ったのは、ボブが返信する前?それとも後?
因果順序 (Causal Order)
「AがBを引き起こしたなら、全員がAが先に起きたことに同意すべきだ。」
写真を投稿して、誰かがそれにコメントしたなら、全員が写真をコメントより先に見るべきです — そうでないと、コメントが突然どこからともなく現れたように見えてしまいます。
因果順序は線形化可能性より弱い保証です(無関係なイベントは順序付けません)が、多くの場合これで十分です。
全順序 (Total Order)
「すべてのイベントが、1つのタイムライン上にグローバルな位置を持つ。」
線形化可能性は全順序を与えてくれます。すべての書き込みに連番(1, 2, 3, …)を割り当てる単一リーダーのシステムも同じです。
Lampardタイムスタンプ
巧妙な仕組み: 各ノードがカウンターを持っています。メッセージを送るときに、そのカウンターをタグ付けします。メッセージを受け取ったときは、自分のカウンターを受け取った値よりも大きな値に更新します。これにより、中央のコーディネーターなしに、ノード間で一貫したイベント順序(必ずしもリアルタイムではないですが)に合意できるのです。
注意点: Lamportタイムスタンプは、すべてのメッセージが到着した後で順序を教えてくれます。リアルタイムで順序を判断する(例: 「このユーザー名は今この瞬間、すでに使われている?」)役には立ちません。
全順序ブロードキャスト (Total Order Broadcast)
これはより強力なプリミティブです: すべてのノードが、同じメッセージを同じ順序で受け取る。
これがあれば、基本的に何でも作れます — データベース、ロックサービス、リーダー選出など。しかし、これを作るには…
合意 (Consensus): 1つの値に合意する
合意はこの章の核心です。定義: 複数のノードが値を提案し、たとえ一部のノードがクラッシュしても、それらの値のうち1つに全員が合意しなければならない。
簡単そうに聞こえますが、そうではありません。冒頭の3人の友達を想像してください。ただし今度は、そのうち2人が会話の途中で寝てしまうかもしれず、メッセージが間違った順序で届くこともあります。それでも合意できるでしょうか?
2フェーズコミット (2PC) — 素朴な試み
分散トランザクションで使われます。1つのノードがコーディネーター、他は参加者です。
- 準備フェーズ: コーディネーターが全員に「コミットできますか?」と尋ねる。
- コミットフェーズ: 全員がYesなら、コーディネーターが「コミット!」と言う。誰かがNoなら、「中止!」と言う。
問題: ステップ1の後、ステップ2の前にコーディネーターがクラッシュすると、参加者は身動きが取れなくなります。コミットすることは約束したけれど、実際にコミットすべきかどうかわかりません。彼らはロックを抱えたまま、永遠に待ち続けることになります。
2PCは技術的には合意プロトコルですが、壊れやすいものです — 障害耐性がないのです。
本物の合意アルゴリズム
Paxos、Raft、Zab、Viewstamped Replication — これらが有名な障害耐性のある合意アルゴリズムです。共通の形があります:
- リーダー(1つのノード)を選び、リーダーが値を提案する。
- リーダーがコミット前に過半数のノードから同意を得る。
- リーダーがクラッシュしたら、他のノードが新しいリーダーを選出する。
「過半数」という仕組みが鍵です: ノードの半分以上が生きていて通信できる限り、システムは前進します。5ノードあれば、2ノード失っても機能し続けます。
これらのアルゴリズムを正しく実装するのは難しいです(特にPaxosは頭がこんがらがるという評判があります)。ほとんどの人はゼロから書くことはせず、テスト済みの実装を使います。
合意の限界
合意は強力ですが、コストがかかります:
- 過半数が必要 — つまり、ダウンしているノードが多すぎると止まります。
- 遅い — どんな決定にも複数回のネットワーク往復が必要です。
- ノードが正直である(ビザンチン障害がない)ことを前提とします。悪意のあるノードを扱える別のアルゴリズム(PBFTなど)もありますが、さらに遅くなります。
だから私たちは、すべての操作に合意を使うわけではないのです。本当に重要な決定にだけ、控えめに使います。
協調サービス: ZooKeeper、etcd、Consul
実際には、自分で合意を実行することは滅多にありません。代わりに、ZooKeeperやetcdを動かす小さなクラスター(通常3または5ノード)を使います。これらは以下を提供します:
- リーダー選出: 「今、誰が責任者?」
- サービスディスカバリ: 「サービスXはどこにある?」
- 分散ロック: 「私がこれをやっているから、他は触らないで。」
- 設定情報: 共有状態の小さな断片。
メインのアプリケーションサーバーは何百台あってもかまいませんが、難しい決定はすべて小さな合意クラスターに委ねます。Kafka、Kubernetes、多くのデータベースなどのシステムは、こうやって協調状態を保っているのです。
重要なポイント
- 一貫性はスペクトルです。保証が強いほど、システムは遅く、壊れやすくなります。
- 線形化可能性 = 「単一のコピーのように振る舞う」。便利だがコストが高い。
- 因果一貫性で十分な場合が多く、ずっと安価です。
- Lamportタイムスタンプは中央時計なしにイベントを順序付けますが、事後にしか機能しません。
- 全順序ブロードキャストは合意と等価です — 互いに作り合えます。
- 2PCはコーディネーターが単一障害点なので脆弱です。
- Paxos/Raftは障害耐性のある合意アルゴリズムで、前進するには過半数が必要です。
- ZooKeeper/etcdは合意をサービスとしてパッケージ化してくれるので、自分で実装する必要がありません。
- より深い教訓: 強い合意には協調が必要で、協調にはコストがかかる。 エンジニアは、どの決定がそのコストを払う価値があるかを判断しながらシステムを設計します。
🌐 Claudeによる翻訳