冒頭まとめ
GCP の 500 Internal Server Error は、1つの意味を持つエラーではありません。エラー区分の定義ファイルを見ると、500 に対応する区分は3つあります。
1つ目は内部のエラーです。定義には、下層の系が前提としていた不変の条件が破られたことを意味し、この区分は深刻なエラーのために予約されている、と書かれています。
2つ目は不明なエラーです。定義では、別の空間から受け取った状態がこちらでは未知のエラーに属する場合や、十分なエラー情報を返さない窓口からのエラーが、この区分に変換されることがある、と説明されています。つまり「原因が分からない」ではなく「原因を伝える経路で情報が落ちた」という意味です。
3つ目は回復不能なデータの損失または破損です。説明はこの一文だけですが、意味は重大です。
この3つで、次にやることが変わります。3つ目が返っているなら、再試行してはいけません。同じ操作を繰り返すより、何が失われたかを確認するのが先です。
残る2つについては、再試行が公式に認められています。運用側の公式文書は、指数的に間隔を伸ばしランダム性を加える再試行を勧めており、500 や 503 のサーバー側のエラーでは最初の間隔を最短1秒としています。503 との違いは、定義の側にあります。503 の定義には、一時的な状態である可能性が高く再試行で解消できると書かれているのに対し、500 の定義にはそうした見込みが書かれていません。直る保証の有無が違うだけで、再試行が禁じられているわけではない、と押さえてください(GCP の 503 の記事)。
エラーの概要
応答の形は他のエラーと共通で、status に区分名が入ります。
{
"error": {
"code": 500,
"message": "Internal error encountered.",
"status": "INTERNAL"
}
}
status の値が INTERNAL、UNKNOWN、DATA_LOSS のいずれかで、意味が変わります。message は多くの場合、内部でエラーが起きたという趣旨の短い文言だけで、それ以上の手がかりはありません。
details に識別子が入っていれば、そこから判断できる場合があります。設計の指針では、すべてのエラー応答が機械が読める識別子を含むべきとされています。ただし 500 の場合、内部の事情を外に出さない方針から、詳細が乏しいことが実際には多くあります。
そのため、このエラーは他と違って、応答だけで原因に辿り着けないのが普通です。調査は記録の側に移ります。
まず最初に:status を読み、再現するかを見る
第一に、status の値を読みます。DATA_LOSS であれば、再試行の前にデータの状態を確認します。他の2つであれば、次に進みます。
第二に、同じ操作が再現するかを確かめます。1回だけであれば一時的なものです。繰り返し同じ場所で起きるなら、要求の内容に何か引き金があります。
第三に、他の操作でも起きているかを見ます。特定の操作だけなら要求側、幅広い操作で起きているなら提供側の問題である可能性が高くなります。
第四に、稼働状況の表示を確認します。ただし、表示が正常でも特定の機能だけが不調なことはあるので、表示だけを根拠に自分側の問題と決めつけないでください。
よくある原因と解決手順
原因1:一時的なもので、再試行で通る
最も多い形です。同じ要求が2回目には通ります。公式の指針では、間隔を指数的に伸ばしてランダム性を加え、最初の間隔は最短1秒です(429 の最短30秒とは扱いが違います。GCP の 429 の記事)。
ただし、再試行してよいかどうかは操作の種類によります。区分の定義には、503 について、同じ結果になるとは限らない操作の再試行が常に安全とは限らない、という注意が添えられています。同じ注意が 500 にも当てはまります。
Before(結果が変わりうる操作を無条件で再送する):
for i in range(3):
r = create_resource() # 作成の操作
if r.ok:
break
time.sleep(2 ** i)
# → 1回目が内部で成功していた場合、二重に作られる
After(作成の操作は、実物を確認してから判断する):
r = create_resource()
if not r.ok:
if resource_exists(name): # 実際に作られていないか確認する
log("すでに作成済み")
else:
r = create_resource()
読み取りの操作であれば、そのまま再試行して構いません。なお、公式のソフトウェア開発キットの多くは、再試行してよい区分の判定と間隔の制御を内部で行います。自分で組む前に、使っている道具の再試行の設定を確認してください。
原因2:DATA_LOSS が返っている
status が DATA_LOSS の場合です。定義の説明は「回復不能なデータの損失または破損」の一文だけですが、扱いは他の2つと明確に違います。
この場合、まずやるべきは再試行ではありません。何が失われたか、あるいは壊れたかを確認することです。
# 対象の状態を確認する
gcloud <サービス> describe <対象> --format="yaml(state, status)"
# 該当時刻前後の記録を確認する
gcloud logging read \
'severity>=ERROR AND timestamp>="<開始時刻>" AND timestamp<="<終了時刻>"' \
--limit=50
再試行によって、壊れた状態の上にさらに操作を重ねる恐れがあります。状況が把握できるまで、書き込みの操作は止めてください。
この区分が返ること自体が稀なので、返った場合は問い合わせの対象になります。
原因3:特定の操作だけで再現する
同じ要求が毎回同じ場所で失敗する場合です。提供側の不調ではなく、要求の内容が引き金になっています。
Before(内容を変えずに再試行を続ける):
for i in 1 2 3; do gcloud <サービス> <操作> --arg=<値> && break; done
After(要求を最小化して、引き金を特定する):
# 必須の項目だけで実行してみる
gcloud <サービス> <操作> --minimal-args
# 通ったら、項目を1つずつ戻す
大きな値、特殊な文字、極端な件数などが引き金になることがあります。最小構成から積み上げれば、どの項目が原因かが分かります。
特定できた場合、それは提供側の不具合の可能性があります。再現する最小の手順が作れていれば、問い合わせの材料としてそのまま使えます。
原因4:記録に手がかりを探す
応答だけでは分からない場合、記録を見ます。要求ごとに識別子が付いているので、それを軸に追えます。
# 500 が出た操作を抽出する
gcloud logging read 'protoPayload.status.code=13 OR protoPayload.status.code=2' \
--limit=50 \
--format="value(timestamp, protoPayload.methodName, protoPayload.status.message)"
区分には番号が割り当てられており、内部のエラーが13、不明が2、データの損失が15です。記録の検索でこの番号を使えば、種類ごとに絞り込めます。
どの操作で多く出ているかが分かれば、原因の範囲が狭まります。1つの操作に集中していれば原因3の形、幅広く出ていれば提供側の問題です。
原因5:問い合わせる
上記で解決しない場合、500 は利用者側で直せない種類のエラーです。定義にも、深刻なエラーのために予約された区分だと書かれています。
問い合わせの際に用意すべき情報は、応答から取れます。要求の時刻、呼び出した窓口と操作の名前、status の値、そして details に識別子があればその値です。設計の指針には、問い合わせや意見の提出の際に添付できる情報として、要求の識別子を伝える構造が定義されています。この値があれば、提供側で該当の要求を特定できます。
再現する最小の手順があれば、あわせて添えてください。原因3の手順で作れます。
補足:似ているが別のもの
まず、どの URL が 500 を返したかを確認してください。この記事が扱うのは、Google Cloud の窓口(googleapis.com への呼び出し)が返す 500 です。自分がデプロイしたサービス(Cloud Run など)の URL が返す 500 は、Google 側ではなく自分のアプリケーションのエラーであり、調査はそのサービスの記録の側で行います。この2系統の分け方は 503 と同じです(GCP の 503 の記事)。
一時的に処理できない場合は 503 です。区分の定義に、一時的な状態である可能性が高く間隔を空けた再試行で解消できる、と明記されています。500 との違いは、直る見込みが仕様として書かれているかどうかです。
時間切れは 504 で、状態を変える操作では成功していても返りうると定義に書かれています(GCP の 504 の記事)。
送った内容そのものに問題がある場合は 400 で、区分が3つに分かれます(GCP の 400 の記事)。上限の超過は 429 です(GCP の 429 の記事)。権限の不足は 403 です(GCP の 403 の記事)。
なお、502 は区分の定義に存在しません。GCP で 502 を受け取った場合、応答を作ったのは窓口ではなく前段の仕組みです(GCP の 502 の記事)。500 と 502 は、出どころが違います。
切り分けの順序
- どの URL が返したかを確認する。自分のサービスの URL なら、自分のアプリ側の調査。
statusの値を読む。DATA_LOSSなら再試行より先に状態の確認。- 同じ操作が再現するかを確かめる。1回だけなら一時的なもの。再試行は最短1秒からの指数間隔で。
- 再試行してよいかを、操作の種類で判断する。作成の操作は実物を確認してから。
- 特定の操作だけで再現するなら、要求を最小構成にして引き金を特定する。
- 幅広い操作で起きているなら、提供側の問題。稼働状況を確認する。
- 記録を種類ごとに絞り込む。番号は内部が13、不明が2、データ損失が15。
- 問い合わせる際は、時刻・操作名・
status・識別子・再現手順を揃える。
確認コマンド集
# 1. 応答の status と識別子を取り出す
curl -sS -H "Authorization: Bearer $(gcloud auth print-access-token)" \
"https://<サービス>.googleapis.com/v1/<資源>" | python3 -c "
import json,sys
d=json.load(sys.stdin)['error']
print(d['code'], d['status'])
print(d['message'])
for x in d.get('details', []):
print(' ', x['@type'].split('.')[-1], x)
"
# 2. 記録から 500 系の区分を抽出する(13=内部, 2=不明, 15=データ損失)
gcloud logging read 'protoPayload.status.code=13' \
--limit=50 --format="value(timestamp, protoPayload.methodName, protoPayload.status.message)"
# 3. どの操作で多いかを数える
gcloud logging read 'protoPayload.status.code=13 OR protoPayload.status.code=2' \
--limit=200 --format="value(protoPayload.methodName)" | sort | uniq -c | sort -rn
# 4. 送受信の内容をそのまま見る
gcloud <サービス> <操作> --log-http 2>&1 | sed -n '/== body start ==/,/== body end ==/p'
# 5. 作成の操作が実際には成功していないかを確認する
gcloud <サービス> list --filter="name:<対象名>"
gcloud compute operations list --filter="status!=DONE"
Editor’s Note
500 に対応する3つの区分のうち、性格がもっとも際立つのは不明なエラーの区分です。定義の説明を読むと、これが「原因不明」を意味しないことが分かります。
書かれているのは2つの状況です。別の空間から受け取った状態が、こちらでは未知のエラーの体系に属している場合。そして、十分なエラー情報を返さない窓口からのエラーが、この区分に変換される場合です。
どちらも、伝達の過程で情報が失われた状況を指します。元のエラーには理由があったはずですが、こちら側の語彙に対応する区分がなかった、あるいは相手が理由を返さなかったために、区分を決められなかった。そういう意味です。
再試行との関係では、サービス自身が再試行を指示してくる場合すらあります。BigQuery の利用者の報告(googleapis/google-cloud-go Issue #5248)に記録されている 500(internalError)の文言は、「通常は一時的な問題によるもので、SLA に記載のバックオフに従ってジョブを再試行すれば解消するはず」という趣旨を含み、続くなら問い合わせるよう案内までしています。報告の主題は、公式のプログラム部品がこの 500 を再試行の対象に含めるべきかという議論で、500 が「再試行禁止」ではなく「保証なしの再試行対象」として扱われている様子がそのまま読み取れます。
一方、内部のエラーの区分には「深刻なエラーのために予約されている」と明記されています。同じ 500 でも、こちらが返っているなら重みが違います。
区分名を見る習慣は、GCP のエラー全般で有効ですが、500 では特に効きます。応答本文に手がかりが乏しいぶん、status の1語が持つ情報量が相対的に大きくなるからです。
免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。
この記事でエラーは解決しましたか?