インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

Podリソース設計|Requestsと停止の猶予

公開

第10回で、開発者に何を書かせるかが決まりました。次に決めるのは、その値をどう算出するかです。

第10回で「開発チームが決める」と分類したのは3つでした。Requests / Limits の値、Probeのエンドポイントと閾値、レプリカ数とHPAの閾値です。本記事は、その3つに具体的な数値を入れるための設計書を作ります。

この領域は、実務で最も「とりあえずの値」が残りやすい場所です。Requestsを他のPodからコピーし、Limitsを2倍にして、Probeは起動が間に合わないのでinitialDelaySecondsを伸ばす。その状態でも動いてしまうため、値の根拠を誰も説明できないまま本番に入ります。そして障害の当日に、その値が誰の判断だったのかを探すことになります。

本記事はGoogle Cloud(GKE)を前提としますが、扱う内容の大部分はKubernetesの仕様であり、クラウドに依存しません。ただし、設定できる値の上限を決めているのはGKE側の仕様です。この2層を混ぜないでください。「Kubernetesではこう書ける」と「GKEではここまでしか効かない」は別の話です。

目次
  1. 10回分の決定が、1つのPodスペックに集まる
    1. この記事で作る成果物
    2. 6つの項目は独立していない
  2. Requests と Limits は、別々の問いに答える
    1. Requestsが決めているのは、置き場所と退避の順番
    2. CPUとメモリでは、超えたときの挙動が違う
    3. QoSクラスは設定項目ではなく、結果である
    4. 測った数値を、どう値に変換するか
    5. CPUのLimitsを設定するかは、2つの材料で決める
    6. VPAの推奨値を使う場合の制約
    7. Autopilotでは、この章の一部が設計項目でなくなる
  3. HPAは「いつ縮めるか」が設計項目
    1. 既定値を3つ押さえる
    2. 閾値はRequestsの割合である、ということの帰結
    3. 起動が遅いアプリでの誤作動
    4. minReplicas と maxReplicas は、他の回の上限と突き合わせる
  4. Probeは「何を返すか」を決めてから設定する
    1. 3つの役割は排他ではない
    2. Startup Probeの要否は式で決まる
    3. 既定値のうち、誤設定が起きやすい1つ
    4. 実装で踏みやすい3つの落とし穴
    5. Probeが担う範囲と、監視が担う範囲
  5. 停止の設計 ― 15秒の壁と、経路から外れるまでの時間差
    1. 終了シーケンスで、並行して起きること
    2. preStopに待機を入れる
    3. terminationGracePeriodSecondsの算出式
    4. 猶予の上限は、3方向から来る
    5. サイドカーがあるときの終了順序
    6. プローブ単位の猶予
  6. 落ちても止まらない配置 ― PDBと分散制約
    1. PDBが効かない3つの経路
    2. minAvailable と maxUnavailable の決め方
    3. topologySpreadConstraintsの既定は、実質的に無効
    4. DoNotScheduleを選ぶと、Pendingが起きる
    5. 分散計算で見落とされる3点
  7. 【実務テンプレート】Podリソース・耐障害性設計書
    1. 耐障害性チェックリスト(本番投入前)
  8. 次回予告

10回分の決定が、1つのPodスペックに集まる

本記事で新しく決めることは、実のところ多くありません。過去10回で決めた制約を、1つのマニフェストの上に落とす回です。

過去回で決まっていること本記事のどこに効くか
第1回: Spot VM上でアプリケーションPodに与えられる猶予は15秒terminationGracePeriodSeconds の上限
第2回: 値は開発が決め、範囲は運用が縛る設計書の各欄を誰が埋めるかの根拠
第3回: ゾーン構成と、Podセカンダリ範囲から導かれる最大ノード数分散制約のドメイン数、HPAの maxReplicas
第4回: ResourceQuota / LimitRange の上下限、SpotノードのtaintRequests / Limits が取りうる範囲、分散計算への影響
第5回: Requests必須のポリシー、Pod Security Standards未設定を許さないという前提
第10回: valuesで公開する3項目本記事の成果物を誰が記入するか

この表の右列が埋まらない項目があるなら、対応する回の決定が未了です。本記事で場当たり的に決めず、該当する回に戻ってください。第10回で書いたのと同じ構造です。

この記事で作る成果物

2つです。Podスペックテンプレート(値と、その値を決めた根拠を並べて記入する様式)と、耐障害性チェックリスト(本番へ入れる前に確認する項目)です。

1つ目で重要なのは根拠の欄です。値だけを書いた設計書は、半年後に見直すときに使えません。「なぜ2Giなのか」が残っていないと、増やすことも減らすこともできず、結局そのまま残ります。

6つの項目は独立していない

本記事で扱うのは、Requests / Limits、HPA、Probe、Graceful Shutdown、PodDisruptionBudget、topologySpreadConstraints の6つです。設定項目の一覧として並べると独立して見えますが、実際には3組の依存関係があります。

  • Requests → HPAの閾値。HPAの目標使用率はRequestsに対する百分率です。Requestsを変えると、同じ「70%」の意味が変わります
  • Probe ↔ 終了処理。Podが経路から外れる契機はProbeの失敗と削除の2つです。両方を設計しないと、片方の経路でリクエストを落とします
  • PodDisruptionBudget ↔ 分散制約。「同時に減ってよい数」と「どこに置くか」は対です。片方だけでは、同じノードに全レプリカが乗った状態でPDBだけが成立するという事態が起きます

以降の章はこの依存に沿って並べています。Requestsから始めるのは、それが他の5つの入力になっているからです。

