冒頭まとめ

GCP で 502 Bad Gateway を受け取ったとき、最初に押さえるべき事実が1つあります。Google が公開しているエラー区分の定義ファイルには、502 が存在しません。各区分には対応する HTTP の状態コードが併記されており、200・400・401・403・404・409・429・499・500・501・503・504 が並びますが、502 はどこにも出てきません。

これは、各サービスの窓口が自分のエラーとして 502 を返す仕組みになっていない、ということです。つまり GCP で 502 を見たら、応答を作ったのは窓口そのものではなく、その手前にいる仕組みです。多くの場合は Cloud Load Balancing、あるいは経路上のプロキシです。504 が窓口自身からも返りうるのとは、この点で性質が違います。

ロードバランサが返す 502 については、原因を絞り込む手段が用意されています。ログstatusDetails という項目です。公式のトラブルシューティング文書には、この値が response_sent_by_backend であればロードバランサは背後の応答をそのまま渡しただけで、それ以外の値であればロードバランサ自身が作った応答だ、と明記されています。値ごとの意味も一覧になっています。

もう1つ、種類による違いがあります。同じ文書によれば、グローバルおよびリージョンの外部アプリケーション ロードバランサは 503 や 504 といった意味のある状態コードを生成しますが、従来型のアプリケーション ロードバランサは常に 502 を使います。従来型の環境では、待ち時間の超過も接続の失敗も、まとめて 502 として現れます。数字だけでは区別できません。

エラーの概要

利用者側に届くのは簡素な応答で、そこに手がかりはほとんどありません。判断の材料はログ側にあります。

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

公式文書に説明がある値のうち、502 に結び付きやすいものは次の3つです。

failed_to_connect_to_backend は、ロードバランサが背後との接続を確立できなかったことを示します。文書では、背後で動いている処理が、バックエンド サービスに定義された番号で待ち受けていない可能性がある、と説明されています。

failed_to_pick_backend は、送り先を選べなかったことを示します。すべての背後が正常でない状態が考えられ、正常性の確認に必要な通信が許可されているかを確かめるよう案内されています。なお同じ文書には、グローバルの構成を変更した直後に、設定が行き渡るまでの短い間だけこの値とともに 502 が出ることがある、とも書かれています。

backend_connection_closed_before_data_sent_to_client は、応答が利用者へ渡される前に、背後が予期せず接続を閉じたことを示します。文書では、間に別の装置が挟まっていて、そちらの待ち時間のほうが短い場合に起きうる、と説明されています。

まず最初に:statusDetails を読む

第一に、ログから 502 の件を取り出し、statusDetails の値を確認します。ここが response_sent_by_backend であれば、502 を作ったのは背後のアプリケーションです。調べる先はそちらになり、ロードバランサの設定を触っても変わりません。

第二に、それ以外の値であれば、上記の3つのどれかに当てはめます。値ごとに疑う場所が変わります。接続できないのか、送り先が選べないのか、途中で切られたのかで、確認すべき対象がまったく違います。

第三に、値ごとの件数を数えます。1種類に偏っているのか、複数が混ざっているのかで、原因が単一か複合かの見当が付きます。断続的に少量だけ発生している場合は、後述する接続維持時間の食い違いを疑う価値があります。

よくある原因と解決手順

原因1:接続維持時間の食い違い(断続的な502の代表)

公式文書には、ロードバランサ側の接続維持時間が10分(600秒)に固定されていて変更できないこと、そして背後のソフトウェアの設定をそれより少し長く、推奨値として620秒に設定すべきことが明記されています。理由も添えられており、背後の設定のほうが短いと、ロードバランサがまだ使えると考えている接続を背後が先に閉じてしまい、5xx が発生する、と説明されています。

この形の特徴は、断続的で、割合が小さく、再現しにくいことです。負荷とも時間帯とも無関係に見えるため、原因の特定が遅れがちになります。

Before(背後の設定がロードバランサ側より短い):

# Nginx を背後に置いている場合
keepalive_timeout 75s;

After(推奨値に合わせる):

keepalive_timeout 620s;

現在値を確認してください。

grep -rn "keepalive_timeout" /etc/nginx/

他のソフトウェアを使っている場合も、同じ意味を持つ設定を探して同様に伸ばします。この1点だけで、断続的な 502 が止まることがあります。

原因2:背後が待ち受けていない、または番号が違う

statusDetailsfailed_to_connect_to_backend の場合です。ロードバランサが接続そのものを確立できていません。

まず、バックエンド サービスに定義されている番号と、背後が実際に待ち受けている番号を突き合わせます。

# バックエンド サービスの設定を確認する
gcloud compute backend-services describe my-backend-service --global \
  --format="yaml(port, portName, healthChecks)"

# 背後の機器で、待ち受けている番号を確認する
netstat -tnl

公式文書には、正常性の確認に使う番号を、実際に提供している番号に合わせることが推奨されています。そうしておけば、本番の通信を受ける前に「正常でない」と判定されるため、接続できない背後へ通信が流れる事態を避けられます。

原因3:送り先を選べていない

statusDetailsfailed_to_pick_backend の場合です。すべての背後が正常でないと判定されている可能性があります。

正常性の判定に必要な通信が許可されているかを確認します。判定用の通信は特定の範囲から届くため、その範囲を許可する規則が必要です。

# 背後の正常性の状態を確認する
gcloud compute backend-services get-health my-backend-service --global

