Pod驱逐机制:节点压力驱逐、污点与容忍度、PodDisruptionBudget配置、优雅终止时间设置

说实话,节点故障这块,我踩过的坑比走过的路还多。

尤其是vLLM推理服务,一个Pod被驱逐,可能意味着几十个正在处理的请求直接断掉。用户那边看到的,就是「服务不可用」五个大字。嗯,今天咱们就把Pod驱逐这件事彻底聊透。

一、节点压力驱逐:K8s的「自我保护」机制

节点压力驱逐,说白了就是节点快扛不住了,K8s帮你做决断。

我遇到过一台GPU节点,显存被某个异常进程吃满,结果kubelet直接开始驱逐Pod。当时我还在睡梦中,被报警电话叫醒……

节点压力驱逐的触发条件,主要看这几个指标:

  • memory.available:可用内存低于阈值
  • nodefs.available:根文件系统空间不足
  • nodefs.inodesFree:inode耗尽
  • imagefs.available:容器镜像存储空间不足

K8s会为每个资源设置两个阈值:

阈值类型 含义 行为
soft eviction 软驱逐阈值 超过后等待宽限期,再驱逐
hard eviction 硬驱逐阈值 超过后立即驱逐,不给面子

核心要点:vLLM推理服务对显存和内存需求极高。我建议你在节点上配置合理的驱逐阈值,避免因为内存抖动导致推理Pod被误杀。

配置示例(kubelet参数):

--eviction-hard=memory.available<500Mi,nodefs.available<10%
--eviction-soft=memory.available<1Gi
--eviction-soft-grace-period=memory.available=2m30s
--eviction-max-pod-grace-period=120

二、污点与容忍度:让Pod「挑」节点

污点(Taint)是节点给Pod贴的标签,意思是「我不欢迎你」。容忍度(Toleration)是Pod的「通行证」。

你想想看,如果某个GPU节点有硬件故障风险,你肯定不希望推理Pod调度上去。这时候污点就派上用场了。

常用的污点效果有三种:

  • NoSchedule:不调度新Pod上去
  • PreferNoSchedule:尽量不调度,但不强制
  • NoExecute:不仅不调度,已有的Pod也要驱逐

我在项目中遇到过这样一个场景:一台A100节点风扇故障,温度飙升。我给它打了污点:

kubectl taint nodes gpu-node-01 hardware-risk=true:NoExecute

然后给vLLM推理Pod加上容忍度:

tolerations:
- key: "hardware-risk"
  operator: "Equal"
  value: "true"
  effect: "NoExecute"
  tolerationSeconds: 300  # 最多容忍300秒

我的习惯:对于推理服务,我通常设置tolerationSeconds。这样节点出问题时,Pod有足够时间完成正在处理的请求,而不是被秒杀。

三、PodDisruptionBudget:给驱逐上个「保险」

PDB(PodDisruptionBudget)是K8s提供的「优雅驱逐」机制。它保证在主动驱逐时,你的服务不会全部挂掉。

说白了,就是告诉K8s:「你可以赶走我的Pod,但至少给我留几个干活儿的。」

配置方式有两种:

  • minAvailable:最少可用的Pod数量
  • maxUnavailable:最多不可用的Pod数量

对于vLLM推理服务,我推荐这样配置:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: vllm-inference-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: vllm-inference

避坑指南:我曾经遇到过PDB配置了minAvailable: 100%,但副本数只有1的情况。结果节点维护时,Pod死活驱逐不了,运维操作被卡住。记住,PDB不能保证100%可用,它只是尽力而为。

PDB生效的场景:

  • 节点维护(drain)
  • 集群自动缩放(Cluster Autoscaler)
  • 主动驱逐(kubectl evict)

但注意,节点压力驱逐不受PDB约束。节点快挂了,K8s可不管你的PDB,直接动手。

四、优雅终止时间:给推理请求一个「体面」的告别

优雅终止(Graceful Shutdown),是K8s给Pod的最后体面。

当Pod被驱逐时,K8s会发送SIGTERM信号,然后等待一段时间(默认30秒),如果Pod还没退出,就发SIGKILL强制杀掉。

对于vLLM推理服务,30秒远远不够。一个长文本生成请求可能耗时几分钟。强行杀掉,用户那边就是「生成到一半,没了」。

我建议这样配置:

apiVersion: v1
kind: Pod
metadata:
  name: vllm-inference-pod
spec:
  terminationGracePeriodSeconds: 300  # 5分钟
  containers:
  - name: vllm
    image: vllm/vllm-openai:latest
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "vllm drain --timeout=240"]

关键点terminationGracePeriodSeconds要大于preStop脚本的执行时间。我一般留出20%的余量。

优雅终止的完整流程:

  1. K8s将Pod状态标记为Terminating
  2. 从Service的Endpoints中移除Pod
  3. 执行preStop hook(如果有)
  4. 发送SIGTERM信号
  5. 等待terminationGracePeriodSeconds
  6. 发送SIGKILL强制终止

vLLM的drain命令会做两件事:

  • 停止接收新请求
  • 等待正在处理的请求完成

我的经验:对于推理服务,terminationGracePeriodSeconds设置300秒是比较稳妥的。如果模型特别大、推理时间特别长,可以适当延长到600秒。但别太长,否则节点维护会卡住。

五、知识体系总览

下面这张图,是我梳理的Pod驱逐机制核心逻辑:

Pod驱逐机制知识体系 节点压力驱逐 污点与容忍度 PodDisruptionBudget 优雅终止时间 Pod驱逐决策 执行驱逐 → 优雅终止流程 内存/磁盘/Inode 节点标签 + Pod容忍 最小可用/最大不可用 注:节点压力驱逐不受PDB约束,但受优雅终止时间影响

这张图把四个核心机制串起来了。节点压力驱逐是「被动触发」,污点与容忍度是「主动标记」,PDB是「保护伞」,优雅终止是「最后防线」。四者配合,才能让vLLM推理服务在节点故障时平稳过渡。

嗯,今天就聊到这儿。记住一句话:不要等到节点挂了才想怎么处理,提前配置好这些机制,比你半夜爬起来修要好得多

专注资料整理