6月のある朝、前夜に走ったバッチのログを開いたら、見慣れない失敗が縦に並んでいました。アプリのレビュー解析、画像メタデータの生成、運用レポートの下ごしらえ。普段なら静かに終わっているはずの日次ジョブが、同じ顔をして全部止まっています。調べてみると、Gemini 側で error 1076 や error 1099 が広範に発生していて、報道では「過去最大級の障害」とまで書かれていました。
正直に言うと、最初に確認したのは復旧見込みではありませんでした。自分のジョブが「どう失敗したか」です。私自身の手で直せるのは障害そのものではなく、障害のあいだ自分のシステムがどう振る舞うかという、自分の側の設計だけだからです。今日はその答え合わせの記録を、運用所感として書き残しておきます。
まず事実関係を短く
今回の障害では、Gemini のアプリや API を含む広い範囲で error 1076 / error 1099 系のエラーが報告され、エンジニアリングチームの緩和策で段階的に回復へ向かった、というのが外から見える経緯です。個別のサービスにどこまで影響があったかは利用者側から正確には見えないので、ここでは自分の手元で観測できたことに限定して書きます。
障害らしき朝に見る場所を、私は順番ごと決めてあります。
- 自分のジョブのログ(どの段階で・どのエラーで止まったか)
- Google Cloud Service Health や公式の告知(影響範囲の確認)
- SNS での同報(自分だけの問題かどうかの切り分け)
順番が大事で、最初に見るのは常に自分のログです。外の情報は「世界のどこが壊れているか」を教えてくれますが、「自分の何が積み残されたか」はログにしか書かれていません。
障害のあいだ、パイプラインは「諦め方」どおりに動いていました
前提を少しだけ。私は個人開発のアプリ運用と複数サイトの運営で、Gemini API を組み込んだ日次ジョブを毎日いくつも動かしています。壁紙アプリのレビューを朝までに整理しておくジョブ、ストア用画像のメタデータを作るジョブ、前日のメトリクスをまとめるジョブ。どれも深夜から早朝のオフピークに分散してあり、人間が寝ているあいだに終わっている前提の運用です。
この運用を組んだときに決めた原則が一つあります。「リトライ → 退避 → ログに記録して静かに終了」。エラーが出たら指数バックオフで数回だけ再試行し、それでも駄目なら処理途中の成果物をローカルに退避して、異常終了ではなく計画的な終了として抜ける、という流れです。
障害当日のログを追うと、各ジョブはきれいにこの順番で諦めていました。3回のリトライがすべて失敗し、未処理分が PENDING として退避され、終了コードは 0。翌朝の私がやることは、退避された分を再実行キューに戻すことだけでした。再実行で同じ内容が二重に処理されないか、という論点もありますが、ここは以前 Gemini API の冪等性キー設計の実装パターン に書いた仕組みがそのまま効いてくれました。
障害が答え合わせをしてくれた3つの設計判断
リトライ上限は「API が直る時間」ではなく「自分が諦めてよい時間」で決めます
リトライというと、障害からの回復を待つための仕組みに聞こえます。しかし大規模障害の前では、リトライはほぼ無力です。広い範囲で何十分も落ちているものは、3回引き直しても落ちています。
それでも私がリトライを外さないのは、一時的な揺らぎ(タイムアウトや単発の 5xx)と本物の障害を、コードに区別させるためです。数回で直るなら揺らぎ、直らないなら障害。そこから先は粘らずに退避へ移ります。リトライは回復の手段というより、諦めの判断を機械に任せるための仕組みだと考えています。
縮退の最終段に「今日は何もしない」を置いています
以前 Gemini API にサーキットブレーカーと段階的縮退を組み込む設計の所感 という記事で、フルレスポンス → 簡略レスポンス → キャッシュ → 停止という4段階の縮退を書きました。今回あらためて実感したのは、最終段の「停止」を後ろめたいものにしない設計の効き目です。
日次のレビュー解析が1日飛んでも、ユーザーは困りません。翌日に2日分やれば追いつきます。「今日はやらない」を正規の状態として設計に組み込んでおくと、障害の夜に人間が起きて張り付く必要がなくなります。逆にここを曖昧にしておくと、半端に成功したジョブと半端に失敗したジョブが混ざり、翌朝の復旧がいちばん重い仕事になってしまいます。
ログは翌朝の自分への手紙として書きます
障害の朝にいちばん助けられたのはログでした。どのジョブが、どの段階まで進み、どのエラーで止まり、何を退避したか。これが揃っていたので、復旧後の再実行リストは grep 一発で作れました。
ログを書くとき、私は「いま困っている自分」ではなく「明日の朝に読む自分」を想定しています。エラーメッセージを生のまま流すのではなく、後続の判断に必要な情報(ジョブ名・到達段階・退避先・再実行に必要な引数)を1行に揃えておく。これだけで、障害対応は半分終わっているようなものです。
障害明けに加えた、ひとつだけの変更
仕組みはおおむね設計どおりに働いてくれたのですが、ログを読み返して一つだけ直したくなった点がありました。各ジョブが個別にリトライして、個別に諦めていたことです。同じ夜に5本のジョブが走れば、5本がそれぞれ3回ずつ、合計15回の無駄打ちになります。障害は「ジョブの問題」ではなく「パイプライン全体の問題」なので、判断も全体で1回やれば足ります。
そこで、夜のバッチ群の先頭に軽量のヘルスチェックを1本だけ置くようにしました。
# 日次バッチ群の先頭で1回だけ実行する軽量ヘルスチェック
# ここで駄目なら後続ジョブをまとめて見送り、翌日に回す
import sys
import time
from google import genai
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
# 再試行する価値があるエラーの目印(一時的な揺らぎ)
RETRYABLE = ("500", "502", "503", "504", "timeout", "deadline", "unavailable")
def preflight(max_retries: int = 2) -> bool:
for attempt in range(max_retries + 1):
try:
client.models.generate_content(
model="gemini-3.5-flash",
contents="ping",
)
return True # 1回通れば充分
except Exception as e:
message = str(e).lower()
if not any(key in message for key in RETRYABLE):
raise # 認証エラーや設定ミスはリトライで直らないため即座に表面化させる
time.sleep(2 ** attempt) # 1秒 → 2秒 → 4秒
return False
if not preflight():
print("SKIP: Gemini API が不安定なため、本日のバッチ群は見送ります")
sys.exit(0) # 異常(1)ではなく計画的な見送り(0)として終了する正常時はこのスクリプトは何も表示せず通過し、障害時には SKIP の1行を出して終了コード 0 で抜けます。終了コードを 0 にしているのは、スケジューラ側のアラートを鳴らさないためです。障害の夜に欲しいのは「止まった」という通知ではなく、「設計どおりに止まることに成功した」という静けさでした。
ヘルスチェックの ping 1回ぶんのトークンはごくわずかで、コストはほぼ誤差です。それで後続ジョブの無駄打ちと、半端な失敗ログの山が消えるなら、個人開発の規模でも充分に元が取れます。
次の一歩
もし API 依存の定期ジョブを運用しているなら、どこか1箇所でいいので「今日は何もしない」を正規の終わり方として許す分岐を入れてみてください。リトライの回数を増やすことよりも、きれいに諦める道を1本用意しておくことのほうが、障害の夜の自分を確実に楽にしてくれます。
障害は迷惑なものですが、こちらの設計の答え合わせをしてくれる機会でもあります。同じように API 依存の自動運用を抱えている方の、何かの参考になれば幸いです。