Requests と Limits は、別々の問いに答える

この2つは「リソース設定」としてひとまとめに語られますが、答えている問いが違います。Requestsは「どこに置くか」と「誰から停止させられるか」、Limitsは「どこで頭打ちにするか」です。

Requestsが決めているのは、置き場所と退避の順番

スケジューラが配置先の判断に使うのはRequestsです。Limitsは配置に影響しません。ここまでは広く知られています。

知られていないのは、その先です。ノードのリソースが逼迫したとき、kubeletはPodを停止させて資源を回収します(node-pressure eviction)。このとき選ばれる順番が、Requestsで決まります。

  1. 使用量がRequestsを超えているBestEffort / BurstableのPod。この中では、Priorityの低いものから、次に超過量の大きいものから選ばれます
  2. GuaranteedのPodと、使用量がRequests未満のBurstableのPod。Priorityの低いものから選ばれます

公式ドキュメントは、kubeletがQoSクラスそのもので順序を決めているわけではないと明記しています。順序を決めているのは「Requestsを超えているかどうか」です。QoSクラスは、その結果を推定するための目安にすぎません。

ここから、実務で最も多い誤りが導かれます。Requestsを実測より低めに置くと、そのPodは退避候補の先頭に立ちます。「余裕を持って低めに申告しておく」という直感が、そのまま逆に働きます。

ノードがOOMに至った場合も同じ方向です。kubeletはコンテナに oom_score_adj を設定します。

QoSクラスoom_score_adj意味
Guaranteed-997最も選ばれにくい
BestEffort1000最も選ばれやすい
BurstableRequestsがノード容量に占める割合から計算(2〜999)Requestsが小さいほど値が大きく、先に選ばれる

Burstableの値は、1000から「メモリのRequestsがノードの搭載メモリに占める割合の1000倍」を引く形で計算されます(下限2・上限999)。Requestsを小さく申告するほど値が大きくなり、OOM killerに選ばれやすくなります。

結論を1文で言うと、Requestsは節約する項目ではなく、優先順位の申告です。設計書の記入欄には「見積もり」ではなく「この値を下回る使用量では業務が成立しない下限」と書いてください。

CPUとメモリでは、超えたときの挙動が違う

Limitsを決める前に、CPUとメモリで強制のされ方が違うことを押さえてください。公式ドキュメントの記述を対比すると明確です。

資源強制のされ方超過したときに起きること
CPUカーネルが強制するハードリミット。スロットリングで抑え込む処理が遅くなる。停止はしない
メモリ事後的な強制。カーネルがメモリ逼迫を検出したときにOOMで停止させるコンテナが停止して再起動する。一時的に超過しても即座に停止するとは限らない

公式は、メモリについて「コンテナはメモリのLimitsを超えて使用することがあるが、その場合は停止させられることがある」と書いています。メモリのLimitsは超えられるが、超えた代償が停止である、という非対称です。

したがって、同じ「Limits」でも決め方の質問文が違います。メモリは「ここを超えたら停止させてよいか」、CPUは「ここで頭打ちにしてよいか」です。メモリのLimitsを実測のピークぎりぎりに置くと、スパイクのたびにコンテナが再起動します。CPUのLimitsを低く置いても停止はしませんが、レイテンシが伸びます。どちらの失敗が許容できないかで、余裕の取り方が変わります。

QoSクラスは設定項目ではなく、結果である

QoSクラスを直接指定するフィールドはありません。Requests と Limits の書き方から自動的に決まります。

QoSクラス成立条件
GuaranteedすべてのコンテナがCPUとメモリの双方にRequestsとLimitsを持ち、いずれも0より大きく、Requests と Limits が等しい
BurstableGuaranteedの条件を満たさず、いずれかのコンテナにCPUかメモリのRequestsまたはLimitsがある
BestEffortどのコンテナにもCPU・メモリのRequestsもLimitsも一切ない

注意点は「すべてのコンテナ」という条件です。サイドカーを1つ足してRequestsを書き忘れると、Pod全体がGuaranteedから外れます。ログ転送のサイドカーを追加したデプロイ以降、退避されやすくなった、という形で現れます。

もう1つ、運用で効く仕様があります。QoSクラスはPodの作成時に決まり、生存期間中は変わりません。稼働中にリソースを変更する機能を使う場合も、QoSクラスが変わるような変更はアドミッションの段階で拒否されます。

第4回で、Requests / Limits が未設定のPodはBestEffortになり、退避時に最初に停止させられると書きました。LimitRangeで既定値を注入していたのは、この結果を避けるためです。Autopilotではプラットフォーム側が最小値まで引き上げるため、BestEffortに落ちる事象自体が起きません。

測った数値を、どう値に変換するか

ここからが算出の手順です。どういうテストをどの環境で実施するかは第12回の管轄なので、本記事では「テストの結果として、コンテナごとのCPUとメモリの時系列が手元にある」状態から始めます。

  1. メモリのRequests。安定後の定常使用量を使います。起動直後のピークではありません
  2. メモリのLimits。実測の最大値に余裕を乗せます。余裕の根拠は「そのアプリケーションが一時的に確保しうるバッファの最大サイズ」であり、一律の倍率ではありません
  3. CPUのRequests。定常状態の使用量を使いますが、平均ではなくパーセンタイルを取ります。平均を採ると、負荷が平均を超えている時間帯は常に他のPodとCPUを奪い合う状態になります。どのパーセンタイルを採るかは「何%の時間帯なら奪い合ってよいか」で決めてください。95パーセンタイルを採るとは、5%の時間帯は奪い合いを許容すると宣言することです
  4. CPUのLimits。設定するかどうかから決めます(次項)

