メインコンテンツまでスキップ

可用性と冗長化 — 壊れる前提で組む

サーバーは壊れます。ネットワークは切れます。これは設計の前提であって、例外ではありません。

だから問いは「壊れないようにするには」ではありません。**「壊れたときに何が起きるか」**です。この問いに答えられる構成と、答えられない構成があります。

この章で学ぶこと

  • 単一障害点 (SPOF) の見つけ方
  • 可用性を掛け算で数える — 直列は下がり、並列は上がる
  • 待機系の型 (アクティブ-スタンバイ / アクティブ-アクティブ)
  • 切り替えにかかる時間と、その間に起きること
  • 縮退運転 — 全部止めずに一部を諦める
  • 性能の問題を可用性の問題に格上げしない
前提知識

スケールの方向 の水平分散を前提にします。リトライと冪等性 の失敗の 3 分類も使います。

この章で扱わないこと

連鎖障害を遮断する仕組みは扱いません。応答しない依存先への呼び出しを一定期間止めてしまう方式 (サーキットブレーカー) や、依存先ごとに使える資源を仕切って一方の詰まりが他方へ波及しないようにする方式 (バルクヘッド) は、サービス間の連携を扱うガイドの領域です。本章は 1 つのサービスの中で冗長化をどう組むかに絞ります。

サーバー側でリクエストの流量を制限する方法Laravel API 開発ガイド — ミドルウェアとレート制限 が扱います。本ガイドで扱うのは、制限された側 (クライアント) の振る舞いです (リトライと冪等性)。

単一障害点を見つける

単一障害点 (SPOF) は、そこが壊れるとサービス全体が止まる箇所です。見つけ方は単純です。

構成図で、1 個しかない箱を探します。

この図でアプリケーションは 2 台あります。**しかしロードバランサーとデータベースは 1 個ずつです。**どちらが落ちてもサービスは止まります。アプリケーションを 10 台に増やしても、この 2 つは変わりません。

箱だけでなく**線も見ます。**2 台のサーバーが同じ電源、同じネットワーク機器、同じ建物にぶら下がっているなら、図の上では 2 台でも実態は 1 つです。クラウドで「別のアベイラビリティゾーンに置く」と言われるのは、この共有をなくすためです。

**外部サービスも数えます。**決済 API が 1 社なら、それも単一障害点です。冗長化できないなら、止まったときに何を諦めるかを決めておきます (後述の縮退運転)。

可用性を掛け算で数える

可用性は「動いている時間の割合」です。数字にすると、直感とずれる 2 つの性質が見えます。

直列は下がる

必要なものが 1 つでも欠けたら動かない関係を直列と呼びます。このとき全体の可用性は、各部品の可用性の積になります (AWS — Availability with dependencies)。

const series = (parts: number[]) => parts.reduce((a, p) => a * p, 1);

// 99.9% の部品が 4 つ、すべて必要
console.log((series([0.999, 0.999, 0.999, 0.999]) * 100).toFixed(3) + "%");
99.601%

**99.9% の部品を 4 つ並べると、全体は 99.6% になります。**部品を足すほど下がります。「どの部品も 3 つの 9 だから全体も 3 つの 9」は成り立ちません。

これは設計の判断に直結します。**部品を増やすこと自体が可用性を下げます。**追加する価値がその低下に見合うかを見ます。

**この積は、部品どうしが独立に壊れると仮定した値です。**実測は両方向にずれます。

  • 積より高く出る向き — 同じ基盤を共有していると故障が重なって起きるので、停止した時間が合算されません。部品が公表値より高い可用性で動くことも珍しくありません。AWS のホワイトペーパーも、99.99% の依存を 2〜3 個持つ構成が 99.99% 以上を達成しうると述べています (同上)
  • 積より低く出る向き — 数え落とした依存があったときです。積に入れていないものが止まれば、計算より短い時間しか動きません

数字は候補を絞るための道具で、保証ではありません。

並列は上がる

同じ役割のものを複数置き、どれか 1 つ動けばよい関係を並列と呼びます。このとき全体が止まるのは全部が同時に壊れたときだけなので、故障確率の積になります (AWS — Availability with redundancy)。

const parallel = (a: number, n: number) => 1 - Math.pow(1 - a, n);

console.log("99% を 2 台: " + (parallel(0.99, 2) * 100).toFixed(4) + "%");
console.log("99% を 3 台: " + (parallel(0.99, 3) * 100).toFixed(4) + "%");
99% を 2 台: 99.9900%
99% を 3 台: 99.9999%

99% のサーバーを 2 台並べると 99.99% になります。**1 台増やすごとに、止まる確率が元の故障確率の分だけ掛け算で小さくなります。**故障確率 1% なら 2 台で 0.01%、3 台で 0.0001% です。元の可用性が高いほど、1 台あたりの効き目も大きくなります。

ただし**この計算は「独立して壊れる」ことが前提です。**同じ建物、同じ電源、同じバグを持つ同じソフトウェアなら、同時に壊れます。冗長化の効果は、どれだけ独立させられたかで決まります。

