4. 運用
4.1 キャパシティ / 可用性
このセクションの内容
surge がローテーション中の Pod 可用性にどう影響するか、および 1 ノード surge バジェットの適用方法。
| 懸念事項 | 対処 |
|---|---|
| Pod pending 時間 | surge によりほぼゼロ |
readyReplicas のディップ | アプリケーション層での緩和 |
| 同時 surge ノード | NodePool 内シリアル(v1) |
- Pod pending 時間: surge により Karpenter Graceful セマンティクス(make-before-break)と同等。
readyReplicasのディップ: Kubernetes の構造的制限 — surge があっても eviction 後に新しい Pod が即座にReadyにならない。緩和: レプリカのオーバープロビジョニング + PDB。スコープ外。- 同時 surge: v1 は NodePool あたり
surge.maxUnavailable = 1(内部シリアル; 異なるプールは同時に surge 可能)。代替ノードは NodePool 所有(placeholder 経由で誘導、§3.3)。
1 ノード surge バジェットの適用方法
spec.limits は リソースバジェット({cpu, memory, …})であり、ノード数ではない。前提条件は placeholder のリクエストが NodePool の残りバジェット(limits − 現在プロビジョニング済み)に収まること。
コントローラーは 開始前にヘッドルームを事前チェック(§5.2 ステップ 3、候補選定後 — placeholder のリクエストは選定された候補に依存するため)。バジェットが 1 ノード分を収容できない場合は警告付きでスキップ。
4.2 オブザーバビリティ
Prometheus メトリクス
/metrics で公開:
| メトリクス | タイプ | ラベル |
|---|---|---|
noderotation_candidates | Gauge | nodepool |
noderotation_in_backoff | Gauge | nodepool |
noderotation_in_progress | Gauge | nodepool |
noderotation_completed_total | Counter | nodepool, outcome |
noderotation_forceful_fallback_total | Counter | nodepool |
noderotation_window_missed_total | Counter | nodepool |
noderotation_duration_seconds | Histogram | nodepool, phase |
noderotation_window_active | Gauge | nodepool |
noderotation_policy_conflict | Gauge | nodepool |
noderotation_freeze_until_timestamp | Gauge | nodepool |
noderotation_age_threshold_seconds | Gauge | nodepool |
noderotation_rotation_chances | Gauge | nodepool |
noderotation_throughput_capacity | Gauge | nodepool |
noderotation_t_rot_estimate_seconds | Gauge | nodepool |
noderotation_t_rot_bound_seconds | Gauge | nodepool |
noderotation_window_period_seconds | Gauge | nodepool |
noderotation_short_lead_nodes | Gauge | nodepool |
noderotation_drain_stuck | Gauge | nodepool |
noderotation_retry_count | Gauge | nodepool |
メトリクス詳細 — クリックで展開
noderotation_candidates: プールあたりの適格 NodeClaim 数noderotation_in_backoff: 年齢トリガーを越えているうえで、失敗した試行によってエスカレートしたretryBackoff中にあるという理由だけで候補カウントから外れている NodeClaim 数。年齢の限定はロードベアリングである: 期限が来ていたときに失敗し、その後に年齢が期限外になった claim(明示指定したageThresholdの引き上げ、リードタイムの短縮 — トリガーはage > expireAfter − leadTimeなので、リードタイムを広げると期限は早く来る — 、expireAfterの延長)は、いま backoff に阻まれてはいるが何も負われておらず、このゲージもwindow_missed_totalもそれを数えない。candidates + in_backoffはwindow_missed_totalが閉じた occurrence を判定する「年齢と状態による未処理」カウントそのものであり、in-window のアラートは同じ判定をライブに適用できる(§5.2)- ウィンドウを意識したバックオフクランプ(§3.2)は、発生の境界で claim を
noderotation_candidatesとnoderotation_in_backoffの間で移動させるが、固定された claim のスナップショットについて見れば、その合計——window_missed_totalが判定に使う未処理作業カウント——は変化しない。しかしクランプはそれ以上のところでは observability に対して中立ではない。より多くの試行を生み出すことこそがクランプの目的だからである:noderotation_completed_total{outcome="failure"}はより頻繁に増加し、noderotation_retry_countはより速く上昇し、NodeRotationRetryCountHighはより早く発火しうるようになり、失敗ログと Event は増え、追加された試行がクローズ時点で成功しているか、あるいはまだ進行中であるかによって、noderotation_window_missed_totalがその発生で発火するかどうかも変わりうる。 - 増えた試行はメトリクスだけの影響ではない:1回ごとに追加の surge NodeClaim が作成され、失敗すれば reap され、placeholder Pod が入れ替わり、対象の本番ノードの cordon/uncordon も繰り返される。クランプ前なら4回で済んでいた試行がクランプ後に6回になるプール——§3.2 のクランプを動機づけたインシデントの形——では、同じ発生でこのクラスタ側のチャーンがおよそ50%増える。
noderotation_in_progress: プールあたりのアクティブローテーション数noderotation_completed_total: 累積完了数;outcome∈ {success,failure,expired}。expired= graceful ローテーション完了前に force-expire(1回のみ発行、success としてカウントしない)noderotation_forceful_fallback_total: surge なし forceful fallback ローテーション開始数(§3.6); 開始時にインクリメントnoderotation_window_missed_total: 年齢と状態から見て未処理の候補(eligible、または年齢トリガーを越えていてretryBackoff中)が残ったまま、その発生に帰属するローテーションが 1 件も完了しないままメンテナンスウィンドウの発生が閉じた回数(§5.2)。ウィンドウがゲートするのはローテーションの開始だけなので、発生の中で始まり境界の後に成功した試行はその発生に帰属し、発生を settle させる。ここでの「残っている」は「コントローラーがローテーションできたはず」ではない: この評価は意図的にプールレベルのゲートより前段で走る(§5.2)ため、static なプールやスケジュールが致命的に実行不能なプールは、年齢と状態から見て未処理の claim を残したまま閉じた発生のたびに報告する。発生ごとに最大 1 回、window-opened-atスタンプをクリアしたパスがインクリメント — クリアが発行より先に着地するため、その間に停止したコントローラーはシグナルを捏造せず落とすnoderotation_duration_seconds: フェーズごと;phase∈ {surge_wait,drain}。surge_wait=started-at → surge_ready;drain=draining-at → 旧 NodeClaim ファイナライズ。成功遷移ごとに最大 1 回観測(リトライ書き込みでの二重カウントなし)noderotation_window_active: 0/1 ウィンドウメンバーシップ表示noderotation_policy_conflict: 0/1 セレクタータイまたは無効ポリシーによりブロック(§5.4)noderotation_freeze_until_timestamp: アクティブ freeze の Unix タイムスタンプ(0 = なし)noderotation_age_threshold_seconds: 導出されたageThreshold(§3.2)noderotation_rotation_chances: 保証回数Gnoderotation_throughput_capacity: layer-2 予測C— 発生あたりの開始数(§3.2)noderotation_t_rot_estimate_seconds: 予測サービス時間t_rot_est = provisioningEstimate + drainEstimatenoderotation_t_rot_bound_seconds: 期限側上限t_rot = readyTimeout + tGP + buffernoderotation_window_period_seconds: 最悪ケース周期Pnoderotation_short_lead_nodes: 自身のspec.expireAfterでK回を保証できない NodeClaim 数(§3.2 layer 3)noderotation_drain_stuck: 0/1 ドレインがtGP + bufferを超過(§5.2)noderotation_retry_count: プール内 NodeClaim の最大retry-count(0 = なし)
ラベルに関する注記
NodePool ごとのウィンドウ(各プールが自身の RotationPolicy を解決、§5.4)により、noderotation_window_period_seconds と noderotation_window_active は ロードベアリング な nodepool ラベルを持つ — P とメンバーシップはプール間で異なりうる。
expireAfter: Never: すべての導出ゲージが0(導出スキップ)- ウィンドウ発生なし(
P ≤ 0): bound/estimate は非ゼロ;throughput_capacityのみ0
ライフサイクル
- シリーズは NodePool 削除時にクリア — ゲージは各 reconcile で再計算
- governing policy を失ったプールも同様にシリーズがドロップ(§5.4)
Kubernetes Events
Warning レベルの状態が kubectl describe で確認可能:
| オブジェクト | Reason | タイミング |
|---|---|---|
| NodePool | KBelowTwo, AVeryAggressive, TGPUnset, HardCapExceeded, RetryBackoffShort, DrainEstimateAboveTGP, ProvisioningEstimateAboveReadyTimeout, ThroughputBelowArrival, ThroughputBurstShortfall, RotationSpansNextWindow, OverrideGBelowK | スケジュール finding がアクティブ |
| NodeClaim | ShortLead | claim が K 回を保証できない |
| NodePool | ForcefulFallback | surge なしローテーション開始 |
| NodePool | StaticNodePool | spec.replicas が設定されており surge では絶対にローテーションできない(§3.3) |
| NodePool | InsufficientHeadroom | spec.limits に placeholder の余地がなく、ローテーションを開始できない(§5.2 step 3) |
| NodePool | WindowMissed | 候補が未ローテーションのままウィンドウが閉じ、その発生に帰属するローテーションが 1 件もなかった(§4.2) |
| NodePool | PolicyConflict | 同一 specificity の RotationPolicy タイ — そのプールはローテーションしない(§5.4) |
| NodePool | GovernanceLost | ガバナンス喪失後に in-flight ローテーションをロールバック(§5.4) |
| NodePool | RotationStarted | 候補選定(Normal) |
| NodePool | RotationCompleted | 旧 NodeClaim ファイナライズ(Normal) |
| NodeClaim | RotationFailed | readyTimeout 超過; ロールバック |
| NodeClaim | SurgeUnschedulable | placeholder PodScheduled=False |
| NodeClaim | SurgeClamped | placeholder クランプ済み(Normal) |
| NodeClaim | SurgeClampBandExceeded | クランプ shortfall > バンド(Warning) |
| NodeClaim | SurgeClampRefused | DaemonSet オーバーヘッドが候補自身の allocatable を消費(Warning) |
- 重複排除: 状態への遷移時に発行; クリアして再発時に再発行
- Fatal finding は Event ではない — ローテーション開始をブロックし §5.2 feasibility gate でログ
状態マシンのログ行
すべての状態遷移が 1 つの INFO ログ行を発行(永続アノテーション書き込み後):
| 行 | 主要フィールド |
|---|---|
rotation candidate selected | nodeclaim, age, deadline, surgeless |
no rotation candidate | reason、census カウント |
surge placeholder created | placeholder, requests、除外カウント、クランプ情報 |
surge placeholder is not schedulable | placeholder, reason, detail |
insufficient limits headroom; cannot surge | candidate, resource, want, remaining, limit |
surge node ready | surgeNode, surgeWait, surgePath |
drain started | node, mode ∈ {surge, forceful-fallback} |
rotation attempt failed | reason, readyTimeout, retryCount, backoffUntil |
rotation complete | mode, drain, surgeNode, surgeWait, surgePath, total |
maintenance window closed with candidates unrotated | windowOpenedAt, eligible, inBackoffTriggered |
surgePath∈ {provisioned,absorbed} はどちらの §3.3 パスがキャパシティを予約したかを示し、surgeWaitを解釈可能にする値である:absorbedの待機時間は既に存在したキャパシティへの bind を測っているだけなので、退避 Pod が稼働するまでの時間を上界しない。パスが確定しなかった場合 — surge なしのフォールバック、または surge ホストの NodeClaim を解決できなかった場合 — は推測せず 省略する。RotationCompletedEvent も同じ値を持つ- レベルトリガー行(
no rotation candidate、surge placeholder is not schedulable、insufficient limits headroom; cannot surge)は遷移 dedup を使用 — reason/census/message が変化した場合のみ再発行 - デバッグ冗長性(
V(1))で dedup なしの各パス findings とハートビートを追加 - ライブネスシグナル: ログの沈黙ではなく
controller_runtime_reconcile_total/ workqueue メトリクスから読み取る
推奨アラート
| アラート | 条件 |
|---|---|
| 失敗/expired | `increase(noderotation_completed_total{outcome=~"failure |
| 追いつけない | noderotation_candidates > 0 が 2 回連続ウィンドウ |
| ウィンドウ喪失 | increase(noderotation_window_missed_total[24h]) > 0 |
| ウィンドウ詰まり(in-window) | window_active == 1、freeze 中でない、成功完了ゼロ、かつ noderotation_candidates > 0 or noderotation_in_backoff > 0 |
| ドレイン停滞 | noderotation_drain_stuck == 1 |
| Short lead | noderotation_short_lead_nodes > 0 |
| 体系的失敗 | noderotation_retry_count >= 3 |
| Forceful fallback | increase(noderotation_forceful_fallback_total[1h]) > 0(severity: info) |
in-window 条件の in_backoff 側はロードベアリングである: エスカレートした retryBackoff 中の NodeClaim は eligible ではないため、プールに未処理の作業が残っていても noderotation_candidates は 0 に落ちる。candidates だけに依存する条件は、ウィンドウ全体が失敗した試行だけで費やされた場合に沈黙する — これはまさに window_missed_total が報告するために追加されたケースである。
このアームが retry_count ではなく in_backoff であるのは、条件のこの半分をウィンドウ閉鎖時の評価と同一の判定にするためである。candidates + in_backoff はその未処理カウントそのものだが、retry_count はバケットに関係なくプールのすべての NodeClaim にわたる最大リトライ回数であり、カウンタが意図的に除外する claim — Node に運用者が karpenter.sh/do-not-disrupt を付与したもの、削除中のもの、既に期限切れのもの — でも高いまま残る。freeze の除外はその一致を広げる: freeze は運用者がプールにローテーション停止を指示するものなので、freeze 下で閉じたウィンドウは喪失ではなく辞退であり、カウンタはそれを記録しない(§5.2)。in-window 条件も同じ根拠でそれを辞退する。
ただし 2 つのシグナルはこのアームを超えては等価ではなく、アラートはカウンタの予測子ではない。設計上、両者は 3 点で異なる。
アラートはウィンドウが開いている間に繰り返しライブに評価し、カウンタは閉鎖時に一度だけ評価する — したがってプールが境界前に持ち直せば、アラートは発火してから解決する。
完了側のアームは completionRange のローリング参照だが、カウンタは last-rotation-at ≥ window-opened-at で成功を帰属させるため、両方向にずれる: occurrence の直前の成功はその occurrence を settle できないのにアラートを抑止し、completionRange より長いウィンドウでは帰属する成功が参照範囲から抜け落ち、occurrence は settle されるのにアラートが発火しうる。どちらも小さく保つには completionRange をおよそ 1 ウィンドウ分に設定する。
意図的にアラートしないケースが 1 つある: backoff が明けて再選択された claim は InFlight であり、どちらのアームにも数えられないため、再選択から readyTimeout が試行をロールバックして retryBackoff に戻すまでこの条件は沈黙する。以前の retry_count アームはその間ずっと発火していた。この縮小は受け入れる — 実際に走っているローテーションは詰まったウィンドウではない — うえで、最終的なシグナルは失敗アラートと、occurrence がその claim を未処理のまま閉じた場合の window_missed_total である。
完了側を outcome="success" に限定することも、同じインシデントに対して同じ向きにロードベアリングである。noderotation_completed_total は failure と expired も数え、readyTimeout のロールバックは failure を記録する。したがってあらゆる完了を進捗とみなす条件では、失敗した試行 1 件ごとにアラートが抑止され、ウィンドウ全体が失敗した試行だけで費やされたときにちょうど沈黙する。表明すべき条件は「ウィンドウが開いていて、作業が残っていて、何も成功していない」である。
Helm chart がオプションの PrometheusRule として提供(prometheusRule.enabled でゲート、デフォルト false)。チューニングは プロダクション Runbook を参照。
4.3 RBAC とクラウド権限
Kubernetes RBAC
- apiGroups: ["karpenter.sh"]
resources: ["nodeclaims"]
verbs: ["get", "list", "watch", "update", "patch", "delete"]
- apiGroups: ["karpenter.sh"]
resources: ["nodepools"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]nodeclaims:createなし — v1 はNodeClaimを作成しない(§3.3)。update/patchは状態アノテーション;deleteはローテーションと failure reap を駆動nodepools:update/patchは active-rotation anchor、状態ミラー、完了アノテーション用nodes:update/patchはdo-not-disrupt/マーカー +spec.unschedulable(cordon)用pods: placeholder Pod を直接管理events: NodePool/NodeClaim の Warning Events + リーダー選出記録leases: リーダー選出
placeholder の PriorityClass は Helm chart により静的にインストール — priorityclasses 権限不要。
クラウド(例: AWS)IAM
- v1: クラウド API への直接呼び出しなし。すべての操作は
NodeClaimCRD 経由。 - v2(pre-pull): Job が新ノード上の Pod として実行され、そのロールを継承。コントローラーレベルの追加クラウド権限なし。
4.4 コスト
要点
各ローテーションは ~10–20 分の重複課金を発生させる。1つのメンテナンスウィンドウの発生(occurrence)内では、試行の繰り返しのペースを制限するのは readyTimeout + failurePause である。ウィンドウを意識したバックオフクランプ(§3.2)がエスカレートする retryBackoff に発生を早期に終わらせないようにするのは、retryBackoff まで下げたステップがなお発生の内側に収まる間だけであり、retryBackoff すら収まらなくなった時点でエスカレートした待ち時間はそのまま確定し、その claim は次の発生へ持ち越される。retryBackoff はこのクランプの下限でもあるため、引き上げればその「何も収まらない」地点により早く到達し claim ごとの試行回数を減らせる——ただしこれは claim ごとに作用し、その claim については窓の残りを切り捨てうるので、プール全体を直接ペーシングするのは引き続き failurePause である。複数の claim が互いに独立して backoff とリトライを交互に行いうる。
通常のローテーションコスト
短時間の重複: surge 中に新旧ノードが同時に課金。
- ローテーションあたり: 1 つの追加オンデマンドインスタンスの ~10–20 分
- 月間(週次ローテーション、N ノード):
≈ N × 4 × hourly_rate × 0.25 - ピーク重複: 同時にローテーションする NodePool 数に比例
whole-node 予約のコスト(§3.3、ADR-0005)
surge.wholeNodeReservation を有効にしたプールでは、予約が吸収されなかったローテーションが上記のオーバーラップ課金を支払う — 1 ノード分にサイズされた placeholder を吸収できるのは、cpu と memory に 1 ノード分の空きがあるホストだけだからである。その頻度はプールの形状に依存する: 空きキャパシティやより大きいインスタンスタイプを抱えるプールでは大半が吸収されうる一方、密に詰まった同種プールではほぼ毎回支払うことになる。ワークロードに対する相対コストが最も大きいのはほぼ空の候補で、どこにでも収まるドレインのために丸ごと 1 インスタンスを予約することになる。
完了後、ドレインされたノードの Pod だけを載せた surge ホストが do-not-disrupt を離れた時点で、consolidation が 1 回走ることがある。
失敗した surge のコスト
失敗した試行は readyTimeout まで surge ノードを課金する可能性あり(未使用時にその後 reap; 再利用されたノードは通常キャパシティとして残存)。
コスト制限メカニズム
| メカニズム | 制限対象 |
|---|---|
readyTimeout + failurePause | 同一 claim への繰り返し試行、1つのメンテナンスウィンドウの発生(occurrence)内(§3.2) |
プールレベル failurePause | 体系的失敗下の候補サイクリング |
failurePauseなし: 体系的原因が ~1 分以内に次の候補に移行し、候補ごとにreadyTimeout分の課金を消費failurePauseあり:readyTimeout + failurePauseあたり最大 1 回の試行(デフォルトで ~25m)——これは引き続き 1 つの発生内で同一 claim がリトライする回数の上限である。ウィンドウを意識したクランプはretryBackoffまで下げたステップがなお収まる間はこのペースでリトライを許すが、収まらなくなった時点でエスカレートした待ち時間が確定し、その claim はその発生ではもう試行を得られないfailurePauseはcooldownAfterと分離 — スループット向上のための settle 短縮がコスト制限を弱めないretryBackoffは claim ごとにエスカレートし続け、発生間の待ち時間を引き続き制限する。これはこのクランプの下限でもあるため、引き上げれば claim が「何も収まらない」地点により早く到達しその claim については窓の残りを切り捨てうる——鈍い、claim ごとの手段であって、プール全体の試行レートの信頼できる制限にはならない: 複数の claim が同時にそれぞれの backoff で独立にリトライしうるnoderotation_retry_countがパターンをアラート(§4.2)