本文へ移動

ホーム / SRE / 第4部 データとアプリケーション

第4部 データとアプリケーション・第9章

アプリケーション設計

★★☆ 重要17トピック・面接 5問

SREはアプリの作りが信頼性を左右する場面に立ち会う。「障害が連鎖しない作り」を説明できることが求められる。

パターン目的要点・落とし穴
タイムアウト遅い依存先に引きずられない未設定が最も危険。上流ほど長く、下流ほど短く設定する
リトライ一時的な失敗からの回復指数バックオフ+ジッター必須。リトライ回数の上限。多段リトライは負荷を掛け算で増やす(リトライストーム)
冪等性リトライや重複実行で結果が変わらない冪等キー(Idempotency-Key)、一意制約、処理済みIDの記録
サーキットブレーカー壊れた依存先への呼び出しを止める失敗率で開閉。開いている間は即座にフォールバック
バルクヘッド1つの依存先の障害を隔離依存先ごとにスレッドプールや接続プールを分ける
レート制限過負荷から自分を守るトークンバケット等。超過時は 429 を返す
グレースフルデグラデーション一部機能を落として全体を守るレコメンドが落ちても購入はできる、など
バックプレッシャー処理能力以上の流入を抑えるキューの長さを見て受付を絞る
種類意味未設定・不適切だと起きること
接続タイムアウト接続確立までの待ち時間相手が応答しないと長時間ぶら下がる。1〜数秒が目安
読み取りタイムアウト応答を待つ時間遅い依存先のせいでスレッドや接続が枯渇する
全体のタイムアウト(デッドライン)リクエスト全体の上限上流がすでに諦めた処理を下流が続ける無駄
アイドルタイムアウト無通信の接続を閉じるまでの時間LBとバックエンドで食い違うと 502 が出る

上流(ユーザーに近い側)ほどタイムアウトを長く、下流ほど短くする。逆だと、上流が諦めた後も下流が処理を続けてしまう。gRPC のようにデッドラインを下流へ伝播させる仕組みもある。

第12章「パフォーマンスチューニング」:接続プールとスレッド数の考え方を図で詳しく

9.3リトライの設計

★★☆ 重要
要素推奨理由
リトライ対象一時的なエラー(タイムアウト、503、接続エラー)のみ400系はリトライしても成功しない
待ち時間指数バックオフ(100ms → 200ms → 400ms…)回復の時間を与える
ジッター待ち時間にランダムな揺らぎを加える全クライアントが同時にリトライする「同期」を防ぐ
回数の上限2〜3回程度無限リトライは障害を長引かせる
リトライ予算全リクエストに対するリトライの割合を制限(例:10%)障害時にリトライが負荷を何倍にもするのを防ぐ
冪等性冪等な処理だけリトライ、または冪等キーを付ける二重決済などを防ぐ

多段のサービスがそれぞれ3回リトライすると、3段で 3×3×3 = 27倍の負荷になる。リトライは1か所(多くは最上流か最下流)に集約するのが望ましい。

ジッターがないと、全員が同じ瞬間にリトライして再び落とす3台のクライアントのリトライ時刻(指数バックオフ)ジッターなしクライアント1クライアント2クライアント3ジッターありクライアント1クライアント2クライアント3時間 →
ジッターの有無によるリトライ時刻の違い

ジッターでリトライの時刻をばらけさせ、回復しかけた相手を再び落とさない。

各層が3回リトライすると、末端の負荷は3 × 3 × 3 = 27倍リトライは1か所に集約し、上限と予算を決めるクライアント1リクエストAPIゲートウェイ最大3回試す→ 3リクエストアプリそれぞれ3回→ 9リクエストDBそれぞれ3回→ 27リクエスト
多段リトライによる負荷の増幅

リトライは掛け算で効く。どこか1か所に集約する。

