冒頭まとめ

GCP の 429 Too Many Requests は、何かの上限を使い切ったことを示します。エラー区分の定義ファイルでは、利用者ごとの割り当てかもしれないし、ファイル置き場の空き容量かもしれない、という書き方になっています。つまり「要求の回数が多すぎる」とは限りません。量や個数の上限も同じ区分に入ります。

このエラーの扱いやすさは、応答に付く details にあります。上限に関する情報が、機械が読める形で定義されています。何に対する上限か(対象)、どの指標か、上限の識別子、上限の値、上限が適用される条件、そして違反の説明です。さらに、待つべき時間を示す構造も別に定義されています。

したがって、どの上限に当たったかを推測する必要はありません。応答に書かれています。旧来の対処のように、待ち時間を適当に入れて様子を見る、という進め方は不要です。

もう1つ、見落としやすい重要な点があります。上限の出どころが、呼び出したサービスとは限りません。定義には具体例が添えられていて、ある管理サービスを呼び出したときに、その内部で別の計算資源のサービスを使い、そちらの上限に当たる場合がある、と説明されています。この場合、応答にはその依存先のサービス名が入ります。呼び出した先の上限だけを調べても見つからないのは、このためです。

エラーの概要

応答の形は次のようになります。上限に関する詳細と、待ち時間の指示が別々に入ります。

{
  "error": {
    "code": 429,
    "message": "Quota exceeded for quota metric 'Requests' and limit 'Requests per minute'",
    "status": "RESOURCE_EXHAUSTED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.QuotaFailure",
        "violations": [
          {
            "subject": "project:my-project",
            "description": "Quota 'CPUS' exhausted. Limit: 24 in region asia-northeast1.",
            "apiService": "compute.googleapis.com",
            "quotaMetric": "compute.googleapis.com/cpus",
            "quotaId": "CPUS-per-project-region",
            "quotaDimensions": { "region": "asia-northeast1" },
            "quotaValue": "24"
          }
        ]
      },
      {
        "@type": "type.googleapis.com/google.rpc.RetryInfo",
        "retryDelay": "30s"
      }
    ]
  }
}

読むべき項目を順に挙げます。

subject は、何に対する上限かを示します。定義では、利用者の接続元を指す形式と、プロジェクトを指す形式が例として挙げられています。ここが接続元であれば、プロジェクト全体ではなく特定の呼び出し元だけが制限されている、と分かります。

apiService は、上限を持っているサービスです。前述のとおり、呼び出したサービスと違う場合があります。

quotaMetric は数える対象の指標、quotaId は上限の識別子です。定義では、この識別子は上限の名前とも呼ばれ、サービスの中で一意だと説明されています。この値で公式文書を検索すれば、該当する上限の説明に辿り着けます。

quotaDimensions は、上限が適用される条件です。地域ごとの上限であれば地域が入り、全体に適用される上限であれば空になります。

quotaValue は、そのときに適用されていた上限の値です。なお定義には、引き上げの適用が進行中の場合に新しい値が入る項目も用意されています。

RetryInforetryDelay は、同じ要求を再試行するまでに待つべき最短の時間です。定義でも、少なくともこの時間は待つべきだと書かれています。

なお、古い形式の応答では details が付かず、message の文言に指標・上限・サービス・利用者が書かれているだけの場合もあります。読む場所が変わるだけで、読むべき内容は同じです。

まず最初に:QuotaFailure を開く

第一に、details の中に上限に関する詳細があるかを見ます。あれば、そこに答えが書かれています。無ければ、message の文言から同じ情報を読み取ります。

第二に、apiService を見ます。呼び出したサービスと一致するかを確認してください。違っていれば、調べるべき上限はそちらのサービスのものです。

第三に、quotaIdquotaDimensions を控えます。この2つがあれば、管理画面や公式文書で該当する上限を一意に特定できます。

第四に、待ち時間の指示があるかを見ます。あれば、その時間だけ待ってから再試行します。

よくある原因と解決手順

原因1:上限の名前を推測して調べている

最も無駄が多い進め方です。応答に識別子が入っているので、推測は不要です。

Before(管理画面を目で探す):

# どの上限か分からないまま、一覧を眺める