ここで「1.5倍」「2倍」といった係数を提示しません。ワークロードによって妥当な係数が違い、根拠のない数字を書くと、読者は自組織の測定をせずに写します。代わりに渡すのは、係数の扱い方です。

  • 係数を使うなら、その係数を設計書に明記する
  • 係数を選んだ理由(何が一時的に増えるのか)を1行で書く
  • 見直しのトリガーを、日付ではなく観測できる条件で書く(OOMKilledが発生した、レイテンシが悪化した、など)

第6回でADRの見直しトリガーを条件で書くと決めました。リソース値も同じ性質の決定です。「四半期ごとに見直す」と書いた設計書は、見直されないまま残ります。

CPUのLimitsを設定するかは、2つの材料で決める

この論点は現場で意見が割れます。判断材料を2つに整理してください。

  1. Guaranteedにしたいか。QoSの条件上、GuaranteedにはCPUについてもRequestsとLimitsの一致が必要です
  2. 空いているCPUを使わせたいか。Limitsを設定しなければ、ノードに空きがある限り使えます。設定すれば、空きがあってもそこで絞られます

切り分けの目安は次のとおりです。レイテンシの安定を最優先する本番のリクエスト処理系はGuaranteed寄り、バッチや非同期処理は空きを使わせる側。前者は「他のPodの影響を受けない」ことに価値があり、後者は「空いている時間に早く終わる」ことに価値があります。

ただし、「決められないからLimitsを付けない」を既定にしないでください。第4回でLimitRangeによる上限を入れているため、無制限にはなりません。設計書には「付けない」という判断と、その理由を書きます。空欄と「付けないと決めた」は違います。

VPAの推奨値を使う場合の制約

初期値の当て推量を減らす手段として、GKEの垂直Pod自動スケーリング(VPA)の推奨値を参照できます。Autopilotクラスタでは既定で有効で、推奨値には相応の履歴が必要なため、公式は24時間以上稼働するワークロードを前提としています。

ただし、設計に直結する制約があります。公式ドキュメントは「CPUまたはメモリでHPAとVPAを併用しないこと」と明記しています。同じ資源で両方を動かすにはマルチディメンションPod自動スケーリングが必要で、カスタムメトリクスや外部メトリクスでHPAを動かす場合は併用できます。

したがって、VPAをPodへ自動適用するかどうかは、HPAをCPUで動かすかで先に決まります。HPAをCPU使用率で動かす前提なら、VPAは適用せず推奨値の参照だけに使うのが衝突のない使い方です。自分たちが出した値と推奨値の差を見て、差が大きい項目だけ測り直します。導入の可否そのものは第6回の枠組み(要否を先に判定する)に従ってください。

Autopilotでは、この章の一部が設計項目でなくなる

Autopilotを選んだ場合、値の自由度が下がります。第3回で運用モードを決めているので、その決定がここに効いてきます。

項目Autopilot(汎用コンピューティングクラス)の扱い
Requests未指定既定値が入る(CPU 0.5 vCPU / メモリ 2 GiB。DaemonSetは 50 mCPU / 100 MiB / 一時ストレージ 100 MiB)
最小値を下回るRequests自動的に引き上げられる
最大値を超えるRequestsワークロードが拒否される(CPU 30 vCPU / メモリ 110 GiB)
CPUとメモリの比率1:1 から 1:6.5 の範囲に制限される

比率の制限は見落とされがちです。メモリだけを大きく取る構成は取れません。メモリを増やすとCPUのRequestsも引き上げられ、その分だけ費用が増えます。第4回で確認したとおりAutopilotの課金はPodの要求リソースに対するものなので、ここでの制約はそのまま費用に直結します。

バーストを使う場合、要件はGKE 1.30.2-gke.1394000 以降です。Limitsを省略するかRequestsより大きく設定すると、ノードの空き容量を使えます。ただし公式の表現は「バーストは日和見的であり、保証されない」です。CPUが超過分を絞られるのに対し、メモリが超過するとGKEがコンテナを終了させます。

バーストを前提に容量計画を立ててはいけません。定常状態で必要な量はRequestsに書き、バーストはまれなスパイクを吸収する余地として扱います。なお、バーストに対応したクラスタでは最小値が 50m CPU / 52 MiB まで下がります(非対応のクラスタは 250m CPU / 512 MiB)。小さなPodを多数動かす構成では、この差が効きます。

HPAは「いつ縮めるか」が設計項目

HPAの設計というと目標使用率を何%にするかの話になりがちですが、既定のままで問題になりやすいのはスケールインの側です。スケールアウトは既定で即座に効きます。

既定値を3つ押さえる

項目既定値意味
制御ループの周期15秒この間隔でメトリクスを取得して再計算する
許容差(tolerance)0.1(10%)目標との比率が1.0に十分近ければ何もしない
安定化ウィンドウスケールアウト 0秒 / スケールイン 300秒スケールインでは直近5分間の推奨値のうち最大値を採用する

さらに、スケーリングの速度を制限するポリシーにも既定があります。ここが非対称です。

  • スケールアウトの既定は2本。15秒あたり「現レプリカ数の100%」または「4Pod」の、大きいほうを採ります
  • スケールインの既定は1本だけ。15秒あたり「現レプリカ数の100%」です。つまり、安定化ウィンドウさえ抜ければ minReplicas まで一度に落ちうる設定です

