没有经历过这个 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.18
的 api-gateway
Pod 持续处于 CrashLoopBackOff
,kubectl get po
的 RESTARTS
指数级增长。kubectl describe po
给出一组非常醒目的事件:
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 的退避算法,重启间隔逐渐拉长。
关于退避算法的更多细节,参考这篇博客.
探针连环失败并不直接产生应用层的 panic
,但由于 kubelet
的 liveness
判定在容器未就绪时连续命中,触发容器反复 SIGTERM
→SIGKILL
,应用在请求处理过程中被杀,连接被对端重置,日志里能抓到典型的 ECONNRESET
堆栈:
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 State
与 Exit Code
变化如下:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Error
Exit Code: 137
137
常见于进程被 SIGKILL
终止。社区讨论中有不少类似现象的截图与描述,可与本次日志相互印证。
liveness
当 readiness
使用官方文档对三类探针的语义有非常明确的界定:liveness
决定是否重启容器,readiness
决定是否把流量打到容器,startupProbe
用来延后前两者的生效。liveness
并不会等待 readiness
成功再开始工作,如果没有额外延迟或 startupProbe
,容器一拉起就会被 liveness
检查。
这次事故里,我们将两个探针都指向 /readyz
,而 /readyz
在应用完成配置加载、数据库连通性检查、缓存预热前会返回 503
。结果就是:容器还没 ready
,liveness
已经开始探测同一个地址,连着几次 503
之后,kubelet
判死、杀进程、拉起新容器,新的容器还没预热完成,又被 liveness
处决,重启退避算法接管,CrashLoopBackOff
形成。Google 与 Kubernetes 官方、以及多家厂商的实践文章都强调过这个区别与踩坑点。
为了把问题讲透,我给出一个可运行的极简复现实例。这个例子会让 /readyz
在启动后 30s
内返回 503
,而 /healthz
始终返回 200
。把 liveness
指向 /readyz
就能稳定复现 CrashLoopBackOff
。
app.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
:
FROM node:18-alpine
WORKDIR /srv
COPY app.js .
EXPOSE 8080
CMD ['node','app.js']
deployment-bad.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 }
部署与观察:
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 failed
、Liveness probe failed
与 Container ... failed liveness probe, will be restarted
。这些文案与模式和社区问题单中的截图、摘录一致。
liveness
与 readiness
指向的路径是否一致,/healthz
与 /readyz
的语义是否正确。官方概念页写得很直白。 kubectl get events --sort-by=.metadata.creationTimestamp
,对照 Back-off restarting failed container
的节奏,判断是不是探针引发的重启.kubectl exec
在容器里本地探测探针命令或 HTTP 端点,第三方教程也建议这种直接验证方法。 核心动作是把 liveness
与 readiness
解耦,增加 startupProbe
推迟健康检查开始时间,并给应用以充足的冷启动时间窗口。官方任务文档对 startupProbe
的使用与示例做了规范说明。
deployment-good.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 }
应用之后,观察现象:
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
。
liveness
与 readiness
的目标端点要分离。liveness
做轻量、极少依赖的活性检测,例如应用线程是否还能响应一个本地内存级别的 ping
;readiness
再去做 DB、消息队列、远程依赖的检查。官方与多家最佳实践都强调端点解耦的必要性。 startupProbe
,或放大 initialDelaySeconds
。别让 liveness
在应用还没 ready
时就开始判死。 timeoutSeconds
太小在资源抖动时会产生误杀,适当提升 periodSeconds
与 failureThreshold
,让探针对瞬时抖动有容忍。故障排查文章也建议从这些参数入手。 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 删除。