Kubernetes Pod 反复重启:从 CrashLoopBackOff 定位退出原因、探针与配置

103 次浏览5 条回复

适用于 Kubernetes 1.24+、kubectl 1.24+。先用 kubectl version 记录客户端和服务端版本,并确认 kubectl 与服务端的版本偏差在官方支持范围内。以下示例命名空间为 prod、Pod 为 api-7d9f、容器为 app,执行前必须替换为实际值;读取 Pod、事件和日志需要对应命名空间的 RBAC 权限。先保留失败现场,不要一开始就删除 Pod、扩大资源限额或关闭探针。

1. 先确认是哪个容器、哪一次退出

CrashLoopBackOff 表示 kubelet 正在延迟下一次重启,不是根因。一个 Pod 可能包含多个业务容器、边车和 init 容器,应先固定对象与时间线:

date -u
kubectl version
kubectl -n prod get pod api-7d9f -o wide
kubectl -n prod describe pod api-7d9f
kubectl -n prod get pod api-7d9f -o jsonpath='{range .status.initContainerStatuses[*]}init/{.name}{"\t"}{.state.waiting.reason}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}{range .status.containerStatuses[*]}container/{.name}{"\t"}{.restartCount}{"\t"}{.state.waiting.reason}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

重点记录 restartCount、当前 state、lastState.terminated.reason、exitCode、开始与结束时间。Completed 对长驻服务可能意味着入口命令正常退出但不符合工作负载模型;Error 需要结合应用日志解释;OOMKilled 才直接指向容器内存限制相关路径。退出码 137 常对应 SIGKILL,但不能脱离 reason、节点日志和时间线直接断定是内存耗尽。

2. 读取上一次实例的日志

容器已经重启时,当前日志可能只有新一轮启动内容,优先读取上一次终止实例:

kubectl -n prod logs api-7d9f -c app --previous --timestamps
kubectl -n prod logs api-7d9f -c app --timestamps
kubectl -n prod get events \
  --field-selector involvedObject.kind=Pod,involvedObject.name=api-7d9f \
  --sort-by=.metadata.creationTimestamp

--previous 只保留前一个已终止实例的日志;节点重启、容器运行时回收或日志轮转后可能不可用。事件也有保留期限,因此应在故障窗口尽早采集。不要在论坛或工单中直接粘贴完整环境变量、Secret、令牌或连接串。

若应用在写日志前就退出,继续检查镜像入口、参数、挂载和安全上下文,而不是把“日志为空”当作没有失败。

3. 核对实际生效的工作负载配置

先找到 Pod 的控制器,再查看当前对象,不要只看本地清单:

kubectl -n prod get pod api-7d9f \
  -o jsonpath='{range .metadata.ownerReferences[*]}{.kind}{"/"}{.name}{"\n"}{end}'
kubectl -n prod get pod api-7d9f -o yaml
kubectl -n prod get deploy api -o yaml
kubectl -n prod rollout history deploy/api

逐项核对 image 与摘要、command、args、workingDir、环境变量来源、ConfigMap/Secret 引用、卷挂载、securityContext 和资源限制。Kubernetes 的 command/args 不会自动经过交互式 shell 解释;需要管道、重定向或变量展开时,应明确使用可审计的脚本或 shell,并正确处理信号转发。

CreateContainerConfigError、ImagePullBackOff 和卷挂载失败发生在应用进程启动前,不属于同一条应用退出路径,应以 describe 中的事件为主,检查对象名、键名、镜像凭据、PVC 与节点挂载错误。

4. 区分内存终止与探针触发的重启

kubectl -n prod get pod api-7d9f \
  -o jsonpath='{range .spec.containers[?(@.name=="app")]}{.resources}{"\n"}{.startupProbe}{"\n"}{.livenessProbe}{"\n"}{.readinessProbe}{"\n"}{end}'
kubectl -n prod describe pod api-7d9f

出现 OOMKilled 时,对齐容器 limits.memory、工作集监控、应用堆限制和节点内存压力。先确认是容器 cgroup 超限还是节点侧驱逐;仅增加 limit 可能掩盖泄漏,也可能让节点承受更大风险。

readinessProbe 失败只会让 Pod 暂停接收流量,不会直接重启容器;livenessProbe 连续失败会触发重启。慢启动应用应考虑与真实启动上界匹配的 startupProbe,避免存活探针在初始化完成前反复终止进程。检查 path、port、scheme、超时和失败阈值,并从 Pod 所在网络与进程监听方式解释结果,不要只在运维终端访问一次就判定探针正确。

5. 验证修复而不是只看 Running

修正控制器模板后观察一次受控发布:

