抢占式实例(Spot实例)是云厂商提供的折扣力度极大的计算资源,价格通常只有按需实例的三分之一甚至更低,代价是云厂商可以在需要回收容量时提前通知并强制收回这些节点。在Kubernetes集群中使用抢占式节点跑无状态服务、批处理任务,是业界常见的降本手段。但不少团队接入之后发现服务频繁抖动、任务莫名失败,根源往往就在于没有正确处理Pod被驱逐这件事。本文围绕抢占式节点上Pod驱逐的完整链路展开分析,并给出一套可落地的应对方案。

抢占式节点上Pod被驱逐的完整流程
要处理好驱逐,首先得明白驱逐是怎么发生的。抢占式节点的驱逐并不是Kubernetes调度器发起的“抢占”(Preemption),而是外部事件驱动的节点回收过程。云厂商在回收实例前,会通过元数据服务(比如AWS EC2的metadata endpoint)发出一个限时通知,AWS默认提前2分钟,GCP的Spot VM也会通过ACPI事件通知操作系统。集群中如果部署了对应的节点控制器(如aws-node-termination-handler或Karpenter内置的Spot中断处理逻辑),它就会监听这个信号,并对即将被回收的节点执行cordon操作,把节点标记为不可调度。
接下来节点会被drain,这个过程中kubelet会按照优雅终止的流程处理每个Pod:先发送SIGTERM,等待terminationGracePeriodSeconds指定的时间(默认30秒),超时后再发送SIGKILL强制结束。这里有一个非常容易踩的坑:通知窗口只有2分钟,如果你的Pod的优雅终止时间设置得比较长,或者drain参数中--grace-period和--timeout配置不当,drain可能在节点被物理回收之前还没跑完,Pod就会被直接断电杀死,数据一致性和清理逻辑都会受影响。
还需要区分两类容易混淆的驱逐:一类是节点压力驱逐(Eviction),由kubelet在内存、磁盘压力下触发;另一类就是抢占式实例回收导致的驱逐。前者的Eviction信号来自节点内部,Pod会被重新调度到健康节点;后者是整个节点消失,调度器要等节点对象被删除(或NotReady超时)之后才会把Pod重新加入调度队列,恢复时间明显更长。排查问题时先看kubectl describe pod输出里的Reason字段,是Evicted还是节点NotFound,处理思路完全不同。
合理的调度配置:让Pod天生抗驱逐
应对驱逐最有效的手段不是等出问题再修,而是在调度层面就把风险分散掉。第一件事是保证副本数与打散策略。无状态服务至少两个副本,并通过反亲和把副本打散到不同节点:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx:1.25
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 45
上面这个配置里有两个细节值得展开。一是topologySpreadConstraints,它强制三个副本分布在三个不同节点上,即使一个抢占式节点被回收,最多只有一个副本受影响,服务整体可用。二是preStop钩子加了一个sleep,目的是等待Service的endpoints更新传播完毕,避免流量继续打到已经在终止的Pod上,配合terminationGracePeriodSeconds: 45,给应用留出足够的时间排空在途请求。
第二件事是污点与容忍度的配合。抢占式节点通常会被打上特定的污点,例如spot=true:NoSchedule,只有显式声明容忍的Pod才会被调度上去。这样做可以把数据库、有状态服务等不适合频繁中断的负载隔离到按需节点,而把批处理、可重试的计算任务放到抢占式节点上。反过来说,如果你的Pod意外出现在抢占式节点上并且频繁被驱逐,第一件事就是检查节点污点和Pod的tolerations配置,很可能是集群自动伸缩组件默认加上了宽松的容忍规则。
结合优先级与自动伸缩构建弹性容错体系
对于批处理任务,抢占式节点几乎是天然的选择,因为任务失败可以重跑。但前提是你得让Job具备重试和重建能力。合理的做法是为Spot负载创建专门的PriorityClass,配合调度器的抢占机制:当Spot Pod被驱逐后重新入队,如果集群资源紧张,它可以抢占低优先级的Pod来获取资源,保证核心批任务不被饿死。同时Job要设置合理的backoffLimit,并在应用内部实现断点续跑,避免每次驱逐都从头计算。
更大的收益来自自动伸缩层面的配合。以Karpenter为例,它原生支持Spot容量优先策略,可以按价格和中断率选择最不容易被回收的实例类型。当Spot节点收到中断通知时,Karpenter会自动创建替换节点并把drain出来的Pod调度过去,整个过程通常在一两分钟内完成。如果使用的是Cluster Autoscaler加aws-node-termination-handler的组合,也能达到类似效果,只是需要自己维护好节点组的容量池类型多样性,避免单一实例类型库存紧张时无节点可扩。
最后补充一点可观测性方面的建议。驱逐事件应该被监控起来,推荐通过Event exporter收集node.termination`相关事件和Pod的Evicted原因,配合告警统计每小时驱逐次数。如果驱逐频率超过业务的容忍阈值,说明当前使用的实例类型中断率过高,需要调整实例类型组合或把部分负载迁回按需节点。成本优化和稳定性之间永远存在权衡,抢占式节点的正确用法不是消除驱逐,而是让驱逐发生时业务无感知,这需要调度、应用优雅退出、自动伸缩三个层面协同工作才能真正做到。
Kubernetes抢占式实例Pod驱逐修改时间:2026-09-13 00:16:35