504
プロキシが上流を待ち、しびれを切らして代わりに答えました。だから犯人を特定する数字はステータスコードではなく経過時間のほうです。それは必ず誰かが設定したタイムアウトです。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
リクエストは失敗するのではなく、固まります。スピナーが 1 分ほど回り続けて、誰もデザインしていないエラーページに解決します。対象は 1 つのエンドポイントで、たいていはレポート、エクスポート、全件検索、アップロードのどれか。そして要求した量が大きいほど悪化します。これがバグではなくタイマーが関わっている決定的な印です。自前の `AbortController` で期限を設けていると、504 を一度も見ないかもしれません。先に自分の abort が発火し、失敗はネットワークエラーとして報告されるので、調査は見当違いの場所へ向かいます。
やること
推理する前に測ってください。ネットワークパネルの timing の列、あるいは `performance.getEntriesByName(url)` が経過時間を教えます。きりのいい数字——60 秒、30 秒、120 秒——は偶然ではなく答えです。そのうえで、サーバー側より意図的に短いクライアントタイムアウトを設定してください。プロキシの HTML ページを受け継ぐのではなく、利用者に見せるメッセージを自分で決められます。そして失敗と一緒に経過時間もログに残せば、バックエンドはスクリーンショットではなくタイマーの数字を受け取れます。POST を再試行しないこと。プロキシが聞くのをやめたあとに上流が作業を終えているかもしれないので、再試行されたエクスポートは 2 件のエクスポートになりますし、再試行された決済は 2 件の決済になりえます。そのエンドポイントに本当に何分も必要なら、必要なのは別の形です——リクエストは即座にジョブ ID を返し、クライアントがそれをポーリングする。どこにどんなタイムアウトを設定しても、あなたとサーバーの間にあるすべてのプロキシとモバイル回線を越えて、長いリクエストを安定させることはできません。
これが出ている理由
自分のタイマーのどれかが切れたということで、nginx はどれかを書き残しています。`upstream timed out (110: Connection timed out) while reading response header from upstream` は `proxy_read_timeout` です。接続はでき、リクエストも送ったのに、上流が答え始めなかった状態。同じメッセージの末尾が `while connecting to upstream` なら `proxy_connect_timeout` で、これは遅いアプリケーションではなくネットワークかリスナーの問題です。`while sending request to upstream` なら `proxy_send_timeout` です。3 つとも既定は 60 秒です。ログ行は上流のアドレスも名指しするので、グループの中で 1 台だけタイムアウトしているなら、遅いエンドポイントではなく不調なインスタンスです。
やること
まずエラーログの行を読んでください。タイマーと上流を名指ししてくれるので、この 2 つの事実だけで、アプリケーションのコードを開く前に探索範囲の大半が消えます。次に上流側の視点を得ます。アプリケーションがリクエストの所要時間をログに出しているなら、504 を生んだリクエストはたいていそこに残っていて、プロキシのタイムアウトより長い所要時間が記録されています。これはプロキシが去ったあとに作業が完了した証拠であり、本当は何秒かかるのかも同時に分かります。そのリクエストがアプリケーションのログに一度も現れないなら、遅かったものは何もありません。リクエストはアプリケーションの手前で待たされていたわけで、これは最適化すべきクエリではなく、飽和したワーカープールという容量の問題です。タイムアウトを上げる前に `proxy_next_upstream` を確認してください。既定が `error timeout` なので、1 台の遅い上流がクライアント側の実時間をグループのサーバー台数倍に引き伸ばします。それを止める 2 つの上限が `proxy_next_upstream_timeout` と `proxy_next_upstream_tries` です。タイムアウトを上げるのは最後の手段で、直すのではなく失敗を移動させるだけです。ブラウザとアプリケーションの間のすべての層が自分のタイマーを持っていて、いちばん短いものが必ず勝ちます。
これが出ている理由
ページは 1 分ほど読み込もうとして、諦めました。サイトは動いているけれど、その一部分が、システムが待つ気のある時間よりはるかに長くかかっている、という意味です。よくあるのは検索、レポート、大きなダウンロード、アップロードです。サイトが混んでいるのかもしれませんし、頼んだものが単に大きいのかもしれません。回線のせいではありません。回線が遅いときはページが少しずつ表示されるのであって、きりのいい秒数でぴたりと止まることはありません。
やること
1 分待ってもう一度だけ試してください。検索やレポートなら、要求を小さくします——期間を短く、件数を少なく、フォルダーではなく 1 ファイルだけ。答えが返るかタイムアウトするかの差は、頼んだ量そのものであることが多いです。アップロードや送信の途中だったなら、繰り返す前に確認してください。タイムアウトは結果を隠しますが操作を取り消しはしないので、ブラウザには何も返ってこなくてもサイト側ではアップロードが完了しているかもしれません。まずアカウントページ、注文履歴、受信トレイを見てください。同じリクエストが何度もタイムアウトするなら、正確な時刻と何を頼んだのかを添えて報告する価値があります。その 2 つがあれば、運営者はログの中から遅いリクエストを見つけられます。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
curl -sS -o /dev/null -D - --max-time 180 -w '\n%{http_code} connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://example.com/slow`--max-time 180` は curl の全体の上限で、サーバー側のタイマーよりゆとりをもって長くしておく必要があります。そうしないと curl が先に中断し、自分がせっかちだったこと以外は何も分かりません。診断になるのは 3 つの時刻です。`%{time_connect}` は TCP 接続が完了した時点、`%{time_starttransfer}` は最初のバイトが届いた時点、`%{time_total}` は終わりです。504 は接続が速く、合計がきりのいい数字に着地します。その数字を文書化された既定値と突き合わせれば層が分かります。60 秒前後なら nginx の `proxy_read_timeout` か Application Load Balancer のアイドルタイムアウト——どちらも既定 60 秒——で、125 秒前後なら Cloudflare の読み取りタイムアウトです。ただしそれは 504 ではなく 524 として報告されるので、125 秒の 504 は Cloudflare のものではありません。どの既定値より明らかに短い合計は、誰かが意図的に設定したタイマーで、製品ではなく設定ファイルに絞り込めます。次に層を分解します。`--resolve example.com:443:203.0.113.10` は元のホスト名と SNI を送りつつオリジンのアドレスへ直接つなぐので、エッジが 60 秒で諦めたのにオリジンが 90 秒で答えるなら、作業は本当に遅く、タイマーは伝令にすぎません。コマンドの実行中はオリジン自身のアクセスログを見ていてください。タイムアウトより長い所要時間でそこに現れるリクエストは、プロキシが待つのをやめたあとに作業が完了した証拠です。ここで書き込みを再試行するのが危険な理由は、まさにこれです。
RFC 9110 §15.6.5 は 504 を、ゲートウェイまたはプロキシとして動作しているサーバーが、リクエストを完了するためにアクセスする必要のあった上流サーバーから、時間内に (timely) 応答を受け取れなかったときに返すステータスと定義しています。不正な応答のために 502 を取ってある §15.6.3 と並べて読むと、この「時間内に」という一語が区別のすべてで、しかもストップウォッチが付いてきます。502 は何かが拒否したか壊れたのでミリ秒で返り、504 はタイマーが切れたのできりのいい数字で返ります。そのきりのいい数字は文書化されていて、層を特定できます。nginx は `proxy_read_timeout`・`proxy_connect_timeout`・`proxy_send_timeout` をそれぞれ既定 60 秒とし、接続タイムアウトは通常 75 秒を超えられない——OS 自身の接続試行がそこで終わるため——と注記しています。AWS は Application Load Balancer の接続アイドルタイムアウトを既定 60 秒、1 から 4000 まで設定可能と説明し、長いアップロードが切られないようアイドル期間が経過する前に少なくとも 1 バイトを送ることを勧めています。Cloudflare 自身の読み取りタイムアウトは 125 秒で、しかも 504 ではなく 524 を生みます。つまり 1 分あたりの 504 は nginx か ALB の既定タイマー、それ以外のきりのいい数字の 504 は誰かが意図的に設定したタイマーです。次の一手を変える性質が 2 つあります。504 は RFC 9110 §15.1 がヒューリスティックにキャッシュ可能と定めるステータスコードには入っていないので、何も指示されていない中間装置が保存することはなく、リクエストごとに作り直されます。そして 502 と違い、504 は作業が実行されたかどうかについて何の証拠も運びません。プロキシが諦めた時点でも上流は接続を握っていたので、その 1 秒後にリクエストを完了させ、頼まれたことを全部書き終えている可能性が十分にあります。だから非冪等なリクエストの無思慮な再試行が、ここでいちばん高くつく間違いです。nginx も同じ用心を実装しています。`proxy_next_upstream` の既定は `error timeout` なのでタイムアウトはグループ内の次のサーバーへ回りますが、非冪等なメソッドのリクエストは、いったん送られたあとは `non_idempotent` を明示しない限り回されません。このパラメータは nginx 1.9.13 からあります。
| 紛らわしいコード | 見分け方 |
|---|---|
| 502 | どちらも中間装置が上流について報告したものですが、RFC 9110 は正確に線を引いています。§15.6.3 は不正な応答、§15.6.5 は時間内に応答が無かった場合です。文言よりも時計のほうが速く決着をつけます。502 は接続が拒否されたか切れたのでミリ秒で返り、504 はタイマーどおりに返ります。ラベルより大事なのは帰結です。502 のあとならリクエストはほぼ確実に何もしていませんが、504 のあとは上流が完了させているかもしれません。 |
| 524 | 524 は IETF のものではなく Cloudflare 独自のコードで、まさにこの状況を、まさに 1 か所について表します。Cloudflare がオリジンに接続したのに、その 125 秒の読み取りタイムアウト内に HTTP 応答が届かなかった、という状態です。だから Cloudflare 越しに届いた 504 は別の何か——自前のプロキシか、ゲートウェイとして動いているオリジン——が生成し、中継されたものです。タイムアウトを延ばしたい場合、Cloudflare のそれを変更できるのは Enterprise ゾーンだけで、上限は 6,000 秒です。 |
| 503 | 503 は前もって下した拒否、504 は切れてしまった待ち時間です。RFC 9110 §15.6.4 が 503 に `Retry-After` を載せられるようにしているのは、サーバーがいつ戻れるかを大まかに知っているからです。504 に相当するものはありません。プロキシには上流が何をしているのか見当もつかないからです。負荷の切り捨てが 504 を生んでいるなら、それは負荷を切り捨てていません。タイマーに殺されるまでリクエストを並べているだけで、上流はどのみち作業のコストを払っています。 |
nginx | エラーログがタイマーを名指しするので、3 つのメッセージは区別して覚える価値があります。`while connecting to upstream` は `proxy_connect_timeout` で、ネットワークかリスナーの不在を指します。`while reading response header from upstream` は `proxy_read_timeout` で、遅いアプリケーションを指します。`while sending request to upstream` は `proxy_send_timeout` で、たいていは止まった読み手に対する大きなリクエストボディです。3 つとも既定 60 秒で、接続タイムアウトは通常 75 秒を超えられません。 |
ELB 504 | AWS は Application Load Balancer の接続アイドルタイムアウトを既定 60 秒、1 から 4000 まで調整可能と説明し、ターゲットがその時間内に応答しない場合の 504 についても書いています。長い処理に対する AWS 自身の助言は、各アイドル期間が経過する前に少なくとも 1 バイトを送ること。ストリーミングの応答が生き残り、無言の応答が死ぬ理由がこれです。ターゲット側の keep-alive も確認してください。AWS は、ロードバランサーのアイドルタイムアウトより短くすべきではないと注記しています。 |
どれもブラウザ内で完結します。アップロードは発生しません。
504 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、server・via・cf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。
ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。
5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。
変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
504 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。