# 判定用の通信を許可する規則があるかを確認する
gcloud compute firewall-rules list --format="table(name, sourceRanges.list(), allowed[].map().firewall_rule().list())"

正常と判定されている背後が1つも無ければ、この値が出ます。逆に言えば、この値が出ている限り、背後のアプリケーションの中身を調べても意味がありません。まず判定が通るようにしてください。

なお、構成を変更した直後の短い間だけこの値が出る場合は、設定が行き渡る途中である可能性があります。数分で収まるかを見てから判断してください。

原因4:背後が502を返している

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

背後がさらに別の窓口を呼んでいて、そちらとの間で問題が起きている、という入れ子の構造が典型です。この場合、調べる対象は背後のアプリケーションと、その先の経路です。

# statusDetails ごとの件数を数え、内訳を把握する
gcloud logging read \
  'resource.type="http_load_balancer" AND httpRequest.status=502' \
  --limit=200 --format="value(jsonPayload.statusDetails)" | sort | uniq -c | sort -rn

背後発とロードバランサ発が混ざっている場合もあります。件数の内訳を見れば、どちらを先に手当てすべきかが決まります。

原因5:従来型のロードバランサで、原因が502に集約されている

前述のとおり、従来型のアプリケーション ロードバランサは常に 502 を使います。したがってこの構成では、待ち時間の超過も、接続の失敗も、送り先の選択の失敗も、すべて 502 として現れます。

この場合、数字を見ても何も分かりません。判断は statusDetails の値だけで行ってください。backend_timeout が出ていれば、内容としては 504 と同じ話なので、待ち時間の設定を確認します(GCP の 504 の記事)。

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

窓口が過負荷や一時的な停止を返す場合は 503 で、区分としては UNAVAILABLE です(GCP の 503 の記事)。窓口の内部で処理が失敗した場合は 500 で、こちらは区分としても定義されています(GCP の 500 の記事)。要求の頻度が上限を超えた場合は 429、権限の不足は 403 です(GCP の 429 の記事403 の記事)。

これらはいずれも区分の一覧に対応する状態コードが定義されています。502 だけが定義に無い、という点が本記事の出発点です。窓口のエラーとして 502 が返ってくることを前提にした調査は、方向が違います。

待ちきれずに諦めた場合は 504 で、正常な応答を得られなかった場合が 502 です。ただし従来型の構成では、前者も 502 として現れます。

切り分けの順序

  1. ログから 502 の件を取り出し、statusDetails の値を確認する。数字ではなくこの値で判断する。
  2. response_sent_by_backend なら、調べる先は背後のアプリケーション。ロードバランサの設定は触らない。
  3. failed_to_connect_to_backend なら、バックエンド サービスの番号と、背後が待ち受けている番号を突き合わせる。
  4. failed_to_pick_backend なら、正常性の判定が通っているか、判定用の通信が許可されているかを確認する。
  5. backend_connection_closed_before_data_sent_to_client なら、間に別の装置が挟まっていないかを疑う。
  6. 断続的で割合が小さいなら、接続維持時間の食い違いを確認する。ロードバランサ側は600秒固定で、背後は620秒が推奨。
  7. 従来型のロードバランサを使っている場合、backend_timeout が出ていれば内容は 504 と同じ。

確認コマンド集

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

# 2. 502 の詳細を、宛先とあわせて取り出す
gcloud logging read \
  'resource.type="http_load_balancer" AND httpRequest.status=502' \
  --limit=20 \
  --format="json(httpRequest.requestUrl, jsonPayload.statusDetails, timestamp)"

# 3. 背後の正常性の状態を確認する
gcloud compute backend-services get-health my-backend-service --global

# 4. バックエンド サービスの番号と正常性確認の設定を見る
gcloud compute backend-services describe my-backend-service --global \
  --format="yaml(port, portName, timeoutSec, healthChecks)"

# 5. 背後が待ち受けている番号を確認する(背後の機器で実行)
netstat -tnl

# 6. 接続維持時間の設定を確認する(背後の機器で実行)
grep -rn "keepalive_timeout" /etc/nginx/

Editor’s Note

断続的な 502 がどれだけ厄介かを示す記録として、GCP の議論の場に残る長い相談があります(Load Balancer - intermittent timeouts and 502s)。ロードバランサを前段に置いた構成で、1日に数回、statusDetailsfailed_to_connect_to_backend の 502 が記録される、という報告から始まり、同じ症状を持つ複数の運用者が集まっています。

読みどころは、報告者たちの粘り強さです。ある人は、1日に1回ほど再現する試験の仕組みを組み上げたと書いています。別の人は、影響を受けた要求の割合を日ごとに図にして共有し、最も悪い日で全体の1.3パーセントほどだったと述べています。さらに別の人は、途中から failed_to_pick_backend という別の値も出るようになった、と報告しています。

この記録が示しているのは、断続的な 502 は感覚では追えない、ということです。1パーセント前後の失敗は、手作業の確認では捕まえられません。一方、statusDetails の値ごとに数えれば、どの経路で何が起きているかは数字で見えます。原因が1つとは限らず、複数の値が混ざることもあります。

本記事で最初に述べたとおり、GCP のエラー区分の定義に 502 はありません。だからこそ、502 を見たときに窓口の内部を想像しても答えは出ません。見るべきは前段のログであり、そこにある1つの項目です。


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