kubectl -n prod rollout status deploy/api --timeout=5m
kubectl -n prod get pods -l app=api -w
kubectl -n prod get deploy api
kubectl -n prod get events --sort-by=.metadata.creationTimestamp

Running 只表示 Pod 已分配且至少有容器在运行,不等于应用可用。闭环应同时满足:新 Pod 达到预期 Ready 数,观察窗口内 restartCount 不再增长,没有新的异常终止或探针失败事件,发布状态完成,并且从真实服务入口执行的受控健康检查和关键请求恢复。若回滚是既定处置流程,应先记录失败版本、镜像摘要和相关日志,再按工作负载的发布策略执行,避免丢失根因证据。

补充一个容易混淆的判断点:lastState.terminated.reason=OOMKilled 不能单独证明一定撞到了该容器的 limits.memory。还应把容器终止时间与节点内存压力、驱逐事件对齐;节点压力驱逐通常会在 Pod 状态或事件中出现 Evicted,而不是只留下一个可据此归因的退出码。

Kubernetes 1.24+ 可先做只读核对(需要读取 Pod、Node 和 Event 的 RBAC 权限):

kubectl -n prod get pod api-7d9f -o jsonpath='{range .status.containerStatuses[?(@.name=="app")]}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\t"}{.lastState.terminated.finishedAt}{"\t"}{.lastState.terminated.message}{"\n"}{end}'

node=$(kubectl -n prod get pod api-7d9f -o jsonpath='{.spec.nodeName}')
kubectl get node "$node" -o jsonpath='{range .status.conditions[?(@.type=="MemoryPressure")]}{.status}{"\t"}{.lastTransitionTime}{"\t"}{.message}{"\n"}{end}'
kubectl get events --field-selector "involvedObject.kind=Node,involvedObject.name=$node" \
  --sort-by=.metadata.creationTimestamp

MemoryPressure=False 只代表当前状态,不能排除故障时刻曾有压力;验证时应以 finishedAt 为轴,对齐 Pod/Node 事件、监控峰值和节点内核日志。若只有当前工作集低于限制,也不能排除采样间隔内的瞬时峰值。最终归因至少应能区分:容器 cgroup 超限、节点全局 OOM、kubelet 驱逐,三者的修复方向并不相同。

补充一个探针排障里很容易产生假阳性的点:Pod 内执行 curl localhost 成功,并不能证明 kubelet 的探针路径正常。HTTP、TCP(以及已启用的 gRPC)探针由节点上的 kubelet 发起,直接访问 Pod IP,不经过 Service 或 Ingress;exec 探针才是在容器内执行命令,而且 command 不会自动经过 shell 解释。

先用 Pod UID 锁定事件,避免 StatefulSet 重建后同名 Pod 的旧事件混进时间线(以下为 Kubernetes 1.24+,需要读取 Pod 与 Event 的 RBAC 权限):

uid=$(kubectl -n prod get pod api-7d9f -o jsonpath='{.metadata.uid}')
kubectl -n prod get pod api-7d9f \
  -o jsonpath='{.metadata.uid}{"\t"}{.spec.nodeName}{"\t"}{.status.podIP}{"\n"}'
kubectl -n prod get events \
  --field-selector "involvedObject.uid=$uid" \
  --sort-by=.metadata.creationTimestamp

若归因于存活探针,应在同一 UID 的时间线上看到 Unhealthy(消息包含 liveness probe failed)以及随后 kubelet 的 Killing,并与容器 lastState.terminated.finishedAt、应用日志对齐。只有探针失败事件、却没有后续终止,可能是 readiness 失败,或尚未达到 failureThreshold,不能直接解释重启。配置了 startupProbe 时,启动探针成功前 liveness/readiness 不会开始。

验证修复时也要按真实路径取证:HTTP 探针可核对应用访问日志中的探针路径、状态码和时间;节点到 Pod IP 的网络策略、监听地址、Host/TLS 配置都可能让它与容器内访问结果不同。至少观察超过配置的探测失败窗口,确认新 Pod UID 下没有新的 Unhealthy/Killing,restartCount 不增长且 Ready 持续稳定。事件有保留期限,故障窗口内应尽早导出。

再补一条 --previous 为空时的取证路径:检查容器终止消息。它独立于常规日志读取,但只保存最近一次终止状态且容量有限,适合记录简短、可脱敏的启动失败摘要,不能替代集中日志。以下适用于 Kubernetes 1.24+,读取 Pod 状态需要对应命名空间的 RBAC 权限。

kubectl -n prod get pod api-7d9f -o jsonpath='{range .spec.containers[?(@.name=="app")]}path={.terminationMessagePath}{"\t"}policy={.terminationMessagePolicy}{"\n"}{end}{range .status.containerStatuses[?(@.name=="app")]}reason={.lastState.terminated.reason}{"\t"}exit={.lastState.terminated.exitCode}{"\t"}finished={.lastState.terminated.finishedAt}{"\n"}message={.lastState.terminated.message}{"\n"}{end}'

