Kubernetes 中替换单个节点看似只是删掉再新建,实际却涉及调度、服务端点、存储和运行时多个层面的协同。节点本身不会替业务完成连接排空,如果直接关机、执行 kubectl delete node 或者让云厂商直接销毁虚拟机,Kubelet 会立即停止心跳,容器进程可能被强制终止,Service 控制器只能事后摘除端点,这段时间进入 Pod 的流量就会产生 5xx、连接重置或请求超时。真正安全的节点替换必须把流量摘除放在容器退出之前,并通过不可变节点构建让新节点能够快速接管。

下文会从节点删除的风险点开始,说明 cordon、drain、PodDisruptionBudget 和应用生命周期钩子如何配合,再给出节点镜像构建与滚动替换的实践步骤,最后梳理验证方法和容易被忽略的坑。
一、为什么直接删除节点会造成业务中断
直接执行 kubectl delete node 或关闭虚拟机时,API 服务器会移除节点对象,但节点上的 kubelet 已经无法再上报状态,节点控制器会按照 pod-eviction-timeout 的默认值等待一段时间后才把 Pod 标记为 Terminating。然而现实环境中,虚拟机一旦关机,容器进程大概率已经退出,这期间 Pod 仍可能被 Service 后端引用,直到 EndpointSlice 控制器发现 Pod 不再 Ready 并完成摘除。请求到达一个已经不存在或正在强制关闭的容器时,失败就不可避免。
另一个隐蔽问题是 preStop 钩子是否真正执行。Kubernetes 只有在 API 层面发起删除 Pod 或节点驱逐时,才会给容器发送 SIGTERM,并等待 terminationGracePeriodSeconds。如果整机断电或强制关机,SIGTERM 根本没有机会发送,应用也就无法在退出前完成反注册、事务收尾或长连接排空。因此节点替换方案不能依赖故障场景下的优雅退出,必须在节点仍健康时主动发起驱逐。
还需要区分 drain 与零停机之间的关系。kubectl drain 只是驱逐 Pod 的工具,它默认通过 Eviction API 发起请求,会检查 PDB,但不会主动修改 Service 端点摘除时序。真正决定请求能否平滑断开的是应用自身的 preStop 逻辑和 readinessProbe 状态变化。若应用没有就绪探针,或探针在容器收到 SIGTERM 后仍返回 200,流量就可能在进程退出时仍被转发。
二、零停机替换节点的命令时序与关键配置
一套安全的节点替换流程至少包含四步:标记节点不可调度、驱逐 Pod、验证节点无业务负载、删除旧节点。第一步使用 cordon 阻止新 Pod 调度到该节点,但不会影响已经运行的 Pod。第二步使用 drain 触发驱逐。为什么不能直接 kubectl delete pod?因为 drain 会根据 PDB 做保护,并且只处理受控的 Pod,删除过程可以被观测和重试。下面是常见的命令组合。
# 1. 标记节点不可调度 kubectl cordon node-192-168-1-10 # 2. 驱逐 Pod,忽略 DaemonSet,允许删除 emptyDir 数据 kubectl drain node-192-168-1-10 \ --ignore-daemonsets \ --delete-emptydir-data \ --grace-period=120 \ --timeout=300s
命令中的 --grace-period 会覆盖 Pod 默认的终止宽限期,给应用足够时间执行 preStop。如果某个服务需要更长的排空时间,应该在该服务的 Pod 定义中显式设置 terminationGracePeriodSeconds,而不是全局无限放大。--delete-emptydir-data 用于跳过 emptyDir 卷保护,因为 emptyDir 数据通常可以随 Pod 删除,但如果节点上运行了临时数据库或缓存,需要先确认数据是否真的不需要保留。DaemonSet 默认会阻止 drain,所以必须加上 --ignore-daemonsets,但要注意 DaemonSet Pod 不会随 drain 离开节点,节点删除后它们会被调度到其他节点重新创建。
PodDisruptionBudget 是防止自愿中断导致副本数低于可用下限的关键配置。它并不是流量摘除机制,而是给 drain 加了一道闸门。例如一个三副本服务配置 minAvailable: 2,在同一次 drain 中最多只能有一个 Pod 被驱逐,这能避免节点维护时整个服务不可用。PDB 必须与应用的副本数和实际可用性要求匹配,否则 drain 会一直返回 429 并最终超时。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: payment-api
应用层的 preStop 钩子同样不能忽略。对于 HTTP 服务,常见做法是先让 readinessProbe 失败或返回非就绪状态,等待几秒后由 Service 摘除端点,再退出进程。对于 gRPC、WebSocket 或 TCP 长连接服务,仅靠 readinessProbe 不够,因为已建立的连接不会主动断开,需要在 preStop 中触发连接排空或服务端优雅关闭。下面是一个带显式宽限期和 preStop 的 Pod 示例。
apiVersion: v1
kind: Pod
metadata:
name: payment-api-7d5c9f8b6-abcde
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
image: registry.ipipp.com/payment-api:v2.4.0
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 8 && nginx -s quit"]
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 3
failureThreshold: 3
这段配置的关键在于 preStop 先等待 8 秒,这段时间足够 readinessProbe 将 Pod 标记为 NotReady,并由 EndpointSlice 控制器把 Pod 从 Service 后端摘除。之后再执行应用退出命令,已到达的请求可以继续处理,新请求则不会进入该 Pod。实际睡眠时长需要根据探针周期、负载均衡同步延迟和业务连接持续时间综合调整。
三、构建不可变节点与滚动替换实践
节点构建的目标是让新节点加入集群后能够立刻承担业务,而不是现场安装依赖。建议使用 Packer、云厂商镜像服务或容器优化系统提前生成节点镜像,把 kubelet 版本、容器运行时、内核参数、安全基线和网络插件全部固化。镜像构建完成后,可以在新节点初始化时预拉取业务镜像,减少 Pod 启动阶段的镜像下载耗时。预拉取可以通过节点初始化脚本或 DaemonSet 完成。
# 节点初始化时预拉取业务镜像 crictl pull registry.ipipp.com/payment-api:v2.4.0 crictl pull registry.ipipp.com/gateway:v1.9.3
滚动替换节点时要按节点池或自动伸缩组分批进行。先把新节点加入集群,等待 NodeReady 并且 kubelet 正常上报资源,再对旧节点执行 cordon 和 drain。不要一次性替换所有节点,尤其是在运行有状态服务或者使用 local 持久卷的集群中,批量替换可能导致数据副本短时不可用或 Pod 无法重新调度。一般建议每批替换节点池总规模的 20% 到 33%,并在每批之间观察 API 错误率、Pod 重启次数和节点资源水位。
在云环境和混合架构中,如果使用 Cluster Autoscaler 或 Karpenter,缩容操作本身会考虑 PDB 和 Pod 驱逐安全性,但手动替换时仍需控制顺序。可以利用 taint 和 toleration 做更细粒度的流量隔离。例如先给旧节点加上 NoSchedule taint,只让新节点接受新调度,再配合 drain 逐步迁移存量 Pod。
# 给旧节点添加 taint 隔离新调度 kubectl taint nodes node-192-168-1-10 maintenance=true:NoSchedule # 回滚时移除 taint kubectl taint nodes node-192-168-1-10 maintenance=NoSchedule-
节点替换流程中还要考虑 DaemonSet。日志采集、监控代理、CNI 插件和安全组件通常以 DaemonSet 方式运行,它们不会因为 drain 而离开节点。新节点加入时这些 Pod 会自动创建,但旧节点删除前需要确认它们不会成为流量依赖。尤其是网络插件,如果新节点的 CNI 尚未就绪,新调度过来的业务 Pod 可能无法获得 IP,导致启动失败。
四、验证与容易被忽略的坑
替换完成后不能只看节点是否变成 Ready,还要确认旧节点上的业务 Pod 已经全部离开,并且 Service 端点中不再包含旧 Pod 的地址。可以用 kubectl get pods -o wide --all-namespaces | grep 旧节点名 检查残留,也可以查看 EndpointSlice 中是否还有指向旧 IP 的端点。只有确认没有旧 Pod 残留,才能安全关闭或删除节点。
拓扑域不均衡是节点替换中的一个隐蔽问题。如果服务副本都集中在某一个可用区或节点池,drain 一个节点就可能导致该服务在某个可用区没有可用副本。通过配置 topologySpreadConstraints 可以让 Pod 在节点替换时仍保持跨可用区或跨节点池分布,避免局部故障放大为服务不可用。
apiVersion: v1
kind: Pod
metadata:
name: payment-api-7d5c9f8b6-xyz
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-api
使用 local 持久卷、hostNetwork 或 hostPort 的服务在节点替换时风险更高。local 卷不会随 Pod 迁移到新节点,drain 后 Pod 可能因为找不到对应本地盘而一直 Pending,这需要提前复制数据或改用 CSI 存储。hostNetwork 的 Pod 占用节点网络命名空间,旧 Pod 退出后端口才会释放,新 Pod 才能绑定,因此需要给新 Pod 足够的调度延迟。hostPort 同理,如果新旧 Pod 同时运行,端口冲突会导致新 Pod 无法启动,最终造成容量不足。
最后建议在正式替换前进行一轮灰度验证。先替换一个低流量节点,观察监控中的 5xx、连接重置和延迟分位数,确认新节点上的 Pod 就绪并通过健康检查后再继续后续批次。节点级零停机不是单一命令能够完成,而是调度策略、应用生命周期、镜像构建和验证流程共同作用的结果。
Kubernetes节点维护零停机PodDisruptionBudget修改时间:2026-08-23 12:05:59