アーキテクチャ●●○○○

タイムアウト

Timeoutタイムアウト

応答を待つ時間に上限を設ける仕組み。無限に待たないことが連鎖障害を防ぐ第一歩。

概要

タイムアウトは、外部への呼び出しや処理の完了を「いつまで待つか」に上限を設ける仕組みです。データベースへの問い合わせ、他サービスへの HTTP リクエスト、ファイルの読み込み — 相手のある処理は、いつでも「返ってこない」可能性を抱えています。タイムアウトはその待ちを一定時間で打ち切り、「遅い」を「失敗」という扱える形に変換して、制御を自分の手に取り戻すための装置です。

一見すると地味な設定値ですが、分散システムの信頼性設計では最も基本的で、最も重要な防御線とされています。リトライもサーキットブレーカーも、まず呼び出しが有限時間で決着すること — つまりタイムアウトがあること — を前提に成り立つからです。「失敗するなら、せめて速く失敗する」というフェイルファストの思想の、いちばん土台にある語彙と言えます。

なぜ生まれたか

ネットワーク越しの通信では、「相手が落ちた」ことを直接知る方法がありません。ソケットの向こうでサーバがクラッシュしても、途中の回線が切れても、こちらに届くのはただの「沈黙」です。応答が来ないのは、処理に時間がかかっているだけなのか、永遠に返らないのか — 待つ側には区別がつきません。上限を決めずに待てば、返らない応答を無限に待ち続けることになります。

そして無限に待つことの本当のコストは、「遅い」ことではなく資源の占有です。応答を待つ間、呼び出し側はスレッドやコネクション、メモリを握ったまま解放しません。依存先が1つ沈黙しただけで、待ち続けるリクエストが積み上がり、スレッドプールやコネクションプールが枯渇し、無関係な処理まで巻き添えで止まる — 障害が呼び出し元へと逆流していきます。「1つの遅いサービスがシステム全体を止める」という連鎖障害の典型は、たいていタイムアウトの欠如から始まります。だからこそ「どんな外部呼び出しにも必ず期限を切る」ことが、信頼性設計の第一歩として確立されたのです。

詳細

一口にタイムアウトと言っても段階がある

外部呼び出しのタイムアウトは、実際にはいくつかの段階に分かれています。まず接続タイムアウトは、TCP の接続確立までの上限です。相手ホストに到達できるかどうかの判定なので、通常は短く(数百ミリ秒〜数秒)設定します。次に読み取りタイムアウトは、接続後にデータが送られてくるのを待つ上限で、相手の処理時間に依存するため接続タイムアウトより長めになります。そして全体タイムアウト(リクエストタイムアウト、デッドラインとも呼びます)は、再接続やリトライも含めた「この呼び出し全体をいつまでに諦めるか」の上限で、利用者への応答時間の約束に直結するのはこれです。

落とし穴になりやすいのは、読み取りタイムアウトが「データの届かない時間」の上限であって、リクエスト全体の上限ではないことです。相手が少しずつデータを送り続ける限り読み取りタイムアウトは発火せず、呼び出しは想定よりはるかに長引きます。全体を確実に打ち切りたいなら、全体タイムアウトを別に設ける必要があります。

タイムアウトは呼び出し階層で連鎖する

現実のシステムでは、呼び出しは階層になっています。ユーザー →API ゲートウェイ→ サービスA → サービスB、というように。ここで各層のタイムアウトがばらばらだと奇妙なことが起こります。上流が3秒で諦めているのに、下流は30秒かけて処理を続け、誰も受け取らない結果のために資源を燃やし続ける、といった具合です。

原則は「上流ほど長く、下流ほど短く」。外側の約束(たとえば「ユーザーには5秒以内に何か返す」)から逆算して、内側の各呼び出しに残り時間を配分します。これを仕組みとして徹底したのがデッドライン伝搬で、gRPC では「この処理全体の締め切り時刻」をリクエストと一緒に下流へ伝え、各サービスが残り時間を見て自ら処理を打ち切れるようになっています。

サービスBサービスAゲートウェイユーザーサービスBサービスAゲートウェイユーザー応答が遅延しているリクエスト 期限5秒呼び出し 残り4秒呼び出し 残り3秒3秒で待ちを打ち切り代替の応答を返す期限内に結果を返す

内側で確実に打ち切られるからこそ、外側は残り時間で代替処理(キャッシュを返す、機能を縮退するなど)に切り替えられます。この「期限内に必ず何かを返す」設計はグレースフルデグラデーションにつながります。

値はどう決めるか — p99 から逆算する

タイムアウト値に万能の正解はありませんが、勘で決めるものでもありません。出発点は依存先の実際のレイテンシ分布、とくに p99(99パーセンタイル。100回中99回はこの時間内に収まる値)です。メトリクスから p99 を測り、その少し上 — たとえば p99 の1.5〜2倍程度 — に設定すれば、「正常なリクエストのほとんどは打ち切らず、異常な遅延だけを切る」バランスになります。

短すぎる正常なのに遅いだけのリクエストまで失敗にするタイムアウトp99 の少し上に置く長すぎる沈黙した相手を待ち続け資源を占有し続ける大半の応答が返る範囲p99応答時間適正値はレイテンシ分布から逆算する — p99 のすぐ上が出発点
タイムアウト値の置きどころ — 大半の応答が返る範囲の少し先に置き、短すぎ・長すぎの両方を避ける

短すぎるタイムアウトは、正常なのに少し遅いだけのリクエストを失敗にし、リトライと組み合わさると負荷をむしろ増やします。長すぎるタイムアウトは、資源の占有時間を延ばして枯渇のリスクを高めます。どちらに倒すかはその呼び出しの性質次第ですが、一番危険なのは「ライブラリのデフォルト値(無限や数分のことも多い)をそのまま使う」ことです。タイムアウトは設定し忘れたときに静かに牙をむく類のもので、負荷試験やカオスエンジニアリングで「依存先が沈黙したとき」の挙動を実際に確かめておく価値があります。

打ち切った後の設計へ

タイムアウトは「諦める」仕組みであって、「解決する」仕組みではありません。打ち切った後にどうするか — 間隔を空けて再試行するのがリトライと指数バックオフ、失敗が続く依存先への呼び出し自体を一時的に止めるのがサーキットブレーカーです。この3つは「まず有限時間で失敗させ、賢く再試行し、駄目なら遮断する」という一連の系譜として設計されます。また、タイムアウトの多発はそれ自体が依存先の異常を示すシグナルなので、監視アラートの対象としても重要です。