跳转至

Pod 生命周期:启动、就绪、重启与终止

Kubernetes 分别用 Pod phase、Pod Conditions 和 Container state 描述 Pod 的生命周期。kubectl get pods 的 STATUS 列是客户端生成的摘要;判断 vLLM 是否已经可用,还要检查应用接口。

default/vllm-demo 没有配置 Startup、Readiness 或 Liveness Probe。vLLM 进程启动后,Kubelet 很快会把容器和 Pod 标记为 Ready,EndpointSlice 也会发布对应的 ready Endpoint;此时模型可能仍在下载、加载权重或初始化 CUDA。/v1/models 返回 Qwen/Qwen3-0.6B 才能证明模型已经由 vLLM 发布。

Pod 状态模型

PodStatus 中的三组字段记录不同粒度的状态。

状态 API 字段 典型值 含义
Pod phase status.phase Pending、Running、Succeeded、Failed、Unknown Pod 所处的粗粒度生命周期
Container state status.containerStatuses[].state Waiting、Running、Terminated 每个容器当前的运行状态
Pod Conditions status.conditions[] PodScheduled、Initialized、ContainersReady、Ready 调度、初始化和就绪等条件是否满足

Pod phase 为 Running,表示 Pod 已经绑定节点,所有容器已经创建,并且至少一个容器正在运行、启动或重启。应用就绪属于另一层状态,需要由探针或应用请求确认。

当前单容器清单没有 Readiness Probe。容器进入 Running 后,ContainersReady 和 Ready 通常很快变成 True,而 vLLM 的 HTTP Server 可能仍在准备。下图展示了 Kubernetes 状态与应用状态之间的时间差。

stateDiagram-v2
    [*] --> Pending: 等待 GPU、PVC 与调度
    Pending --> ContainerCreating: 准备卷、Sandbox、网络和设备
    ContainerCreating --> RunningLoading: vLLM 进程启动,Pod 很快 Ready
    RunningLoading --> Serving: /v1/models 返回目标模型
    RunningLoading --> Restarting: 进程退出
    Serving --> Restarting: 进程退出
    Restarting --> RunningLoading: Kubelet 重启同一容器
    RunningLoading --> Terminating: Pod 被删除
    Serving --> Terminating: Pod 被删除
    Terminating --> [*]: 进程退出,节点资源清理

图中只有 Pending 是 Pod phase。ContainerCreating 和 Terminating 是 kubectl STATUS 列中的显示值;RunningLoading、Serving 和 Restarting 是根据应用行为划分的观察状态。API 记录 status.phase、containerStatuses 和 conditions,HTTP 请求用于确认 Serving。

应用就绪验证

EndpointSlice 的 conditions.ready=true 来自 Pod Ready Condition。对当前单容器 Pod,这个值反映默认的容器就绪判断,不包含 /v1/models 响应或真实推理结果。

setup-course.sh 在 vLLM 容器内请求本机端口,用这一步单独检查应用启动。下面的命令执行同一项检查:

pod_name="$(kubectl get pod -n default \
  -l app=vllm-demo \
  -o jsonpath='{.items[0].metadata.name}')"

kubectl exec -n default "$pod_name" -- python3 -c '
import json
import urllib.request

with urllib.request.urlopen("http://127.0.0.1:8000/v1/models", timeout=10) as response:
    models = json.load(response)

assert any(model["id"] == "Qwen/Qwen3-0.6B" for model in models["data"])
print("Qwen/Qwen3-0.6B is ready")
'

请求失败时,查看 vLLM 日志中的模型下载、CUDA 初始化和 HTTP Server 启动记录。trace-vllm-deployment.sh 通过 vllm.default.svc.cluster.local:8000 执行同样的模型检查,将 Service DNS 和 ClusterIP 纳入验证。网络路径见 网络:通信与流量转发。

生产环境探针

vllm-demo 当前清单没有健康探针。生产部署可以在 containers[] 中加入下面的配置,让 Service Endpoint 跟随应用状态,并在进程失去响应时重启容器。

startupProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 180
readinessProbe:
  httpGet:
    path: /v1/models
    port: 8000
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3
livenessProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3

Startup Probe 成功以前,Kubelet 暂停 Readiness 和 Liveness Probe。这个窗口应覆盖模型下载、权重加载和 CUDA 初始化,避免 Liveness Probe 把正常的慢启动当作故障。

Readiness Probe 失败会把 Pod Ready Condition 改为 False,EndpointSlice Controller 随后更新 Endpoint 的 ready 状态,容器继续运行。Liveness Probe 连续失败达到阈值后,Kubelet 终止并重启容器。

