はじめに
Kubernetesには自己修復の仕組みがあるため、ノードに障害が発生してもPodは別のノードへ再スケジュールされ、大きな問題にはならないと考えている方は多いと思います。
ただし、障害検知・Podの削除要求・代替Podの作成は、それぞれ別の処理です。Node Lifecycle Controller・TaintManager・PodGCなどが段階的に関わり、Podを管理するリソースによっても挙動が変わります。特にStatefulSetは、旧Podのオブジェクトが残っている間、同じ番号の代替Podを作成しません。
この記事では、ワーカーノードが物理的に壊れて応答不能になった場合に、Nodeオブジェクトがどう状態遷移するか、そしてその上で動いていたPodがDeployment/StatefulSet/DaemonSet/Jobそれぞれの管理単位でどう扱われるかを整理しました。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| kubelet | v1.37.1 | kubernetes/kubernetes | ノードのハートビート(NodeStatus/Lease)送信の実装確認 |
| kube-controller-manager | v1.37.1 | kubernetes/kubernetes | ノードの生死判定・Taint付与・Pod削除・Volumeのデタッチ・各ワークロードの再作成ロジックの実装確認 |
| cloud-controller-manager | v1.37.1 | kubernetes/kubernetes | クラウドプロバイダのAPIと連携したNodeオブジェクトの自動削除ロジックの実装確認 |
| kube-apiserver | v1.37.1 | kubernetes/kubernetes | PodへのデフォルトのToleration付与ロジックの実装確認 |
TL;DR
- 障害検知とPodの削除要求には待ち時間があります。 デフォルトの監視猶予は50秒、通常のPodのTolerationは300秒です。合計約350秒は目安であり、障害発生からの固定の期限ではありません。
-
削除要求と、コンテナの停止・Podオブジェクトの削除完了は別です。 kubeletが応答しないと、Podは
Terminatingのまま残ることがあります。 -
代替Podを作る条件は管理単位で異なります。 Deploymentと通常のJobは旧Podの終了を待たずに補充できますが、StatefulSetは同じOrdinalの旧Podオブジェクトが消えるまで待ちます。
Parallelでもこの条件は変わりません。 - 強制削除の前には、旧ノードが処理を続けないことの確認が必要です。 ハートビートの停止だけでは、電源断とネットワーク分断を区別できません。
- PVCを使う場合はVolumeの復旧も別に確認します。 Podの削除・再作成とVolumeのデタッチ・アタッチは非同期に進みます。
管理単位ごとの挙動の違いをまとめると、次のようになります。
| 管理単位 | 代替Podの作成タイミング | ノード障害時の挙動 |
|---|---|---|
| Deployment |
Terminatingな旧Podの実終了を待たず、即座に作成(6.1) |
配置条件を満たす別ノードがあればスケジュールされる。PVCを使う場合は、Volumeのデタッチが完了するまでContainerCreatingのまま待たされる(7.3) |
| StatefulSet | 同じOrdinalのPodオブジェクトが実際にetcdから削除されるまで作成されない(6.2) | 応答不能が続く場合、Node削除に伴うPodGCやout-of-serviceTaintなどによる旧Podの削除が必要(3.1・3.2)。podManagementPolicy: Parallelでも同じOrdinalの旧Podの削除を待つ(6.2) |
| DaemonSet | 作成されない(そもそも他ノードへ再作成する設計ではない)(6.3) | デフォルトでTaint-based Evictionの対象外。ノードに紐づいたまま、ノードの復旧か削除を待つ(6.3) |
| Job |
podFailurePolicy未指定時のデフォルトでは、Terminatingな旧Podの実終了を待たず、Deploymentと同様に即座に作成(6.4) |
DeletionTimestampが付いた事実だけでbackoffLimitの消費にカウントされ得る。PVCを使う場合はDeploymentと同様にVolumeのデタッチ待ちが起きる(7.3)。podReplacementPolicy: FailedにするとFailed確定を待つようになる(6.4) |
第1章: kubeletのハートビートが止まってからNotReadyになるまで
ノードに障害が発生してから、NodeオブジェクトがNotReadyのステータスに遷移するまでの流れを説明します。
1.1 kubeletが送るノードステータス更新とLease
kubeletはデフォルトで10秒ごとにノードの状態を計算します。NodeStatusをAPIへ報告するのは、状態が変化したとき、または報告間隔が経過したときです。未変更時の報告間隔は、設定を省略した場合は5分です。
if obj.NodeStatusUpdateFrequency == zeroDuration {
obj.NodeStatusUpdateFrequency = metav1.Duration{Duration: 10 * time.Second}
}
(参考: defaults.go#L136-L147)
この状態確認はwait.JitterUntilによってnodeStatusUpdateFrequency間隔で繰り返し呼び出されています(参考: kubelet.go#L1956)。
NodeStatusの更新に加えて、kubeletはより軽量なNode Lease(coordination.k8s.io/v1 のLeaseオブジェクト)の更新も行っています。デフォルトのNodeLeaseDurationSecondsは40秒です。
if obj.NodeLeaseDurationSeconds == 0 {
obj.NodeLeaseDurationSeconds = 40
}
(参考: defaults.go#L149-L150、Node Heartbeats)
1.2 Node Lifecycle Controllerによる生死判定
kube-controller-manager内のNode Lifecycle Controllerは、デフォルトの5秒間隔(NodeMonitorPeriod)でNodeの状態をポーリングし、最後に更新があった時刻(NodeStatusまたはLeaseのRenewTime)から一定時間応答が無いノードを不健全と判定します。この一定時間がNodeMonitorGracePeriodで、デフォルト値は50秒です。
// NodeMonitorGracePeriod is set to a default value of 50 seconds.
// This value should be greater than the sum of HTTP2_PING_TIMEOUT_SECONDS (30s)
// and HTTP2_READ_IDLE_TIMEOUT_SECONDS (15s) from the http2 health check
// to ensure that the server has adequate time to handle slow or idle connections
// properly before marking a node as unhealthy.
if obj.NodeMonitorGracePeriod == zero {
obj.NodeMonitorGracePeriod = metav1.Duration{Duration: 50 * time.Second}
}
if obj.NodeStartupGracePeriod == zero {
obj.NodeStartupGracePeriod = metav1.Duration{Duration: 60 * time.Second}
}
if obj.NodeMonitorPeriod == zero {
obj.NodeMonitorPeriod = metav1.Duration{Duration: 5 * time.Second}
}
(参考: defaults.go#L36-L52)
コメントにある通り、50秒という値はHTTP/2のPINGタイムアウト(30秒)とアイドルタイムアウト(15秒)の合計より大きく取る必要がある、という通信レイヤーの制約から決められています。NodeStartupGracePeriod(60秒)は、ReadyのConditionがまだ存在しない、新規に参加したノードにのみ使われる猶予期間です。
実際の判定処理はtryUpdateNodeHealthで行われます。Node LeaseのRenewTimeが更新されていれば、NodeStatus自体が更新されていなくても生存確認とみなされる点が実装上のポイントです(参考: node_lifecycle_controller.go#L844-L962)。
1.3 Ready ConditionのUnknownへの書き換えとTaintの付与
NodeMonitorGracePeriodを超えて応答が無いと判定されると、NodeのReady ConditionがUnknownへ書き換えられます。この時点でkubectl get nodesのSTATUS列はNotReadyと表示されます(参考: Node Controller)。
Node Lifecycle Controllerは、Ready Conditionの状態に応じて次のTaintをNodeに付与します。
nodeConditionToTaintKeyStatusMap = map[v1.NodeConditionType]map[v1.ConditionStatus]string{
v1.NodeReady: {
v1.ConditionFalse: v1.TaintNodeNotReady,
v1.ConditionUnknown: v1.TaintNodeUnreachable,
},
...
(参考: node_lifecycle_controller.go#L74-L94)
TaintNodeNotReady = "node.kubernetes.io/not-ready"
TaintNodeUnreachable = "node.kubernetes.io/unreachable"
(参考: well_known_taints.go#L20-L27)
Podの退去に関わるのは、これらのキーを持つEffect: NoExecuteのTaintです。新規配置を制限するNoScheduleのTaintも別途付与されます。ハートビートが完全に止まった(応答不能)場合はReadyがUnknownになるためnode.kubernetes.io/unreachableが、kubelet自体は動いているがヘルスチェックが不健全と自己申告している場合はReadyがFalseになるためnode.kubernetes.io/not-readyが付与される、という違いになります(参考: Taint Nodes by Condition)。
第2章: Taint-based EvictionによるPodの削除要求
NotReadyになったノードにTaintが付与された後、そのノード上のPodがどのタイミングで、どういう形で削除要求を受けるのかを説明します。
2.1 DefaultTolerationSecondsが付与するデフォルトの許容時間
NoExecuteのTaintが付いたノード上のPodは、対応するTolerationを持っていない限り即座に削除対象になります。ただし多くのPodは、明示的にTolerationを書いていなくても即座には削除されません。これはDefaultTolerationSecondsというAdmission Pluginが、Pod作成時にデフォルトのTolerationを自動で追加しているためです。
defaultNotReadyTolerationSeconds = int64(300)
defaultUnreachableTolerationSeconds = int64(300)
(参考: admission.go#L44-L45)
Podがnode.kubernetes.io/not-ready:NoExecute・node.kubernetes.io/unreachable:NoExecuteのいずれかに対する明示的なTolerationを持たない場合、このAdmit処理がtolerationSeconds: 300のTolerationをPodに追加します(参考: admission.go#L125以降)。つまり、Taintが付いてからデフォルトの300秒(5分)は、そのノード上のPodはまだ削除されません。
対応するTolerationを明示すれば、待ち時間を変更できます。そのTolerationでtolerationSecondsを省略すると無期限になります。Toleration自体を省略した場合は、上記のデフォルト値が追加されます。
2.2 TaintManagerが行うのはGraceful Delete
tolerationSecondsが経過すると、TaintManager(pkg/controller/tainteviction)が対象のPodを削除します。ここで実際に呼ばれているのは通常のGraceful Delete要求です。
func addConditionAndDeletePod(ctx context.Context, c clientset.Interface, name, ns string) (err error) {
...
return c.CoreV1().Pods(ns).Delete(ctx, name, metav1.DeleteOptions{})
}
(参考: taint_eviction.go#L129-L147)
metav1.DeleteOptions{}はGracePeriodSecondsを明示的に0にしていない、デフォルトのDelete呼び出しです。Force Delete(grace-period=0相当)ではありません。この呼び出しでPodにDeletionTimestampが付与され、あとはそのノード上のkubeletがこのタイムスタンプを検知してコンテナの終了処理を行い、最終的にPodオブジェクトの削除が完了する、という通常のPod終了の流れに委ねられます。
2.3 応答しないノードではPodが"Terminating"のまま残る
問題は、この終了処理を担うはずのkubeletが応答不能な状態にある点です。DeletionTimestampは付与されますが、それを検知してコンテナを止め、Podオブジェクトの削除を完了させる主体が存在しません。結果として、そのノード上のPod(DaemonSetのPodを除きます。理由は第6章で説明します)はkubectl get pods上でTerminatingのまま残り続けます。
次の第3章では、Nodeの削除とout-of-serviceTaintを起点とするPodGCの処理を説明します。kubeletの復旧による通常の削除完了や、運用者によるPodの強制削除も別の経路です。
第3章: 残り続けるPodを強制的に取り除く2つの手段
Terminatingのまま残ったPodを最終的に取り除く仕組みを、cloud-controller-managerが動いているかどうかで分けて説明します。
この2つの手段を理解するうえで押さえておきたいのが、Kubernetesの各コンポーネントが観測できるのは「一定時間、応答が無い」という事実だけだという点です。それが、ハードウェア障害・電源断でノードが本当に停止しているのか、それとも一時的なネットワーク不通でノード自体(kubelet、その上のコンテナ)は正常に動き続けているだけなのかを、Kubernetes自身は区別できません。この区別がつかないという制約が、以降で見る「強制削除の自動化がどこまでできるか」を左右します。
3.1 cloud-controller-managerが動いている場合: Nodeオブジェクトの削除に追随するPodGCの自動処理
cloud-controller-managerにはCloudNodeLifecycleControllerという、クラウドプロバイダのAPIでインスタンスの生死を確認するコンポーネントがあります。
// MonitorNodes checks to see if nodes in the cluster have been deleted
// or shutdown. If deleted, it deletes the node resource. If shutdown it
// applies a shutdown taint to the node
func (c *CloudNodeLifecycleController) MonitorNodes(ctx context.Context) {
(参考: node_lifecycle_controller.go#L143-L146)
このコントローラーが、インスタンスが実際に無くなったことを確認すると、Nodeオブジェクトを削除します。
if err := c.kubeClient.CoreV1().Nodes().Delete(ctx, node.Name, metav1.DeleteOptions{}); err != nil {
(参考: node_lifecycle_controller.go#L195)
このNode削除をきっかけに、PodGCController(pkg/controller/podgc)のgcOrphaned処理が動きます。
対象のノードが(デフォルトで40秒のquarantineTimeの猶予を置いたうえで)ノード一覧に存在しないと判定されると、そのノードに紐づく全Pod(コントローラーの種類を問いません)がPodのStatusをFailedへ更新したうえで、GracePeriodSeconds 0のForce Deleteで削除されます。
const (
quarantineTime = 40 * time.Second
)
func (gcc *PodGCController) gcOrphaned(ctx context.Context, pods []*v1.Pod, nodes []*v1.Node) {
...
(参考: gc_controller.go#L51、gc_controller.go#L229-L268、gc_controller.go#L346-L366)
cloud-controller-managerが動いている場合、クラウドプロバイダのAPIで「インスタンスが本当に無くなった」ことを確認してからNodeオブジェクトを削除するため、Nodeの削除からPodGCによる後処理まで自動で進みます。ただし、応答不能でもインスタンスが存在すると判断される場合は、この経路で削除されるとは限りません。
3.2 cloud-controller-managerが動いていない場合: Non-Graceful Node ShutdownとNode.out-of-service Taint
一方、cloud-controller-managerが動いていない、あるいはインスタンスの生死を確認する手段を持たない環境(例えばオンプレミスのベアメタルサーバーなど)では、ハードウェア障害が起きてもNodeオブジェクトが自動で削除されるとは限りません。誰かが明示的に削除しない限り、Nodeオブジェクトはクラスタ上に残り続けます。この場合、3.1のPodGCの自動処理は働かず、PodはTerminatingのまま放置されます。
この状況に対応するために用意されているのが、Non-Graceful Node Shutdown(KEP-2268)という機能です。v1.28でGAになった、Kubernetesの標準機能です(参考: Non-graceful node shutdown handling、Kubernetes 1.28: Non-Graceful Node Shutdown Moves to GA)。実際に動作検証した記事としてKubernetes: Non Graceful Node Shutdownの動作検証も参考になります。
この機能は、運用者がノードの死亡を確認したうえで、node.kubernetes.io/out-of-serviceというTaintをNodeへ手動で付与することを起点に動きます。
func (gcc *PodGCController) gcTerminating(ctx context.Context, pods []*v1.Pod) {
...
if !nodeutil.IsNodeReady(node) && taints.TaintKeyExists(node.Spec.Taints, v1.TaintNodeOutOfService) {
// 強制削除の対象にする
(参考: gc_controller.go#L148-L169)
PodGCはNodeがReadyでなく、かつこのTaintが付いている場合に、TerminatingなPodを強制削除します。さらに、Volumeの強制デタッチを担うAttach/Detach Controllerも同じTaintを見ています。
func (rc *reconciler) hasOutOfServiceTaint(nodeName types.NodeName) (bool, error) {
...
return taints.TaintKeyExists(node.Spec.Taints, v1.TaintNodeOutOfService), nil
}
(参考: reconciler.go#L147-L153)
これにより、kubeletからの「アンマウントが完了した」という確認を待たずにPersistentVolumeを強制的にデタッチできます。PVを使うStatefulSetの復旧では、このVolume側の強制デタッチも合わせて動くことが重要になります(詳細は第7章で説明します)。
Kubernetes自体は、ノードが本当に停止しているのか、単にネットワークが分断されて実際には動き続けているだけなのかを区別できません。そのためnode.kubernetes.io/out-of-serviceTaintの付与は自動化されておらず、運用者が(クラスタの外部から、ノードのOSに依存しない方法で)ノードの停止を確認してから手動で行う操作として設計されています。復旧の可能性が残っている段階でこのTaintを付けると、旧Podが実際には動き続けたまま新しいPodが別ノードで起動し、同じデータへ同時に書き込むといった事故につながる可能性があります。付与する前に、ノードが本当に停止していることを確認してからにするのが良いと思います。
第4章: 状態遷移の全体タイムライン
第1〜3章で見た内容を、デフォルト値だけを使ったタイムラインとして1つの表にまとめます。実際の値は環境ごとの設定と非同期処理の進み方で変わります。T+50秒・T+350秒は説明用の目安です。監視猶予はコントローラーが最後にハートビートの更新を観測した時点から数えられ、障害発生時点とは一致しません。
まず、共通の処理と管理単位ごとの分岐を図で示します。DaemonSet以外は、not-ready/unreachableに対するデフォルトのTolerationを使う場合を想定しています。JobはpodFailurePolicyを指定せず、podReplacementPolicy: TerminatingOrFailedを使う場合です。
代替Podの作成後も、配置先の確保やVolumeの準備を待つ場合があります。時間の目安とPodGCの処理経路は、次の表にまとめます。
| 経過時間(デフォルト値) | 何が起きるか |
|---|---|
| T+0 | ノードが物理障害で応答不能になる。kubeletからのNodeStatus/Lease更新が止まる |
| 〜T+50秒 | Node Lifecycle ControllerがNodeMonitorGracePeriod超過を検知。Ready ConditionをUnknownに、node.kubernetes.io/unreachable(NoExecute)Taintを付与。kubectl get nodesはNotReady
|
| 〜T+350秒 |
DefaultTolerationSecondsのデフォルトの300秒が経過。TaintManagerが対象Podに対しGraceful Delete要求(DeletionTimestamp付与)。DaemonSetのPodはこの対象になりません(6.3) |
| T+350秒以降 | kubeletが応答しないため実際の終了処理が進まず、対象PodはTerminatingのまま残る。Deployment/Jobはこの時点で既に代替Podの作成を開始している(6.1・6.4)。StatefulSetは同じOrdinalの代替Podをブロックしたまま待機する(6.2) |
| Nodeオブジェクトが削除された時点(cloud-controller-manager経由) | PodGCのgcOrphanedが(40秒のquarantineTime後に)対象PodをFailedへ更新して強制削除する(3.1) |
運用者がout-of-serviceTaintを付与した時点(cloud-controller-manager無し) |
PodGCのgcTerminatingとAttach/Detach Controllerが強制削除・強制デタッチを行い、StatefulSetのブロックが解除される(3.2) |
第5章: 一時的なネットワーク障害から復旧した場合
ここまでは、ノードが応答不能なまま復旧しないケースを前提に説明してきました。第3章で見た通り、Kubernetes自身はノードが本当に停止しているのか、一時的なネットワーク不通で実際には動き続けているだけなのかを区別できません。ここでは後者だった場合、つまりPodの削除が進んでいる途中でネットワークが復旧した場合に何が起きるかを、復旧のタイミング別に確認します。
| 復旧のタイミング | Ready Condition・Taint | Podの状態 |
|---|---|---|
| 不健全と判定される前 | 一度もFalse/Unknownに変わらず、Taintも付与されない |
何も起きない。kubectl get nodes上でNotReadyが観測されることすらない(5.1) |
| 不健全と判定された後、削除要求が実行される前 | 一度NotReadyと判定されTaintは付与されるが、ReadyがTrueに戻ると自動で除去される |
TaintManagerが既にスケジュール済みの削除予定をキャンセルするため、Podは一度も削除されない(5.2) |
| 削除要求が実行された後 | Taintは除去されるが、既に発行されたDelete要求(DeletionTimestamp)自体は取り消せない |
復旧したkubeletが通常の終了処理を引き継いで完了させる。管理単位やPVCの有無によっては、それだけでは済まない影響が残る(5.3〜5.5) |
5.1 そもそもNotReadyと判定されない場合
1.2節で見たtryUpdateNodeHealthは、最後に更新があった時刻(NodeStatusまたはLeaseのRenewTime)からNodeMonitorGracePeriod(デフォルト50秒)を超えて応答が無い場合にのみ、Ready Conditionを書き換えます。逆に言えば、不健全と判定される前にハートビートの更新を観測できれば、このハートビート停止を理由とするUnknownへの変更とTaintの付与は起きません。
この場合、Node Lifecycle Controller・TaintManagerのどちらにも処理は発生せず、ユーザーから見てもkubectl get nodes上でNotReadyが観測されることすらありません。以下の5.2〜5.5節で扱うのは、一度NotReadyと判定され、Taintが付与された後に復旧するケースです。
この50秒という閾値は、実際には複数の要因で変動します。kubeletのNode Lease更新間隔はNodeLeaseDurationSeconds(デフォルト40秒)の25%、つまりデフォルトで10秒ごとです。
// nodeLeaseRenewIntervalFraction is the fraction of lease duration to renew the lease
nodeLeaseRenewIntervalFraction = 0.25
(参考: kubelet.go#L236-L237)
leaseDuration := time.Duration(kubeCfg.NodeLeaseDurationSeconds) * time.Second
renewInterval := time.Duration(float64(leaseDuration) * nodeLeaseRenewIntervalFraction)
(参考: kubelet.go#L1155-L1156)
監視猶予の起点は、コントローラーが最後にハートビートの更新を観測した時点です。障害発生時点とのずれに加えて、監視周期やAPI通信の遅延もあるため、「障害から50秒以内に復旧すれば削除されない」とは断定できません。
5.2 Delete要求より前に復旧した場合
Node Lifecycle Controllerは、ReadyがTrueに戻るとmarkNodeAsReachableを呼び出し、not-ready・unreachable両方のNoExecute Taintを除去します。
case v1.ConditionTrue:
removed, err := nc.markNodeAsReachable(ctx, node)
if err != nil {
logger.Error(nil, "Failed to remove taints from node. Will retry in next iteration", "node", klog.KObj(node))
}
if removed {
logger.V(2).Info("Node is healthy again, removed all taints", "node", klog.KObj(node))
}
(参考: node_lifecycle_controller.go#L820-L826)
func (nc *Controller) markNodeAsReachable(ctx context.Context, node *v1.Node) (bool, error) {
err := controller.RemoveTaintOffNode(ctx, nc.kubeClient, node.Name, node, UnreachableTaintTemplate)
...
err = controller.RemoveTaintOffNode(ctx, nc.kubeClient, node.Name, node, NotReadyTaintTemplate)
...
return nc.zoneNoExecuteTainter[nodetopology.GetZoneKey(node)].Remove(node.Name), nil
}
(参考: node_lifecycle_controller.go#L1310-L1326)
このTaint除去をTaintManager(pkg/controller/tainteviction)が検知し、まだ実行していない削除予定を明示的にキャンセルします。
if len(taints) == 0 {
logger.V(4).Info("All taints were removed from the node. Cancelling all evictions...", "node", klog.KObj(node))
for i := range pods {
tc.cancelWorkWithEvent(logger, types.NamespacedName{Namespace: pods[i].Namespace, Name: pods[i].Name})
}
return
}
(参考: taint_eviction.go#L575-L580)
つまり、ネットワークが復旧し、削除処理が実行される前にTaint除去と削除予定のキャンセルが完了すれば、Podは一度も削除されず、代替Podも作られません。
5.3 Delete要求より後に復旧した場合
TaintManagerのaddConditionAndDeletePod(2.2節)が既にPods().Delete()を呼んでいる場合、PodのDeletionTimestampはetcdに確定済みです。Kubernetesには「削除の取り消し」という操作が無いため、この後でTaintを除去してもDeletionTimestampは消えません。
ただし、復旧した(ハートビートが再開した)kubeletは、自分が担当するPod一覧を同期する際にこのDeletionTimestampを見つけ、通常の終了処理(preStopフック・コンテナ停止・Volumeのアンマウント)を実行します。2.3節で説明した「応答しないノードではPodがTerminatingのまま残る」状態は、kubelet自身が戻ってくることで自然に解消します。
5.4 管理単位によって復旧後の見え方が変わる
Delete要求より後に復旧した場合、旧Podが実際に終了するまでの間、第6章で見る管理単位ごとの判断が先に進んでしまっている点に注意が必要です。
| 管理単位 | 復旧後に起きること |
|---|---|
| Deployment(ReplicaSet)・Job | 代替PodはT+350秒の時点で既に作成済み(6.1・6.4節)のため、復旧しても取り消されません。旧Podが正常終了するまでの間、一時的に望ましいレプリカ数より1つ多いPodが同時に稼働します |
| StatefulSet(デフォルトのOrderedReady) | 同じOrdinalの代替Podはブロックされたままでしたが(6.2節)、旧Podが実際にetcdから削除されることで、out-of-serviceTaintや強制削除を使わずに通常の形でブロックが解除されます。ネットワーク障害が本当に一時的だったなら、これが正規の回復です |
StatefulSetが、自身のコントローラーの外側にあるPodGCやout-of-serviceTaintに頼らず自力で回復できるのは、このようにネットワークが実際に復旧した場合だけです。本当にノードが停止している場合は、第3章・第8章で説明した強制削除の仕組みを待つ必要があります。
5.5 PVCがある場合: 6分タイマーの起点
第7章で説明する6分の自動強制デタッチ(ReconcilerMaxWaitForUnmountDuration)のカウント起点を確認すると、ノードがNotReadyになった時刻(T+50秒)ではなく、Attach/Detach ControllerがそのVolumeを実際にデタッチすべきと判断した時刻でした。
// If there is no previous detach request, set it to the current time
if nodeObj.detachRequestedTime.IsZero() {
nodeObj.detachRequestedTime = time.Now()
...
}
return time.Since(nodeObj.detachRequestedTime), nil
(参考: actual_state_of_world.go#L420-L439)
この判断は、PodがDeleteされて望ましい状態から外れたタイミング(おおむねT+350秒)に行われるため、6分のカウントもここから始まり、説明用の計算ではT+710秒(約12分)になります。ただし、デタッチ対象となる条件や各コントローラーの処理時点によって変わり、障害発生からの固定の期限ではありません。
ネットワーク分断中にタイムアウトによる強制デタッチが進み、旧PodがVolumeを使い続けていた場合、旧ノードでのアンマウント完了を確認せず処理するため、旧ノード側では実際にはまだマウントされたまま、という食い違いが起こり得ます。 これは7.1節で引用した公式ドキュメントの警告(force-detachされたVolumeを使い続けるワークロードはCSI仕様違反を起こし、データ破損の可能性がある)が想定している状況そのもので、out-of-serviceTaintを手動付与していなくても、自動タイムアウトだけで起こり得る点に注意が必要です。
第6章: 管理単位ごとのPodの挙動
ここまではTerminatingになったPod一般の話でしたが、実際にどう再作成されるか(あるいはされないか)はPodを管理しているリソースの種類によって異なります。Deployment/StatefulSet/DaemonSet/Jobの4種類について、それぞれのコントローラーの実装から確認します。
6.1 Deployment
ReplicaSet controllerは、望ましいレプリカ数を計算する際にIsPodActiveでPodを絞り込みます。
func IsPodActive(p *v1.Pod) bool {
return v1.PodSucceeded != p.Status.Phase &&
v1.PodFailed != p.Status.Phase &&
p.DeletionTimestamp == nil
}
(参考: controller_utils.go#L1085-L1089)
DeletionTimestampが付いた(第2章で見た、Taint-based EvictionによるTerminatingな)Podはアクティブなレプリカとして数えられません。ReplicaSet controllerはこの絞り込み後のactivePodsをもとに、不足分を即座に補充します。
allActivePods := controller.FilterActivePods(logger, allRSPods)
...
manageReplicasErr = rsc.manageReplicas(ctx, activePods, rs)
(参考: replica_set.go#L804、replica_set.go#L819)
diff := len(activePods) - int(*(rs.Spec.Replicas))
...
err := rsc.podControl.CreatePods(ctx, rs.Namespace, &rs.Spec.Template, rs, metav1.NewControllerRef(rs, rsc.GroupVersionKind))
(参考: replica_set.go#L650、replica_set.go#L678)
つまり、Deploymentが管理するPodは、旧Podが実際にそのノード上で終了しているかどうかを待たずに、DeletionTimestampが付いた時点で不足分として数えられ、代替Podが作成されます。別ノードで実行できるかは、空きリソースや配置条件にも依存します。第4章のタイムラインで言えば、デフォルト値ではT+350秒の時点で代替Podの作成が始まります。旧Podは、第3章で説明したいずれかの強制削除の仕組みが働くまでそのままTerminatingとして残りますが、レプリカ数の充足という観点では既に解決済みの扱いになります。
6.2 StatefulSet
StatefulSetのisTerminating判定もDeletionTimestampの有無だけで決まります。
func isTerminating(pod *v1.Pod) bool {
return pod.DeletionTimestamp != nil
}
(参考: stateful_set_utils.go#L491-L493)
ここからの挙動がReplicaSetとは対照的です。StatefulSetはデフォルトでspec.podManagementPolicy: OrderedReady(未指定時のデフォルト値)で動作しており、この場合の更新ポリシーは実装コード中ではmonotonicと呼ばれています。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sample
spec:
podManagementPolicy: OrderedReady # 省略した場合もこれがデフォルト値
replicas: 3
...
// UpdateStatefulSet executes the core logic loop for a stateful set, applying the predictable and
// consistent monotonic update strategy by default - scale up proceeds in ordinal order, no new pod
// is created while any pod is unhealthy, and pods are terminated in descending order.
(参考: stateful_set_control.go#L84-L86、OrderedReady Pod Management)
このポリシーのもとでは、同じOrdinal(番号)のPodがTerminatingである間、新しいPodの作成そのものがブロックされます。
if isTerminating(replicas[i]) && monotonic {
logger.V(4).Info(...)
return true, nil
}
(参考: stateful_set_control.go#L467-L471)
スケールダウン対象のPod(ソースコード上の変数名はcondemned)についても同様のブロックが入ります(参考: stateful_set_control.go#L513-L519)。StatefulSetのPodには固定された名前(Ordinal)と、volumeClaimTemplatesから紐づく専用のPersistentVolumeClaimがあり、同じ名前のPodが実際にはまだ動いている可能性がある間に別ノードで新しいPodを起動してしまうと、同じストレージへ二重に書き込むといった問題が起こり得ます。そのため、StatefulSet controllerは自分自身の判断だけでこのブロックを解除しません。
ここまでの挙動はspec.podManagementPolicy: OrderedReady(デフォルト値)を前提にしています。次のようにParallelを指定すると、monotonicはfalseになり、他のOrdinalのPodがReadyになるまで待つ、といった順序制約が緩和されます。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sample
spec:
podManagementPolicy: Parallel
replicas: 3
...
monotonic := !allowsBurst(set)
// allowsBurst is true if the alpha burst annotation is set.
func allowsBurst(set *apps.StatefulSet) bool {
return set.Spec.PodManagementPolicy == apps.ParallelPodManagement
}
(参考: stateful_set_control.go#L662、stateful_set_utils.go#L500-L502)
Parallelが変えるのは、異なるOrdinalのPod間の順序制約です。同じOrdinalの旧Podが存在する間に、その代替Podを作る設定ではありません。processReplicaは未作成のPodに対してのみCreateStatefulPodを呼ぶため、Terminatingな旧Podが残っていれば、Parallelでも同じ名前の代替Podは作成されません。
(参考: stateful_set_control.go#L422-L454、Parallel Pod Management)
このブロックが解除されるのは、Podオブジェクトが実際にetcdから削除された時だけです。つまり第3章で説明した、cloud-controller-manager経由のPodGCの自動処理、あるいはout-of-serviceTaintを起点とした強制削除のいずれかが実行されて初めて、StatefulSet controllerは同じOrdinalに対して新しいPodを作成できるようになります。新しいPodは別の(健全な)ノードへスケジュールされますが、名前とPersistentVolumeClaimは元のPodと同じものを引き継ぎます。
緊急対応として、これらの自動化された仕組みを待たずに運用者が手動でkubectl delete pod --grace-period=0 --forceを実行する手段も用意されていますが、ドキュメントでは「対象のPodが本当に終了していることを確認した後にのみ実行するべき」と明記されています(参考: Force Delete StatefulSet Pods)。手動のForce Deleteもout-of-serviceTaintも、判断の中身は同じで、Kubernetesの外側にいる運用者が「そのPodは本当に動いていない」ことを保証する必要がある点は変わりません。
6.3 DaemonSet
DaemonSetのPodは、他のノードへ再作成されることがありません。DaemonSet controllerが新規Podを作る際、そのPodのNodeAffinityを対象ノードの名前で固定するためです。
podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(
podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
(参考: daemon_controller.go#L1089-L1090)
さらに、DaemonSetのPodには作成時点で、not-ready・unreachable両方のNoExecute Taintに対する無期限のTolerationが自動付与されます。
// Add infinite toleration for taint notReady:NoExecute here
// ...
// Add infinite toleration for taint unreachable:NoExecute here
(参考: daemonset_util.go#L48-L67)
tolerationSecondsを指定しないTolerationは無期限を意味するため、DaemonSetのPodは第2章で説明したTaint-based Evictionの対象にそもそもなりません。ノードがNotReadyになっても、DaemonSetのPodはTerminatingにはならず、そのノードに紐づいたまま残ります。ノードが復旧すれば元のPodがそのまま動き続けますし、ノードオブジェクト自体が削除された場合は、コントローラーの種類を問わないPodGCのgcOrphaned処理(3.1)によって、DaemonSetのPodも他のPodと同様に片付けられます。
6.4 Job
Job controllerも、アクティブなPod数を数える際にReplicaSetと同じFilterActivePods(IsPodActive)を使います。
activePods := controller.FilterActivePods(logger, pods)
(参考: job_controller.go#L1035)
代替Pod作成の判断は次の式で行われます。
if diff := wantActive - terminating - active; diff > 0 {
(参考: job_controller.go#L1820)
ここでterminatingが意味を持つのは、spec.podReplacementPolicyがFailedの場合だけです。
func onlyReplaceFailedPods(job *batch.Job) bool {
return job.Spec.PodReplacementPolicy != nil && *job.Spec.PodReplacementPolicy == batch.Failed
}
(参考: job_controller.go#L2216-L2218)
podFailurePolicyを指定しない場合、podReplacementPolicyのデフォルト値はTerminatingOrFailedです。podFailurePolicyを指定する場合はFailedが使われます。(参考: types.go#L154-L165)。このデフォルト値のもとではonlyReplaceFailedPodsはfalseになるため、上の式のterminatingは実質0として扱われ、Job controllerはDeployment(ReplicaSet)と同じく、TerminatingなPodの実終了を待たずに代替Podを即座に作成します。
さらに、デフォルトのpodReplacementPolicyはbackoffLimitの判定にも影響します。
func isPodFailed(p *v1.Pod, job *batch.Job) bool {
if p.Status.Phase == v1.PodFailed {
return true
}
if onlyReplaceFailedPods(job) {
return false
}
// Count deleted Pods as failures to account for orphan Pods that
// never have a chance to reach the Failed phase.
return p.DeletionTimestamp != nil && p.Status.Phase != v1.PodSucceeded
}
(参考: job_controller.go#L2131-L2141)
デフォルトのTerminatingOrFailedポリシーでは、kubeletが実際にFailedフェーズを報告できない(ノードが応答不能な)場合でも、DeletionTimestampが付いているという事実だけでこのPodは失敗としてカウントされ、backoffLimitの消費に加算されます。コメントにある通り、これは「Failedフェーズに到達する機会が無いまま孤立するPod」を救済するための仕組みです。backoffLimitが小さいJobがノード障害のタイミングと重なると、実際にはコンテナが失敗していなくても、この扱いによってBackoffLimitExceededに到達し得ます。
podReplacementPolicy: Failedを明示的に指定した場合は、この即時カウントが行われなくなり、Podが実際にFailedフェーズに確定するまで代替Podの作成もbackoffLimitへの加算も待つようになります。ただしノードが応答不能なままではその確定自体が起こらないため、この設定を使う場合は第3章で説明した強制削除の仕組みが正しく構成されていることが前提になります。
第7章: PersistentVolumeClaimを使うPodの追加の考慮点
ここまではPodオブジェクトの削除・再作成だけを見てきましたが、PersistentVolumeClaim(PVC)を使うPodには、Volumeのデタッチ・アタッチという別の制約が加わります。この制約はStatefulSetに限らず、Deployment・Jobが管理するPodでも同様に発生します。
ただし、ここで説明するのはあくまでPod/VolumeというKubernetesのオブジェクトレベルでの挙動です。Volumeが強制デタッチされた後にファイルシステムが実際に壊れていないか、同じデータへの二重書き込みにアプリケーションが耐えられるかといったストレージ層・アプリケーション層での安全性はこの章の範囲外であり、WAL再生・冪等性・分散ロックといったアプリケーション側の設計で別途担保する必要があります。
7.1 Volumeのデタッチにはデフォルトの自動フォールバックがある
第3章で見たout-of-serviceTaintとは別に、Attach/Detach Controllerにはデフォルトで有効な、タイムアウトベースの自動強制デタッチがあります。
// maxWaitForUnmountDuration is the max amount of time the reconciler will wait
// for the volume to be safely unmounted, after this it will detach the volume
// anyway (to handle crashed/unavailable nodes). If during this time the volume
// becomes used by a new pod, the detach request will be aborted and the timer
// cleared.
var DefaultTimerConfig = TimerConfig{
ReconcilerLoopPeriod: 100 * time.Millisecond,
ReconcilerMaxWaitForUnmountDuration: 6 * time.Minute,
...
(参考: reconciler.go#L62-L66、attach_detach_controller.go#L87-L91)
forceDetachTimeoutExpired := maxWaitForUnmountDurationExpired && !rc.disableForceDetachOnTimeout
...
forceDetach := !isHealthy && forceDetachTimeoutExpired
...
if attachedVolume.MountedByNode && !forceDetach && !hasOutOfServiceTaint {
continue
}
(参考: reconciler.go#L225-L240)
disableForceDetachOnTimeoutはkube-controller-managerの--disable-force-detach-on-timeoutフラグに対応し、デフォルト値はfalseです。
fs.BoolVar(&o.DisableForceDetachOnTimeout, "disable-force-detach-on-timeout", false, "Prevent force detaching volumes based on maximum unmount time and node status. If this flag is set to true, the non-graceful node shutdown feature must be used to recover from node failure. ...")
(参考: attachdetachcontroller.go#L40)
つまりデフォルトの構成では、ノードがUnhealthyになってから6分(ReconcilerMaxWaitForUnmountDuration)経過すると、out-of-serviceTaintが無くてもVolumeは自動的に強制デタッチされます。公式ドキュメントでも、このデフォルトの動作は明確にリスクとともに説明されています。
In any situation where a pod deletion has not succeeded for 6 minutes, kubernetes will force detach volumes being unmounted if the node is unhealthy at that instant. Any workload still running on the node that uses a force-detached volume will cause a violation of the CSI specification... In such circumstances, volumes on the node in question might encounter data corruption.
(参考: Forced storage detach on timeout)
7.2 Volumeのデタッチと、Podオブジェクトの削除は別々に進む
この6分タイムアウトは、あくまでVolumeの状態に関する処理です。第6章6.2で見たStatefulSetの代替Pod作成のブロックは、Volumeの状態とは無関係に、PodオブジェクトのDeletionTimestampの有無だけで判定されています。PodGCのgcTerminatingがPodを強制削除するトリガーはout-of-serviceTaintの有無だけであり、Volumeのような「一定時間経過で自動的に」という仕組みはPodの強制削除には存在しません。
つまりPVCを使うStatefulSetのPodがノード障害に遭遇した場合、次の2つは別々の条件で進みます。
| 対象 | デフォルトの自動フォールバック |
|---|---|
| Volumeのデタッチ | あり。デタッチ要求の待機開始からデフォルトで6分経過し、ノードが不健全な場合に自動強制デタッチ(disableForceDetachOnTimeoutがデフォルトでfalse) |
| Podオブジェクトの削除 | 無し。Nodeオブジェクトの削除、またはout-of-serviceTaintの付与のいずれかが無い限り進まない |
Volumeが6分後に自動でデタッチされても、StatefulSetのPodオブジェクト自体はTerminatingのまま残るため、同じOrdinalの代替Podは作られません。cloud-controller-managerが動いていないなど、Nodeオブジェクトが自動削除されない構成では、Podの削除だけは結局、運用者(または外部システム)によるout-of-serviceTaintの付与を待つ必要があります。
StatefulSetでout-of-serviceTaintを使う場合を、シーケンス図にすると次のようになります。旧Podは既にTerminatingで、ノードの停止確認が済んでいるケースです。APIへの読み書きの一部は省略しています。
図の並行する2つの処理には、完了順序の保証がありません。代替Podが先に作られても、旧ノードからのデタッチと新ノードへのアタッチが済むまではVolumeを使って起動できません。
7.3 この制約はStatefulSetに限らない: Deployment/Jobでも同じVolume待ちが起きる
Volume側の制約は、Podを管理しているのがStatefulSetかDeploymentかを問いません。Attach/Detach Controllerは、同じPVC(ノード単位でしかアタッチできない、いわゆるsingle-attachのVolume)がまだ別ノードでアタッチ済みと判定している間、新しいPodへのアタッチを待たせます。
// reportMultiAttachError sends events and logs situation that a volume that
// should be attached to a node is already attached to different node(s).
func (rc *reconciler) reportMultiAttachError(logger klog.Logger, volumeToAttach cache.VolumeToAttach, nodes []types.NodeName) {
...
simpleMsg, _ := volumeToAttach.GenerateMsg("Waiting for detach", "Volume is already exclusively attached to one node, waiting on detach before it can be attached to another node")
(参考: reconciler.go#L376-L396)
第6章6.1で見た通り、Deployment(ReplicaSet)はTerminatingな旧Podの実終了を待たずに代替Podをすぐ作成します。しかし、その代替Podが新しいノードへスケジュールされた後、PVCが指すVolumeがまだ旧ノードにアタッチされたままだと、新しいPodはFailedAttachVolumeイベント(Waiting for detach: ...)を出しながらContainerCreatingのまま待たされます。つまりDeploymentでは代替Podオブジェクトはすぐできるものの、実際にコンテナが起動できるようになるタイミングは、StatefulSetと同じくVolumeのデタッチ完了に左右されます。
Jobも同じです。第6章6.4で見た通り、デフォルトのpodReplacementPolicy(TerminatingOrFailed)では、JobもTerminatingな旧Podの実終了を待たずに代替Podを即座に作成します。Jobでも、PVCの共有範囲はアクセスモードと配置に依存します。例えば、parallelism: 1のJobが単一のPVCを直接マウントしている場合、Deploymentと同じ理由で代替PodがContainerCreatingのままVolumeのデタッチを待たされます。
podReplacementPolicy: Failedでは、代替Podの作成は旧PodのFailed確定を待ちます。ただし、Podの状態更新とVolumeのデタッチは別のコントローラーが処理するため、この設定でもデタッチ完了は保証されません。代替PodがVolume待ちになる可能性は残ります。
PVCを複数Podで共有できる範囲はアクセスモードに依存します。ReadWriteOnceは単一ノードからの読み書きを意味し、同じノード上の複数Podで共有できる場合があります。複数ノードからの読み書きにはReadWriteManyなどの対応が必要です。また、Attach処理が必要かどうかはCSIドライバーによって異なります。
第8章: 手動でのTaint付与を自動化する動きの現状
第3章で見た、運用者がnode.kubernetes.io/out-of-serviceTaintを手動で付与しなければならないという制約について、これを解消しようとする取り組みがkubernetes/enhancementsにあるかを確認します。
8.1 KEP-2268自身が明記する今後の課題
Non-Graceful Node Shutdown(KEP-2268)自身のGoalsの節には、次のように明記されています。
Requires user intervention to detect node shutdown/failure cases, because currently there is no good way of detecting whether node is in the middle of restarting.
Once the feature in this KEP is developed and tested, we plan to work on automatic way of detecting and fencing node for shutdown/failure use cases and re-evaluating some approaches listed in the Alternatives section.
(参考: KEP-2268: Non graceful node shutdown)
KEP-2268は、自動検知・フェンシングを今後の課題として挙げています。これは本記事の対象バージョンで自動化が提供されているという意味ではありません。KEP内で代替案として検討された「Node Fencing」も、ノード停止に必要なコマンドが環境ごとに異なり、汎用的なin-tree実装としては踏み込みすぎているという理由で採用が見送られています(参考: 同README「Node fencing」節)。
8.2 Taintの判定自体は、付与した主体を区別しない
Taintの付与は、運用環境に合わせた外部システムでも実行できます。PodGCのgcTerminatingとAttach/Detach ControllerのhasOutOfServiceTaintは、いずれもnode.kubernetes.io/out-of-serviceTaintを確認しており、それを人間がkubectl taintで付けたのか、他の主体が付けたのかを区別していません。
if !nodeutil.IsNodeReady(node) && taints.TaintKeyExists(node.Spec.Taints, v1.TaintNodeOutOfService) {
(参考: gc_controller.go#L148-L169)
func (rc *reconciler) hasOutOfServiceTaint(nodeName types.NodeName) (bool, error) {
...
return taints.TaintKeyExists(node.Spec.Taints, v1.TaintNodeOutOfService), nil
}
(参考: reconciler.go#L147-L153)
公式ドキュメントでも「a user can manually add the taint」という表現が使われていますが、これは想定される典型的な運用フローの説明であり、技術的な制約ではありません。
To mitigate the above situation, a user can manually add the taint
node.kubernetes.io/out-of-service...
(参考: Non-graceful node shutdown handling)
つまり、ノードのヘルスチェックを行い、本当に停止していると判断した場合にこのTaintをNodeオブジェクトへ付与するカスタムコントローラーを用意すれば、8.1で見た「手動介入」の部分だけを自動化でき、PodGCとAttach/Detach Controller側の強制削除・強制デタッチはそのまま動作します。8.1のKEP-2268で見送られた「Node Fencing」という代替案も、まさにこの「外部システムが確認してノードを隔離する」というパターンであり、in-tree実装としては採用されなかっただけです。
ただし、このヘルスチェックをどこから行うかには注意が必要です。node-problem-detector(NPD)はノードの健全性を監視する代表的なプロジェクトですが、カーネルデッドロックやファイルシステム破損、コンテナランタイムの無応答といった項目を、NPD自身がそのノード上で動き続けて検知し、API Serverへ報告する作りになっています。
node-problem-detector aims to make various node problems visible to the upstream layers in the cluster management stack. It is a daemon that runs on each node, detects node problems and reports them to apiserver.
(参考: node-problem-detector README)
NPDが障害をAPI Serverへ報告できることと、NodeのReady Conditionは別です。NPDの報告だけではノード全体の停止は確認できません。また、ノードが完全に停止すれば、その上のNPDも停止します。out-of-serviceTaintを自動付与するには、外部からの監視やフェンシングによって、旧ノードが処理や書き込みを続けないことを確認する仕組みが必要です。
この判断に使えるヘルスチェックは、ノードのOSやネットワークスタックに依存しない、外部からの確認手段である必要があります。IPMI/BMC/Redfishでの電源状態の問い合わせや、ハイパーバイザー側からの停止確認、複数の観測点の合意を必要とするフェンシングといった仕組みがこれにあたります。NPDのREADMEが挙げている「Remedy Systems」の中には、まさにこうした確認と対処を組み合わせた実装の例があります。
- mediK8S is an umbrella project for automatic remediation system build on Node Health Check Operator (NHC)... Poison-Pill is a remediator that will reboot the node and make sure all statefull workloads are rescheduled.
- MachineHealthCheck of Cluster API are responsible for remediating unhealthy Machines.
(参考: node-problem-detector README「Remedy Systems」節)
これらのプロジェクトが内部で具体的にどのような手段(IPMI経由の再起動、out-of-serviceTaintの付与、Nodeオブジェクトの削除など)でノードを隔離しているかは、本記事ではソースコードまで確認しておらず、一般的な理解に基づく記述です。
8.3 KEP-6371: ノード生存判定の外部化(直接の解決策ではない)
隣接する領域の提案として、KEP-6371「External Node Liveness Detection」です。v1.38でAlpha導入予定で、2026-09-14に起票されたばかりの、まだ新しいKEPです(kubernetes/kubernetes側にはfeature gate ExternalNodeLivenessDetectionの実装がまだ反映されていません)。
このKEPは、第1章で説明したNode Lifecycle Controller自身の生存判定を、外部の実装に置き換え可能にするものです。動機は自動化の高度化ではなくコストで、5000ノード規模のスケーラビリティ検証ではNode Leaseの更新がkube-apiserverへの全書き込みの約29%を占めており、外部システムの方がノードの健全性をより高速に検知できる場合がある、としています。
Node lease writes can be expensive. At scale, they are the largest single class of writes to the API server (~29% of all writes in a 5000-node scalability run), and the cost grows with every node added.
(参考: KEP-6371: External Node Liveness Detection)
このKEPが解決しようとしているのは、あくまで第1章で説明した「ノードが本当に応答しなくなってからNotReadyと判定されるまで」の検知手段の差し替えであり、第3章で説明した「out-of-serviceTaintを誰が・どう付与するか」という判断そのものを自動化するものではありません。実際、Motivationの節では、外部システムがReady=Unknownを書き込んでも、現状のNode Lifecycle Controllerの実装(monitorNodeHealthが自身の更新前後だけを比較する作り)ではPodをNotReadyにできないという別のギャップも指摘されています。
Today, the node lifecycle controller does not cooperate well with external sources of node liveness. If an external system writes
Ready=Unknown, the node lifecycle controller taints and evicts the node but never marks the node's pods NotReady.
(参考: KEP-6371: External Node Liveness Detection)
KEP-6371は、本記事で参照した版ではv1.38でのAlpha導入を目指す提案です。Non-Goalsにも「デフォルトの挙動を変えない」「外部の生存判定コンポーネント自体は提供しない」と明記されています。ノード障害検知の高速化や大規模クラスタでのコスト削減には寄与し得ますが、StatefulSetが応答不能ノード上で自動復旧するかどうかとは別の話として捉えるのが良いと思います。
おわりに
ノード障害時の復旧は、障害検知、Podの削除、代替Podの作成、Volumeの切り替えに分かれ、それぞれ非同期に進みます。Podオブジェクトが作られたことと、アプリケーションが安全に処理を再開できることを分けて確認する必要があります。
オンプレミスで運用するなら、クラスタ外部からの監視とフェンシング、停止確認後のout-of-serviceTaint付与、故障ノードの交換までを復旧手順として用意しておきたいと感じました。特にPVCを使うワークロードでは、二重書き込みの防止と再実行時の冪等性を含めて、障害試験で確認しておくことが大切です。
参考
参考にした情報を記事に載せた順番で紹介します。
- Node Heartbeats
- Node Controller
- Taint Nodes by Condition
- Non-graceful node shutdown handling
- Kubernetes 1.28: Non-Graceful Node Shutdown Moves to GA
- Kubernetes: Non Graceful Node Shutdownの動作検証
- OrderedReady Pod Management
- Force Delete StatefulSet Pods
- Forced storage detach on timeout
- KEP-2268: Non graceful node shutdown
- node-problem-detector README
- KEP-6371: External Node Liveness Detection