この2つを押さえると、現場でよく聞く2つの不満が別の設定項目に対応していることが分かります。

症状原因対処
負荷が落ち着いてもレプリカが減らない300秒の安定化ウィンドウscaleDownstabilizationWindowSeconds を短くする
減りすぎて、直後の再上昇で詰まるスケールインのポリシーが実質無制限scaleDown にPods数や割合のポリシーを足し、selectPolicyMin にする

安定化ウィンドウを短くしても減りすぎは直らず、ポリシーを足しても減り始めの遅さは直りません。症状と設定項目を取り違えると、片方を触って「効かない」という結論になります。

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    - type: Percent
      value: 20
      periodSeconds: 60
    - type: Pods
      value: 2
      periodSeconds: 60
    selectPolicy: Min

上の例は「1分あたり20%、かつ2Podまで」に制限する書き方です。値そのものは業務要件から決めてください。目安になるのは、1Podが起動してリクエストを受けられるようになるまでの時間です。起動に90秒かかるなら、減らす速度もその時間を意識して抑えます。減らしすぎた後の復旧に90秒かかるためです。

なおGKEにはHPAパフォーマンスプロファイルがあり、1.31以降で最大1,000個、1.33以降で最大5,000個のHPAオブジェクトを扱いながら15秒の再計算周期を維持できます。1.33以降は要件を満たすクラスタで既定で有効です。HPAの数が増えると再計算が遅れるという問題は、新しいバージョンでは前提が変わっています。

閾値はRequestsの割合である、ということの帰結

目標使用率を指定した場合、コントローラはコンテナのRequestsに対する百分率として使用率を計算します。ノードの容量に対する割合ではありません。ここから3つの帰結が出ます。

  1. Requestsを変えると、同じ「70%」の意味が変わります。設計書ではHPAの閾値とRequestsを同じセクションに置き、片方だけを変更できないようにしてください
  2. Requestsが設定されていないコンテナが1つでもあると、そのPodのCPU使用率は未定義になり、HPAはそのメトリクスで動作しません。公式は「オートスケーラーはそのメトリクスについて何のアクションも取らない」と明記しています。サイドカーの書き忘れで、HPAが黙って止まります
  3. Pod単位の使用率は、全コンテナの合計です。小さな本体と大きなサイドカーの組み合わせでは、本体の負荷が薄まってスケールアウトしません

3つ目への対処は ContainerResource メトリクスです。Kubernetes 1.30で安定版になっており、Pod内の特定のコンテナを指定してスケーリングできます。ログ収集やプロキシのサイドカーを持つ構成では、こちらを既定にしてください。

2つ目は、第5回で「Requests必須」をポリシーで強制すると決めた理由の1つです。ポリシーで弾いていなければ、HPAが動いていないことに誰も気づきません。エラーにならず、ただ増えないだけだからです。

起動が遅いアプリでの誤作動

起動時にCPUを使い切るアプリケーション(JVMのウォームアップ、キャッシュの構築など)では、HPAが起動直後の使用率を見てスケールアウトし、増えたPodがまた起動時にCPUを使い、さらにスケールアウトするという連鎖が起きます。

Kubernetesには、これを抑えるためのクラスタ全体の設定が2つあります。

設定既定値内容
--horizontal-pod-autoscaler-cpu-initialization-period5分Pod起動後この期間は、Readyかつ計測がReady中に取られたものでない限りCPUを無視する
--horizontal-pod-autoscaler-initial-readiness-delay30秒起動直後にReadyとUnreadyを往復するPodを初期化中として扱う

この2つはクラスタ全体の設定であり、ワークロードごとには変えられません。したがって、ワークロード側で打てる手は別にあります。公式が挙げている対処は次の2つです。

  • 高いCPU使用が収まるまで成功しないStartup Probeを構成する
  • Readiness ProbeがCPUスパイクの収束後にのみReadyを返すようにする

つまり、HPAの誤作動対策がProbeの設計になります。次章のProbeは、可用性のためだけの設定ではありません。スケーリングの正しさもProbeが支えています。

minReplicas と maxReplicas は、他の回の上限と突き合わせる

maxReplicas は希望ではなく、技術的な上限との突き合わせで決めます。3つの天井があり、最も低いものが実際の限界です。

  1. 第3回のIP計画。Podセカンダリ範囲から導かれる最大ノード数と、ノードあたりの最大Pod数の積が上限です
  2. 第4回のResourceQuota。NamespaceのCPU・メモリの総量に加え、Pod個数の上限も効きます
  3. ノードの空き容量。ノードが増えなければPodは Pending のまま残ります

maxReplicas に大きな数を書いても、当たるのは最も低い天井です。設計書には、3つの上限それぞれの値と、そこから決めた maxReplicas を並べて書いてください。スケールアウトが必要な日に増えなかった原因を、その場で探さずに済みます。

minReplicas は、後半で扱う分散要件から下限が決まります。3ゾーンに1つずつ置きたいなら最低3です。PodDisruptionBudgetと分散制約は、この値を前提に成立します。

最後に運用上の注意を1つ。HPAを有効にしたら、DeploymentやStatefulSetのマニフェストから spec.replicas を外すことが公式に推奨されています。残したままだと、kubectl apply のたびにその値へ戻され、レプリカ数が上下します。第10回でレプリカ数を公開パラメータに分類したので、この注意はvaluesの仕様書に書く必要があります。HPAを使う場合はレプリカ数の項目を無効にする、という記述です。

Probeは「何を返すか」を決めてから設定する