方法仕組み例
冪等キークライアントがリクエストごとに一意なキーを付け、サーバーは処理済みキーを記録して同じ結果を返すStripe の Idempotency-Key
一意制約DBの一意制約で二重登録を防ぐ注文ID、イベントID
状態遷移のチェック「未払い → 支払い済み」のような遷移が既に済んでいれば何もしない決済ステータス
処理済みIDの記録メッセージIDを記録し、重複受信を無視するキューのコンシューマー
絶対値での更新「+1する」ではなく「値を5にする」設定値の更新

第6章「Web技術(HTTP・URI・REST)」:HTTPメソッドの安全性と冪等性を図で詳しく

状態動作次の状態へ移る条件
Closed(通常)呼び出しを通し、失敗率を計測失敗率がしきい値を超えたら Open
Open(遮断)呼び出さずに即座にエラーかフォールバックを返す一定時間経過で Half-Open
Half-Open(試行)少数の呼び出しだけ通して様子を見る成功すれば Closed、失敗すれば Open
壊れた依存先への呼び出しを止め、時間を置いて少しずつ戻すClosed(通常)呼び出しを通し失敗率を計測Open(遮断)呼ばずに即フォールバックHalf-Open(試行)少数だけ通して様子を見る失敗率がしきい値超え一定時間が経過試行が失敗試行が成功
サーキットブレーカーの状態遷移 · 3状態

Openの間は依存先を呼ばないので、待ちが積み上がらず自分まで倒れるのを防げる。

レート制限は、同じ相手(IP アドレス・ユーザー・API キーなど)からのリクエストの回数に上限を設け、超えた分を断る仕組みです。一部の利用者やボットの大量アクセス、リトライの嵐から、サービス全体と後ろの DB を守ります。断るときは 429 Too Many Requests を返すのが一般的です。

回数の数え方(アルゴリズム)によって、短い時間に集中するアクセス(バースト)をどこまで許すかや、必要なメモリが変わります。面接では「どの単位で数えるか」「複数のサーバーで回数をどう共有するか」まで話せると強いです。

アルゴリズム仕組み特徴
固定ウィンドウ1分ごとに回数を数える実装が簡単。境界の前後で2倍の流量を許してしまう
スライディングウィンドウ直近N秒の回数を数える正確だがメモリを使う
トークンバケット一定速度でトークンが溜まり、リクエストごとに消費バーストを許容できる。最も一般的
リーキーバケット一定速度で処理し、溢れたら破棄出力を平準化する

サーバーが複数台ある環境では、台ごとに数えると上限が台数倍になってしまうので、Redis でカウンタを共有するのが定番です。

下の「ブラウザで試す」の図では、API サーバーが2台あるときに、回数を Redis で共有する流れを順に追えます。台ごとに数えると上限が2倍になるところから始めて、固定ウィンドウ(INCR と EXPIRE を MULTI でまとめる)で 101 回目に 429 と Retry-After を返す手順、窓の境界の前後で2倍通ってしまう手順、スライディングウィンドウで「数えてから足す」を別々に送ると数え間違えるので Lua スクリプトにまとめる手順、トークンバケットでバーストを通し、使い切ったら待たせる手順と続きます。最後に、何をキーにして数えるか(ユーザー・API キー・IP)と、Redis が落ちたときに通すか止めるか(fail-open/fail-closed)を確かめてください。

別のページで開く →

ネットワーク図を読み込んでいます…

9.7キャッシュ

★★☆ 重要
テーマ要点
Cache-Asideアプリがキャッシュを見て、なければDBから取得して書き込む。最も一般的
Write-Through / Write-Behind書き込み時にキャッシュも更新。Write-Behind は速いが欠損リスク
無効化TTL、更新時の削除。整合性が最も難しい部分
キャッシュスタンピード人気キーの期限切れで大量リクエストがDBに殺到。ロック、期限前の再計算、TTLのばらつきで防ぐ
キャッシュ障害時キャッシュが落ちるとDBに全負荷が来る。DBがそれに耐えられるかを考えておく
CDN静的コンテンツや一部APIをエッジでキャッシュ。Cache-Control、無効化の手順

第6章「Web技術(HTTP・URI・REST)」:HTTPキャッシュと多層キャッシュを図で詳しく