**故障が重なることの意味は、直列と並列で逆になります。**並列では冗長化が効かなくなるので困りますが、直列では停止した時間が合算されないので数字の上では有利に働きます。どちらも「独立している」という仮定が外れた結果で、狙って起こすものではありません。

9 の数と年間停止時間

可用性の数字は、年間の停止時間に直すと実感しやすくなります。

const MIN_PER_YEAR = 365 * 24 * 60;
for (const a of [0.99, 0.999, 0.9999, 0.99999]) {
const down = MIN_PER_YEAR * (1 - a);
console.log(`${(a * 100).toFixed(3)}% → 年間 ${down.toFixed(0)} 分 (${(down / 60).toFixed(1)} 時間)`);
}
99.000% → 年間 5256 分 (87.6 時間)
99.900% → 年間 526 分 (8.8 時間)
99.990% → 年間 53 分 (0.9 時間)
99.999% → 年間 5 分 (0.1 時間)

**9 を 1 つ増やすと停止時間は 10 分の 1 になります。**そして 9 を増やすほど、必要なコストは跳ね上がります。年間 53 分と 5 分の差に見合う投資かどうかは、サービスの性質が決めます。

冗長化の型

同じ「2 台」でも、待機側をどう使うかで違いが出ます。

待機側の状態切り替え時間平常時の資源
アクティブ-スタンバイ起動しているが処理しない検知 + 切り替えのぶんかかる待機側が遊ぶ
アクティブ-アクティブ両方が処理している配り先から外すだけ両方使える
コールドスタンバイ停止している起動から始まるので最も長い費用が安い

アクティブ-アクティブが常に良いわけではありません。両方が同時に書き込む構成では、整合性の設計が必要になります (データの分散)。読み取り中心のアプリケーションサーバーでは自然に組めますが、データベースでは話が変わります。

**アクティブ-アクティブでも、片方が落ちたときの容量を確認しておきます。**2 台で負荷を分け合っている構成は、1 台になると負荷が 2 倍になります。1 台で捌けないなら、それは冗長化になっていません。

切り替えにかかる時間

「冗長化してあるから大丈夫」と言うとき、切り替えの間サービスがどうなるかを見ていないことがあります。

切り替えは瞬時ではありません。段階があります。

  1. 障害を検知する — 応答が無いことを何回・何秒で判断するか
  2. 待機系を昇格させる — データベースなら、書き込みを受け付ける状態にする
  3. 接続先を切り替える — 名前解決を書き換えるか、接続を張り直す
  4. クライアントが新しい接続先を掴む — キャッシュされた名前解決が切れるまでの時間が乗る

Amazon RDS のマルチ AZ DB インスタンス配置では、切り替えはデータベースの名前 (CNAME) をスタンバイ側へ向け替える形で行われ、障害の検知から処理の再開まで通常 1〜2 分とされています (Amazon RDS Multi-AZ)。読み取り可能なスタンバイを持つマルチ AZ DB クラスター配置では通常 35 秒未満で、同じ「マルチ AZ」でも構成によって桁が違います (AWS CLI — failover-db-cluster)。

**切り替えの間、アプリケーションは接続に失敗します。**だから接続の再試行はアプリケーション側に要ります。ここが リトライと冪等性 の「不確定」に当たります。切り替えの瞬間に投げたリクエストは、処理されたかどうかが分かりません。

名前解決の結果を長く保持していると、切り替え後も古い接続先を掴み続けます。フェイルオーバーを前提にする構成では、保持時間を短くしておきます (DNS)。

縮退運転 — 全部止めずに一部を諦める

冗長化できない部分が壊れたとき、選択肢は「全部止める」だけではありません。壊れた機能だけを諦めて、残りを動かし続けられます。

壊れたもの全部止める縮退させる
おすすめ表示の APIサイト全体が 500おすすめ枠だけ非表示にして、商品ページは出す
検索エンジン検索できないのでエラー新着順の一覧に切り替える
決済購入できないのでエラー購入だけ止め、閲覧とカートは動かす

縮退運転を成立させるには、設計の時点でどの機能が必須で、どれが無くても成り立つかを分けておく必要があります。必須でない依存先の呼び出しは、失敗したときに全体を巻き込まない形にします。

**この分類は、可用性の計算にも効きます。**必須でない依存は直列の積に入りません。おすすめ API が落ちても商品ページが出るなら、その API の可用性は全体の可用性を下げません。

性能の問題を可用性の問題にしない

遅いだけだったものが、対応の仕方によって「止まる」に変わることがあります。

典型は 3 つです。

  • ヘルスチェックが厳しすぎる — 一時的な遅延で台数が減り、残った台の負荷が上がり、さらに減ります (スケールの方向)
  • タイムアウトが長すぎる — 応答を待つ接続が溜まり、新しいリクエストを受けられなくなります。遅い依存先 1 つが全体を止めます
  • 失敗したリクエストを即座に再送する — 過負荷の相手にさらに負荷をかけます。リトライと冪等性 の「過負荷」に当たり、待ってから送る必要があります