Probeの設計は、秒数を決める作業だと思われがちです。先に決めるのは、そのエンドポイントが何を確認して何を確認しないかです。秒数はその後に決まります。

3つの役割は排他ではない

種類失敗したときに起きること設計上の問い
Livenessコンテナが停止され、再起動ポリシーに従って再起動する再起動すれば直る状態とは何か
ReadinessEndpointSliceから外れ、Serviceの経路から抜けるいま受けられない状態とは何か
Startupコンテナが停止される。成功するまで他の2つは実行されない起動完了とは何か

公式は、Liveness Probeについて明確な警告を出しています。誤った実装はカスケード障害を招きます。高負荷でコンテナが再起動され、リクエストが失敗し、残ったPodの負荷が上がって、そちらも再起動する、という連鎖です。

ここから最も重要な設計指針が出ます。Liveness Probeに依存先の検査を入れないでください。データベースや外部APIの疎通を確認するエンドポイントをLivenessに指定すると、依存先が落ちた瞬間に全Podが再起動を繰り返します。依存先の検査はReadinessに置きます。経路から外れるだけなら、依存先が戻れば自動的に復帰します。

公式が挙げている定石は、LivenessとReadinessで同じ低コストのエンドポイントを使い、Livenessの failureThreshold を高くするという構成です。こうすると、Readyから外れた状態がしばらく続いてから、はじめて強制的に再起動されます。

Startup Probeの要否は式で決まる

公式は判定式を示しています。コンテナの起動が通常

initialDelaySeconds + failureThreshold × periodSeconds

を超えるなら、Liveness Probeと同じエンドポイントを見るStartup Probeを設定します。そして failureThreshold を起動に十分な値まで上げ、Liveness Probe側の値は既定のまま短く保ちます。

ここで押さえてほしいのは、「起動が遅いから initialDelaySeconds を伸ばす」が誤りだということです。伸ばした分だけ、稼働後のデッドロック検出も遅れます。起動の遅さは起動専用のProbeで吸収し、稼働中の検出速度は落とさない。この分離がStartup Probeの目的です。

startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 10
livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3

上の構成では、起動に最大300秒(30×10秒)まで許容しつつ、起動後の異常は30秒(3×10秒)で検出します。設計書には、この30という数字ではなく、起動時間の実測値(最悪ケース)と、そこから逆算した積を書いてください。

既定値のうち、誤設定が起きやすい1つ

フィールド既定値備考
initialDelaySeconds0Startup Probeがある場合、他の2つの遅延はStartup成功後から始まる
periodSeconds10最小値は1
timeoutSeconds1ここが問題になりやすい
successThreshold1LivenessとStartupでは1しか設定できない
failureThreshold3連続でこの回数失敗すると失敗と判定される

タイムアウトの既定は1秒です。ガベージコレクションの停止やスレッドプールの枯渇で応答が1秒を超えるアプリケーションでは、健全なコンテナが再起動されます。しかも負荷が高いときほど起きやすく、最も再起動されてほしくないタイミングで発動します。

対処は2択です。ヘルスチェックのエンドポイントを1秒以内に必ず返る実装にするか、timeoutSeconds を実測に合わせて上げるか。設計時にどちらを採るかを決め、その判断を設計書に残してください。既定のまま放置するのが最も危険です。

実装で踏みやすい3つの落とし穴

  1. HTTPプローブの応答本文は10KiBで読み取りが打ち切られます。成功判定はステータスコードだけで行われるため結果は成功ですが、接続が途中で切られ、アプリケーション側に接続リセットのエラーログが残ります。通常のネットワーク障害と区別がつきません。ヘルスチェック専用の小さな応答を返すエンドポイントを用意してください
  2. 別ホストへのリダイレクトは追跡されず、プローブは成功として扱われますProbeWarning イベントが記録されます)。認証で外部にリダイレクトするエンドポイントをProbeに指定すると、アプリが壊れていても常に成功します
  3. exec プローブは実行のたびにプロセスを生成します。Pod密度が高く間隔が短いクラスタでは、ノードのCPU使用に影響します。HTTPやTCPで代替できるならそちらを選んでください

2つ目は特に見つけにくい誤りです。「Probeが成功しているのに障害が続く」という状態は、Probeが何も見ていない可能性を疑ってください。設計書のProbe欄に「何を確認するか」を書かせるのは、この確認のためです。

Probeが担う範囲と、監視が担う範囲

Probeは、kubeletが自動的に行動を起こすための入力です。人に通知するための検査ではありません。「依存先が遅い」を検知して人を呼ぶのは第15回の管轄です。

この線引きを守らないと、Probeに判定を足すたびに、再起動と経路外しが予期しない条件で発動します。Probeに書いてよいのは「この状態なら再起動すべき」「この状態なら経路から外すべき」と言い切れる判定だけです。

もう1つ、公式の注記を挙げておきます。Pod削除時のドレインだけが目的なら、Readiness Probeは必ずしも必要ありません。Podが削除されると、EndpointSliceの ready 条件が false になるためです。次章の主題はここから始まります。

停止の設計 ― 15秒の壁と、経路から外れるまでの時間差

デプロイのたびに数秒だけエラーが出る、という相談を受けることがあります。多くの場合、これはアプリケーションの不具合ではありません。終了処理の設計で埋めるべき時間差です。

終了シーケンスで、並行して起きること

Podの削除が要求されてからの流れは次のとおりです。

  1. APIサーバがPodに終了予定時刻と猶予を記録する。Podは Terminating になる
  2. kubeletがローカルの停止処理を開始し、preStop フックを実行する
  3. コンテナのプロセス1にTERMシグナルを送る
  4. 猶予が切れたら、残っているプロセスにSIGKILLを送る