キャッシュは、DB などから読んだ結果を Redis のような速い置き場に一時的にコピーしておき、次からはそこから返す仕組みです。応答が速くなり DB の負荷も下がりますが、DB とキャッシュの2か所にデータがあるので、「誰がいつキャッシュに入れ、更新のときにどう扱うか」を決めておかないと、古い値を返し続けたり、データを失ったりします。

その決め方がキャッシュ戦略です。読み取りと書き込みのそれぞれで、アプリとキャッシュ層のどちらが DB とやりとりするかに注目して比べます。

戦略読み取り書き込みメリットデメリット
Cache-Asideアプリがキャッシュを見て、なければDBから取得して格納DBを更新し、キャッシュを削除単純、キャッシュ障害に強い初回は遅い、短時間の不整合
Read-Throughキャッシュ層がDBから取得-アプリが単純になるキャッシュ層の実装が必要
Write-Through-キャッシュとDBを同時に更新常に最新書き込みが遅い
Write-Behind-キャッシュに書き、後でまとめてDBへ書き込みが速いキャッシュ障害でデータ消失

更新時に「キャッシュを更新」ではなく「キャッシュを削除」が推奨されるのは、同時更新で古い値が残る競合を減らせるため。

Cache-Aside:読むときはキャッシュから、書くときはキャッシュを消すアプリキャッシュDB読み取りGET user:1なし(ミス)SELECT値SET user:1(TTL付き)書き込みUPDATEDEL user:1更新でなく削除
Cache-Asideの読み取りと書き込み

書き込み時は値を入れ直さず削除する。次の読み取りで最新が入る。

下の「ブラウザで試す」の図は、いちばんよく使う Cache-Aside の流れです。まず Redis を見て、なければ(キャッシュミス)DB から読んで TTL 付きで Redis に入れ、次の読み取りはヒットして DB に行かない、という順番を見られます。更新のときに DB を更新してからキャッシュを DEL で消すところと、消し忘れても TTL が古さの上限になるところに注目してください。

別のページで開く →

ネットワーク図を読み込んでいます…

キャッシュは「DB の前の盾」として負荷を引き受けているので、キャッシュが効かなくなった瞬間に、その負荷がまとめて DB に向かいます。下の問題はどれも、形は違っても「本来キャッシュが受けるはずのリクエストが DB に流れ込む」か「一部に集中する」ことで起きます。

平常時のヒット率だけを見ていると気づきにくく、ピークの時間帯やデプロイ・障害の直後に表に出がちです。DB の CPU や接続数が急に跳ねたときに、キャッシュの側で何が起きたかを疑えるようにしておきます。

問題内容対策
キャッシュスタンピード人気キーの期限切れで大量のリクエストがDBへ再計算をロックで1つに絞る、期限前の再計算、TTLにばらつき
キャッシュペネトレーション存在しないキーへのアクセスが毎回DBへ「存在しない」こともキャッシュ、ブルームフィルタ(存在しないキーを少ないメモリで見分ける仕組み)
キャッシュの一斉失効同じTTLのキーが同時に切れるTTLにランダムな幅を持たせる
ホットキー1つのキーにアクセスが集中しノードが過負荷ローカルキャッシュの併用、キーの複製
キャッシュ全損キャッシュサーバー障害で全負荷がDBへDBの余力確保、段階的なウォームアップ、レプリカ構成

下の「ブラウザで試す」の図の後半で、表のうち2つを順に見られます。1秒に数千回読まれる人気のキーの TTL が切れて全員が同じ重い集計を DB に投げるキャッシュスタンピードと、SET NX でロックを取れた1つのリクエストだけに作り直させる対策、そして Redis そのものが止まって全部のリクエストが DB に向かうキャッシュ全損です。DB の負荷がどこで跳ね上がるかに注目してください。

別のページで開く →

ネットワーク図を読み込んでいます…

ひとことで

キャッシュは「どの層に、何を、何秒、いつ消すか」を決めること。決めずに入れると、古いデータと他人のデータが返る事故の元になる。

