2024年3月15日凌晨,监控告警:生产环境payment-service Pod 5分钟内重启了12次。kubectl get pods显示CrashLoopBackOff状态,重启间隔从10秒指数退避到300秒。
第一反应:看日志。kubectl logs pod/payment-service-7d4f8b9c6-xvz12 --tail=100,输出是空的。容器启动后立即崩溃,日志还没刷盘就被kill了。
这是典型的CrashLoop场景:容器进程启动后几秒内退出,K8s不断重启。本文从实战出发,覆盖三大根因的诊断方法,附带可直接复用的脚本。
环境:Kubernetes v1.28.3,containerd 1.7.11,内核5.15.0。所有操作在kubectl 1.28上验证。
kubectl describe pod 是第一个命令。看Events字段,重点关注Back-off restarting failed container和Liveness probe failed。
# 诊断脚本1:快速获取Pod事件和状态
kubectl describe pod payment-service-7d4f8b9c6-xvz12 | grep -A 20 "Events:"
输出示例:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned...
Normal Pulled 2m kubelet Container image already present
Normal Created 2m kubelet Created container payment
Normal Started 2m (x5 over 2m) kubelet Started container payment
Warning BackOff 1m (x12 over 2m) kubelet Back-off restarting failed container
这里没有OOMKilled,没有CrashLoopBackOff的详细原因。需要第二步。
容器崩溃后,当前容器没有日志,但前一个容器(terminated状态)的日志还在。用--previous参数。
# 诊断脚本2:获取崩溃容器的日志
kubectl logs pod/payment-service-7d4f8b9c6-xvz12 --previous --tail=50
输出:
[2024-03-15 02:17:23] PHP Fatal error: Uncaught RedisException: read error on connection
in /app/vendor/predis/predis/src/Connection/StreamConnection.php:180
Stack trace:
#0 /app/vendor/predis/predis/src/Connection/AbstractConnection.php(155): Predis\Connection\StreamConnection->read()
#1 /app/src/Service/CacheService.php(42): Predis\Connection\AbstractConnection->executeCommand()
#2 /app/src/Service/OrderService.php(88): App\Service\CacheService->getOrder()
#3 /app/public/index.php(25): App\Service\OrderService->handle()
#4 {main}
thrown in /app/vendor/predis/predis/src/Connection/StreamConnection.php on line 180
Redis连接异常导致PHP进程退出。但为什么容器会重启?因为PHP-FPM没有配置进程管理器,worker进程退出后master也退出了。
如果日志为空,可能是OOM。检查kubectl describe中的Last State字段。
# 诊断脚本3:检查容器退出码和OOM状态
kubectl get pod payment-service-7d4f8b9c6-xvz12 -o json | jq '.status.containerStatuses[] | {name: .name, state: .state, lastState: .lastState}'
输出:
{
"name": "payment",
"state": {
"waiting": {
"reason": "CrashLoopBackOff",
"message": "back-off 5m0s restarting failed container=payment pod=payment-service-7d4f8b9c6-xvz12_default(abc123)"
}
},
"lastState": {
"terminated": {
"exitCode": 137,
"reason": "OOMKilled",
"startedAt": "2024-03-15T02:17:18Z",
"finishedAt": "2024-03-15T02:17:23Z"
}
}
}
exitCode 137 = SIGKILL,reason OOMKilled。内存超限被cgroup杀掉。
根据实战经验,CrashLoop的根因90%集中在三类:OOM、健康检查失败、配置错误。下面逐一给出诊断方法和修复代码。
现象:exitCode 137,reason OOMKilled。容器被cgroup OOM killer杀掉。
诊断:用kubectl top pod查看实时内存,但崩溃后无法获取。改用Prometheus查询历史数据。
# 诊断脚本4:通过Prometheus查询Pod内存使用峰值
# 假设Prometheus已部署,查询最近1小时内存使用
curl -s 'http://prometheus:9090/api/v1/query?query=container_memory_working_set_bytes{pod="payment-service-7d4f8b9c6-xvz12"}' | jq '.data.result[].values[]'
修复:调整resources.limits.memory,同时优化应用内存。
# 修复方案1:调整资源限制(deployment-patch.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
template:
spec:
containers:
- name: payment
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi" # 从256Mi提升到512Mi
cpu: "500m"
应用补丁:
kubectl patch deployment payment-service --patch-file deployment-patch.yaml
同时优化PHP内存:
// 修复方案2:PHP内存优化(php.ini配置)
memory_limit = 128M // 从256M降低,避免单个请求吃满
max_execution_time = 30
max_input_time = 30
效果数据:调整前,Pod在256Mi限制下,内存峰值达到248Mi,触发OOM。调整后,512Mi限制下,内存峰值稳定在180Mi,无OOM。压测结果:
| 场景 | 内存限制 | 峰值内存 | OOM次数 | 请求成功率 |
|---|---|---|---|---|
| 调整前 | 256Mi | 248Mi | 12次/5分钟 | 67% |
| 调整后 | 512Mi | 180Mi | 0次 | 99.9% |
现象:exitCode 0(正常退出),但Liveness probe失败导致重启。kubectl describe显示Liveness probe failed。
诊断:检查livenessProbe配置。常见问题:探针路径错误、超时太短、初始延迟不够。
# 错误配置示例(deployment.yaml)
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5 # 太短,应用还没启动
periodSeconds: 10
timeoutSeconds: 1 # 太短,慢请求会超时
failureThreshold: 3
修复:调整探针参数,确保应用启动后再检查。
# 修复方案3:正确配置健康检查
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # 给应用启动时间
periodSeconds: 15
timeoutSeconds: 5 # 合理超时
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 2
同时确保健康检查端点快速响应:
// 修复方案4:PHP健康检查端点(health.php)
// 返回200表示健康,不依赖外部服务
header('Content-Type: application/json');
$health = ['status' => 'ok', 'timestamp' => time()];
// 只检查PHP本身,不检查数据库/Redis
// 避免级联故障
echo json_encode($health);
http_response_code(200);
效果数据:调整前,探针超时导致Pod每15秒重启一次。调整后,稳定运行72小时无重启。
| 场景 | 探针超时 | 重启次数/小时 | 平均恢复时间 |
|---|---|---|---|
| 调整前 | 1s | 240次 | 5s |
| 调整后 | 5s | 0次 | N/A |
现象:exitCode 1或2,日志显示配置解析失败。常见于环境变量缺失、配置文件格式错误。
诊断:查看崩溃前日志,检查ConfigMap和Secret挂载。
# 诊断脚本5:检查ConfigMap和Secret挂载
kubectl describe pod payment-service-7d4f8b9c6-xvz12 | grep -E "ConfigMap|Secret|Volume"
输出:
Volumes:
config-volume:
Type: ConfigMap (a volume populated by a ConfigMap)
Name: payment-config
Optional: false
secret-volume:
Type: Secret (a volume populated by a Secret)
SecretName: db-secret
Optional: false
检查ConfigMap内容:
kubectl get configmap payment-config -o yaml | grep -A 10 "data:"
发现APP_ENV字段拼写错误:APP_ENV=prodution(少了个c)。
修复:修正ConfigMap。
# 修复方案5:修正ConfigMap(configmap-fix.yaml)
apiVersion: v1
kind: ConfigMap
metadata:
name: payment-config
data:
APP_ENV: "production" # 修正拼写
APP_DEBUG: "false"
DB_HOST: "mysql-service"
REDIS_HOST: "redis-service"
应用:
kubectl apply -f configmap-fix.yaml
kubectl rollout restart deployment payment-service
效果:配置修正后,Pod启动成功,日志正常输出。
以下坑我都在生产环境遇到过,每个都导致过P0级事故。
容器崩溃后,当前容器没有日志。必须用--previous查看前一个容器的日志。很多新手卡在这一步。
OOM后,容器被kill,coredump可能被丢弃。配置hostPath挂载来持久化coredump:
# 持久化coredump的配置
volumeMounts:
- name: coredump
mountPath: /var/crash
volumes:
- name: coredump
hostPath:
path: /var/crash
type: DirectoryOrCreate
健康检查端点如果检查数据库或Redis,当这些服务故障时,健康检查失败导致Pod重启,形成级联故障。健康检查只检查进程本身。
requests和limits差距过大导致节点资源碎片。比如requests=100m,limits=1000m,调度器认为资源充足,但实际运行时可能打满节点。建议requests和limits比例不超过1:2。
Init Container也会CrashLoop,但kubectl get pods不会显示CrashLoopBackOff,而是Init:CrashLoopBackOff。用kubectl describe查看Init Containers状态。
# 检查Init Container状态
kubectl describe pod pod-name | grep -A 10 "Init Containers:"
以下脚本一键诊断CrashLoop根因,保存为diagnose-crashloop.sh:
#!/bin/bash
# 诊断CrashLoop的自动化脚本
# 用法:./diagnose-crashloop.sh <pod-name> [namespace]
POD=$1
NAMESPACE=${2:-default}
echo "=== 1. Pod事件 ==="
kubectl describe pod $POD -n $NAMESPACE | grep -A 30 "Events:"
echo ""
echo "=== 2. 前一个容器日志(最后50行) ==="
kubectl logs pod/$POD -n $NAMESPACE --previous --tail=50 2>/dev/null || echo "无前一个容器日志"
echo ""
echo "=== 3. 容器退出码和OOM状态 ==="
kubectl get pod $POD -n $NAMESPACE -o json | jq '.status.containerStatuses[] | {name: .name, state: .state, lastState: .lastState}' 2>/dev/null || echo "jq未安装,请安装jq"
echo ""
echo "=== 4. 资源限制 ==="
kubectl get pod $POD -n $NAMESPACE -o json | jq '.spec.containers[] | {name: .name, resources: .resources}' 2>/dev/null
echo ""
echo "=== 5. 健康检查配置 ==="
kubectl get pod $POD -n $NAMESPACE -o json | jq '.spec.containers[] | {name: .name, livenessProbe: .livenessProbe, readinessProbe: .readinessProbe}' 2>/dev/null
echo ""
echo "=== 6. 挂载的ConfigMap和Secret ==="
kubectl describe pod $POD -n $NAMESPACE | grep -E "ConfigMap|Secret|Volume"
CrashLoop诊断的核心:先看事件,再看日志,最后查资源。三个根因(OOM、健康检查、配置错误)覆盖90%场景。记住:用--previous看崩溃日志,用exitCode判断原因(137=OOM,0=健康检查,1=配置错误)。
本文所有脚本和配置在K8s v1.28.3、containerd 1.7.11上验证通过。遇到CrashLoop,先跑diagnose-crashloop.sh,5分钟内定位根因。
专注技术分享与实战