Skip to content

필사 모드: Kubernetes CrashLoopBackOff 分原因诊断与解决 — 日志为空时该看什么

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — RESTARTS 数字一直涨,日志却是空的

发布之后查看 Pod 列表,是这个样子。

kubectl get pod -n payments

NAME                        READY   STATUS             RESTARTS        AGE
checkout-7d9f6c5b4d-2xk9p   0/1     CrashLoopBackOff   6 (2m41s ago)   11m
checkout-7d9f6c5b4d-lm4vz   0/1     CrashLoopBackOff   6 (2m38s ago)   11m
checkout-7d9f6c5b4d-q8w2n   0/1     CrashLoopBackOff   6 (2m44s ago)   11m

想看日志,却什么都没有。

kubectl logs checkout-7d9f6c5b4d-2xk9p -n payments

Error from server (BadRequest): container "checkout" in pod "checkout-7d9f6c5b4d-2xk9p" is waiting to start: CrashLoopBackOff

到这一步,很多人会断定这是一个不打日志的应用,然后开始翻日志配置。这是走错了方向。日志是在的。你现在要查询的容器,只是还没有启动的下一代容器而已。

CrashLoopBackOff 不是原因,而是重启等待

要把这个名字读准确。CrashLoopBackOff 完全没有告诉你容器为什么会死。它只表示 kubelet 宣布了一件事:这个容器一直在死,所以我先把重启推迟一会儿。

kubelet 的重启延迟遵循固定规则。

  • 第一次失败后等待 10 秒
  • 每失败一次等待时间翻倍 — 10 秒、20 秒、40 秒、80 秒、160 秒
  • 上限是 300 秒,也就是 5 分钟
  • 容器正常运行足够久(默认 10 分钟)之后,延迟计时器会重置,重新从 10 秒开始

从这条规则可以得出两个实务结论。

第一,被放置很久的 Pod,重启间隔已经拉长到 5 分钟。推送修好的镜像之后,不必坐在那里等着问「怎么还没起来」。把 Pod 删掉,新的 Pod 没有退避历史,会立刻启动。

kubectl delete pod checkout-7d9f6c5b4d-2xk9p -n payments

第二,RESTARTS 计数缓慢增长的 Pod 有可能根本不会显示为 CrashLoopBackOff。撑过 10 分钟以上才死的应用,每次都会把退避重置,所以状态列显示 Running,只有 RESTARTS 在悄悄增加。这种模式反而更危险。要用下面的命令定期扫一遍。

kubectl get pods -A --sort-by='.status.containerStatuses[0].restartCount' | tail -20

NAMESPACE   NAME                          READY   STATUS    RESTARTS         AGE
search      indexer-6c4d9f7b8-h2klp       1/1     Running   17 (43m ago)     6d
payments    ledger-5f8b7c6d9-nm3xt        1/1     Running   23 (12m ago)     9d

诊断的起点 — Last State、Exit Code 以及 previous 日志

诊断顺序永远一样。先用 describe 确认死亡原因,然后读已死容器的日志。

kubectl describe pod checkout-7d9f6c5b4d-2xk9p -n payments

Containers:
  checkout:
    Container ID:   containerd://3f2a91c4e88b7d5641a0c2f9e37b1d80
    Image:          registry.example.com/checkout:1.14.2
    Image ID:       registry.example.com/checkout@sha256:9c1f...
    Port:           8080/TCP
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Sun, 26 Jul 2026 09:41:12 +0900
      Finished:     Sun, 26 Jul 2026 09:41:13 +0900
    Ready:          False
    Restart Count:  6
Events:
  Type     Reason     Age                    From     Message
  ----     ------     ----                   ----     -------
  Normal   Pulled     3m12s (x5 over 11m)    kubelet  Container image "registry.example.com/checkout:1.14.2" already present on machine
  Normal   Created    3m12s (x5 over 11m)    kubelet  Created container checkout
  Normal   Started    3m11s (x5 over 11m)    kubelet  Started container checkout
  Warning  BackOff    2m41s (x24 over 10m)   kubelet  Back-off restarting failed container checkout

需要读的值有三个。

  • Last State 里的 Reason — 是 Error、OOMKilled 还是 Completed
  • Exit Code — 与后文的退出码词典对照
  • Started 与 Finished 的间隔 — 相差 1 秒说明启动过程中当场死亡,相差 30 秒以上说明起来了但之后才死