Kubelet 的 HTTP Probe 将 200 到 399 的响应状态码视为成功,不检查 /v1/models 的响应体是否包含指定模型。即使增加 Readiness Probe,验证脚本仍应检查模型 ID,并发送一次真实的推理请求。

容器重启与 Pod 替换

Deployment 创建的 Pod 使用 restartPolicy: Always。vLLM 进程正常退出、返回非零退出码或被 Linux OOM Killer 终止后,Kubelet 都会在同一个 Pod 中重启容器。普通容器重启会保留 Pod UID、Pod IP、PVC 引用和节点,restartCount 增加,上一进程的结束原因进入 lastState。

容器反复失败时,Kubelet 会逐步延长下一次重启前的等待时间。CrashLoopBackOff 出现在部分 kubectl 命令的 STATUS 列中,表示容器正在经历失败和退避循环;对应的 Pod phase 通常仍是 Running。

ReplicaSet Controller 创建替代 Pod 时,对象身份会改变。新 Pod 获得新的 UID,Scheduler 重新选择节点,Kubelet 重新准备 Sandbox、网络、PVC 和 GPU;vllm-model-cache PVC 保持不变,因此模型缓存可以继续使用。

变化 执行组件 UID / Pod IP GPU 路径
vLLM 进程退出 Kubelet 不变 在同一个 Pod 中重新创建容器
手工删除 Pod ReplicaSet Controller 新 UID,通常是新 IP Scheduler 与 Kubelet 重新分配
修改 Deployment template Deployment Controller 默认 RollingUpdate 创建新 ReplicaSet 和 Pod 新旧 Pod 在更新期间可能同时申请 GPU
setup-course.sh 或 trace-vllm-deployment.sh 重建 Deployment 脚本、API Server、Controller Deployment、ReplicaSet 和 Pod 都获得新 UID 旧对象删除后重新走调度与设备分配

单卡环境的 RollingUpdate

vllm-demo 省略了 spec.strategy,API Server 将更新策略默认化为 RollingUpdate。单副本 Deployment 的默认 maxSurge: 25% 向上取整为 1,maxUnavailable: 25% 向下取整为 0,因此 Controller 可以先创建一个新 Pod,并保留旧 Pod,直到新 Pod Available。

k8s-ai-infra-worker 的 allocatable 中只有一个 nvidia.com/gpu,对应一张 A100。旧 vLLM Pod 已经请求 nvidia.com/gpu: 1 时,新 Pod 会因 Insufficient nvidia.com/gpu 保持 Pending;旧 Pod 又要等待新 Pod 可用,更新无法继续。直接修改镜像、启动参数或 Pod template 都可能触发这条路径。

setup-course.sh 和 trace-vllm-deployment.sh 都先执行前台级联删除并等待完成。追踪脚本随后重新提交 Deployment:

kubectl delete deployment vllm-demo \
  --namespace default \
  --cascade=foreground \
  --wait=true

kubectl apply -f examples/chapter-01/12-vllm/02-deployment.yaml

前台级联删除会等待旧 ReplicaSet 和 Pod 对象消失,再提交新的 Deployment。节点上的 CNI、CSI 和 Device Manager 按各自的控制循环清理资源;新 Pod 创建后,GPU 或卷尚未释放时仍可能短暂等待。这种操作会产生服务空窗,同时避免创建一个等待第二张 GPU 的 surge Pod。

生产服务需要按容量和可用性目标选择更新方式。可用 GPU 足够时可以使用 RollingUpdate;只有一张 GPU 时,可以设置 Recreate,也可以把 RollingUpdate 配置为 maxSurge: 0、maxUnavailable: 1。当前清单依靠脚本先删除再创建 Deployment,没有配置这些策略字段。

当前资源边界

当前容器只声明一个 GPU limit:

examples/chapter-01/12-vllm/02-deployment.yaml
resources:
  limits:
    nvidia.com/gpu: "1"

Kubernetes 允许 GPU 只写在 limits 中,并把同名 request 取为相同值。Scheduler 因此按一张 GPU 核算资源,Kubelet Device Manager 再分配具体设备。

清单没有声明 CPU 和内存 request 或 limit,对应的 request 为 0,也没有由 PodSpec 设置的 CPU quota 或内存上限。Kubernetes 根据 CPU 和内存的 request/limit 计算 QoS,因此这个 Pod 属于 BestEffort。节点资源紧张时,它比 Burstable 和 Guaranteed Pod 更容易被驱逐。

