首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一个 Kubernetes 运维那些年踩过的坑:把业务打进 CrashLoopBackOff 的真实事故复盘

一个 Kubernetes 运维那些年踩过的坑:把业务打进 CrashLoopBackOff 的真实事故复盘

原创
作者头像
编程小妖女
发布2025-09-19 23:26:49
发布2025-09-19 23:26:49
810
举报
文章被收录于专栏:后端开发后端开发

没有经历过这个 bug 折磨的同行,可能觉得我有点无病呻吟了,但我现在回忆起一个月前处理这个 bug 的那段时间,都还心有余悸。可以说是吃饭睡觉时都在想着这个 bug.

所以,嗯,一定要用文章记录下来,做个纪念!

运行环境 • Kubernetes v1.27.x(托管在自建裸机集群,容器运行时为 containerd) • CNI 为 Calico,集群节点为 Ubuntu 22.04 LTS • 业务容器:基于 Node.js 18 的 HTTP 服务,暴露端口 8080 • Ingress 为 NGINX Ingress Controller 1.10 • 监控与日志:Prometheus + Grafana,节点日志由 fluent-bit 收集到 Loki


现场现象

一次常规的滚动发布后,新版本 v2025.09.18api-gateway Pod 持续处于 CrashLoopBackOffkubectl get poRESTARTS 指数级增长。kubectl describe po 给出一组非常醒目的事件:

代码语言:sh
复制
Warning  Unhealthy  kubelet  Readiness probe failed: Get http://10.244.3.218:8080/readyz: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Warning  Unhealthy  kubelet  Liveness probe failed: Get http://10.244.3.218:8080/readyz: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Normal   Killing    kubelet  Container api-gateway failed liveness probe, will be restarted

我当时使用这些错误消息作为关键字,到网络上搜索了一番,发现这类信息在社区案例里也常见,kubelet 会明示 Liveness probe failed, will be restarted,配合 Readiness probe failed 一起出现,最终状态就会滚进 CrashLoopBackOff

发布窗口的 10 分钟里,业务侧监控出现 5xx 峰值,入口层 HTTP 502/504 数量升高。kubectl get events --sort-by=.metadata.creationTimestamp 能清楚看到失败→回收→重建→再次失败的循环。这个循环对应 Kubernetes 的退避算法,重启间隔逐渐拉长。

关于退避算法的更多细节,参考这篇博客.


真实错误消息与应用 callstack

探针连环失败并不直接产生应用层的 panic,但由于 kubeletliveness 判定在容器未就绪时连续命中,触发容器反复 SIGTERMSIGKILL,应用在请求处理过程中被杀,连接被对端重置,日志里能抓到典型的 ECONNRESET 堆栈:

代码语言:sh
复制
Error: read ECONNRESET
    at TLSWrap.onStreamRead (node:internal/stream_base_commons:217:20)
    at TLSSocket.socketOnData (node:_http_client:523:22)
    at TLSSocket.emit (node:events:517:28)
    at TLSSocket.emit (node:domain:489:12)

与此同时,容器的 Last StateExit Code 变化如下:

代码语言:sh
复制
State:          Waiting
  Reason:       CrashLoopBackOff
Last State:     Terminated
  Reason:       Error
  Exit Code:    137

137 常见于进程被 SIGKILL 终止。社区讨论中有不少类似现象的截图与描述,可与本次日志相互印证。


误配根因:把 livenessreadiness 使用

官方文档对三类探针的语义有非常明确的界定:liveness 决定是否重启容器,readiness 决定是否把流量打到容器,startupProbe 用来延后前两者的生效。liveness 并不会等待 readiness 成功再开始工作,如果没有额外延迟或 startupProbe,容器一拉起就会被 liveness 检查。