キャッシュは、ブラウザ、CDN、リバースプロキシ、アプリ(Redisなど)、DBのバッファプールと、多くの層に置けます。それぞれの使い方は、HTTPキャッシュと多層キャッシュ、キャッシュ戦略の比較、キャッシュで速くするで解説しています。ここでは、どれを使うかを決めるための全体の考え方をまとめます。

データの種類おすすめの置き場所期限と消し方
JS・CSS・画像(ファイル名にハッシュつき)ブラウザ+CDN1年など長く。デプロイで別名になる
HTML・公開ページCDN(短め)数十秒〜数分、またはデプロイ時に削除
誰でも同じAPIの結果(ランキングなど)CDNまたはRedis数秒〜数分のTTL
ログイン中の個人のデータアプリ内(Redis)。CDNには置かない更新時に削除。no-store/private
重い集計結果Redis、事前計算テーブル定期的に作り直す
設定・マスターデータアプリのメモリ+Redis変更時に削除、または短いTTL
キャッシュを入れる前に確認する5つの質問全部に答えられないなら、まだ入れない方がよい1 何度も読む?同じ結果を繰り返し使うか2 古くてよい?何秒までのずれを許すか3 どの層に?ブラウザ・CDN・アプリ・DB4 いつ消す?TTLか、更新時に削除するか5 外れたら?全部外れてもDBが耐えるか
キャッシュを入れる前の5つの質問

5番目の「全部外れたら」に答えられないキャッシュは、障害の引き金になる。

面接ではこう答える

キャッシュは、どの層に、何を、何秒置き、いつ消すかを決めて使います。ハッシュつきの静的ファイルはブラウザとCDNに長く、HTMLは短く、個人のデータはCDNに置かずアプリ側で更新時に削除します。入れる前に、何度も読まれるか、どれだけ古くてよいか、どこに置くか、いつ消すか、全部外れたときにDBが耐えるかを確認します。

非同期処理では、すぐには終わらない処理(メール送信・画像の変換・集計など)を「メッセージ」にしてメッセージブローカー(メッセージを預かって受け手に渡す仕組み)に預け、別の処理(受け手、コンシューマー)があとから取り出して処理します。リクエストを受けたアプリは、預けた時点で応答を返せます。

ブローカーには大きく2つの型があります。キュー型(Amazon SQS など)は、受け手が処理し終えたメッセージを消します。1つのメッセージは1つの受け手が処理するので、仕事を分け合うのに向きます。ログ型(Apache Kafka など)は、メッセージを読んでも消さずに、トピック(メッセージの種類ごとの置き場)に届いた順に溜めておきます(保持期間や容量の上限を過ぎると消えます)。受け手はそれぞれ「どこまで読んだか」の位置(オフセット)を持つので、複数の受け手が同じメッセージを別々に読んだり、過去の位置から読み直したりできます。トピックはパーティションに分けて並列に読み書きし、順番が保たれるのはパーティションの中だけです。Amazon Kinesis Data Streams も同じログ型で、パーティションにあたるものをシャードと呼びます。

テーマ要点
キューを使う理由処理の平準化、疎結合、重い処理をリクエストから切り離す
配信保証at-most-once / at-least-once / exactly-once。現実には at-least-once + 冪等な受信側が基本
デッドレターキュー繰り返し失敗するメッセージを退避し、全体を止めない
順序保証SQS FIFO、Kafka のパーティション単位。必要な範囲だけ保証する
監視すべき指標キューの滞留数、最古メッセージの経過時間、処理失敗率
保証意味実現方法注意点
at-most-once最大1回(欠損あり)受信したら先にACK処理失敗でメッセージが消える
at-least-once最低1回(重複あり)処理後にACK受信側を冪等にする必要
exactly-onceちょうど1回トランザクション、重複排除実現は難しく、多くは at-least-once+冪等で代替
項目SQSSNSKafka / Kinesis
モデルキュー(1つのメッセージを1つの受信者が処理)Pub/Sub(複数の購読者に配信)ログ(複数の受信者が各自の位置から読む)
順序標準は保証なし、FIFOは保証保証なし(FIFOトピックあり)パーティション内で保証
再読み込み不可不可保持期間内なら可能
使いどころジョブの非同期処理通知のファンアウトイベント駆動、大量データ、再処理が必要な場合
手法概要向き不向き
2フェーズコミット全参加者の準備完了を確認してからコミット強い一貫性。調整役が障害点になり、マイクロサービスでは避けられがち
Sagaローカルトランザクションの連鎖。失敗時は補償処理で取り消すマイクロサービス向き。途中状態が見える点に注意
Outbox パターンDB更新とイベント発行を同じトランザクションで記録し、後から送る「DBは更新したがイベントが飛ばない」問題を防ぐ
Saga:失敗したら逆順に「取り消し処理」を流す1 注文を作成注文サービス2 在庫を確保在庫サービス3 決済決済サービス→ 失敗補償 1:在庫を戻す確保した在庫を解放補償 2:注文を取り消すステータスを取消に結果途中状態が一時的に見えることを許容する
Sagaパターンの補償処理