ここで見落とされるのが、これと並行して起きることです。公式ドキュメントは「kubeletがPodの正常終了を開始するのと同時に、コントロールプレーンは終了中のPodをEndpointSliceオブジェクトから削除するかどうかを評価する」と書いています。

順番ではなく、同時です。そして、経路からの除去が実際にノードの転送設定やロードバランサに反映されるまでの時間はゼロではありません。

したがって、SIGTERMを受けた瞬間に受付を閉じるアプリケーションは、まだ到達しているリクエストを落とします。これがデプロイのたびに出る数秒のエラーの正体です。アプリケーション側で「SIGTERMを受けたら即座にシャットダウンを開始する」という一般的な実装をしているほど、確実に起きます。

preStopに待機を入れる

対処は、preStop で待機し、その間はリクエストを受け続けることです。待機のあいだに経路からの除去が伝播し、新しいリクエストが来なくなってからSIGTERMが届きます。

待機の長さは実測で決めます。デプロイ時にエラーが出ている時間を測り、それを上回る値を置きます。推測で5秒や10秒と書かないでください。外部ロードバランサ経由の場合、Kubernetes内部の伝播に加えてロードバランサ側のバックエンド更新と接続ドレインの時間が乗るため、構成によって必要な長さが変わります。

この値は運用チームが共通Chartの既定として持つべき項目です。開発チームごとに変える理由が薄く、しかも間違えると全チームで同じ障害が起きます。第10回の公開・非公開の対応表に、この項目を追加してください。

terminationGracePeriodSecondsの算出式

猶予は次の3つの合計です。既定は30秒です。

terminationGracePeriodSeconds
  = preStopの待機時間
  + 進行中の処理が完了するまでの最長時間
  + 余裕

重要なのは、preStop の実行時間が猶予に含まれることです。preStop で猶予を使い切ると、SIGTERMを受けたアプリケーションには何も残りません。公式も、preStop フックが既定の猶予より長くかかる場合は terminationGracePeriodSeconds を変更しなければならないと明記しています。なお、猶予切れの時点で preStop がまだ実行中なら、kubeletは一度だけ2秒の延長を与えます。当てにできる長さではありません。

設計書には、合計値だけでなく3つの内訳を分けて記入させてください。合計だけでは、後で見直すときにどこを削れるのかが分かりません。

Podの削除要求と同時にEndpointSliceからの除去とkubeletの停止処理が並行して始まり、ロードバランサへの反映が終わるまでリクエストが届き続けるため、preStopの待機でその時間を覆い、その合計が終了猶予になることを示した図
経路からの除去とSIGTERMは同時に始まります。反映までの時間をpreStopの待機で覆い、その合計を猶予として確保します。

猶予の上限は、3方向から来る

ここが本章で最も重要な部分です。算出した猶予がそのまま与えられるとは限りません。停止のさせられ方によって、上限が違います。

停止のさせられ方実効的な上限出所
Spot VMのプリエンプションアプリケーションPodに15秒(全体30秒の内訳が通常Pod 15秒+システムPod 15秒)。超過で電源が落ちるため、terminationGracePeriodSeconds を延ばしても延長できない第1回・第4回で確定
GKEのノード更新(サージ)PDBと猶予を尊重するのは最大1時間。その後は残ったPodを強制的に排除して更新を進めるGKE ノードプール アップグレード戦略
kubeletのnode-pressure evictionPDBも terminationGracePeriodSeconds も尊重しない。ハード閾値では猶予0秒で即座に停止Kubernetes node-pressure eviction

3行目は特に知られていません。公式は「kubeletは設定されたPodDisruptionBudgetもPodの terminationGracePeriodSeconds も尊重しない」と明記しています。ノードのメモリが逼迫したときの停止は、設計した猶予の外側で起きます。

したがって、長い猶予を設計できるのは自発的な中断のときだけです。この事実は、リソース設計に戻ってきます。Requestsを実測どおりに置くこと自体が、猶予を守るための設計です。Requestsを下回る使用量に抑えられていれば、node-pressure evictionの最初の候補群には入りません。

そして第1回・第4回との突き合わせです。第4回でSpotに乗せると決めたワークロードは、terminationGracePeriodSeconds を15秒以内に収めるか、乗せる判断をやり直すかの二択になります。第1回の条件2で「15秒以内に終わらない処理はSpotに乗せない」と判定した内容が、ここで実装レベルの制約として再登場します。

設計した猶予が、自発的な中断ではそのまま使われる一方で、Spot VMではアプリケーションPodに15秒しか与えられず、kubeletのnode-pressure evictionではPDBも猶予も尊重されないという、停止のさせられ方による上限の違いを示した図
猶予の上限は3方向から来ます。設計できるのは自発的な中断のときだけで、残る2つは設計の外側で起きます。

サイドカーがあるときの終了順序

サイドカーコンテナ(restartPolicy: Always を持つinitコンテナ)を使っている場合、kubeletは最後の主コンテナが完全に終了するまで、サイドカーへのTERM送信を遅らせます。そして定義の逆順で停止させます。

設計上の帰結は2つです。

  • 主コンテナの終了が遅いと、サイドカーの終了もその分だけ遅れます。猶予を超えると、残った全コンテナが短い猶予で同時に終了させられます。Pod全体の猶予を決めているのは、主コンテナの最長処理時間です
  • 順序制御のための preStop は不要になります。kubeletが順序を管理するため、サイドカー方式に移行したなら、以前書いた順序制御用のフックは削除できます

