1. 分散システムとは何か
複数のコンピュータが協調して、利用者からは1つのシステムに見えるように動くもの——これが分散システムです。
Googleで検索するとき、あなたは「1つのGoogle」を使っている感覚ですが、実際には世界中の何十万台ものサーバーが裏で動いています。この「実態は多数、見た目は1つ」という状態を作るのが分散システムの技術です。
Leslie Lamport(この分野の巨人)の有名な定義がこの本質を突いています。曰く、分散システムとは「あなたが存在すら知らなかったコンピュータが壊れたせいで、自分のコンピュータが使えなくなる」システムのことである、と。つまり見えない依存関係と、避けられない部分的な故障が本質だということです。
身近な例
| システム | 何を分散しているか |
|---|---|
| Webサービス(Amazon等) | リクエスト処理、商品データ、在庫管理 |
| 銀行のATM網 | 口座残高の照会・更新 |
| メッセージングアプリ | メッセージの配送と保存 |
| ブロックチェーン | 台帳そのもの(信頼できる中央管理者なし) |
2. なぜ分散させるのか
1台の高性能なマシンで済むならその方が圧倒的に楽です。それでも分散させるのは、次の理由があるからです。
① スケーラビリティ
- 垂直スケール(スケールアップ):1台のマシンをより強力にする。簡単だが物理的な上限があり、高価。
- 水平スケール(スケールアウト):安いマシンを何台も並べる。理論上は上限がないが、分散の難しさを引き受けることになる。
② 可用性(Availability)
1台が壊れてもサービスが止まらないようにする。「壊れないマシンを作る」のではなく「壊れることを前提に、全体は生き残る設計をする」という発想の転換です。
③ 地理的な分散(レイテンシ削減)
光の速度は有限です。東京〜ニューヨーク間は往復で最低でも約130msかかります。ユーザーの近くにサーバーを置くことでしか解決できません。
④ 組織のスケール
100人のエンジニアが1つの巨大なコードベースを触ると衝突だらけになります。サービスを分割することで、チームが独立してデプロイできるようになります(コンウェイの法則への対応)。
3. 分散システムが難しい理由
「分散コンピューティングの8つの誤謬」
1994年にSun Microsystemsのエンジニアがまとめた、初心者が必ず踏む地雷のリストです。以下はすべて「間違い」です。
- ネットワークは信頼できる
- レイテンシはゼロである
- 帯域は無限である
- ネットワークは安全である
- ネットワーク構成は変わらない
- 管理者は1人である
- 転送コストはゼロである
- ネットワークは均質である
最大の敵:「部分的な故障」
単体のプログラムなら、関数は「成功」か「失敗」かのどちらかです。しかしネットワーク越しの呼び出しには第3の状態があります。応答が来ないとき、何が起きたのか呼び出し側には原理的に区別できません。
- ネットワークで消えて相手に届かなかった(処理されていない)
- 相手が処理した後にクラッシュした(処理されている)
- 相手は処理して応答を返したが、応答が消えた(処理されている)
- 相手はまだ処理中で、遅いだけ(これから処理される)
「送金しました」の応答が来なかったとき、もう一度送金してよいのか?——この一点だけで分散システムの難しさが体感できると思います。
4. 基礎となる構成要素
ノードとクラスタ
- ノード:システムを構成する1台のマシン(またはプロセス)
- クラスタ:協調する複数ノードの集まり
レプリケーション vs パーティショニング
| レプリケーション(複製) | パーティショニング(分割) | |
|---|---|---|
| やること | 同じデータを複数ノードにコピー | データを分割して別々のノードに配置 |
| 主な目的 | 可用性・読み取り性能 | 容量・書き込み性能 |
| 別名 | replica | シャーディング(sharding) |
実務では両方を組み合わせます。例えば「データを4分割し、各分割を3ノードに複製する」といった構成です。
通信の方式
- 同期(Synchronous):呼び出して応答を待つ。分かりやすいが、相手の遅さが自分の遅さになる。
- 非同期(Asynchronous):メッセージを投げて先に進む。疎結合になるが、状態の追跡が複雑になる。
代表的なプロトコル:REST/HTTP(汎用的で扱いやすい)、gRPC/RPC(高速・型安全。ただし「ローカル関数呼び出しのように見える」ことが罠になりやすい)、メッセージキュー(送信者と受信者を時間的に切り離す)。
5. 時間と順序の問題
物理時計は信用できない
各マシンの時計は微妙にずれます(クロックドリフト)。NTPで同期しても数msから数十msの誤差が残り、時にはうるう秒や設定ミスで時計が巻き戻ることすらあります。したがって「タイムスタンプが新しい方が後に起きた」と判断するロジックはバグの温床です。
論理時計(Logical Clock)
そこで「実際の時刻」ではなく「順序関係」だけを扱う仕組みが使われます。
Lamportクロック:各ノードがカウンタを持ち、イベントが起きたらカウンタを+1、メッセージ送信時に自分のカウンタを添付、受信時は 自分のカウンタ = max(自分, 受信値) + 1 とします。これにより「AがBの原因になり得る」なら A < B が保証されます(逆は成立しない点に注意)。
ベクタークロック:各ノードが全ノード分のカウンタ配列を持つ方式。Lamportクロックと違い、2つのイベントが「並行(どちらが先とも言えない)」であることを検出できます。この「並行」の検出が競合解決の基礎になります。
分散システムには「グローバルな現在時刻」も「イベントの唯一の正しい順序」も存在しない。あるのは因果関係だけ。
6. 一貫性(Consistency)
「データを複製したとき、どのノードを読んでも同じ値が見えるか?」という問題です。
主要な一貫性モデル(強い順)
- 線形化可能性(Linearizability / 強一貫性):複製が存在しないかのように振る舞う。書き込みが完了した瞬間から、すべての読み取りが新しい値を返す。最も分かりやすいが最も高コスト。
- 逐次一貫性(Sequential Consistency):全ノードが同じ順序でイベントを見るが、「リアルタイムでの最新」は保証しない。
- 因果一貫性(Causal Consistency):因果関係のある操作の順序だけを保証。「投稿 → その投稿へのコメント」の順序は守られるが、無関係な2つの投稿の順序は保証されない。実用上のバランスが良いモデル。
- 結果整合性(Eventual Consistency):「更新が止まれば、いずれ全ノードが同じ値に収束する」だけを保証。SNSの「いいね」の数などはこれで十分。
CAP定理
ネットワーク分断(Partition)が起きたとき、一貫性(Consistency)と可用性(Availability)は両立できない。
| 選択 | 挙動 | 例 | |
|---|---|---|---|
| CP | 一貫性優先 | 分断中はエラーを返す(止まる) | 銀行残高、在庫の確定処理 |
| AP | 可用性優先 | 分断中も応答するが古い値かもしれない | SNSのタイムライン、CDN |
よくある誤解:「CAPのうち2つを選ぶ」という説明は不正確です。ネットワーク分断は選べる選択肢ではなく必ず起きる現実です。したがって実際の選択は「分断が起きたときにCとAのどちらを捨てるか」の二択だけです。
PACELC定理
CAP定理をより実務的に拡張したもの。Partition時は Availability か Consistency か、Else(平常時)は Latency か Consistency か。つまり分断していない平常時ですら、強い一貫性を求めれば必ずレイテンシを払うことになる、という指摘です。日々の設計判断にはこちらの方が効いてきます。
7. 合意(Consensus)
複数ノードが「1つの値」について同意すること。リーダーの決定、設定の変更、トランザクションのコミット可否など、あらゆる場面の基盤です。
FLP不可能性
完全な非同期システムにおいて、1つでもノードが故障する可能性があるなら、常に合意に到達するアルゴリズムは存在しない。
絶望的に聞こえますが、実務ではタイムアウトを導入する(=部分同期モデルを仮定する)ことで回避しています。「理論上は永遠に決まらないケースがあるが、現実にはほぼ起きない」という妥協です。
2フェーズコミット(2PC)
コーディネータ → 全参加者: 「コミットできる?」(準備フェーズ)
全参加者 → コーディネータ: 「YES」or「NO」
コーディネータ → 全参加者: 「コミットせよ」or「中止せよ」(コミットフェーズ)
致命的な弱点:準備フェーズの後にコーディネータが落ちると、参加者はロックを握ったまま永久に待ち続けます(ブロッキング)。
PaxosとRaft
これらは過半数(quorum)の考え方で2PCの弱点を克服します。全員の同意ではなく「過半数の同意」で先に進むため、少数のノードが落ちても止まりません。
- Paxos:理論的に美しいが、実装が極めて難しいことで有名
- Raft:「理解しやすさ」を設計目標に作られた等価なアルゴリズム。現在の主流
Raftの仕組み(概要)
- リーダー選出:一定時間リーダーからの生存確認が来なければ、誰かが立候補し、過半数の票を得てリーダーになる
- ログ複製:すべての書き込みはリーダーを通り、フォロワーに複製される。過半数に書き込めた時点で「コミット済み」とする
- 安全性:任期(term)番号により、古いリーダーの復活を無害化する
なぜ過半数(N/2+1)なのか:どの2つの過半数集合も必ず1つ以上のノードを共有するためです。これにより新しいリーダーは必ず最新のデータを持つノードを含むことになり、矛盾した2つの決定が同時に成立しません。3ノードなら1台、5ノードなら2台の故障に耐えられます。
ビザンチン障害
ここまでは「ノードは止まるだけ」という前提(クラッシュ障害モデル)でした。しかし嘘をつくノードが混じる場合はさらに難しくなり、耐えるには全体の1/3未満に故障ノードを抑える必要があります(BFT)。ブロックチェーンが扱っているのはこの領域です。通常の社内システムではここまで考える必要はありません。
8. レプリケーションとパーティショニング
レプリケーションの3方式
① シングルリーダー(マスター・スレーブ)
書き込みは必ず1台のリーダーへ、読み取りはフォロワーからも可能。長所はシンプルで書き込みの競合が起きないこと。短所はリーダーが単一障害点であり、フォロワーの読み取りは古い可能性があること(レプリケーションラグ)。MySQLやPostgreSQLの標準構成がこれです。
② マルチリーダー
複数拠点それぞれに書き込み可能なリーダーを置く。地理分散に強く拠点ごとに低レイテンシですが、書き込み競合が必ず発生します。その解決策(後勝ち、マージ、CRDT等)を設計する必要があります。
③ リーダーレス(Dynamoスタイル)
クライアントが複数ノードに直接読み書きします。クォーラム:全レプリカ数 N、書き込み成功必要数 W、読み取り必要数 R としたとき、W + R > N なら読み取りは必ず最新の書き込みを含むノードに当たります(例:N=3, W=2, R=2)。CassandraやDynamoDBが該当します。
パーティショニングの方式
- キー範囲による分割:
A〜F,G〜Mのように分ける。範囲検索に強いが、偏り(ホットスポット)が起きやすい。 - ハッシュによる分割:キーのハッシュ値で分ける。均等に分散するが、範囲検索ができない。
コンシステントハッシュ:単純な hash(key) % ノード数 には致命的な問題があります。ノードが1台増減しただけでほぼ全データの配置がずれるのです。コンシステントハッシュはハッシュ空間を円環(リング)と見立て、キーとノードの両方をリング上に配置し、「キーから時計回りで最初に見つかったノード」を担当とします。これによりノード増減の影響がそのノードの周辺のみに限定されます。実際には「仮想ノード」を導入して偏りをさらに減らします。
9. 障害への対処
障害検知
基本はハートビート(定期的な生存確認)です。ただし前述の通り「応答がない」と「遅い」は区別できません。タイムアウトを短くすれば健全なノードを誤って切り離し、長くすれば障害検知が遅れます。この綱引きに完璧な答えはありません。
必須の防御パターン
① 冪等性(Idempotency)
同じ操作を何回実行しても結果が変わらない性質。分散システムではリトライが避けられないため、これが安全網になります。実装方法は、クライアントが一意のリクエストID(冪等キー)を生成して送り、サーバー側で処理済みIDを記録して重複を弾くというもの。決済APIが必ずこの仕組みを持っているのはこのためです。
② 指数バックオフ + ジッター
失敗したら 1秒 → 2秒 → 4秒 → 8秒 と待ち時間を延ばして再試行します。さらにランダムなゆらぎ(ジッター)を加えることが重要です。これがないと全クライアントが一斉に再試行して、障害から復旧しかけたサーバーを再び潰します(サンダリングハード問題)。
③ サーキットブレーカー
障害中のサービスを呼び続けても無駄にリソースを消費するだけです。失敗率が閾値を超えたら回路を「開く」=即座に失敗を返し、一定時間後に少しだけ試行して回復を確認します。
Closed(正常) --失敗が閾値超え--> Open(遮断)
Open --一定時間経過--> Half-Open(試験的に通す)
Half-Open --成功--> Closed / --失敗--> Open
④ バルクヘッド(隔壁)
船の防水区画に由来。サービスAへの呼び出し用スレッドプールとサービスB用を分離しておけば、Aの障害がB向けのリソースまで食い潰すことはありません。
⑤ タイムアウトとデッドライン伝播
すべての外部呼び出しにタイムアウトを設定するのは大前提です。さらにリクエストの「締め切り時刻」を下流サービスまで引き継ぐと、すでに諦められた処理を無駄に続けることを防げます。
⑥ 縮退運転(Graceful Degradation)
推薦機能が落ちたら「おすすめ」欄を非表示にして商品ページ自体は表示する、といった設計。全か無かにしないことが可用性を大きく高めます。
カスケード障害に注意
1つのサービスの遅延 → 呼び出し元のスレッドが枯渇 → その呼び出し元も遅延 → …と連鎖してシステム全体が倒れる現象です。上記の防御パターンは、ほぼすべてこの連鎖を断ち切るために存在します。
10. 分散トランザクション
ACID と BASE
ACID(従来のDB):Atomicity(原子性)、Consistency(整合性)、Isolation(独立性)、Durability(永続性)。
BASE(分散寄り):Basically Available、Soft state、Eventually consistent。
分散環境で厳密なACIDを保とうとすると、2PCのブロッキング問題や大幅な性能劣化に直面します。そこで登場するのが次のパターンです。
Sagaパターン
長いトランザクションを小さなローカルトランザクションの連鎖に分解し、失敗したら補償トランザクション(打ち消し処理)を逆順に実行します。
① 航空券予約 → ② ホテル予約 → ③ レンタカー予約
↓ 失敗!
ホテル予約取消 ← レンタカー処理中止
↓
航空券予約取消
注意点:Sagaは原子性を「見かけ上」再現するだけで、途中経過が他から見えてしまいます(Isolationがない)。「予約したのに直後にキャンセルされた」という状態をユーザーにどう見せるかは業務設計の問題になります。
実装スタイルは2種類。コレオグラフィは各サービスがイベントを購読して自律的に動く方式(疎結合だが全体像が追いにくい)。オーケストレーションは中央のコーディネータが手順を指揮する方式(見通しが良いが中央への依存が生まれる)。
Outboxパターン
「DBを更新し、かつメッセージを発行する」という処理はよくありますが、この2つは別システムなので原子的に実行できません。DB更新後にプロセスが落ちるとメッセージが失われます。
解決策は同じDBトランザクション内で、業務テーブルと「outboxテーブル」の両方に書き込むこと。その後、別プロセスがoutboxテーブルをポーリング(またはCDCで追跡)してメッセージブローカーに送信します。これで「DBは更新されたのにメッセージが出ない」事態を防げます。
Exactly-once の真実
「ちょうど1回だけ配信」は、ネットワーク越しでは厳密には実現不可能です。実務での正解は次の組み合わせです。
at-least-once(最低1回)配信 + 受信側の冪等な処理 = 実質的に exactly-once
Kafkaなどが「exactly-once」を謳う場合も、内部的にはこの組み合わせです。
11. メッセージングとストリーミング
メッセージキュー vs Pub/Sub
- キュー:1つのメッセージを1つのコンシューマが処理する(タスク分配向け)
- Pub/Sub:1つのメッセージを購読者全員が受け取る(イベント通知向け)
ログベースのブローカー(Kafkaなど)
メッセージを消費しても消さず、追記専用ログとして保持し続けるのが特徴です。各コンシューマは「自分がどこまで読んだか(オフセット)」を管理します。これにより、過去に遡って再処理でき(リプレイ)、新しいコンシューマを後から追加して全履歴を処理でき、同一パーティション内での順序が保証されます。
バックプレッシャー
生産者が消費者より速いとキューが無限に膨らみます。速度を制御する仕組み(バックプレッシャー)か、あふれた分を捨てる方針(ロードシェディング)を必ず決めておく必要があります。
デッドレターキュー(DLQ)
何度リトライしても処理できないメッセージ(いわゆる「毒メッセージ」)は専用のキューに退避させます。これがないと、1件の不正データがコンシューマを永久に詰まらせます。
12. アーキテクチャパターン
モノリス vs マイクロサービス
| モノリス | マイクロサービス | |
|---|---|---|
| デプロイ | 全体を一度に | サービス単位で独立 |
| 障害範囲 | 全体に波及しやすい | 隔離しやすい |
| 開発の初速 | 速い | 遅い(基盤整備が必要) |
| 運用の複雑さ | 低い | 非常に高い |
| トランザクション | DBのACIDで完結 | Sagaなどが必要 |
現実的な助言:いきなりマイクロサービスにするのはほぼ常に間違いです。モジュール化されたモノリスから始め、境界が明確になり、かつスケールや組織の都合で本当に必要になった部分だけを切り出すのが定石です。マイクロサービスは技術的な解決策というより、組織をスケールさせるための解決策だと理解してください。
支える基盤要素
- サービスディスカバリ:動的に増減するサービスの居場所を見つける仕組み
- ロードバランサ:リクエストを複数インスタンスに振り分ける
- APIゲートウェイ:認証、レート制限、ルーティングを一箇所に集約する入口
- サービスメッシュ:リトライやTLSなどの通信制御をアプリコードから分離し、サイドカープロキシに任せる
CQRS
Command Query Responsibility Segregation。書き込み用のモデルと読み取り用のモデルを分離します。読み取りが圧倒的に多いシステムで、読み取り側だけを独立して最適化・スケールできます。ただし2つのモデル間には必ずラグが生じます。
イベントソーシング
現在の状態ではなく、状態を変化させたイベントの列を正とする方式。「残高: 5000円」ではなく「入金3000、入金4000、出金2000」を保存し、現在の状態は再生して求めます。完全な監査ログが得られる一方、実装難易度は高めです。
キャッシュ
- キャッシュスタンピード:人気データのキャッシュが同時に期限切れになり、大量のリクエストが一斉にDBへ殺到する。対策は有効期限にランダム性を持たせる、再生成をロックで1つに絞るなど。
- 無効化(Invalidation):元データが更新されたときにキャッシュをどう捨てるか。「コンピュータサイエンスの2大難問の1つ」と言われる古典的難題です。
13. 観測性(Observability)
分散システムでは、問題が起きてから調べる手段を用意していないと何も分かりません。設計段階から組み込む必要があります。
3本柱
- ログ:個々のイベントの記録。構造化(JSON等)して集約基盤に集めること
- メトリクス:数値の時系列。リクエスト数、エラー率、レイテンシ、リソース使用率
- 分散トレーシング:1つのリクエストが複数サービスをどう流れたかを追跡。トレースIDをすべての呼び出しに伝播させることで実現します。マイクロサービスでは事実上必須
レイテンシは平均で見ない
平均レイテンシは役に立ちません。必ずパーセンタイル(p50 / p95 / p99)で見ます。p99が悪いということは「100人に1人がひどい体験をしている」ということで、そのユーザーはたいてい最も利用頻度の高い顧客です。
SLI / SLO / エラーバジェット
- SLI:実際に測る指標(例:成功リクエストの割合)
- SLO:目標値(例:99.9%)
- エラーバジェット:許容される失敗の量(99.9%なら月あたり約43分)
エラーバジェットが残っていれば積極的に機能をリリースし、使い切ったら安定化に専念する——という形で、開発速度と信頼性のトレードオフを数値で議論できるようにするのが狙いです。
14. 学習の進め方
段階別ロードマップ
ステップ1:直感を作る
- 「8つの誤謬」と「部分的故障」を腹落ちさせる
- CAP定理を、暗記ではなく「分断時に何を捨てるか」として理解する
ステップ2:手を動かす
- 単一DBのアプリに、リードレプリカとキャッシュを追加してみる
- わざとネットワークを遅延・遮断させて、何が壊れるか観察する(カオスエンジニアリングの入口)
- リトライ + 冪等キーを自分で実装してみる
ステップ3:深く潜る
- Raftの論文を読み、Raft可視化サイトでリーダー選出の動きを見る
- KafkaやRedis Clusterの設計ドキュメントを読む
定番の書籍・資料
- 『データ指向アプリケーションデザイン』(Martin Kleppmann) — この分野で最も評価の高い入門〜中級書。まずこれ一冊で構いません
- 『SREサイトリライアビリティエンジニアリング』(Google) — 運用と信頼性の観点。無料で公開されています
- 『マイクロサービスアーキテクチャ』(Sam Newman) — サービス分割の実践論
- Raft論文 “In Search of an Understandable Consensus Algorithm” — 論文としては非常に読みやすい部類
まとめ:押さえるべき5つの原則
- 故障は例外ではなく通常状態である。 壊れない設計ではなく、壊れても続く設計をする。
- ネットワークは信用できない。 タイムアウト、リトライ、冪等性はセットで考える。
- 完璧な一貫性は高くつく。 業務ごとに「どこまで古くて良いか」を決めるのが設計の本質。
- 分散は最後の手段。 単純な構成で足りるなら、それが最良の設計。
- 観測できないものは運用できない。 ログ・メトリクス・トレースは後付けではなく設計の一部。