默认终止宽限期

当前 PodSpec 使用默认的 terminationGracePeriodSeconds: 30,也没有配置 preStop hook。删除 Deployment 或 Pod 时,API Server 写入 deletionTimestamp,EndpointSlice Controller、Kubelet、containerd、CSI、CNI 和 Device Manager 随后分别处理自己的状态。

sequenceDiagram
    autonumber
    participant A as API Server
    participant E as EndpointSlice Controller
    participant K as Kubelet
    participant R as containerd
    participant V as vLLM
    participant C as CNI
    participant P as Volume / Device Manager

    A->>A: 写 deletionTimestamp 与 30s grace
    par Endpoint 更新
        A-->>E: Watch 到 Pod 进入终止
        E->>A: 更新 EndpointSlice 状态
    and 节点终止
        A-->>K: Watch 到 Pod 删除
        K->>R: StopContainer
        R->>V: 发送终止信号
        alt 30 秒内退出
            V-->>R: 进程结束
        else 宽限期耗尽
            R->>V: 强制结束进程
        end
        K->>R: StopPodSandbox
        R->>C: DEL Pod 网络
        K->>P: 清理卷和 GPU 分配
        K->>A: 更新 Pod 终止状态
    end
    Note over E,K,P: Endpoint、进程与节点资源分别收敛

EndpointSlice 更新、应用退出和节点资源清理由不同组件分别推进,时间戳可能交错。Pod 进入终止状态后,新的正常 Service 流量停止选择该 Endpoint;已有 TCP 连接和正在生成的请求仍由客户端、转发层和 vLLM 的终止行为共同决定。

30 秒宽限期耗尽后,容器运行时会强制结束仍在运行的进程。需要排空长生成请求的服务应结合 vLLM 的关闭行为配置更长的 grace period、preStop hook 或专用 draining 机制,并采集删除阶段的应用与 containerd 日志验证实际耗时。

状态排障

现场 判断
Pending,PodScheduled=False Scheduler 仍在处理 GPU、PVC 或其他节点约束
已调度,Container state 为 Waiting Kubelet 正在准备 Sandbox、卷、镜像或设备
Running 且 Ready=True,/v1/models 仍失败 Kubernetes 已启动容器,vLLM 仍在加载模型或初始化 HTTP Server
Endpoint 为 ready,Service 请求仍失败 当前清单没有 Readiness Probe,检查应用日志、监听端口和 Service 数据面
restartCount 增加,UID 不变 Kubelet 在同一 Pod 内重启容器
新 UID 出现 Controller 创建了替代 Pod,或脚本重建了 Deployment
lastState.reason=OOMKilled Linux 主机内存路径终止了进程,需要结合节点内存与事件继续判断
deletionTimestamp 已设置 Pod 正在终止,等待进程与节点资源清理

CUDA out of memory 通常记录在 vLLM 日志中。它表示设备显存分配失败,与 lastState.reason=OOMKilled 所指的 Linux 内存路径不同。

状态检查

下列命令同时读取 Pod phase、Conditions、Container state 和 QoS:

pod_name="$(kubectl get pod -n default \
  -l app=vllm-demo \
  -o jsonpath='{.items[0].metadata.name}')"

kubectl get pod "$pod_name" -n default -o json | jq '{
  uid: .metadata.uid,
  phase: .status.phase,
  qosClass: .status.qosClass,
  conditions: .status.conditions,
  containerState: .status.containerStatuses[0].state,
  lastState: .status.containerStatuses[0].lastState,
  restartCount: .status.containerStatuses[0].restartCount
}'

Event、EndpointSlice 与应用日志分别保留控制面、Service 和进程证据:

kubectl get event -n default \
  --field-selector "involvedObject.name=$pod_name" \
  --sort-by='.lastTimestamp'

kubectl get endpointslice -n default \
  -l kubernetes.io/service-name=vllm \
  -o json | jq '.items[].endpoints'

kubectl logs -n default "$pod_name" --tail=200

Deployment 对象会显示 API Server 默认补充的 RollingUpdate 策略:

kubectl get deployment vllm-demo -n default -o json | jq '{
  strategy: .spec.strategy,
  generation: .metadata.generation,
  observedGeneration: .status.observedGeneration,
  replicas: .status.replicas,
  readyReplicas: .status.readyReplicas,
  unavailableReplicas: .status.unavailableReplicas
}'

持续观察时,在两个终端分别运行 kubectl get pod -n default -l app=vllm-demo -w 和 kubectl logs -n default -l app=vllm-demo -f --tail=100。

相关资料