这次事故里,我们将两个探针都指向 /readyz,而 /readyz 在应用完成配置加载、数据库连通性检查、缓存预热前会返回 503。结果就是:容器还没 readyliveness 已经开始探测同一个地址,连着几次 503 之后,kubelet 判死、杀进程、拉起新容器,新的容器还没预热完成,又被 liveness 处决,重启退避算法接管,CrashLoopBackOff 形成。Google 与 Kubernetes 官方、以及多家厂商的实践文章都强调过这个区别与踩坑点。


复现与排查步骤

为了把问题讲透,我给出一个可运行的极简复现实例。这个例子会让 /readyz 在启动后 30s 内返回 503,而 /healthz 始终返回 200。把 liveness 指向 /readyz 就能稳定复现 CrashLoopBackOff

1) 极简应用(Node.js 18)

app.js

代码语言:js
复制
const http = require('http');

let ready = false;
setTimeout(() => { ready = true; }, 30000); // 启动后 30 秒才就绪

const server = http.createServer((req, res) => {
  if (req.url === '/readyz') {
    if (ready) { res.writeHead(200); res.end('ok'); }
    else { res.writeHead(503); res.end('warming up'); }
    return;
  }
  if (req.url === '/healthz') {
    res.writeHead(200); res.end('alive');
    return;
  }
  res.writeHead(200); res.end('hello');
});

const port = process.env.PORT || 8080;
server.listen(port, () => console.log(`listening on ${port}`));

// 打印连接被硬杀时的可读日志
process.on('SIGTERM', () => {
  console.error('got SIGTERM, shutting down gracefully...');
  setTimeout(() => { process.exit(0); }, 5000);
});

Dockerfile

代码语言:dockerfile
复制
FROM node:18-alpine
WORKDIR /srv
COPY app.js .
EXPOSE 8080
CMD ['node','app.js']

2) 误配的 Deployment(会触发 CrashLoopBackOff)

deployment-bad.yaml

代码语言:yaml
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: probe-mixup-bad
spec:
  replicas: 1
  selector:
    matchLabels: { app: probe-mixup-bad }
  template:
    metadata:
      labels: { app: probe-mixup-bad }
    spec:
      containers:
        - name: app
          image: probe-mixup:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet: { path: /readyz, port: 8080 }
            periodSeconds: 5
            failureThreshold: 3
            timeoutSeconds: 2
          livenessProbe:
            httpGet: { path: /readyz, port: 8080 }   # 错误:指向 readiness
            periodSeconds: 5
            failureThreshold: 3
            timeoutSeconds: 2
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits:   { cpu: 500m, memory: 256Mi }

部署与观察:

代码语言:bash
复制
kubectl apply -f deployment-bad.yaml
kubectl get po -w
kubectl describe po -l app=probe-mixup-bad
kubectl logs -l app=probe-mixup-bad --previous

describe 输出里,你会看到与上文类似的事件条目:Readiness probe failedLiveness probe failedContainer ... failed liveness probe, will be restarted。这些文案与模式和社区问题单中的截图、摘录一致。

3) 定位思路清单

  • 确认 livenessreadiness 指向的路径是否一致,/healthz/readyz 的语义是否正确。官方概念页写得很直白。
  • 检查事件流与退避节奏:kubectl get events --sort-by=.metadata.creationTimestamp,对照 Back-off restarting failed container 的节奏,判断是不是探针引发的重启.
  • kubectl exec 在容器里本地探测探针命令或 HTTP 端点,第三方教程也建议这种直接验证方法。

修复方案与可运行清单

核心动作是把 livenessreadiness 解耦,增加 startupProbe 推迟健康检查开始时间,并给应用以充足的冷启动时间窗口。官方任务文档对 startupProbe 的使用与示例做了规范说明。

1) 正确的 Deployment(修复版)

deployment-good.yaml