サービスをまたぐ処理は、ロールバックの代わりに取り消し処理を流す。

DB更新とイベント記録を1つのトランザクションに入れる1つのトランザクション注文テーブルを更新outboxテーブルにイベントを記録送信プロセス未送信を読むキュー他サービスへ送信に失敗しても「未送信」のまま残るので再送できる
Outboxパターン

「DBは更新したがイベントが飛ばなかった」を防ぐ定番の手法。

項目モノリスマイクロサービス
デプロイ全体を一括サービスごとに独立
障害の影響1か所のバグが全体に波及しやすい隔離しやすいが、連鎖障害の経路が増える
運用の難しさ単純監視、トレース、ネットワーク、データ整合性が複雑
チーム少人数で効率的チームごとに独立して開発できる
SREの関心事デプロイの頻度と安全性、スケールの粒度分散トレーシング、サービス間のSLO、タイムアウト連鎖

「マイクロサービスにすべきか」と聞かれたら、組織の規模とチーム構成、運用能力を前提に判断すると答えるのがよい。

  • グレースフルシャットダウン:SIGTERM 受信後に新規受付を止め、処理中のリクエストを完了させる。K8s の preStop、LB の登録解除待ちと組み合わせる

  • ヘルスチェックの設計:liveness(再起動すべきか)と readiness(トラフィックを受けられるか)を分ける。依存先DBの状態を liveness に含めると連鎖再起動を招く

  • 設定と秘密情報:環境変数、Secrets Manager / Parameter Store。コードに埋め込まない

  • フィーチャーフラグ:デプロイとリリースを分離し、問題時に即座に無効化できる

  • N+1・重いクエリ:APMのトレースで発見し、開発チームと改善する流れを話せるとよい

第12章「パフォーマンスチューニング」:N+1問題の検知と直し方を図で詳しく

ひとことで

使っている部品を固定し、小さく頻繁に自動で更新する。放置すると、脆弱性が見つかったときに上げられなくなる。

アプリは、言語のランタイム(Node.js、Python、Ruby、Javaなど)と多数のライブラリでできています。バージョンはセマンティックバージョニング(メジャー.マイナー.パッチ)で表すのが一般的で、メジャーが上がると互換性のない変更があり得ます。ロックファイル(package-lock.json、poetry.lock、Gemfile.lock、go.sumなど)で、実際に入る版を固定し、誰がいつビルドしても同じ結果になるようにします。

やること内容
バージョンを固定ロックファイルをコミットし、コンテナのベースイメージもダイジェストで固定
ランタイムの版を揃える.tool-versions や mise・asdf で、開発・CI・本番を同じ版に
更新を自動化RenovateやDependabotで更新PRを自動作成し、CIで検証
更新の方針パッチは自動マージも検討、マイナーは週次、メジャーは変更点を読んで計画
サポート終了(EOL)の管理ランタイム、フレームワーク、DBエンジン、OSのEOL日を一覧にし、半年前から計画
脆弱性への対応スキャンで見つかったら、深刻度と悪用状況で期限を決めて更新

