冒頭まとめ

GCP で 504 Gateway Timeout を受け取ったとき、出どころは大きく2つに分かれます。1つは各サービスの窓口(API)が返すもので、内部的には DEADLINE_EXCEEDED という区分に対応します。もう1つは Cloud Load Balancing が返すもので、こちらは背後の処理が時間内に応答しなかった場合に発生します。両者は調べる場所も直し方も違うため、最初に切り分ける必要があります。

窓口が返す 504 については、公式の定義そのものに重要な記述があります。GCP のエラー区分を定めた定義ファイルには、DEADLINE_EXCEEDED の説明として「状態を変更する操作の場合、操作が正常に完了していてもこのエラーが返ることがある」と書かれています。理由も添えられており、成功の応答が遅れて届いた結果、締め切りのほうが先に過ぎた場合がある、とされています。対応する HTTP の状態コードが 504 であることも同じ場所に記載されています。

つまり、作成や更新の操作で 504 を受け取ったとき、それは「失敗した」という通知ではありません。「結果が分からない」という通知です。そのまま再実行すると、二重に作ってしまう恐れがあります。

Cloud Load Balancing が返す 504 については、判別の手がかりがログにあります。statusDetails という項目が response_sent_by_backend であれば、ロードバランサは背後の応答をそのまま渡しただけです。それ以外の値であれば、ロードバランサ自身が作った応答です。公式のトラブルシューティング文書に、この見分け方が明記されています。

なお、種類によって挙動が違う点にも注意が要ります。公式文書によれば、グローバルおよびリージョンの外部アプリケーション ロードバランサは 503 や 504 といった意味のある状態コードを生成しますが、従来型のアプリケーション ロードバランサは常に 502 を使います。従来型を使っている環境では、待ち時間の超過も 502 として現れます。

エラーの概要

窓口が返す場合、応答には区分の名前が含まれます。

{
  "error": {
    "code": 504,
    "message": "Deadline exceeded",
    "status": "DEADLINE_EXCEEDED"
  }
}

各社の道具やソフトウェアから呼んでいる場合、この区分名がそのままエラー文に現れることが多く、DEADLINE_EXCEEDED の文字列が手がかりになります。

ロードバランサが返す場合、応答は簡素なもので、詳細はログ側にあります。

{
  "httpRequest": { "status": 504 },
  "jsonPayload": {
    "@type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
    "statusDetails": "backend_timeout"
  },
  "resource": { "type": "http_load_balancer" }
}

backend_timeout は、公式文書に「背後の応答に時間がかかりすぎた」と説明されており、対処としてバックエンド サービスの待ち時間の設定を見直すか、なぜ応答に時間がかかっているかを調べることが挙げられています。

まず最初に:どちらの系統かを確定する

第一に、応答に区分名が含まれているかを見ます。DEADLINE_EXCEEDED があれば窓口側、無ければロードバランサや経路側の可能性が高い、と判断できます。

第二に、ロードバランサを経由している構成なら、ログstatusDetails を確認します。response_sent_by_backend なら、504 を作ったのは背後のアプリケーションです。調べる先はそちらになります。それ以外の値なら、ロードバランサが作った応答です。

第三に、操作の種類を見ます。読み取りなら、再実行して構いません。作成・更新・削除なら、実際にどうなったかを確認してから次の手を決めます。前述のとおり、公式定義が「成功していても返りうる」と明記しているためです。

よくある原因と解決手順

原因1:作成や更新で504を受けたが、実際には完了している

最も注意が必要な形です。窓口までは要求が届き、処理も進んだものの、応答が返る前に締め切りが過ぎた状態です。

再実行の前に、実物を確認してください。

# 例:Compute Engine のインスタンスが出来ているかを確認する
gcloud compute instances list --filter="name=example-instance"

# 例:長時間かかる操作の進行状況を確認する
gcloud compute operations list --filter="status!=DONE"

GCP の多くのサービスでは、時間のかかる操作を「操作」という単位で追跡できます。504 を受け取っても、操作自体は登録されていて進行中、ということがあります。一覧に残っていれば、待てば完了します。

確認せずに再実行すると、同じ資源を2つ作る、あるいは同じ更新を二重に適用することになります。識別子を自分で決められる操作であれば、同じ識別子を指定して再実行することで、二重作成を避けられる場合があります。対応しているかはサービスごとに違うため、使っている窓口の文書を確認してください。

原因2:ロードバランサのバックエンド待ち時間が短い

statusDetailsbackend_timeout の場合です。背後の処理が、設定された待ち時間の中で終わっていません。

Before(既定の待ち時間のまま、時間のかかる処理を通す):

gcloud compute backend-services describe my-backend-service --global \
  --format="value(timeoutSec)"

After(待ち時間を実際の処理時間に合わせる):

gcloud compute backend-services update my-backend-service --global \
  --timeout=300

ただし、全体の待ち時間を伸ばすのは影響が広い変更です。時間がかかるのが一部の経路だけであれば、経路ごとの設定で対応するほうが安全です。

伸ばしても直らない場合は、そもそも処理側が遅くなっている可能性を疑います。待ち時間の設定は、遅さを隠すための道具ではありません。

原因3:バックエンドが自分で504を返している

statusDetailsresponse_sent_by_backend の場合です。ロードバランサは正常に働いており、背後のアプリケーションが 504 を返しています。

この場合、ロードバランサの設定を変えても何も変わりません。背後がさらに別の窓口を呼んでいて、そちらで時間切れになっている、という入れ子の構造が典型です。