After(応答から識別子を取り出して絞り込む):

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']
for x in d.get('details', []):
    t=x['@type'].split('.')[-1]
    if t=='QuotaFailure':
        for v in x['violations']:
            print('service :', v.get('apiService'))
            print('quotaId :', v.get('quotaId'))
            print('metric  :', v.get('quotaMetric'))
            print('value   :', v.get('quotaValue'))
            print('dims    :', v.get('quotaDimensions'))
            print('subject :', v.get('subject'))
    if t=='RetryInfo':
        print('retry after:', x.get('retryDelay'))
"

取り出した識別子で、現在の上限と使用量を確認できます。

gcloud services quota list \
  --service=<apiService の値> \
  --consumer=projects/<プロジェクト> \
  --filter="metric:<quotaMetric の値>"

原因2:上限を持っているのが別のサービス

前述のとおり、依存先のサービスの上限に当たる場合があります。この形は、呼び出したサービスの上限を調べても見つからないため、原因の特定が遅れます。

apiService の値が、自分が呼び出したサービスと違っていれば、この形です。定義に挙げられている例では、管理サービスを呼び出したのに、内部で作られる計算資源の上限に当たっています。

対処は、その依存先のサービスの上限を確認することです。引き上げの申請も、そちらに対して行います。

原因3:待つべき時間を無視している、または待っても回復しない型の上限

RetryInfo が付いている場合、そこに書かれた時間より短い間隔で再試行しても通りません。定義に、少なくともこの時間は待つべきだと明記されています。

Before(一定の間隔で再試行する):

for i in range(5):
    r = call_api()
    if r.ok:
        break
    time.sleep(1)      # 指示を読んでいない

After(指示された時間を使う):

for i in range(5):
    r = call_api()
    if r.ok:
        break
    delay = extract_retry_delay(r)   # RetryInfo から取り出す
    time.sleep(delay if delay else 30 * (2 ** i))

指示が付いていない場合の公式の指針は、指数的に間隔を伸ばし、ランダム性を加える方式です。429 のクォータのエラーについては、最初の間隔を最短でも30秒とするよう公式文書に書かれています(500 や 503 のサーバー側のエラーは最短1秒で、扱いが違います)。一定の間隔で叩き続けると、上限を使い切った状態から抜け出せません。

もう1つ、待つ前に確認すべきことがあります。公式文書は上限をレート型と割り当て型に分けています。レート型は、サービスごとに定められた間隔で自動的にリセットされるため、待てば回復します。割り当て型は、稼働中の資源の個数などを数える上限で、時間では回復しません。資源を解放するか、引き上げを申請する必要があります。上の応答例の CPU 数の上限は割り当て型で、この場合に再試行を繰り返しても意味がありません。quotaId と説明文で、どちらの型かを先に見分けてください。

なお、公式のソフトウェア開発キットの多くは、再試行の処理を内部で行います。自分で組む前に、使っている道具が対応していないかを確認してください。

原因4:制限の対象が接続元や利用者になっている

subject に接続元が入っている場合や、上限の名前に利用者ごとの単位が付いている場合です。プロジェクト全体の上限ではなく、特定の呼び出し元に対する制限です。

この形では、上限の引き上げを申請しても解決しないことが多くあります。複数の実行環境や身元に分散させる、あるいは呼び出しの間隔を空けるなど、呼び出し側の構成を変える必要があります。

subjectプロジェクトを指していれば、引き上げの申請が有効な選択肢になります。この違いを最初に確認してください。

原因5:並列に動く実行環境が上限を分け合っている

自動で台数が増える構成で起きます。1台あたりの呼び出しは控えめでも、台数分だけ上限が消費されます。

台数が増えたときに 429 が出始めたなら、この形です。対処は3つあります。呼び出しを1か所に集約する、台数の上限を設ける、あるいは要求を順番待ちの仕組みに通して流量を一定に保つことです。

いずれも構成の変更を伴うため、まず quotaDimensions を見て、上限がどの単位で適用されているかを確認してください。地域ごとであれば、地域を分けることで緩和できる場合があります。

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

権限が足りない場合は 403 です。区分の定義には、資源を使い切ったことによる拒否に 403 の区分を使ってはならず、量の超過の区分を使うこと、と明記されています(GCP の 403 の記事)。ただし、例外が公式に文書化されています。公式のクォータのトラブルシューティングによれば、Compute Engine のクォータ超過は 403 のまま返り、識別子は QUOTA_EXCEEDED、レートクォータの場合は RATE_LIMIT_EXCEEDED になります。上限の問題が常に 429 で現れるとは限らない、と押さえてください。

