Claude apps gatewayでPostgres障害中も/readyzを保つ2つの設定
Claude apps gatewayでPostgresが落ちても全レプリカが一斉に外れないよう、store.readiness_grace_secondsとconnect_timeout_secondsの役割と値の決め方をまとめます。
Claude apps gatewayは、Postgresが落ちても、サインイン済みの開発者の推論を処理できます。それでも既定のままだと、データベースのフェイルオーバーが始まった瞬間に、全レプリカが同時にロードバランサーから外れます。store.readiness_grace_secondsはこの一斉離脱を遅らせる設定です。
もう1つのstore.connect_timeout_secondsは、似た場面で名前が出ますが役割が違います。こちらは接続を開く側の待ち時間です。本記事では2つのキーを分けて、どの症状にどちらを触るかと、値の決め方を扱います。
既定では、Postgres障害で/readyzが全レプリカ同時に落ちる
ゲートウェイはGET /healthzを生存確認、GET /readyzを受け入れ可否の確認として公開します。/readyzはストアに届くかを見ます。どちらもaccess_control.allow_cidrsの対象外なので、アクセス元を絞ったリスナーでもプローブは通ります。
Postgresが止まったときの動きは、機能ごとに分かれます。
| 対象 | Postgres停止中の動き |
|---|---|
| サインイン済みの開発者 | Postgres停止中の動き継続。ベアラートークンはJWTシークレットでローカル検証され、セッション更新もストアに触れない |
| 新規サインイン | Postgres停止中の動き失敗。デバイスフローとレート制限のカウンターがPostgresにあるため |
| 支出上限の判定 | Postgres停止中の動き既定ではフェイルオープンで、推論は流れる |
/readyz | Postgres停止中の動き既定ではストアに届かなくなった時点でnot-readyを返す |
問題は最後の行です。/readyzが不通になると、全レプリカが同時にreadinessチェックに落ちます。通過したレプリカにしかトラフィックを送らない構成では、ゲートウェイ自身はまだ推論を処理できるのに、全リクエストが失敗します。/healthzは障害の間も通り続けるので、再起動ループにはなりません。止まるのは到達性のほうです。
readiness_grace_secondsで離脱を遅らせる
store.readiness_grace_secondsは、Postgresが応答しなくなってから/readyzがreadyを返し続ける最大秒数です。0〜3600の整数で、既定は0(猶予なし)です。
store:
postgres_url: "postgres://gateway@db.internal:5432/gateway"
password: "${file:/run/secrets/db-password}"
readiness_grace_seconds: 300 # フェイルオーバーより長く猶予の間、レプリカはreadyのままなので、サインイン済みの開発者の推論は止まりません。猶予を超えてもストアが戻らなければ、/readyzは通常どおりnot-readyに変わります。
秒数はフェイルオーバーの実測から決める
目安は「データベースのフェイルオーバーにかかる時間より長く」です。マネージドDBの切り替え時間は構成ごとに違うので、ステージングでフェイルオーバーを起こして、接続が戻るまでの秒数を測ります。そこに余裕を足した値が出発点です。例として挙がるのは300です。
長ければ安全、とはなりません。理由は支出上限にあります。支出上限を使わない環境なら、未計量になるものがないので、余裕を厚めに取る判断もできます。上限を有効にしている環境では、enforcement.fail_closed_on_errorの値で猶予の意味が変わります。
支出上限ありのときの猶予
fail_closed_on_error: false(既定)
レプリカがreadyの間の推論は、Postgresが戻るまで計量されません。猶予が長いほど未計量の時間も延びるので、フェイルオーバーを覆える範囲で最小にします。
fail_closed_on_error: true
猶予の長さに関係なく、サインイン済みの開発者の推論は429で拒否されます。猶予が効くのは/readyzの到達性だけなので、秒数は未計量の量ではなく、ロードバランサーから外れる時間で決めます。
上限の仕組みは開発者ごとの支出上限にまとめています。fail_closed_on_errorはadmin:ブロックがある構成でだけ働きます。trueにしてadmin:が無いと、ゲートウェイは起動しません。
プローブ側の設定と合わせて実時間を見る
/readyzがreadyを返し続ける秒数は、ゲートウェイ側の猶予です。ロードバランサーやオーケストレーターがレプリカを外すまでの実時間は、これにプローブ側の失敗判定が加わります。KubernetesのreadinessProbeなら、periodSecondsとfailureThresholdの積が上乗せされます。
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 10
failureThreshold: 3この例では、猶予が300秒なら、ストアが止まってから約330秒後にレプリカが外れる計算です。フェイルオーバーより長いかを確かめるときは、猶予だけでなく、この上乗せ分も含めて見ます。支出上限を使う環境では、未計量の時間もそれだけ延びます。
postgres_urlは1ホストだけ
store.postgres_urlに書けるのは1ホストです。ノードが複数あるデータベースでは、手前のアドレスを書きます。マネージドサービスのエンドポイント、ロードバランサー、仮想IPのいずれかです。複数ホストを並べてゲートウェイ側で切り替えさせる構成は取れません。
このため、フェイルオーバーの切り替えは、1つのアドレスの向こう側で起きます。ゲートウェイからは「しばらく答えない」状態に見えるので、猶予の秒数はその間を覆う長さで測ります。分散SQLデータベースのように、Postgresプロトコルだけを実装したものは対象外です。
猶予が効かない場面
次の3つは、猶予を入れても挙動が変わりません。
- フェイルクローズ:
enforcement.fail_closed_on_error: trueにしていると、Postgresが戻るまで、サインイン済みの開発者の推論は429のspend limit unavailableで拒否されます。レプリカがreadinessに通っていても同じです。猶予が守るのは到達性までで、推論の成否は決めません - 新規サインイン: デバイスフローがPostgresを使うので、猶予があっても復旧までは失敗します
- 復旧しないレプリカ: 猶予を過ぎればnot-readyに戻ります
readinessのプローブを/healthzに向けると何が起きるか
猶予を使わない代案として、readinessプローブ自体を/healthzに向ける方法もあります。障害中も全レプリカが通過し続けるので、短い停止なら一斉離脱は避けられます。
欠点も明らかです。/healthzはnot-readyを返さないため、Postgresとの接続が戻らないレプリカも通過し続けます。ストアに永久に届かない異常なレプリカが、トラフィックを受け続けることになります。/readyzと猶予の組み合わせなら、猶予を過ぎた時点でそのレプリカを外せます。
使い分けは次のとおりです。
| プローブの向け先 | 障害中 | 戻らないレプリカ |
|---|---|---|
/readyz(猶予0) | 障害中全レプリカが即座に外れる | 戻らないレプリカ外れる |
/healthz | 障害中全レプリカが通る | 戻らないレプリカ通り続ける |
/readyz + 猶予 | 障害中猶予の間は通る | 戻らないレプリカ猶予後に外れる |
connect_timeout_secondsは「接続を開く待ち時間」
store.connect_timeout_secondsは、ゲートウェイがPostgresへの接続を開くときに待つ秒数です。1〜60の整数で、既定は5です。
readinessの猶予とは別物です。猶予は「ストアが答えなくなったあと、/readyzをいつまでreadyにするか」で、タイムアウトは「接続を試みる1回あたり、どれだけ待つか」です。
触る場面は起動時です。データベースに届かない状態でゲートウェイを起動すると、2秒間隔で3回まで接続を試して、それでも届かなければ終了します。ログには次のような行が出ます。
could not connect to Postgres at boot, attempt 1 of 3この行が出ても、異常とは限りません。コールドインスタンスのネットワークが整う前に起動した場合にも出ます。その後に起動が完了するなら、対処は要りません。
見分け方は次のとおりです。
- 起動が完了するなら、そのまま様子を見る
could not connect to Postgresで終了するなら、store.postgres_urlが1ホストを指しているかと、データベースまでのネットワーク経路を確認する- 拒否ではなく、試行がタイムアウトで失敗しているなら、
connect_timeout_secondsを上げる
store:
postgres_url: "postgres://gateway@db.internal:5432/gateway"
connect_timeout_seconds: 15 # 既定は5。冷えたインスタンスの起動が遅いとき3つ目が決め手です。接続が拒否されているなら、待ち時間を伸ばしても直りません。URLかファイアウォールが原因です。
追加する順番とロールバックの注意
2つのキーは、サーバー側のバージョンが条件です。
| キー | 必要なバージョン |
|---|---|
store.connect_timeout_seconds | 必要なバージョンClaude Code v2.1.274以降 |
store.readiness_grace_seconds | 必要なバージョンClaude Code v2.1.282以降 |
古いゲートウェイは、これらのキーを見つけると起動を拒否します。ローリングデプロイの途中で新旧が混在している間にキーを入れると、旧レプリカが起動に失敗します。順序は次のとおりです。
キーを足すまでの順序
- 1
全レプリカを上げる
v2.1.282以降のゲートウェイに全レプリカを更新します。
- 2
設定にキーを足す
gateway.yamlのstoreにキーを追加して、再度ロールします。 - 3
ステージングで検証する
フェイルオーバーを起こして、
/readyzが猶予の秒数だけ通り続けるかを見ます。
ロールバックにも注意が要ります。設定は旧バイナリのスキーマで再検証されるので、新しいリリースで足したキーが残っていると、旧バージョンの起動が失敗します。旧バージョンへ戻す前に、2つのキーを設定から外します。マイグレーションは追記のみなので、バイナリを戻すこと自体は安全です。
フェイルオーバーの検証手順
ステージングでフェイルオーバーを起こし、別ターミナルから/readyzのステータスを1秒おきに記録します。
while true; do
date +%T
curl -s -o /dev/null -w '%{http_code}\n' \
http://gateway.internal:8080/readyz
sleep 1
done見るのは2点です。
- フェイルオーバーの開始後も、猶予の秒数のあいだステータスが変わらないか
- DBが戻る前に猶予が尽きたとき、ステータスが変わるか
開始直後にステータスが変わるなら、猶予が効いていません。設定がそのレプリカに反映されているか、バージョンがv2.1.282以降かを確かめます。逆に猶予が尽きても変わらないなら、プローブが/healthzに向いている可能性があります。
デプロイ先がAWSかGCPなら、構成例はAWSとGCPの記事にあります。プローブや運用全般はデプロイと運用、全キーの一覧は設定リファレンスでたどれます。キーが入ったバージョンの変更点はv2.1.282とv2.1.274のリリースノートです。
まとめ
Postgres障害の影響を減らす設定は2つあり、触る場面は別です。
- 障害中にトラフィックまで止まるなら、
readiness_grace_secondsをフェイルオーバーより少し長く設定する。支出上限を使うなら、必要最小限にする - 起動時の接続がタイムアウトで失敗するなら、
connect_timeout_secondsを上げる
どちらも、先に全レプリカをv2.1.282以降へ上げてから足します。フェイルクローズと新規サインインは猶予では救えないので、その2点は障害時の前提として運用側に伝えておくと迷いません。