# ロードバランサのログから、504 の内訳を確認する
gcloud logging read \
  'resource.type="http_load_balancer" AND httpRequest.status=504' \
  --limit=20 \
  --format="json(httpRequest.requestUrl, jsonPayload.statusDetails)"

statusDetails の値ごとに件数を数えると、ロードバランサ発と背後発の割合が分かります。両方が混ざっている場合もあります。

原因4:接続の維持時間の食い違い

これは 504 そのものではありませんが、時間に関する設定の食い違いとして併せて確認する価値があります。公式文書には、ロードバランサ側の接続維持時間が10分(600秒)に固定されていて変更できないこと、そして背後のソフトウェアの設定をそれより少し長く、推奨値として620秒に設定すべきことが明記されています。

背後の設定のほうが短いと、ロードバランサがまだ使えると思っている接続を背後が先に閉じてしまい、5xx が発生します。時間切れ系の不具合を追うときは、この値も確認してください。

# 例:Nginx を背後に置いている場合
grep -n "keepalive_timeout" /etc/nginx/nginx.conf

原因5:従来型のロードバランサで502として現れている

公式文書によれば、従来型のアプリケーション ロードバランサは常に 502 を使います(GCP の 502 の記事)。したがって、待ち時間の超過であっても利用者側には 502 として見えます。

この構成で「時間のかかる要求だけが 502 になる」場合、内容は 504 と同じです。調べ方も同じで、ログstatusDetails を見ます。backend_timeout であれば、原因2の手順に進んでください。状態コードの数字だけで判断すると、調査の方向を誤ります。

補足:似ているが別のもの

窓口が過負荷や一時的な停止を返す場合は 503 で、区分としては UNAVAILABLE です。時間切れとは別の区分なので、対処も違います(GCP の 503 の記事)。要求の頻度が上限を超えた場合は 429 です(GCP の 429 の記事)。権限の不足は 403 で、待っても変わりません(GCP の 403 の記事)。

呼び出す側が自分で設けた締め切りが先に切れた場合は、504 になりません。応答が返ってきていないため、状態コードそのものが存在しないからです。この場合は道具ごとの時間切れのエラー文になります。504 が出ているということは、少なくとも何らかの応答は届いている、ということです。

クライアント側の送信が遅い場合は 408 が返ることがあります。公式文書には、statusDetailsclient_timed_out の 408 について、要求または応答の中継の進み方が不十分だった場合に出ると説明されています。

切り分けの順序

  1. 応答に DEADLINE_EXCEEDED が含まれるかを見る。含まれれば窓口側の系統。
  2. 操作の種類を確認する。作成・更新・削除なら、再実行の前に実物と操作の一覧を確認する。
  3. ロードバランサ経由なら、ログstatusDetails を見る。
  4. response_sent_by_backend なら、調べる先は背後のアプリケーション。ロードバランサの設定は触らない。
  5. backend_timeout なら、バックエンド サービスの待ち時間と、実際の処理時間を突き合わせる。
  6. 従来型のロードバランサでは、待ち時間の超過も 502 として現れる。数字ではなく statusDetails で判断する。
  7. 接続の維持時間が、ロードバランサ側の固定値より背後のほうが短くなっていないかを確認する。

確認コマンド集

# 1. ロードバランサのログから 504 とその内訳を取り出す
gcloud logging read \
  'resource.type="http_load_balancer" AND httpRequest.status=504' \
  --limit=50 --format="json(httpRequest.requestUrl, jsonPayload.statusDetails)"

# 2. statusDetails ごとの件数を数える
gcloud logging read \
  'resource.type="http_load_balancer" AND httpRequest.status>=500' \
  --limit=200 --format="value(jsonPayload.statusDetails)" | sort | uniq -c | sort -rn

# 3. バックエンド サービスの待ち時間を確認する
gcloud compute backend-services describe my-backend-service --global \
  --format="value(timeoutSec)"

# 4. 進行中の操作を確認する(504 を受けたあとに必ず行う)
gcloud compute operations list --filter="status!=DONE"

# 5. 応答時間の分布を確認する(待ち時間の設定が妥当かを判断する)
gcloud logging read 'resource.type="http_load_balancer"' \
  --limit=200 --format="value(httpRequest.latency)"

Editor’s Note

本記事で繰り返し述べた「504 は失敗の通知ではない」という点は、私見ではありません。GCP を含む Google のエラー区分を定めた公開の定義ファイルに、そのまま書かれています(google/rpc/code.proto)。

該当箇所には、締め切りが操作の完了前に過ぎたこと、状態を変更する操作ではこのエラーが成功時にも返りうること、その例として成功の応答が遅れて締め切りを越える場合があること、そして対応する HTTP の状態コードが 504 であることが、順に記されています。仕様として明記されているということは、これが実装の不具合ではなく、想定された挙動だということです。

この一文の重みは、設計に跳ね返ります。504 を受け取ったら再実行する、という素直な作りは、状態を変える操作については安全ではありません。同じ識別子を指定できる窓口ならそれを使う、操作の一覧で進行状況を確認する、あるいは実物を照会してから判断する、といった手順を挟む必要があります。

一方、ロードバランサが返す 504 は性質が違い、こちらはログstatusDetails という1つの項目で、責任の所在がその場で決まります。同じ数字でも、意味も、次にやることも別です。504 を見たら、まずどちらの系統かを確かめてください。


免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。