上面的例子里 Started 与 Finished 相差 1 秒。意思是进程刚起来就死了,所以该怀疑的是启动时刻的配置,而不是运行时逻辑。

现在读已死容器的日志。关键是 --previous 参数。kubelet 重启容器时会创建新容器,所以不带参数的 kubectl logs 指向的是还没启动、或者刚刚出生的容器。已死容器的日志在上一代里。

kubectl logs checkout-7d9f6c5b4d-2xk9p -n payments --previous

2026-07-26T00:41:12.881Z INFO  starting checkout 1.14.2
2026-07-26T00:41:13.104Z ERROR config: required key PAYMENT_SIGNING_KEY is not set
2026-07-26T00:41:13.105Z FATAL exiting with status 1

常见的错误答案:因为日志是空的就判断应用不打日志。实际上绝大多数情况只是没有敲 --previous 而已。如果连 --previous 也是空的,那时才说明进程一行日志都没写就死了,这种情况下原因几乎总是镜像入口点或者卷挂载。

如果 Pod 里有多个容器,要先确定是哪个容器在死。

kubectl get pod checkout-7d9f6c5b4d-2xk9p -n payments \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

checkout    6    Error    1
istio-proxy 0    <none>   <none>

退出码词典 — 0、1、127、137、139、143

退出码能把诊断工作量砍掉一半。大于 128 的值是 128 加上信号编号,可以反推出是被哪个信号杀死的。

  • 0 — 正常退出。意思是进程干完自己的活儿结束了。Deployment 的 restartPolicy 永远是 Always,所以即使正常退出,kubelet 也会重新拉起,最终变成 CrashLoopBackOff。Reason 里显示的是 Completed 而不是 Error。原因大多是没有把进程放在前台运行。以守护进程模式启动 nginx,或者 shell 脚本把最后一条命令扔到后台就结束,都是典型场景。
  • 1 — 一般性的应用错误。框架没接住的异常、配置校验失败、必需环境变量缺失都集中在这里。原因一定在日志里。
  • 2 — shell 用法错误。传了错误参数时 shell 会返回它。
  • 126 — 文件存在但没有执行权限。入口点脚本没有加执行位的情况。
  • 127 — 找不到命令。要么 command 里有拼写错误,要么在 distroless 或 scratch 镜像里调用了不存在的 shell。
  • 137 — 128 加 9,也就是 SIGKILL。内核 OOM killer 或者 kubelet 强制杀掉了它。Reason 是 OOMKilled 就是超出内存上限,Reason 是 Error 就是收到 SIGTERM 后没能在宽限期内退出而被强制终止。
  • 139 — 128 加 11,也就是 SIGSEGV。原生代码的段错误。常见于原生扩展模块、JNI、CGO,或者架构不匹配的镜像。
  • 143 — 128 加 15,也就是 SIGTERM。有人请求了正常关闭,进程也照做了。在滚动更新或节点排空过程中这是正常的,但如果 143 毫无缘由地反复出现,很可能是 liveness 探针在杀容器。

常见的错误答案:一看到 137 就去调高内存上限。137 只告诉你这是 SIGKILL。是不是内存导致的,要看 Reason 字段是不是 OOMKilled 来判断。给一个因为 liveness 失败后拒绝退出而被强制杀死的容器加内存,什么都不会改变。如果确定是内存问题,直接跳到 OOMKilled 137 诊断 那一篇会更快。

原因分支与诊断命令

收敛到 CrashLoopBackOff 的路径大体上有七条。逐一排除即可。

原因典型信号诊断命令解决
应用异常导致立即退出Exit Code 1、Reason Error、运行不到 1 秒kubectl logs POD --previous照着日志里的堆栈直接修
必需配置或 Secret 缺失Exit Code 1、日志里打出了键名kubectl get secret, kubectl describe pod核对 ConfigMap 与 Secret 的键名和命名空间
正常退出后反复重启Exit Code 0、Reason Completedkubectl logs POD --previous把进程放到前台运行
OOMKilledExit Code 137、Reason OOMKilleddescribe 里的 Last State上调上限或调整运行时堆设置
liveness 探针导致的强制终止Exit Code 143 或 137、Events 里有 Unhealthy 和 Killingkubectl get events --field-selector reason=Unhealthy添加 startupProbe,放宽阈值
错误的 command 或 entrypointExit Code 126 或 127、Message 里有 executable file not foundkubectl describe pod 的 Message修正 command 与 args,确认镜像里有没有 shell
卷挂载失败容器根本起不来、Events 里有 FailedMountkubectl describe pod 的 Events确认 PVC 绑定与 Secret 是否存在
init 容器失败STATUS 是 Init:CrashLoopBackOffkubectl logs POD -c INIT_NAME --previous修正 init 逻辑,确认依赖服务的启动顺序