更新を長く放置すると、メジャーバージョンを何段も一度に上げることになり、影響が大きすぎて手が出せなくなります。その状態で重大な脆弱性が出ると、修正版に上げられず、回避策でしのぐしかなくなります。「小さく・頻繁に」が結果的に一番安全で安くつきます。

依存関係の更新は「小さく・頻繁に・自動で」流すメジャー更新は変更点を読んで計画的に。ランタイムのサポート終了日は一覧で管理する1 バージョンを固定ロックファイル・イメージのダイジェスト2 更新PRを自動作成Renovate・Dependabot3 CIで検証テスト・脆弱性スキャン・ビルド4 段階的に反映ステージング→本番(カナリア)
依存関係を更新する流れ

更新が止まっているリポジトリは、それ自体がリスクとして管理する。

面接ではこう答える

ロックファイルとイメージのダイジェストで実際に使う版を固定し、開発・CI・本番でランタイムの版を揃えます。RenovateやDependabotで更新PRを自動で作り、CIで検証して段階的に反映し、小さく頻繁に上げます。ランタイムやフレームワークのEOLは一覧で管理し、放置して一度に上げられなくなる状態を避けます。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • タイムアウト・リトライ・冪等性はセットで設計する
  • キャッシュは更新ではなく削除。スタンピードと全損時の負荷に備える
  • メッセージは at-least-once + 冪等な受信側が現実的な答え

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1外部APIが遅くなったとき、自分のサービスまで落ちないようにするには?

模範回答

まず必ずタイムアウトを設定し、遅い依存先にスレッドや接続を占有されないようにします。リトライは一時的なエラーだけに限り、指数バックオフとジッター、回数の上限をつけます。失敗が続けばサーキットブレーカーで呼び出しを止めてフォールバックし、依存先ごとに接続プールを分けて影響を隔離します。そのうえで、その機能が使えなくても主要機能は動くように縮退運転を設計します。

深掘りされたら

  • タイムアウトの値はどう決めますか?
  • リトライストームとは?

見られているポイント複数の対策を組み合わせて語れるか

Q2冪等性とは何で、なぜ重要ですか?

模範回答

同じ操作を何回実行しても結果が変わらない性質です。ネットワーク障害やタイムアウトでは成功したかどうか分からないためリトライが必要になり、冪等でないと二重決済や二重登録が起きます。冪等キーを付けて処理済みを記録する、一意制約で重複を防ぐ、などで実装します。

深掘りされたら

  • メッセージキューのコンシューマーではどう実装しますか?

見られているポイントリトライとの関係

Q3キャッシュの整合性をどう保ちますか?

模範回答

一般的なCache-Asideでは、更新時にDBを更新してからキャッシュを削除し、次の読み取りで最新を入れ直します。更新ではなく削除にするのは、同時更新で古い値が残る競合を減らすためです。それでも短時間の不整合は起きうるのでTTLで上限を設け、厳密さが必要なデータはキャッシュしない判断もします。

深掘りされたら

  • キャッシュが全部落ちたらどうなりますか?
  • キャッシュスタンピードの対策は?

見られているポイント不整合を前提にした設計

Q4マイクロサービス間でデータの整合性をどう保ちますか?

模範回答

サービスをまたぐ2フェーズコミットは避け、各サービスのローカルトランザクションをつなぐSagaを使うことが多いです。途中で失敗したら、完了した処理を補償処理で取り消します。DB更新とイベント送信を確実に両立させるにはOutboxパターンを使い、受信側は冪等にして重複に備えます。

深掘りされたら

  • 補償処理も失敗したらどうしますか?

見られているポイント結果整合性を受け入れた設計

Q5ヘルスチェックはどう設計しますか?

模範回答

再起動すべきかを判断するlivenessと、トラフィックを受けてよいかを判断するreadinessを分けます。livenessにDBなど依存先の状態を含めると、DBが一時的に遅いだけで全台が再起動し続ける事故になるため、プロセス自身の健全性だけを見るようにします。

深掘りされたら

  • 起動に時間がかかるアプリではどうしますか?

見られているポイント連鎖障害を防ぐ視点