Kubernetes 基础体系 · 第 71/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 工作负载排障:Pending、CrashLoop、ImagePull、OOM 和 Probe
Kubernetes 中“Pod 不工作”不是一种故障。Pending、CrashLoopBackOff、ImagePullBackOff、OOMKilled 和探针失败分别位于不同的状态层次:
Pending是 Pod Phase(阶段),表示 Pod 已被 API Server 接受,但尚未进入可运行状态。CrashLoopBackOff是容器状态中常见的 等待原因(reason),表示容器反复退出,kubelet 正在执行重启退避。ImagePullBackOff是容器状态中常见的 等待原因,表示镜像拉取失败后进入重试退避。OOMKilled通常是容器上一次终止状态的 reason,表示进程因超出内存约束或节点内存压力相关机制而被杀死。Probe是 kubelet 对容器健康状态进行检查的机制。探针失败本身不一定重启容器:存活探针通常会触发重启,读取探针通常会影响 Service Endpoint,启动探针则会延迟另外两类探针。
因此,排障的第一步不是直接执行 kubectl delete pod,而是确定:
- 这是调度问题、镜像问题、进程问题、资源问题,还是健康检查配置问题?
- 故障发生在哪个组件和状态转换上?
- 当前看到的是根因,还是根因发生后的二次表现?
一、先建立 Kubernetes 工作负载的状态模型
1. Pod Phase 不是容器健康状态
Pod 的 .status.phase 是一个有限集合,常见值包括:
PendingRunningSucceededFailedUnknown
它描述的是 Pod 层面的粗粒度阶段,而不是“应用是否可用”。
例如,一个 Pod 可能处于:
phase: Running
container state: Waiting
reason: CrashLoopBackOff
也就是说,Pod Phase 仍然是 Running,但容器当前并没有正常提供服务。
另一个 Pod 可能是:
phase: Pending
container state: Waiting
reason: ImagePullBackOff
这里 Pending 只说明 Pod 尚未完成启动流程,真正原因要看容器状态和事件。
不要把下面几个概念混为一谈:
| 字段或状态 | 所属层次 | 含义 |
|---|---|---|
.status.phase |
Pod | Pod 的粗粒度生命周期阶段 |
.status.conditions |
Pod | 调度、就绪等条件及其原因 |
.status.containerStatuses[].state |
容器 | 当前是 Waiting、Running 还是 Terminated |
.status.containerStatuses[].lastState |
容器 | 上一次容器状态,特别适合分析重启 |
restartCount |
容器 | 容器被重启的累计次数 |
CrashLoopBackOff |
容器等待原因 | 容器反复退出后的重启退避 |
OOMKilled |
容器终止原因 | 上一次容器终止的原因之一 |
Ready Condition |
Pod | 是否满足就绪条件,通常影响 Service 流量 |
2. 一个 Pod 从创建到运行的关键路径
典型路径如下:
sequenceDiagram
participant C as 客户端
participant A as API Server
participant S as Scheduler
participant K as Kubelet
participant R as 容器运行时
participant N as Service/EndpointSlice
C->>A: 创建 Deployment/Pod
A-->>C: 接受对象
A->>S: 未绑定节点的 Pod
S->>A: 绑定节点,设置 PodScheduled=True
A->>K: 节点上的 Pod 期望状态
K->>R: 创建 Sandbox、拉取镜像、创建容器
R-->>K: 容器启动或失败
K->>A: 更新 containerStatuses
K->>A: 执行 startup/readiness/liveness 探针
A->>N: 根据 Ready 状态更新 EndpointSlice
在这个过程中,故障位置不同,排查入口也不同:
- Scheduler 无法找到节点:通常是
Pending。 - Kubelet 或运行时无法拉取镜像:常见是
ImagePullBackOff。 - 进程启动后退出:常见是
CrashLoopBackOff。 - 进程因内存限制退出:上一次状态常见为
OOMKilled。 - 进程仍在运行但探针失败:可能是重启、未就绪,或启动阶段被探针阻塞。
二、统一排障入口:先确认上下文,再确认事实
1. 先确认 kubectl 操作的是哪个集群
生产环境最危险的排障错误之一,是命令本身正确,但执行到了错误的集群或命名空间。
kubectl config current-context
kubectl config get-contexts
kubectl config view --minify
kubectl get ns
current-context 只能说明当前上下文名称;config view --minify 可以进一步查看该上下文关联的集群和用户。不要只根据上下文名称判断环境,例如 prod、production、cluster-a 都可能被人为命名。
排障时尽量显式指定命名空间:
kubectl -n payments get pods
kubectl -n payments describe pod payments-api-7f8d9c6b7d-x2kq4
如果命令会修改资源,建议同时显式指定上下文:
kubectl --context=prod-cluster -n payments rollout restart deployment/payments-api
kubectl 的参数位置可以影响可读性和脚本兼容性。常见形式是:
kubectl --context=prod-cluster -n payments get pod
2. 用四个视角获取同一个 Pod 的事实
假设 Pod 名称为 payments-api-7f8d9c6b7d-x2kq4:
NS=payments
POD=payments-api-7f8d9c6b7d-x2kq4
kubectl -n "$NS" get pod "$POD" -o wide
kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o json
kubectl -n "$NS" get events \
--field-selector involvedObject.kind=Pod,involvedObject.name="$POD" \
--sort-by=.lastTimestamp
这四条命令分别解决不同问题:
get -o wide:快速查看 Phase、IP、节点、重启次数。describe:汇总容器状态、探针、挂载、调度信息和事件。get -o json:获取不会被表格隐藏的精确字段。get events:按时间观察调度器、kubelet、镜像运行时报告的失败。
describe 输出底部的 Events 很重要,但事件不是永久审计日志,可能因 TTL、事件聚合或集群配置而消失。因此,事件只能作为当前证据之一,不能替代应用日志、控制器历史和节点日志。
可以用 JSONPath 快速提取关键信息:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{.status.phase}{"\n"}{range .status.containerStatuses[*]}{.name}{" state="}{.state}{" lastState="}{.lastState}{" restart="}{.restartCount}{"\n"}{end}'
如果 Pod 有多个容器,必须逐个检查。一个 Sidecar 失败,也可能使整个 Pod 看起来“不正常”,但根因并不在业务容器。
3. 日志必须同时看当前实例和上一个实例
kubectl -n "$NS" logs "$POD" -c api --timestamps
kubectl -n "$NS" logs "$POD" -c api --previous --timestamps
logs查看当前容器实例的标准输出和标准错误。--previous查看上一次已经终止的容器实例,特别适合CrashLoopBackOff。- 如果容器从未成功启动,例如镜像一直拉取失败,通常没有容器日志可看。
- 应用把日志写到文件而不是 stdout 时,
kubectl logs可能为空,这不是“应用没有报错”的证据。
若容器正在重启,可以限制时间和行数,降低对 API Server 的负担:
kubectl -n "$NS" logs "$POD" -c api --previous --since=10m --tail=200
三、Pending:先区分“未调度”和“已调度但未启动”
1. Pending 的形式化含义
从 Pod Phase 看,Pending 大致表示:
Pod 已被 API Server 接受
且 Pod 尚未完成启动所需的全部步骤
这里的“启动所需步骤”不只包括调度,还可能包括:
- 等待调度器选择节点;
- 等待 init container 执行完成;
- 等待镜像拉取;
- 等待网络 Sandbox 或存储卷创建;
- 等待容器真正启动。
因此:
Pending ≠ 一定是 Scheduler 问题
需要首先查看:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{.status.phase}{"\n"}{.status.conditions}{"\n"}'
最关键的调度条件通常是:
type: PodScheduled
status: "False"
reason: Unschedulable
如果 PodScheduled=True,说明 Scheduler 已经绑定了节点,此时继续盯着调度器通常没有意义,应转向 kubelet、存储、网络或镜像问题。
2. 未调度:Scheduler 如何判断节点可行
简化地说,Scheduler 要为 Pod 找到一个满足约束的节点:
而 Feasible 通常需要同时满足多个条件:
这些变量的含义是:
ResourceFit:节点可分配资源能够满足 Pod 的 requests;TaintsTolerated:Pod 的 toleration 能容忍节点 taint;NodeSelectorMatch:节点标签满足nodeSelector或 node affinity;AffinityMatch:Pod 与其他 Pod 的亲和或反亲和约束满足;VolumeConstraints:卷的拓扑、挂载和访问模式允许;TopologyConstraints:拓扑分布约束没有被违反。
其中资源调度使用的是 requests,而不是 limits。对某个资源 ,调度器通常比较:
allocatable 是节点可供 Pod 使用的容量,不等于物理机总容量。系统组件和 kubelet 预留可能已经占用一部分容量。
检查调度事件:
kubectl -n "$NS" describe pod "$POD"
kubectl get nodes
kubectl describe node <node-name>
常见事件及其因果关系包括:
0/5 nodes are available: 5 Insufficient memory
表示没有节点满足 requests 资源要求,不表示容器已经发生 OOM。
0/5 nodes are available: 3 node(s) didn't match Pod's node affinity
表示标签或亲和约束排除了节点。
0/5 nodes are available: 2 node(s) had untolerated taint
表示节点 taint 存在,但 Pod 没有对应 toleration。
3. 一个可验证的 Pending 算例
假设三个节点的可分配内存分别为:
node-a: 4 GiB,已有请求 3 GiB
node-b: 8 GiB,已有请求 7.5 GiB
node-c: 4 GiB,已有请求 3.5 GiB
新 Pod 的内存 request 为 1Gi:
node-a 剩余可调度内存 = 4 - 3 = 1Gi
node-b 剩余可调度内存 = 8 - 7.5 = 0.5Gi
node-c 剩余可调度内存 = 4 - 3.5 = 0.5Gi
仅从内存看,node-a 可行。但如果 Pod 还有:
nodeSelector:
workload: batch
而 node-a 没有该标签,那么最终仍然没有可行节点:
ResourceFit(node-a) = true
NodeSelectorMatch(node-a) = false
Feasible(node-a) = false
此时盲目降低内存 limit 没有帮助,因为调度判断使用的是 request。正确的修复方向可能是:
- 修改不正确的标签选择器;
- 为符合条件的节点增加容量或标签;
- 调整 requests;
- 重新评估反亲和或拓扑约束。
4. 已调度但仍 Pending
如果:
kubectl -n "$NS" get pod "$POD" -o wide
已经显示 NODE,并且:
PodScheduled: True
那么要继续看容器状态和事件:
kubectl -n "$NS" describe pod "$POD"
可能的原因包括:
ImagePullBackOff:镜像拉取失败;ErrImagePull:本次拉取失败;ContainerCreating:卷、网络 Sandbox 或容器创建尚未完成;- init container 尚未完成;
- Secret、ConfigMap 或 PVC 挂载失败;
- CNI、CSI 或容器运行时故障。
检查 init container:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{range .status.initContainerStatuses[*]}{.name}{" state="}{.state}{" last="}{.lastState}{" restart="}{.restartCount}{"\n"}{end}'
init container 不是普通并行容器。只要某个 init container 没有成功退出,业务容器就不会启动。此时查看业务容器日志往往为空,应查看 init container:
kubectl -n "$NS" logs "$POD" -c init-config --previous
四、ImagePull:镜像拉取失败发生在哪里
1. ErrImagePull 和 ImagePullBackOff 的区别
这两个状态经常一起出现,但含义不同:
ErrImagePull:某次镜像拉取操作失败;ImagePullBackOff:拉取失败后,kubelet 暂停一段时间再重试。
这和 CrashLoopBackOff 的区别在于:
ImagePullBackOff:容器通常还没有成功创建或启动
CrashLoopBackOff:容器已经启动过,但反复退出
查看精确原因:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{" state="}{.state}{"\n"}{end}'
还要查看事件,因为运行时经常把 registry 返回的错误写在那里:
kubectl -n "$NS" describe pod "$POD"
2. 镜像拉取的故障链
容器镜像拉取一般经过如下路径:
Pod spec 中的 image
↓
kubelet 根据 imagePullPolicy 请求容器运行时
↓
运行时解析 registry/repository/tag 或 digest
↓
认证 registry
↓
解析 manifest/index
↓
下载各层 blob
↓
校验并解压
↓
创建容器
每一步都可能失败:
| 阶段 | 典型原因 |
|---|---|
| 名称解析 | repository 拼写错误、registry 地址错误 |
| 认证 | imagePullSecrets 缺失、Secret 类型错误、凭据过期 |
| manifest | tag 不存在、架构不匹配、私有仓库权限不足 |
| blob 下载 | 网络超时、代理配置错误、registry 限流 |
| 解压 | 节点磁盘不足、镜像层损坏 |
| 创建容器 | runtime、CNI、挂载或安全策略问题 |
检查 Pod 使用的镜像和拉取策略:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{range .spec.containers[*]}{.name}{" image="}{.image}{" imagePullPolicy="}{.imagePullPolicy}{"\n"}{end}'
注意 imagePullPolicy 的默认推导规则具有版本和镜像写法相关性。常见规则是:
- 使用显式 digest 时通常按本地已有内容处理;
- 使用
:latest时通常默认Always; - 使用非
latesttag 时通常默认IfNotPresent; - 显式配置
imagePullPolicy后,以显式值为准。
生产部署不应依赖浮动 tag。image: app:latest 既不便于审计,也可能导致不同节点实际运行不同内容。更可验证的形式是固定不可变 tag 或 digest:
image: registry.example.com/payments/api@sha256:...
digest 必须来自可信发布流程,不能随意手工填写。
3. 私有仓库认证
检查 Pod 是否引用了拉取凭据:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{.spec.imagePullSecrets}{"\n"}'
imagePullSecrets 必须位于同一个命名空间。命名空间 payments 中的 Secret 不能直接被 orders 中的 Pod 使用。
查看 Secret 元数据而不是直接输出内容:
kubectl -n "$NS" get secret regcred \
-o jsonpath='{.type}{"\n"}'
通常 Docker Registry 凭据的类型为:
kubernetes.io/dockerconfigjson
不要在排障记录、终端共享或工单中执行:
kubectl get secret regcred -o yaml
因为这可能暴露经过 Base64 编码的凭据。Base64 不是加密。
如果确认是凭据问题,修复 Deployment 的模板,而不是只修改当前 Pod:
kubectl -n "$NS" get deployment payments-api \
-o jsonpath='{.spec.template.spec.imagePullSecrets}{"\n"}'
因为 Deployment 会持续创建新的 Pod。只修当前 Pod 可能在下一次滚动更新或节点故障后再次失败。
4. 节点侧问题不能只看 Pod
当事件显示:
failed to pull and unpack image:
no space left on device
应检查节点,而不是修改应用配置:
kubectl describe node <node-name>
kubectl get node <node-name> -o jsonpath='{.status.conditions}{"\n"}'
节点上的镜像缓存、容器可写层、日志和临时目录都可能占满磁盘。节点级修复通常需要节点管理员或云厂商工具,不能通过删除业务 Pod 自动解决。
五、CrashLoopBackOff:容器为什么反复启动和退出
1. 它不是一个应用错误码
CrashLoopBackOff 的含义是:
容器退出
→ kubelet 根据 RestartPolicy 尝试重启
→ 容器再次退出
→ kubelet 增加重启间隔
它描述的是 kubelet 当前的重启行为,不说明应用为什么退出。根因可能是:
- 进程参数错误;
- 配置文件或 Secret 缺失;
- 依赖服务连接失败后主动退出;
- 监听端口失败;
- 启动脚本执行错误;
- 进程收到 SIGTERM、SIGKILL 或其他信号;
- 探针失败导致 kubelet 重启;
- 内存超限导致
OOMKilled。
首先查看当前和上一次状态:
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{"\n"}{"state: "}{.state}{"\n"}{"lastState: "}{.lastState}{"\n"}{"restartCount: "}{.restartCount}{"\n\n"}{end}'
对于典型的进程崩溃,可能看到:
lastState:
terminated:
exitCode: 1
reason: Error
startedAt: ...
finishedAt: ...
对于 OOM,可能看到:
lastState:
terminated:
exitCode: 137
reason: OOMKilled
137 通常等于 128 + 9,表示进程最终对应 SIGKILL,但不要只根据数字断言一定是容器内存超限。应结合 reason: OOMKilled、容器限制、节点事件和监控判断。
2. RestartPolicy 决定“退出后是否重启”
Pod 的 restartPolicy 有:
AlwaysOnFailureNever
对于 Deployment、ReplicaSet、StatefulSet、DaemonSet 等长期运行工作负载,通常使用 Always,并且控制器会维持副本数。Job 常使用 OnFailure 或 Never,由 Job 控制器根据成功或失败决定是否创建后续 Pod。
restartPolicy 作用在 Pod 内的容器退出上,不是“整个 Pod 被删除重建”的开关。容器可以在同一个 Pod 内被 kubelet 重启,同时 Pod UID 不变。
如果一个容器:
exitCode = 0
而 restartPolicy=Always,它仍然会被重启。这对一个本应执行一次就结束的进程非常重要:把一次性任务错误地放进长期运行的 Deployment,可能产生看似异常的重启。
3. 重启退避如何影响观察
kubelet 通常对反复失败的重启采用指数退避,常见观察序列近似为:
立即重启
约 10 秒
约 20 秒
约 40 秒
...
直到约 5 分钟上限
这是 kubelet 的常见实现行为,具体细节可能随 Kubernetes 版本和运行时实现变化,不应把时间间隔作为业务契约。
退避的直接后果是:
kubectl logs可能刚好错过上一次启动;- 容器短暂运行时,实时
logs -f不一定稳定; --previous往往比当前日志更有价值;- 删除 Pod 只会重置观察对象,不会修复导致进程退出的配置或代码。
4. 用退出码和信号缩小范围
常见退出码只能作为线索:
| 表现 | 可能方向 |
|---|---|
exitCode=0,仍被重启 |
restartPolicy=Always 或进程设计不匹配 |
exitCode=1 |
应用主动报告一般错误,需要看日志 |
exitCode=2 |
参数、命令行或 shell 使用错误,具体含义取决于程序 |
exitCode=126/127 |
无执行权限或命令不存在,常见于镜像/command 配置 |
exitCode=137 |
SIGKILL,结合 OOMKilled 和节点状态判断 |
exitCode=143 |
常见为 SIGTERM,可能是正常终止、滚动更新或优雅关闭 |
查看 Pod 的终止状态:
kubectl -n "$NS" get pod "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{" exit="}{.lastState.terminated.exitCode}{" reason="}{.lastState.terminated.reason}{" signal="}{.lastState.terminated.signal}{"\n"}{end}'
如果应用没有把启动错误写到 stdout,可以通过临时调试容器或本地复现确认实际命令、环境变量和文件系统。
5. 找到控制器,而不是只处理单个 Pod
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{.metadata.ownerReferences}{"\n"}'
也可以:
kubectl -n "$NS" describe pod "$POD" | sed -n '/Controlled By:/p'
如果 Pod 由 Deployment 管理,应检查模板:
kubectl -n "$NS" get deployment payments-api -o yaml
kubectl -n "$NS" rollout history deployment/payments-api
直接编辑或删除单个 Pod 可能只是让控制器创建一个同样错误的新 Pod。修复应发生在 Deployment、StatefulSet 或 Job 的 Pod 模板上。
六、OOM:区分容器超限、节点压力和业务内存增长
1. OOM 的三个不同层次
“内存不够”至少有三种含义:
-
容器超过自身 memory limit
cgroup 对容器施加内存上限,进程可能被内核 OOM 机制杀死,容器状态常见reason: OOMKilled。 -
节点整体内存压力
节点可用内存不足,kubelet 可能触发 Eviction,驱逐 Pod。此时 Pod 可能以Evicted、Failed或相关事件表现出来,和单个容器超限不是同一条路径。 -
调度器认为没有足够 requests
Pod 可能一直Pending,因为没有节点的可分配内存满足 request。此时容器甚至尚未运行,不可能已经发生容器内 OOM。
这三个路径可表示为:
Pod memory request 过大
↓
Scheduler 找不到可行节点
↓
Pending
容器实际使用量超过 memory limit
↓
cgroup/内核终止进程
↓
OOMKilled
↓
RestartPolicy 触发重启
↓
可能表现为 CrashLoopBackOff
节点整体内存压力
↓
kubelet Eviction 或节点级 OOM
↓
Pod 被驱逐或节点上的进程受影响
2. 查看 OOM 证据
kubectl -n "$NS" get pod "$POD" \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{" current="}{.state}{" last="}{.lastState.terminated.reason}{" exit="}{.lastState.terminated.exitCode}{"\n"}{end}'
检查资源配置:
kubectl -n "$NS" get pod "$POD" -o jsonpath='{range .spec.containers[*]}{.name}{" requests="}{.resources.requests}{" limits="}{.resources.limits}{"\n"}{end}'
检查节点压力:
kubectl describe node <node-name>
kubectl get events --field-selector involvedObject.kind=Node,involvedObject.name=<node-name> --sort-by=.lastTimestamp
如果集群安装了 Metrics Server,可以观察当前使用量:
kubectl top pod "$POD" -n "$NS" --containers
kubectl top node <node-name>
kubectl top 依赖 Metrics Server 或兼容的 metrics API。它提供的是当前或近期采样,不是历史峰值,也不能替代内核 OOM 证据。
3. request、limit 与 QoS
对内存而言:
request主要影响调度;limit主要形成容器运行时可用的资源上限;- 实际内存使用可以低于 request,也可以暂时接近或达到 limit;
- limit 不是性能保证,也不是应用应该长期使用的目标值。
Pod QoS 类别会影响节点压力下的驱逐排序:
Guaranteed:每个容器对 CPU 和内存都设置 request,且 request 等于 limit;Burstable:至少设置了部分 request 或 limit,但不满足 Guaranteed;BestEffort:没有设置 CPU 和内存 request/limit。
QoS 影响 Eviction 优先级,但不能简单推导为“Guaranteed 永远不会被杀”。例如节点发生不可恢复的内核级 OOM、节点故障或管理员主动删除时,Guaranteed Pod 仍可能终止。
4. 完整 OOM 算例
假设容器配置:
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi"
业务进程使用:
heap 380Mi
native memory 90Mi
线程栈 30Mi
文件缓存/其他 40Mi
总计 540Mi
即使应用的堆只使用 380Mi,容器总内存仍可能达到:
因为:
所以容器可能被杀死并显示 OOMKilled。只把 JVM、Node.js 或 Go 的堆上限设置为 512Mi,并不代表进程总内存可以安全使用 512Mi。
修复时不能机械地“把 limit 调大”。应先判断:
- 是真实流量增长还是内存泄漏;
- 是 heap 增长还是 native memory、线程、缓存或 mmap 增长;
- 节点是否有足够容量;
- 增大 limit 后是否会造成更多 Pod 被调度到同一节点;
- 是否需要配合 request 调整,否则调度模型与运行时风险可能失配。
对于使用 emptyDir 的应用,如果配置了内存介质:
volumes:
- name: cache
emptyDir:
medium: Memory
写入该卷的数据会消耗节点内存,并可能计入容器或 Pod 的内存使用。把大量缓存从堆移到内存盘并不会绕过内存约束。
七、Probe:健康检查如何改变 Pod 的生命周期
1. 三种探针分别解决不同问题
Startup Probe:应用是否完成启动
启动探针用于启动较慢的容器。只要 startup probe 尚未成功,kubelet 不会执行 liveness 和 readiness 探针。
startup 成功
↓
开始执行 liveness/readiness
startup 失败达到阈值
↓
容器被认为启动失败,通常按存活失败处理并重启
Readiness Probe:是否接收业务流量
readiness probe 失败时,容器通常不会被杀死,而是 Pod 不再满足 Ready 条件。Service 控制器据此从 EndpointSlice 中移除对应端点。
因此:
readiness 失败 ≠ 容器退出
readiness 失败 ≈ 暂停向该 Pod 发送 Service 流量
这适合数据库连接未建立、缓存未预热、依赖暂时不可用等情况,但前提是应用能继续运行并等待恢复。
Liveness Probe:进程是否需要被重启
liveness probe 失败达到阈值后,kubelet 会按照 Pod 的重启策略处理容器。它适合检测进程卡死、死锁、无法恢复的内部状态,不适合把所有外部依赖故障都定义为“重启”。
2. 探针的执行条件
探针可以使用:
httpGettcpSocketexecgrpc
示例:
startupProbe:
httpGet:
path: /startup
port: 8080
periodSeconds: 5
failureThreshold: 24
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
livenessProbe:
httpGet:
path: /live
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
每次探针有成功、失败或超时结果。连续失败次数达到 failureThreshold 后才执行相应动作;偶发一次失败不一定触发状态变化。
探针检测时间可以粗略估算为:
这只是便于推理的近似,因为 kubelet 的调度、探针执行和容器状态更新存在实现细节。对上面的 liveness 配置,理论上大约需要多个探测周期才会触发重启,而不是第一次失败就立即重启。
3. HTTP Probe 的常见边界
HTTP 探针请求的是 Pod IP 和指定端口。它不等价于:
- 从集群外部访问负载均衡器;
- 通过 Service DNS 访问自身;
- 验证完整业务链路;
- 验证所有下游依赖都健康。
如果应用只监听 127.0.0.1,而 kubelet 从 Pod 网络地址访问 Pod IP,HTTP Probe 可能失败。通常应用应监听适合容器网络的地址,例如 0.0.0.0,但是否这样配置要结合应用安全边界判断。
探针端口可以是数字或容器端口名。端口名修改后,探针引用失效可能导致 connection refused,而容器自身仍然正常。
4. exec Probe 的风险
readinessProbe:
exec:
command:
- /bin/sh
- -c
- test -f /tmp/ready
exec 探针在容器内执行命令,依赖镜像中存在的 shell 或工具。精简镜像中可能没有 /bin/sh、curl 或 wget,导致探针永远失败。
还要注意:
- 探针命令会消耗 CPU 和进程资源;
- 复杂命令可能制造额外负载;
- 命令中的凭据可能泄露到进程或配置;
- 探针不应执行破坏性操作;
- 探针脚本成功退出码必须是
0。
5. 探针失败与 CrashLoop 的因果链
一种常见故障链是:
应用启动较慢
↓
未配置 startupProbe
↓
livenessProbe 过早执行
↓
连续失败达到 failureThreshold
↓
kubelet 杀死并重启容器
↓
容器永远无法完成启动
↓
CrashLoopBackOff
这时容器日志可能显示“启动中”,而 describe pod 的事件显示:
Liveness probe failed: HTTP probe failed with statuscode: 503
Container failed liveness probe, will be restarted
修复应先验证应用真实启动时间和端点语义,再考虑:
- 用 startup probe 覆盖启动窗口;
- 增大合理的
initialDelaySeconds; - 调整
timeoutSeconds、periodSeconds和failureThreshold; - 将 liveness 设计为轻量的“进程是否还能自我恢复”检查;
- 将依赖可用性放进 readiness,而不是 liveness。
6. 通过事件和状态确认探针故障
kubectl -n "$NS" describe pod "$POD"
检查:
kubectl -n "$NS" get pod "$POD" -o yaml
重点看:
spec:
containers:
- name: api
livenessProbe: ...
readinessProbe: ...
startupProbe: ...
status:
conditions: ...
containerStatuses: ...
Ready=False 只能说明 Pod 当前未就绪,不能单独证明是 readiness probe。还要查看 conditions[].reason、事件和容器状态。
八、终止、删除和 Eviction:为什么“重启 Pod”可能改变证据
1. Pod 终止是一个有序过程
删除 Pod 时,API Server 通常先设置删除时间戳,kubelet 观察到后执行终止流程:
- Pod 进入终止阶段;
- 执行
preStop(如果配置); - 向容器主进程发送 SIGTERM;
- 等待
terminationGracePeriodSeconds; - 超时后发送 SIGKILL;
- kubelet 更新状态并清理容器。
应用需要正确处理 SIGTERM。若应用只在 SIGTERM 后继续阻塞,最终可能被 SIGKILL 终止,表现为非正常退出。
查看终止信息:
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.metadata.deletionTimestamp}{"\n"}{.spec.terminationGracePeriodSeconds}{"\n"}'
强制删除:
kubectl -n "$NS" delete pod "$POD" --grace-period=0 --force
这是高风险操作。它可能绕过正常的 kubelet 确认流程,使 API Server 很快移除对象,但节点上的进程或外部资源清理可能滞后。对有状态服务、持久卷、分布式锁和网络连接尤其危险。除非 Pod 已经卡在终止状态并经过确认,否则不应把强制删除作为普通排障手段。
2. Eviction 不是 OOMKilled
Eviction 是 kubelet 在节点资源压力下主动驱逐 Pod 的机制,常见信号包括:
memory.availablenodefs.availablenodefs.inodesFreeimagefs.available
驱逐前,kubelet 可能根据 Pod 的 QoS、优先级、资源使用情况等因素选择对象。被驱逐的 Pod 往往在事件中出现:
The node was low on resource: memory
或者对象原因显示为 Evicted。
对比:
OOMKilled:
某个容器/进程被杀死,通常与容器内存约束或内核 OOM 路径有关。
Evicted:
kubelet 因节点资源压力驱逐 Pod,属于节点级资源管理行为。
二者可能连续发生,但证据不同。不要看到 exitCode=137 就直接断言是业务容器 limit 不够,也不要看到 Pending 就断言节点发生 OOM。
九、从症状到根因的完整排障流程
下面流程适用于单个工作负载,但每一步都应保留输出,避免边查边破坏现场。
第一步:确认对象、控制器和节点
kubectl --context="$CTX" -n "$NS" get pod "$POD" -o wide
kubectl --context="$CTX" -n "$NS" get pod "$POD" \
-o jsonpath='{.metadata.uid}{"\n"}{.metadata.ownerReferences}{"\n"}'
要确认:
- 是否看的是正确 Pod;
- Pod 是否被控制器管理;
- 是否已经绑定节点;
- 是否有多个容器;
- 重启次数是否持续增加。
第二步:查看状态、事件和日志
kubectl --context="$CTX" -n "$NS" describe pod "$POD"
kubectl --context="$CTX" -n "$NS" logs "$POD" -c "$CONTAINER" --previous --timestamps --tail=300
kubectl --context="$CTX" -n "$NS" logs "$POD" -c "$CONTAINER" --timestamps --tail=300
选择日志参数时要注意:
- 未启动过的容器没有
--previous日志; - 正在频繁重启的容器优先看
--previous; - Sidecar 需要分别查看;
- 日志中可能包含 Secret、Token、用户数据,复制到工单前要脱敏。
第三步:按状态分流
Pending
先看:
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.conditions}{"\n"}'
PodScheduled=False:检查 requests、taint/toleration、affinity、selector、PVC 和拓扑约束;PodScheduled=True:检查镜像、init container、卷、CNI、CSI 和 runtime;ContainerCreating:重点查看 Events 和节点 kubelet/runtime;- init container 未完成:查看 init container 日志。
ImagePullBackOff
检查:
kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.spec.imagePullSecrets}{"\n"}'
按事件中的具体错误区分:
not found:镜像名、仓库、tag 或 digest 错误;unauthorized:权限或凭据错误;no matching manifest:架构或平台不匹配;x509:证书或信任链问题;timeout:节点到 registry 的网络问题;no space left:节点磁盘或镜像文件系统问题。
CrashLoopBackOff
检查:
kubectl -n "$NS" logs "$POD" -c "$CONTAINER" --previous
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.containerStatuses}{"\n"}'
kubectl -n "$NS" describe pod "$POD"
重点寻找:
lastState.terminated.reason;- exit code 和 signal;
- liveness/startup 失败事件;
- 配置、挂载、权限和依赖错误;
- 容器 command/args 是否覆盖了镜像默认启动命令。
OOMKilled
检查:
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.containerStatuses}{"\n"}'
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.spec.containers[*].resources}{"\n"}'
kubectl describe node <node-name>
kubectl top pod "$POD" -n "$NS" --containers
然后区分:
- 容器使用量超过 limit;
- 节点整体内存压力;
- 内存泄漏或峰值;
- sidecar 或内存盘消耗;
- limits/request 与实际工作负载不匹配。
Probe 失败
检查事件和 YAML:
kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o yaml
验证:
- 探针路径和端口是否真实存在;
- 应用监听地址是否可被 kubelet 访问;
startupProbe是否覆盖真实启动时间;- readiness 和 liveness 是否承担了错误职责;
- exec 命令是否存在;
- 探针超时是否过短;
- 探针自身是否造成高负载。
第四步:确认修复的是源对象
如果 Pod 由 Deployment 管理:
kubectl -n "$NS" get deployment payments-api -o yaml
kubectl -n "$NS" rollout status deployment/payments-api
修改镜像、资源、探针或 Secret 引用时,应更新 Deployment 的 Pod 模板,使用版本控制和发布流程。不要把手工 kubectl edit pod 当作长期修复,因为控制器会重新生成 Pod。
十、Debug:当日志不足时如何建立安全的现场
1. 临时查看文件系统和进程
对于仍在运行但无法通过网络访问的容器,可以使用:
kubectl -n "$NS" exec -it "$POD" -c "$CONTAINER" -- sh
前提是镜像中有 sh,且容器尚未退出。
如果镜像很精简,没有 shell,可以考虑临时调试容器:
kubectl -n "$NS" debug -it "$POD" \
--image=busybox:1.36 \
--target="$CONTAINER" \
-- sh
kubectl debug 的具体行为依赖集群版本、容器运行时和节点配置。--target 通常请求调试容器加入目标容器的进程命名空间,但不保证所有环境都能看到目标进程。调试镜像还可能带来:
- 额外的镜像拉取问题;
- 更高权限;
- 对生产 Pod 的侵入;
- Secret、进程参数和文件内容暴露;
- 临时容器对象残留在审计和状态中。
调试容器不应默认使用特权模式,也不应把生产 Secret 复制到调试环境。
2. 创建临时副本进行复现
对已被控制器持续重建、无法稳定观察的工作负载,可以复制配置到隔离命名空间,或者创建专门的 Debug Pod。复制时要清理:
- 生产 ServiceAccount;
- Secret;
- hostPath;
- 特权安全上下文;
- 持久卷;
- 生产网络入口;
- 不必要的云厂商身份绑定。
调试的目标是验证命令、镜像、配置和探针行为,而不是扩大生产权限。
十一、Patch 和恢复操作:先知道修改的对象和副作用
1. 使用 Patch 修改探针时要谨慎
例如,给 Deployment 增加一个 startup probe:
kubectl -n "$NS" patch deployment payments-api \
--type='strategic' \
-p '{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "api",
"startupProbe": {
"httpGet": {
"path": "/startup",
"port": 8080
},
"periodSeconds": 5,
"failureThreshold": 24
}
}]
}
}
}
}'
这个命令的成立条件是:
- 资源类型是 Deployment;
- 容器名确实是
api; /startup在容器内可访问;- 端口
8080是正确的容器端口; - 当前用户有修改权限;
- 集群 API 版本支持该字段。
修改 Pod 模板通常会触发新的 ReplicaSet 和滚动更新。执行后必须验证:
kubectl -n "$NS" rollout status deployment/payments-api
kubectl -n "$NS" get rs
kubectl -n "$NS" get pods -l app=payments-api -o wide
如果 Patch 只为了临时恢复,仍应把最终变更提交到配置仓库,否则下一次发布可能覆盖它。
2. 不要用修改 liveness 的方式掩盖真实故障
临时删除 liveness probe 可能让 Pod 不再重启,但并不代表应用健康。若应用已经死锁或无法服务,结果可能变成:
容器 Running
Pod Ready
但请求持续失败
更安全的顺序是:
- 先收集
--previous日志和事件; - 判断探针是否错误;
- 如果探针确实过早或语义错误,修改为符合应用生命周期的检查;
- 用 rollout 验证新版本;
- 观察 Ready 状态和实际业务请求,而不是只看 Pod 是否 Running。
3. 删除 Pod 什么时候有意义
删除单个 Pod 适合以下场景:
- 修复已经写入控制器模板,需要让新 Pod 使用新配置;
- Pod 处于不可恢复的异常状态;
- 节点或运行时问题已处理,需要重新调度;
- 需要验证控制器是否能正常重建副本。
删除 Pod 不适合用来解决:
- 错误的镜像地址;
- 错误的环境变量;
- 缺失的 Secret;
- 代码启动崩溃;
- 资源 limit 过小;
- 永远失败的探针。
这些问题会在新 Pod 中重现。
十二、常见误判及其反例
误判一:Pending 就是“没有资源”
反例:
PodScheduled=True
container state=Waiting
reason=ImagePullBackOff
这个 Pod 已经完成调度,资源不是当前根因。应该检查镜像事件和 registry,而不是继续调整节点容量。
误判二:Running 就是应用正常
反例:
phase: Running
Ready: False
readiness probe failed
Pod 进程可以存在,但 Service 不应把流量发给它。Running 与“可接收请求”是不同维度。
误判三:CrashLoopBackOff 一定是代码崩溃
反例:
lastState.terminated.reason: OOMKilled
这里的根因可能是内存限制或内存增长,而不是业务异常退出。
另一个反例是:
Events:
Liveness probe failed
Container failed liveness probe, will be restarted
容器可能没有自行崩溃,而是被 kubelet 根据探针结果重启。
误判四:退出码 137 一定是容器 limit 超了
反例:
- 节点内核在全局 OOM 时杀死了进程;
- 运行时或外部系统发送了 SIGKILL;
- Pod 被强制终止。
因此必须联合检查 reason、节点事件、容器资源和监控。
误判五:给所有探针都访问 /health
如果 /health 同时检查数据库、消息队列、第三方 API,并被 liveness 使用,那么外部依赖短暂故障可能导致所有应用实例被重启,形成级联故障。
更准确的划分是:
/live :进程和内部状态是否还能自我恢复
/ready :此刻是否适合接收流量
/startup:启动过程是否完成
这不是 Kubernetes 强制规定的路径,而是应用端点语义的工程约定。Kubernetes 只关心探针结果,不理解路径名称本身。
十三、把状态、事件、日志和指标拼成因果证据
一个可靠的故障结论至少应回答四个问题:
-
对象状态是什么?
查看 Phase、Conditions、ContainerStatuses、RestartCount。 -
最后一次状态转换由谁触发?
查看 Events、终止原因、探针失败记录和控制器历史。 -
底层组件看到了什么?
调度问题看 Scheduler 事件;拉取问题看 kubelet/runtime 与 registry;节点资源问题看 Node Conditions 和 Eviction 事件。 -
修复后如何证明恢复?
查看新 Pod 的 UID、Ready Condition、EndpointSlice、滚动发布状态和实际业务请求。
例如,下面是一条完整的 OOM 因果链:
容器 memory limit = 512Mi
↓
监控显示工作集持续超过 500Mi
↓
lastState.reason = OOMKilled
↓
exitCode = 137
↓
restartCount 增加
↓
容器启动后再次达到峰值
↓
状态显示 CrashLoopBackOff
这里 CrashLoopBackOff 是最终外显状态,OOMKilled 才是更接近根因的证据。若只执行 delete pod,新 Pod 仍会沿同一链路失败。
相反,若证据是:
PodScheduled=False
reason=Unschedulable
event=Insufficient memory
则容器尚未启动,不应分析应用日志或 liveness probe。
十四、生产环境中的恢复验证
修复完成后,验证不能只看一条命令。
1. 验证控制器发布
kubectl -n "$NS" rollout status deployment/payments-api
kubectl -n "$NS" get deployment payments-api
2. 验证 Pod 层状态
kubectl -n "$NS" get pods -l app=payments-api -o wide
kubectl -n "$NS" get pod <new-pod> \
-o jsonpath='{.status.phase}{" Ready="}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{"\n"}'
3. 验证容器没有继续重启
kubectl -n "$NS" get pods -l app=payments-api \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount'
需要连续观察一段覆盖启动、探针和业务初始化的时间。单次看到 Running 不能证明故障已恢复。
4. 验证 Service 实际端点
kubectl -n "$NS" get endpointslice \
-l kubernetes.io/service-name=payments-api -o yaml
如果 Pod 为 Running 但 readiness 失败,它可能不会出现在可用端点中。只有 Pod Ready、Service 选择器匹配且 EndpointSlice 更新后,流量路径才真正恢复。
结语:先定位状态层,再沿故障路径取证
这几类状态可以用一张简化表收束:
| 现象 | 先看什么 | 常见根因 |
|---|---|---|
Pending |
PodScheduled、Events、init container |
调度约束、资源 request、卷、镜像 |
ImagePullBackOff |
Events、镜像名、Secret、节点网络 | 镜像不存在、认证、架构、registry 或磁盘 |
CrashLoopBackOff |
--previous、lastState、退出码、探针事件 |
应用退出、配置错误、OOM、liveness |
OOMKilled |
limit、使用量、节点压力、历史指标 | 容器超限、泄漏、峰值、节点 OOM |
| Probe 失败 | 探针定义、事件、应用端点语义 | 路径/端口错误、启动过慢、超时、依赖故障 |
正确的排障不是把状态清零,而是重建状态变化的因果链:
API 对象
→ 调度条件
→ 节点与 kubelet 事件
→ 容器当前/上次状态
→ 日志与退出原因
→ 探针和资源约束
→ 控制器恢复与业务端点验证
只有当修复后的 Pod 能持续运行、满足正确的 Ready 条件,并在实际流量路径上可用时,才可以认为工作负载真正恢复。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes 成本治理:资源归属、闲置、Spot、存储网络和优化边界
- 下一篇:Kubernetes 节点排障:Kubelet、Runtime、DiskPressure、PID 和网络
- 延伸:Pod 生命周期:Phase、Condition、RestartPolicy、终止和 Eviction
- 延伸:kubectl 完整工作流:Context、查询、Patch、Debug、输出和安全
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论