配置与 Secret 缺失

这是最常见的。先确认 Pod spec 引用的键实际存在不存在。

kubectl get secret payment-keys -n payments -o jsonpath='{.data}' | tr ',' '\n'

{"PAYMENT_API_URL":"aHR0cHM6...","PAYMENT_WEBHOOK_SECRET":"czNjcjN0"}

spec 要求 PAYMENT_SIGNING_KEY,而 Secret 里没有这个键。原因是键名拼错了。

这里有一个重要的设计要点。在 env 下用 secretKeyRef 引用并给出 optional: true,那么即使键不存在 Pod 也会起来,应用会在运行时死掉。反过来,不写 optional,kubelet 连容器都不会启动,直接停在 CreateContainerConfigError。后者要好诊断得多。

env:
  - name: PAYMENT_SIGNING_KEY
    valueFrom:
      secretKeyRef:
        name: payment-keys
        key: PAYMENT_SIGNING_KEY
        # 省略 optional 时默认值为 false — 键不存在就不启动容器

这时 Pod 状态显示的不是 CrashLoopBackOff 而是 CreateContainerConfigError,describe 里会写明确切原因。

kubectl describe pod checkout-7d9f6c5b4d-2xk9p -n payments | grep -A3 "Warning  Failed"

  Warning  Failed     9s (x3 over 25s)  kubelet  Error: couldn't find key PAYMENT_SIGNING_KEY in Secret payments/payment-keys

liveness 探针正在杀容器的情况

Events 里 Unhealthy 和 Killing 一起出现,那凶手就是探针。

kubectl get events -n payments --field-selector involvedObject.name=checkout-7d9f6c5b4d-2xk9p --sort-by=.lastTimestamp

LAST SEEN   TYPE      REASON      OBJECT                          MESSAGE
4m12s       Warning   Unhealthy   pod/checkout-7d9f6c5b4d-2xk9p   Liveness probe failed: Get "http://10.42.3.17:8080/healthz": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
4m12s       Normal    Killing     pod/checkout-7d9f6c5b4d-2xk9p   Container checkout failed liveness probe, will be restarted

给一个需要 40 秒才能起来的应用挂上 initialDelaySeconds 10 的 liveness,它永远起不来。这种情况下调大 initialDelaySeconds 只是权宜之计,正解是 startupProbe。详细的计算方式整理在 三种探针的设计 那一篇里。

错误的 command 与 entrypoint

Message 里会原封不动地写出找不到可执行文件这句话。

kubectl describe pod migrate-runner-0 -n payments | grep -A2 "Last State"

    Last State:     Terminated
      Reason:       StartError
      Message:      failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "/app/entrypoint.sh": permission denied

权限问题是 126,文件本身不存在是 127。在 distroless 镜像里写 command: ["sh", "-c", ...],因为没有 shell 就会得到 127。

卷挂载失败与 init 容器

如果容器根本没能启动,要看的就不是日志而是 Events。

kubectl describe pod ledger-0 -n payments | tail -8

Events:
  Type     Reason       Age                 From               Message
  ----     ------       ----                ----               -------
  Warning  FailedMount  2m15s (x9 over 12m) kubelet            MountVolume.SetUp failed for volume "ledger-data" : rpc error: code = Internal desc = volume attachment is being deleted
  Warning  FailedMount  38s                 kubelet            Unable to attach or mount volumes: unmounted volumes=[ledger-data], unattached volumes=[ledger-data kube-api-access-x9k2m]: timed out waiting for the condition

init 容器失败时 STATUS 列会加上 Init 前缀,主容器完全不会运行。看日志时必须指定容器名。

kubectl get pod ledger-0 -n payments

NAME       READY   STATUS                  RESTARTS      AGE
ledger-0   0/1     Init:CrashLoopBackOff   4 (48s ago)   3m

kubectl logs ledger-0 -n payments -c wait-for-db --previous

waiting for postgres.payments.svc.cluster.local:5432 ...
timeout after 30s

让容器保持存活并进入其中

光靠日志解不开,就得亲自看看正在死去的容器内部。方法有两种,用途不同。

第一种,覆盖 command,让容器不跑进程而只睡觉。可以在与原状态完全相同的条件下确认文件系统、环境变量和 DNS。