默认路径通常是 /dev/termination-log,默认策略是 File。应用若能在退出前把简短错误写入该路径,kubelet 会把它放进 Pod 状态;内容不要包含 Secret、令牌、连接串或完整环境变量。若应用无法写该文件,可评估在工作负载模板中设置:

terminationMessagePolicy: FallbackToLogsOnError

该策略只在容器非零退出且终止消息文件为空时,从容器日志尾部取一段作为终止消息;它不会让已经轮转或丢失的完整日志恢复,也可能把日志中的敏感值带入 Pod 状态,因此启用前应先审查启动日志。

验证时应在受控环境更新控制器模板,确认新 Pod 的 terminationMessagePolicy 实际生效;待一次已知的非敏感失败后,对齐 finishedAt、退出码和 message,同时确认原始退出码没有被包装脚本改写。由于后续重启会覆盖 lastState,仍应在故障窗口尽早采集。

还可以先判断故障是随工作负载版本出现,还是只集中在某个节点或个别 Pod。否则直接围绕单个 Pod 调参,容易漏掉坏镜像、模板变更或节点侧运行时问题。以下适用于 Kubernetes 1.24+;需要读取 Deployment、ReplicaSet 和 Pod 的 RBAC 权限,示例对象、容器名与标签选择器必须替换为实际值。

kubectl -n prod rollout history deploy/api
kubectl -n prod get rs -o wide --sort-by=.metadata.creationTimestamp

kubectl -n prod get pods -l app=api -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.pod-template-hash}{"\t"}{.spec.nodeName}{"\t"}{range .status.containerStatuses[?(@.name=="app")]}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.imageID}{end}{"\n"}{end}'

pod-template-hash 相同且多个节点上的副本都在相近时间失败,优先核对该 ReplicaSet 的镜像摘要、启动参数、ConfigMap/Secret 引用及资源配置;只有某一节点上的副本异常,则进一步对齐该节点的 kubelet、容器运行时、磁盘、网络和挂载事件。不要只比较镜像标签:Pod 状态中的 imageID 更适合确认各副本实际运行的镜像内容,尤其是在使用可变标签时。

修复后应验证新 ReplicaSet 的期望副本与 Ready 副本一致,并按新的 pod-template-hash 再次汇总各 Pod:确认副本分布到多个节点后 restartCount 均不再增长、imageID 符合预期,且旧 ReplicaSet 没有继续创建异常 Pod。若只把故障 Pod 调度到别处后恢复,应保留原节点时间线继续查根因,不能把重新调度本身当作修复。

补充一个时间线取证细节:Event 可能被聚合更新,metadata.creationTimestamp 往往只是该事件对象首次创建的时间。CrashLoop 期间反复出现的 BackOff、Unhealthy 或 Killing 可能只增加 count 并更新最近观测时间,因此仅按创建时间排序,容易把最新一次失败放错位置。

以下适用于 Kubernetes 1.24+,需要读取 Pod 与 Event 的 RBAC 权限。先用 Pod UID 排除同名重建对象,再同时保留各时间字段:

uid=$(kubectl -n prod get pod api-7d9f -o jsonpath='{.metadata.uid}')
kubectl -n prod get events \
  --field-selector "involvedObject.uid=$uid" \
  -o custom-columns='CREATED:.metadata.creationTimestamp,FIRST:.firstTimestamp,LAST:.lastTimestamp,EVENT:.eventTime,SERIES_LAST:.series.lastObservedTime,COUNT:.count,REASON:.reason,TYPE:.type,MESSAGE:.message'

kubectl -n prod get pod api-7d9f \
  -o jsonpath='{range .status.containerStatuses[?(@.name=="app")]}restarts={.restartCount}{"\t"}started={.lastState.terminated.startedAt}{"\t"}finished={.lastState.terminated.finishedAt}{"\t"}reason={.lastState.terminated.reason}{"\n"}{end}'

不同事件记录可能只填充 lastTimestamp、eventTime 或 series.lastObservedTime 中的一部分,出现 <none> 不代表没有发生;应结合 count 与容器的 finishedAt 判断。若可以受控复现,可在另一个终端提前观察同一 UID,避免事后只剩聚合结果:

kubectl -n prod get events \
  --field-selector "involvedObject.uid=$uid" \
  --watch -o json

验证归因时,探针触发重启应能把 Unhealthy、后续 Killing 和终止时间对齐;应用自行退出则可能只有终止状态与 BackOff,没有 Killing。事件有保留期限,lastState 也只保留最近一次终止状态,采集后还应先脱敏再进入工单或共享记录。