代码语言:yaml
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: probe-mixup-good
spec:
  replicas: 2
  selector:
    matchLabels: { app: probe-mixup-good }
  template:
    metadata:
      labels: { app: probe-mixup-good }
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: probe-mixup:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          # 让容器先自由启动,直到 /healthz 稳定
          startupProbe:
            httpGet: { path: /healthz, port: 8080 }
            failureThreshold: 30   # 30 * 1s = 30s 启动宽限
            periodSeconds: 1
          # liveness 只关心存活,不做依赖检查
          livenessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 3
            timeoutSeconds: 2
          # readiness 负责依赖与预热,决定是否接流量
          readinessProbe:
            httpGet: { path: /readyz, port: 8080 }
            periodSeconds: 5
            failureThreshold: 3
            timeoutSeconds: 2
          lifecycle:
            preStop:
              exec:
                command: ['sh','-c','sleep 5'] # 给 LB/drain 一点时间
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits:   { cpu: 500m, memory: 256Mi }

应用之后,观察现象:

代码语言:bash
复制
kubectl apply -f deployment-good.yaml
kubectl rollout status deploy/probe-mixup-good
kubectl get po -l app=probe-mixup-good -w
kubectl describe po -l app=probe-mixup-good | egrep 'Readiness|Liveness|Started|Killing' -n

这次你应该只会在启动初期看到 readiness 未通过的提示,而不会再有 liveness 杀进程的条目。容器稳定后进入 1/1 Running,不再出现 CrashLoopBackOff


经验提炼:如何避免把探针变成自雷

  • livenessreadiness 的目标端点要分离。liveness 做轻量、极少依赖的活性检测,例如应用线程是否还能响应一个本地内存级别的 pingreadiness 再去做 DB、消息队列、远程依赖的检查。官方与多家最佳实践都强调端点解耦的必要性。
  • 冷启动复杂或镜像较大时,配置 startupProbe,或放大 initialDelaySeconds。别让 liveness 在应用还没 ready 时就开始判死。
  • 校准超时与周期:timeoutSeconds 太小在资源抖动时会产生误杀,适当提升 periodSecondsfailureThreshold,让探针对瞬时抖动有容忍。故障排查文章也建议从这些参数入手。
  • 观测 Back-off 节奏:当你在事件或 kubectl describe 中看到 Back-off restarting failed container,十有八九是进程在短时间内反复退出或被杀。对照日志和探针事件,快速定位探针误杀还是应用缺陷.
  • 在容器内直接执行探针命令验证,例如 kubectl exec pod -- wget -qO- http://127.0.0.1:8080/healthz,比从外部网络路径探测更能隔离网络因素。这是许多运维指南推荐的做法。

我的一些反思

  • 为什么 readiness 失败也会出现 5xx 峰值? Ingress/NLB 的健康检查与 Service 的 endpoints 更新存在传播延迟,在滚动场景里短暂的 readiness 震荡仍可能打到少量未完全下线的后端。提高 preStop 延迟与 terminationGracePeriodSeconds 能缓解抖动。
  • 是否所有情况都需要 startupProbe? 如果应用启动足够快、liveness 有合理的 initialDelaySeconds,可以不配;但对需要预热的 Java、.NET、数据密集型 Node.js 应用,startupProbe 是更稳妥的开关。

避坑指南

这次事故的本质并不是 Kubernetes 不稳定,而是我们把 liveness 当成了 readiness,让 kubelet 在预热期就不断处决进程。理解三类探针的边界、让端点各司其职、用 startupProbe 稳住冷启动,是把事故从 CrashLoopBackOff 拉回 Running 的关键。官方文档对探针的定义与生效时机是判断与修复的基准,建议团队把探针策略固化到应用脚手架或 Helm Chart 模板里,避免类似的误配在将来重演。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 现场现象
  • 真实错误消息与应用 callstack
  • 误配根因:把 liveness 当 readiness 使用
  • 复现与排查步骤
    • 1) 极简应用(Node.js 18)
    • 2) 误配的 Deployment(会触发 CrashLoopBackOff)
    • 3) 定位思路清单
  • 修复方案与可运行清单
    • 1) 正确的 Deployment(修复版)
  • 经验提炼:如何避免把探针变成自雷
  • 我的一些反思
  • 避坑指南
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档