マイクロサービスの実務
この部の 10 / 13 章 ・ 全体で 52 / 76 章 ・ 読了目安 45 分
- リトライが障害を悪化させる仕組みを説明できる
- 冪等性が必要な理由を説明できる
- トレースから遅延の原因を特定できる
1つのアプリケーションに、すべての機能が入っている—— これをモノリスと呼びます。そして、最初はこれが正解です。
では、なぜ分割する話が出てくるのか。規模が大きくなると、次が起きます。
□ 100人の開発者が、同じコードベースを触る
→ コンフリクトが頻発し、レビューが渋滞する
□ リリースが週1回に固定される
→ 1行の修正でも、全機能のテストが終わるまで出せない
□ 誰かの変更が原因で、全体が落ちる
→ 障害の影響範囲が「サービス全部」になる
□ 一部の機能だけスケールしたいのに、全体を増やすしかない
→ コストが跳ね上がる
□ 新しい言語や技術を試せない
→ すべてが1つの技術に縛られる
「チームの人数」と「変更の速度」が、技術ではなく組織の問題として詰まる—— これがマイクロサービスという発想が生まれた背景です。
そして分割すると、問題は消えるのではなく、場所が変わります。
モノリス: 1つの中が複雑になる(コードの複雑さ)
マイクロサービス: サービスとサービスの「あいだ」が複雑になる(分散の複雑さ)
この章で扱うのは、後者です。 そして分散の複雑さは、関数呼び出しでは絶対に起きなかった種類の問題を持ち込みます。
なぜ分割するのか、その代償
分けて得られるもの
- チームごとに独立してデプロイできる(他チームを待たなくていい)
- 障害の影響範囲を限定できる
- サービスごとに適した技術を選べる
分けて失うもの
| モノリスなら | マイクロサービスでは |
|---|---|
| 関数呼び出し(絶対に失敗しない) | ネットワーク越し(普通に失敗する) |
| デバッガで1つ追える | 複数のログを突き合わせる |
| 1つのトランザクションで整合性 | 分散トランザクション(非常に難しい) |
| 起動すれば全部動く | ローカルで全部動かすのが大変 |
「マイクロサービスが正しい」わけではありません。
小さなチームであれば、モノリスの方が速く、安全です。 分割は「チームが大きくなり、デプロイで衝突するようになった」時の解決策です。
既にマイクロサービスの現場に入ったなら、 なぜこの粒度で分かれているのかを先輩に聞いてみてください。 たいてい歴史的な理由があります。
前提: ネットワークは失敗する
分散システムを考える上での出発点です。
- パケットは失われる
- 相手は落ちているかもしれない
- 遅いだけかもしれない(落ちているのと区別できない)
- 応答が返らなくても、相手は処理を完了しているかもしれない
最後の項目が最も重要です。 「タイムアウトした = 失敗した」ではありません。
タイムアウト
すべての外部呼び出しにタイムアウトを設定してください。
設定しないと、相手が応答しない時にこちらのリソースが解放されず、 自分も道連れで落ちます。
上流ほど長く
ユーザー ──3秒──→ [API GW] ──2秒──→ [注文] ──1秒──→ [在庫]
呼ぶ側のタイムアウトは、呼ばれる側より長くします。逆にすると、
❌ [注文: 1秒] ──→ [在庫: 3秒]
在庫サービスが2秒かけて正常に処理を終えても、 注文側は1秒で諦めており、その処理は無駄になります。 しかも在庫側は「成功した」と記録しているかもしれません。
ユーザーに3秒で返したいなら、その3秒をどう配分するかを考えます。
API GW 3秒
├ 認証 0.2秒
├ 注文 2.0秒
│ └ 在庫 0.8秒
└ 整形 0.1秒
各サービスが好き勝手に設定すると、必ず矛盾します。 チーム間で数字を合わせる必要があります。
リトライ
失敗したら再試行する。単純に見えて、危険な機能です。
リトライしてよい条件
| 条件 | 理由 |
|---|---|
| 一時的な失敗 | UNAVAILABLE DEADLINE_EXCEEDED 503 429 |
| 冪等な操作 | 何度実行しても結果が同じ |
INVALID_ARGUMENT や 400 はリトライしても無駄です(何度送っても同じ結果)。
リトライストーム
新人が最も理解しておくべき失敗パターンです。
在庫サービスが遅くなる
↓
注文サービスがタイムアウトしてリトライ
↓
在庫サービスへのリクエストが 2倍・3倍に
↓
在庫サービスがさらに遅くなる
↓
もっとリトライされる
↓
完全に停止 → 全体が停止
弱っている相手を、善意のリトライで殺します。
正しいリトライ
// 危険: すぐに、何度も
for (let i = 0; i < 5; i++) {
try { return await call(); } catch { /* すぐ再試行 */ }
}
// 安全: 間隔を空け、ばらつかせ、回数を絞る
async function callWithRetry() {
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await call();
} catch (error) {
if (!isRetryable(error) || attempt === 2) throw error;
// 指数バックオフ + ジッター
const base = 100 * 2 ** attempt; // 100, 200, 400ms
const jitter = Math.random() * base; // ばらつかせる
await sleep(base + jitter);
}
}
}| 要素 | 目的 |
|---|---|
| 指数バックオフ | 待ち時間を倍々にして、相手に回復の余地を与える |
| ジッター(ゆらぎ) | 全クライアントが同時に再送するのを防ぐ |
| 回数制限 | 無限に粘らない |
各層が3回ずつリトライすると、3 × 3 × 3 = 27倍のリクエストになります。
GW(3回) → 注文(3回) → 在庫(3回)
リトライはどこか1箇所で行うのが原則です。 チームで「どの層がリトライするか」を決めてください。
冪等性
同じ操作を何回実行しても、結果が変わらないという性質です。
冪等なもの: 「残高を1000円にする」→ 何回やっても1000円
冪等でないもの: 「残高に1000円足す」→ やるたびに増える
英語では idempotent。読み方は「べきとう」です。
分散システムでは同じリクエストが複数回届くことが避けられないため、 この性質が設計の中心になります。 この教科書では、非同期処理とメッセージング・バッチとジョブ・ 間違えられない処理を作るでも繰り返し登場します。
リトライを安全にするための鍵です。
同じ操作を何回実行しても、結果が1回分と同じになる性質
GET /items/42 冪等(何回読んでも同じ)
DELETE /items/42 冪等(2回目は「もう無い」で同じ結果)
POST /orders 冪等でない ← 呼ぶたびに注文が増える
冪等キー
POST を冪等にするには、クライアントが一意なキーを付けます。
POST /payments
Idempotency-Key: 7f3c1e2a-...
{ "orderId": "42", "amount": 1200 }サーバー側の処理はこうなります。
1. このキーで処理済みか確認する
2. 済みなら、前回の結果をそのまま返す(再処理しない)
3. 未処理なら実行し、キーと結果を保存する
これで、ネットワークが不安定でも二重決済が起きません。
1. 注文APIを呼ぶ → タイムアウト
2. 「失敗したはず」と考えて、もう一度呼ぶ
3. 実は1回目も成功していた → 注文が2件できる
決済・注文・メール送信など、副作用のある操作では必ず冪等性を考えてください。 「タイムアウト = 失敗」ではない、が出発点です。
決済APIがタイムアウトしました。リトライする前に必要なことは?
サーキットブレーカー
相手が明らかに壊れている時、呼ぶのをやめる仕組みです。
Closed(通常) ──失敗が閾値を超えた──→ Open(遮断)
↑ │
│ 一定時間後
└──── 成功したら戻る ──── Half-Open(試しに1本流す)
Open の間は、相手を呼ばずに即座に失敗を返します。
意味があるのは2つです。
- 相手に回復の時間を与える(叩き続けない)
- 自分のリソースを守る(応答待ちで詰まらない)
遮断した時、何を返すかを決めておきます。
- キャッシュした古いデータを返す
- 「おすすめ」なら空リストを返して、その部分だけ非表示にする
- どうしても必要なら、正直にエラーを返す
一部が壊れても全体は動く(グレースフルデグラデーション)という設計です。 おすすめ機能が落ちただけで商品ページ全体が見られなくなる、 というのは避けたい状況です。
分散トレース
複数サービスにまたがるリクエストを追跡する仕組みです。
[trace: abc123]
├── API Gateway 5ms
│ └── 注文サービス 850ms ← 遅い
│ ├── ユーザー取得 20ms
│ ├── 在庫確認 30ms
│ └── 商品取得 780ms ← 犯人はここ
│ ├── 商品1 26ms
│ ├── 商品2 26ms
│ ├── ...(30回)
このウォーターフォールを見れば、どこで時間を使っているかが一目で分かります。 上の例は、商品取得が N+1 になっているパターンです(データベース)。
よく見るパターン
| 形 | 原因 |
|---|---|
| 同じスパンが大量に並ぶ | N+1。まとめて取得する |
| スパンが階段状に並ぶ | 直列実行。並列化できる |
| 特定のスパンだけ長い | そのサービスの問題 |
| スパンが途中で切れている | トレースの伝播漏れ(ctx を渡していない) |
Goの ctx の話がここに繋がります。
ctx を下流に渡さないと、トレース ID が伝わらず、
そこから先が別のトレースとして記録されます。
障害調査の時に「途中から追えない」となる原因です。
Go で ctx context.Context が第1引数にあるのは、これも理由の1つです。
構造化ログと相関 ID
トレースがなくても、相関 ID があればログを繋げられます。
{"level":"info","msg":"order created","trace_id":"abc123","order_id":"42","service":"order"}
{"level":"info","msg":"stock reserved","trace_id":"abc123","item_id":"7","service":"stock"}
{"level":"error","msg":"payment failed","trace_id":"abc123","service":"payment"}trace_id で検索すれば、1つのリクエストの全経路が一列に並びます。
これがないと、「10時5分ごろのエラー」を各サービスのログから 目視で突き合わせることになります。障害対応の速度が桁違いに変わります。
「今すぐは一致しないが、いずれ一致する」という考え方です。
強い整合性 書いた瞬間から、誰が読んでも同じ値が見える
結果整合性 少し遅れて反映される(数ミリ秒〜数秒)
分散したシステムでは、すべてを即座に一致させると遅く・脆くなります。 そこで「注文したのに、まだ履歴に出ない(3秒後には出る)」を許容します。
どこまで遅れてよいかは、業務の判断です(ドメインとは何か)。 残高照会は即時が必要でも、閲覧履歴は数分遅れて構いません。
データの整合性
マイクロサービスで最も難しい部分です。
注文サービス: 注文を作成した
在庫サービス: 在庫を減らそうとして失敗した
↓
注文だけ残ってしまう
1つの DB なら、トランザクションで両方なかったことにできます。 サービスが分かれていると、それができません。
現実的な対処
| 方法 | 内容 |
|---|---|
| Saga パターン | 失敗したら「打ち消す操作」を実行する(注文をキャンセルする) |
| 結果整合性を許容する | 一時的にずれることを許し、後で揃える |
| そもそも分割しない | 強い整合性が必要な処理は、同じサービスに置く |
「注文と在庫を別サービスにしたせいで整合性が保てない」なら、 そもそも分割の境界が間違っている可能性があります。
一緒に変更されるものは、一緒に置く。 これが分割の基本原則です。 技術で解決する前に、境界を疑ってください。
新人が最初に気をつけること
実務に入ってすぐ役立つ順に並べます。
ctxを必ず下流に渡す — キャンセルとトレースが伝わる- 外部呼び出しには必ずタイムアウト — 設定しないと道連れで落ちる
- ログに
trace_idを含める — 調査できるかどうかが決まる - リトライを足す前に相談する — ストームと二重実行のリスクがある
- 副作用のある操作は冪等性を考える — 「タイムアウト = 失敗」ではない
実務の落とし穴まとめ
- タイムアウトを設定しない — 相手の不調で自分も落ちる
- タイムアウトが上流 < 下流 — 無駄な処理が走る
- 各層でリトライ — 27倍のリクエストになる
- バックオフなしのリトライ — リトライストームで相手を殺す
- 冪等性を考えずにリトライ — 二重決済
ctxを渡さない — トレースが切れ、キャンセルも効かない
まとめ
- マイクロサービスの難しさは サービスの「あいだ」 にある
- ネットワークは失敗する。「タイムアウト = 失敗」ではない
- すべての外部呼び出しにタイムアウト。上流ほど長く
- リトライは一時的な失敗 × 冪等な操作の時だけ。 指数バックオフ + ジッター + 回数制限、そして1箇所だけ
- 副作用のある操作には冪等キー
- 壊れた相手はサーキットブレーカーで切り離し、フォールバックを返す
trace_idを全ログに。分散トレースで N+1 と直列実行が見える- 整合性が難しいなら、まず分割の境界を疑う
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| microservices.io(パターン集) | https://microservices.io/patterns/ |
| Google SRE Book(全文無料) | https://sre.google/books/ |
| The Amazon Builders Library | https://aws.amazon.com/builders-library/ |
章末問題
下流サービスが遅くなったので、リトライ回数を3回から10回に増やしました。何が起きますか。
分散トレースを見たら、同じ名前のスパンが30個並んでいました。何が起きていますか。
注文サービスのタイムアウトが1秒、呼び出す在庫サービスのタイムアウトが3秒に設定されています。何が問題ですか。
これで第5部は終わりです。
サービスを書き、繋ぎ、動かす—— ここまでが、Web アプリ開発の現場で必要になる技術の骨格です。
次の章では、それが利用者が集中しても倒れないかを扱います。 1件が速いことと、1000件同時でも動くことは別の問題です。