プローブ単位の猶予

プローブ単位に terminationGracePeriodSeconds を設定できます(Kubernetes 1.28で安定版)。Pod単位の値より優先され、Readiness Probeには設定できません(APIサーバが拒否します)。使いどころは非対称な要求です。通常の終了には長い猶予が必要だが、Livenessが失敗した場合は復帰不能と判断しているのだから待たずに停止させたい、という場合に効きます。Pod単位の猶予を長く取っている構成ほど、Livenessの失敗から実際の再起動までが遠くなるためです。

落ちても止まらない配置 ― PDBと分散制約

PodDisruptionBudgetは「同時に減ってよい数」の宣言、topologySpreadConstraints は「どこに置くか」の宣言です。両方を書かないと成立しません。全レプリカが同じノードに乗った状態でも、PDBの条件だけは満たされます。

PDBが効かない3つの経路

第1回で「PDBを設定したから落ちないという保証は存在しない」と書きました。実装レベルで見ると、抜け道は3つあります。

  1. Eviction APIを経由しない削除はバイパスします。公式は「DeploymentやPodを削除する操作はPodDisruptionBudgetをバイパスする」と明記しています。kubectl delete pod は止まりません
  2. 非自発的な中断は防げないうえ、予算だけが消費されます。「非自発的な中断はPDBでは防げないが、予算には計上される」という記述です。ノード障害でPodが1つ落ちた直後は、自発的な中断に使える余地がその分減っています
  3. kubeletのnode-pressure evictionはPDBを尊重しません。前章の表のとおりです

加えてGKEでは、ノード更新時にPDBが尊重されるのは最大1時間で、その後は強制的に排除されます。

ここから、PDBに期待してよい範囲が定まります。PDBはノードの更新やドレインを止める道具ではなく、その速度を制限する道具です。「PDBを設定したので更新時も安全」という説明を設計書に書かないでください。書くべきは「ドレインが1台ずつ進むため、レプリカが同時に失われない」という限定された効果です。

minAvailable と maxUnavailable の決め方

  • minAvailable: 業務が成立する最小のPod数を書きます。性能要件から降りてくる値です
  • maxUnavailable: 同時に落ちてよい数を書きます

注意点が3つあります。

1つ目。割合で書くと、レプリカ数が変わったときに意味が変わります。HPAでレプリカ数が変動する構成では、最小レプリカ時に何Podが残るかを確認してください。「50%」はレプリカ2のとき1Podです。

2つ目。maxUnavailable: 0(あるいは全レプリカを minAvailable に指定する書き方)はドレインをブロックします。GKEでは1時間後に強制排除されるため、結果は「更新を1時間遅らせたうえで、結局は強制的に排除される」となります。更新のたびに1時間が加算されるという運用コストだけが残ります。第14回のバージョンアップ計画で、この時間は必ず問題になります。

3つ目。レプリカ1のワークロードにPDBを付けると、ドレインが進まなくなるだけです。付けるかどうかは可用性の判断ではなく、ノード更新の運用に対する判断になります。

もう1つ、公式が推奨している設定があります。unhealthyPodEvictionPolicyAlwaysAllow にすることです。既定の挙動は、アプリケーションのPodが健全になるまでドレインを待ちます。つまり起動に失敗して健全にならないPodが、ノードのドレインを止め続けます。壊れているPodを守る意味はないため、明示的に設定してください。

topologySpreadConstraintsの既定は、実質的に無効

ワークロード側に何も書かず、クラスタ側でも設定していない場合、スケジューラは次の既定で動きます。

トポロジーキーmaxSkew満たせないとき
kubernetes.io/hostname3ScheduleAnyway
topology.kubernetes.io/zone5ScheduleAnyway

いずれも強制力がありません。偏りの許容幅も大きく、ゾーンについては5です。レプリカ6のワークロードなら、5対1の配置でも既定を満たします。

つまり、「マルチゾーンのクラスタなので自動的に分散されている」は成り立ちません。分散を要件とするなら、ワークロード側に明示的に書くか、スケジューラのプロファイルで既定を上書きします。第3回でリージョンクラスタを選んだ組織ほど、ここを既定のままにしていると設計と実態が乖離します。

topologySpreadConstraints:
- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  nodeTaintsPolicy: Honor
  labelSelector:
    matchLabels: { app: checkout }
  matchLabelKeys:
  - pod-template-hash

DoNotScheduleを選ぶと、Pendingが起きる

whenUnsatisfiable の既定は DoNotSchedule です。制約を満たすノードがなければ、Podは Pending のまま残ります。これは分散を強制した代償であり、スケールアウトが必要な日に増えないという形で表面化します。

選ぶ前に、3つの上限と突き合わせてください。第3回の最大ノード数とゾーン数、第4回のノードプール構成、前章で決めた maxReplicas です。3ゾーンに maxSkew: 1 で10レプリカを分散させるには、各ゾーンに4Pod分の空きが必要です。

判断基準としては、可用性が業務要件として定義されている系(第3回のSLA目標が根拠になっているもの)は DoNotSchedule、それ以外は ScheduleAnyway を既定にしてください。強制するかどうかを、なんとなくで決めないためです。