apiVersion: v1
kind: Pod
metadata:
  name: checkout-shell
  namespace: payments
spec:
  restartPolicy: Never
  containers:
    - name: checkout
      image: registry.example.com/checkout:1.14.2
      command: ['sh', '-c', 'sleep infinity']
      envFrom:
        - secretRef:
            name: payment-keys
      volumeMounts:
        - name: config
          mountPath: /etc/checkout
  volumes:
    - name: config
      configMap:
        name: checkout-config
kubectl apply -f checkout-shell.yaml
kubectl exec -it checkout-shell -n payments -- sh

/ # env | grep PAYMENT
PAYMENT_API_URL=https://api.example.com
PAYMENT_WEBHOOK_SECRET=s3cr3t
/ # /app/checkout
ERROR config: required key PAYMENT_SIGNING_KEY is not set

想不动现有 Deployment 而达到同样效果,就做一个副本。

kubectl debug checkout-7d9f6c5b4d-2xk9p -n payments \
  --copy-to=checkout-debug \
  --container=checkout \
  -- sleep infinity

kubectl exec -it checkout-debug -n payments -- sh

第二种,临时容器。在原镜像没有 shell 的 distroless 环境下尤其有用。它与目标容器共享进程命名空间,所以连正在死去的进程也能观察。

kubectl debug -it checkout-7d9f6c5b4d-2xk9p -n payments \
  --image=busybox:1.36 \
  --target=checkout \
  -- sh

Defaulting debug container name to debugger-7v2mp.
/ # ls /proc
1  14  self ...
/ # cat /proc/1/cmdline
/app/checkout
/ # wget -qO- http://localhost:8080/healthz
wget: can't connect to remote host: Connection refused

临时容器不修改 Pod spec,所以不会触发重启。不过卷默认不共享,如果需要查看挂载的文件,就通过 /proc/1/root 路径访问 --target 指定的那个容器的文件系统。

/ # ls /proc/1/root/etc/checkout
application.yaml  logging.yaml

防止复发 — 让崩溃在发布流水线里暴露出来

要不再重复同样的故障,就在三个地方装上机关。

第一,让应用在启动时刻校验全部配置,并带着明确的消息退出。把必需的键留到运行时第一个请求才去读的代码,表现出来的不是 CrashLoopBackOff 而是 500 错误,发现得晚得多。

第二,让发布命令等到失败之后再返回。kubectl apply 提交资源后立刻返回成功,于是就造出了 CI 是绿灯而生产环境已经死掉的状态。

kubectl apply -f deploy/checkout.yaml
kubectl rollout status deployment/checkout -n payments --timeout=180s

Waiting for deployment "checkout" rollout to finish: 0 of 3 updated replicas are available...
error: deployment "checkout" exceeded its progress deadline

在 Deployment 上写明 progressDeadlineSeconds,这个判定就会自动生效。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: payments
spec:
  replicas: 3
  progressDeadlineSeconds: 180
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

maxUnavailable: 0 保证在新 Pod 变成 Ready 之前不杀掉旧 Pod。即使发布了会崩溃的新版本,服务依然维持。

第三,用告警抓住重启次数的增长。CrashLoopBackOff 很显眼,但前面说的那种「每 10 分钟悄悄重启一次」,不盯着看板的话会被放置好几周。

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pod-restart-alerts
  namespace: monitoring
spec:
  groups:
    - name: pod-health
      rules:
        - alert: PodRestartingRepeatedly
          expr: increase(kube_pod_container_status_restarts_total[1h]) > 3
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "{{ $labels.namespace }}/{{ $labels.pod }} restarted more than 3 times in an hour"
        - alert: PodInCrashLoop
          expr: kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"} == 1
          for: 5m
          labels:
            severity: critical

结语 — 日志为空就先敲 previous

CrashLoopBackOff 的诊断靠的不是天赋而是顺序。用 describe 确认 Last State 的 Reason 和 Exit Code,用 kubectl logs --previous 听已死容器的最后一句话,还是没有就覆盖 command 让容器活着,然后进去看。这三步能让大多数故障在 10 分钟内收尾。

要记住的一句话是这个。CrashLoopBackOff 不是诊断名,而是候诊室的名字,真正的诊断名永远写在 Exit Code 和上一代容器的日志里。

현재 단락 (1/222)

发布之后查看 Pod 列表,是这个样子。

작성 글자: 0원문 글자: 10,441작성 단락: 0/222