Claude Media
Claude apps gatewayでPostgres障害中も/readyzを保つ2つの設定

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停止中の動き既定ではフェイルオープンで、推論は流れる
/readyzPostgres停止中の動き既定ではストアに届かなくなった時点で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

この行が出ても、異常とは限りません。コールドインスタンスのネットワークが整う前に起動した場合にも出ます。その後に起動が完了するなら、対処は要りません。

見分け方は次のとおりです。

  1. 起動が完了するなら、そのまま様子を見る
  2. could not connect to Postgresで終了するなら、store.postgres_urlが1ホストを指しているかと、データベースまでのネットワーク経路を確認する
  3. 拒否ではなく、試行がタイムアウトで失敗しているなら、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. 1

    全レプリカを上げる

    v2.1.282以降のゲートウェイに全レプリカを更新します。

  2. 2

    設定にキーを足す

    gateway.yamlのstoreにキーを追加して、再度ロールします。

  3. 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点は障害時の前提として運用側に伝えておくと迷いません。

この記事を共有:XはてブLinkedIn