分散計算で見落とされる3点

  1. nodeTaintsPolicy は未設定だと Ignore 相当で、taintの付いたノードも計算に含まれます。第4回でSpotノードプールにtaintを付けた構成では、tolerationを持たないPodがスケジュールされ得ないノードまで、分散先として数えられます。意図した分散になりません。Honor を検討してください
  2. matchLabelKeyspod-template-hash を指定すると、リビジョンごとに分散を計算します。指定しないと、ローリングアップデート中に旧リビジョンのPodが計算に混ざり、新しいPodの配置が偏ります
  3. 制約は維持されません。公式は「Podが削除されたときに制約が満たされ続ける保証はない」と明記しています。スケールインで偏ります。再配置が必要なら別のツール(Descheduler等)が要りますが、その導入可否は第6回の枠組みで判断してください

もう1つ、オートスケーリングと組み合わせたときの落とし穴があります。ノードプールが0台までスケールインすると、そのトポロジードメインは存在しないものとして扱われます。スケジューラはクラスタが持つゾーンを事前に知っているわけではなく、現存するノードから判断するためです。「ゾーンが3つあるから3分散」という前提が、台数0のゾーンで崩れます。

最後に1点、範囲の外側を明示しておきます。本章と第2章で触れたとおり退避の順序はPriorityの影響を受けますが、PriorityClassの体系設計は本連載のどの回にも割り当てていません。何段階のクラスを作り、どのワークロードに割り当てるかは、必要とする組織が別途決めてください。決めた場合は次のテンプレートに欄を追加します。設計書に存在しない項目は、後から誰も見直しません。

【実務テンプレート】Podリソース・耐障害性設計書

ワークロード単位で作成します。記入者は第2回のRACIと第10回の公開範囲に従い、値は開発チーム、範囲と既定は運用チームが埋めます。

セクション1: 前提

  • 対象ワークロード名 / 第1回の判定結果(Spotに乗せてよいか) / 第3回のゾーン構成とSLA目標
  • 第4回のResourceQuota・LimitRangeの上下限 / 運用モード(Autopilot または Standard)

セクション2: リソース(コンテナごとに1行。サイドカーも書く)

  • CPU Requests / CPU Limits(設定しない判断を含む) / Memory Requests / Memory Limits
  • 結果としてのQoSクラス
  • 根拠: 測定日 / 測定条件 / 使用したパーセンタイル / 余裕の係数と、その係数を選んだ理由

セクション3: HPA

  • 使うか使わないか / メトリクスの種類(Resource / ContainerResource / カスタム / 外部) / 目標値
  • minReplicasmaxReplicas、および3つの上限(IP・Quota・ノード)それぞれの値
  • scaleDown を既定のままにするかの判断と理由 / マニフェストから spec.replicas を外したか

セクション4: Probe(3種類それぞれ)

  • 設定するか / エンドポイント / 検査内容(何を確認し、何を確認しないか)
  • periodSeconds / timeoutSeconds / failureThreshold
  • 起動時間の実測値(最悪ケース)と、Startup Probeの要否判定の結果
  • Livenessに依存先の検査を含めていないことの確認

セクション5: 終了処理

  • preStop の待機時間とその測定方法 / 進行中の処理が完了するまでの最長時間 / 余裕
  • terminationGracePeriodSeconds3つの内訳を分けて記入
  • Spotに乗せる場合、15秒以内に収まっているか

セクション6: PodDisruptionBudget

  • minAvailablemaxUnavailable のどちらを使うか / 値と根拠(業務が成立する最小Pod数)
  • 最小レプリカ時に残るPod数 / unhealthyPodEvictionPolicy / レプリカ1の場合の判断

セクション7: 分散

  • topologyKey / maxSkew / whenUnsatisfiable / nodeTaintsPolicy / matchLabelKeys
  • DoNotSchedule を選んだ場合、各ドメインに必要な空き容量があることの確認

セクション8: 見直し

  • 値を再測定するトリガー。日付ではなく観測できる条件で書く(OOMKilledの発生、レイテンシの悪化、ノード構成の変更、依存先の増加など)

耐障害性チェックリスト(本番投入前)

  • すべてのコンテナ(サイドカーを含む)にRequestsが設定されている
  • Requestsが実測の定常値以上である(低く見積もっていない)
  • Liveness Probeに依存先サービスの検査が含まれていない
  • 起動時間の実測値が initialDelaySeconds + failureThreshold × periodSeconds を超える場合、Startup Probeがある
  • timeoutSeconds がヘルスチェックの応答時間の実測と整合している
  • preStop の待機時間が、経路から外れるまでの実測時間以上である
  • terminationGracePeriodSecondspreStop の待機時間と最長処理時間の合計を上回っている
  • Spotに配置するワークロードの猶予が15秒以内に収まっている
  • PDBがドレインを永久にブロックする設定になっていない
  • unhealthyPodEvictionPolicy を明示的に設定している
  • topologySpreadConstraints を明示的に設定している(既定に依存していない)
  • DoNotSchedule を使う場合、各ドメインに必要な空き容量がある

次回予告

値を決めました。次に確かめるのは、その値が本当にそのとおり動くかです。

次回の第12回「カオスエンジニアリング・E2Eテスト計画書」では、障害注入のシナリオ、Kubernetesのバージョンアップ時に必要な動作確認、負荷テストとストレステストの方法論、そしてテスト環境を本番同等にするか縮小構成にするかの判断を扱います。

接続は2方向です。本記事で決めた terminationGracePeriodSeconds もPDBも分散制約も、Podを実際に停止させるまでは検証していない値です。そして本記事の第2章で使ったリソースの測定値は、第12回で設計する負荷テストの出力です。第11回と第12回は、入力と出力が互いに向き合う関係にあります。

前の記事
次の記事