共通するのは、「遅い」への反応が状況を悪くしていることです。設計では、遅さを遅さのまま受け止める余地を残します。タイムアウトを決め、待つ量に上限を置き、再送の間隔を空けます。

判断の手順

  1. 構成図で 1 個しかない箱と線を数える — 外部サービスと物理的な共有も数える
  2. 止まってよい時間を決める — 年間何分か。ここが決まらないと投資の判断ができない
  3. 直列の積を計算する — 必須の依存を並べて掛ける。目標に届かないなら、部品を減らすか冗長化する
  4. 冗長化の型を選ぶ — 片方が落ちたときの容量を確認する
  5. 切り替え時間を見積もる — その間クライアントに何が起きるかまで見る
  6. 縮退の設計をする — 必須でない依存を洗い出し、失敗しても全体を巻き込まない形にする
  7. 「遅い」への反応を点検する — タイムアウト、ヘルスチェック、再送の間隔

よくある誤解

「2 台にすれば可用性は 2 倍」 — 割合なので 2 倍にはなりません。99% が 99.99% になります。停止時間で見ると 100 分の 1 です。

「冗長化してあるから落ちない」 — 独立して壊れる場合の話です。同じ電源、同じ設定ミス、同じソフトウェアのバグは両方を同時に壊します。冗長化の効果は独立性で決まります。

「可用性は高いほどよい」 — 9 を 1 つ増やすコストは、直前の 9 より大きくなります。年間 53 分の停止が許容できるなら、5 分にする投資は他に回せます。決めるのはサービスの性質です。

「フェイルオーバーは自動なので気にしなくてよい」 — 切り替えの間、アプリケーションは接続に失敗します。再試行と、名前解決の保持時間の設定が要ります。

確認問題

問 1. 可用性 99.9% の部品が 5 つあり、すべてが必要です。全体の可用性はいくつですか。目標が 99.9% だとしたら、どうしますか。

全体は約 99.5% です (0.999 の 5 乗 = 0.99501)。年間の停止時間にすると約 44 時間で、目標の 8.8 時間を大きく超えます。

打ち手は 2 つの方向です。

  • 直列の部品を減らす — 必須でない依存を縮退の対象に回す。おすすめ表示のように無くても成り立つ機能を積から外す
  • 部品を冗長化する — 5 つのうち、可用性が最も低いものから並列にする。99.9% を 2 台にすると 99.9999% になり、積への寄与がほぼ無くなる

**先に見るのは 1 つ目です。**冗長化はコストがかかりますが、依存を必須から外すのは設計の変更で済むことがあります。

問 2. データベースをアクティブ-スタンバイで冗長化しました。「これで単一障害点は無くなった」と言えますか。

言えません。確認すべき点が残っています。

見るものなぜ
2 台が独立して壊れるか同じ建物・同じ電源なら同時に落ちる
切り替えの間どうなるか構成により数十秒から数分は接続できない。その間の扱いを決める必要がある
切り替えが本当に動くか試していない切り替えは動かないことがある
他の箱はどうかデータベースを直しても、ロードバランサーが 1 個なら単一障害点は残る

**「壊れたら切り替わる」は、実際に壊して確かめるまで仮説です。**定期的に切り替えを試す運用が要ります。

問 3. 外部の決済 API が応答しなくなり、サイト全体が 500 を返しました。何を直しますか。

決済 API の障害が、決済以外の機能まで巻き込まないようにします。

原因は多くの場合 2 つのどちらかです。

  • 応答を待ち続けている — タイムアウトが無いか長すぎる。待っている接続が溜まり、他のリクエストを受けられなくなる
  • 例外が上まで伝わっている — 決済の失敗が、ページ全体のエラーとして扱われている

対処はこの順です。

  1. タイムアウトを設定する — 何秒待つかを決める。決めないと、遅い依存先が全体を止める
  2. 失敗したときの代わりの動きを決める — 購入ボタンだけを無効にし、閲覧とカートは動かす
  3. 再送の間隔を空ける — 過負荷が原因なら、即座の再送は悪化させる (リトライと冪等性)

**1 つの依存先の障害が全体の障害になる構成が問題です。**決済 API そのものは冗長化できなくても、影響の範囲は設計で決められます。

まとめ

  • 単一障害点は、構成図で 1 個しかない箱と線を探すと見つかります。外部サービスと物理的な共有も数えます
  • **直列は積で下がります。**99.9% の部品 4 つで 99.6% です。部品を足すこと自体が可用性を下げます
  • 並列は故障確率の積で上がります。99% を 2 台で 99.99%。ただし独立して壊れる場合に限ります
  • 冗長化の型は、待機側を使うかどうかで分かれます。片方が落ちたときの容量を確認します
  • **切り替えは瞬時ではありません。**その間クライアントは接続に失敗するので、再試行が要ります
  • 縮退運転を設計すると、必須でない依存が可用性の計算から外れます
  • タイムアウト・ヘルスチェック・再送の設計を誤ると、性能の問題が可用性の問題に変わります
関連リファレンス

次に読む