値が有効な範囲の外にある場合は 400 で、区分としては範囲の超過にあたります(GCP の 400 の記事)。1回の要求に含められる個数の上限を超えた場合は、量の問題ではなく値の問題として扱われることがあります。status で見分けてください。

一時的に処理できない場合は 503 です(GCP の 503 の記事)。混雑という点では似ていますが、区分が違うため対処も違います。時間切れは 504 です(GCP の 504 の記事)。

切り分けの順序

  1. details に上限の詳細があるかを見る。あれば推測は不要。無ければ message の文言を読む。
  2. apiService を見る。呼び出したサービスと違えば、調べる先はそちら。
  3. quotaIdquotaDimensions を控える。この2つで上限が一意に決まる。
  4. 上限の型を見分ける。レート型は待てば回復、割り当て型は資源の解放か引き上げ。
  5. subject を見る。プロジェクトなら引き上げが有効、接続元や利用者単位なら呼び出し側の構成を変える。
  6. 待ち時間の指示があれば、その時間を守る。無ければ間隔を倍々に伸ばす。公式の指針では最初の間隔は最短30秒。
  7. 台数が増えたときに出始めたなら、並列の実行環境が上限を分け合っている。
  8. 引き上げの申請は、quotaId を添えて行う。名前を伝えられれば手続きが早い。

確認コマンド集

# 1. 応答から上限の識別子と待ち時間を取り出す
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']
for x in d.get('details', []):
    t=x['@type'].split('.')[-1]
    if t=='QuotaFailure':
        for v in x['violations']:
            print(v.get('apiService'), '|', v.get('quotaId'), '|', v.get('quotaValue'), '|', v.get('quotaDimensions'))
    if t=='RetryInfo':
        print('retryDelay:', x.get('retryDelay'))
"

# 2. 現在の上限と使用量を確認する
gcloud services quota list \
  --service=<サービス>.googleapis.com \
  --consumer=projects/<プロジェクト>

# 3. 記録から 429 を抽出し、どの操作で多いかを数える
gcloud logging read 'protoPayload.status.code=8' \
  --limit=200 --format="value(protoPayload.methodName)" | sort | uniq -c | sort -rn

# 4. 上限の使用率を継続的に見る指標を確認する
gcloud monitoring metrics list --filter="metric.type:quota" --limit=10

# 5. 有効になっているサービスの一覧(依存先を探す手がかり)
gcloud services list --enabled

Editor’s Note

429 の応答に含まれる情報の細かさは、他のエラーと比べても際立っています。定義ファイルを読むと、上限に関する項目が7つ用意されていることが分かります。対象、説明、上限を持つサービス、指標、識別子、適用条件、上限値。さらに、引き上げの適用が進行中である場合に、新しい値を伝えるための項目まであります。

ここまで揃っているのは、上限の問題が「どの上限か」を特定できないと何もできない性質だからでしょう。回数なのか量なのか、プロジェクト単位なのか地域単位なのか、そもそもどのサービスの上限なのか。これらが分からないまま、待ち時間を伸ばしたり呼び出しを減らしたりしても、当たっているかどうかすら判断できません。

利用者単位の上限の実例として、HashiCorp が公式のサポート記事(Google API Error 429 Quota exceeded for ‘Queries’)で扱っている形があります。Terraform の GCP プロバイダで大量の資源を管理していると、実行のたびに全資源の状態を確認しに行くため、「Queries per minute per user」という利用者単位のレート上限に当たる、というものです。応答は古い形式で details が無く、指標・上限・サービス・利用者がすべて message に書かれていました。示されている解決策は、長寿命の資源を別の作業単位に分離して確認の回数自体を減らす、自動の実行を止める、実行の間隔を空ける、複数の身元に分散する——いずれも呼び出し側の構成を変える方向です。利用者単位のレート上限では引き上げよりも呼び出し方を変える、という原因4の結論と重なります。

とりわけ有用なのが、上限を持つサービスを示す項目です。定義の説明には、上限の問題が呼び出したサービス以外から生じる場合がある、という前置きが付いています。挙げられている例は、管理サービスを呼び出したときに、その内部で計算資源のサービスを使い、そちらの上限に当たるという状況です。この構造は、外から見ているだけでは決して分かりません。

429 に当たったら、まず details を開く。quotaId を控える。そして、レート型か割り当て型かを見分ける。待つべきか、減らすべきか、解放すべきかは、そこで決まります。


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