容器与 K8s 排查:OOMKilled 与重启
容器里的排查有两个额外难点:进程看到的“内存”不等于节点的内存,而重启往往连现场一起带走了。本章按 Pod 状态分类给出固定的命令链,并把 exit code 与探针配置这两个最高频的坑讲透。
先按状态分类
| 状态 | 含义 | 第一反应 |
|---|---|---|
| OOMKilled | 容器超过 memory limit 被内核杀掉 | 看 describe 里的 Last State 与 resources |
| CrashLoopBackOff | 启动即退出并不断重试,退避时间指数增长 | kubectl logs --previous,查启动参数与依赖 |
| ImagePullBackOff / ErrImagePull | 镜像拉不下来 | 查镜像名与 tag、仓库凭据、节点网络 |
| Evicted | 节点资源压力导致 Pod 被驱逐 | kubectl describe node 看 DiskPressure 与 MemoryPressure |
| Pending | 调度失败 | 资源不足、亲和性冲突、PVC 未绑定 |
| Running 但服务不可用 | Readiness 探针失败,已被摘出 Service | 查探针路径、端口、超时与启动耗时 |
| Terminating 卡住 | 优雅停机未完成或 finalizer 阻塞 | 看 terminationGracePeriodSeconds 与 preStop |
排查命令链
kubectl get pods -o wide
kubectl -n prod get pods -o wide | grep -vE 'Running|Completed'
kubectl -n prod describe pod order-api-7d9c8f5b6-abcde
kubectl -n prod logs order-api-7d9c8f5b6-abcde --tail=200
kubectl -n prod logs order-api-7d9c8f5b6-abcde --previous --tail=200
kubectl -n prod top pod
kubectl -n prod get events --sort-by=.lastTimestamp | tail -30
kubectl -n prod exec -it order-api-7d9c8f5b6-abcde -- sh
kubectl -n prod get pod order-api-7d9c8f5b6-abcde -o yaml | grep -A20 'resources:'
顺序很重要:describe 的 Events 段信息量最大(调度失败、拉镜像失败、探针失败、OOM 记录都在里面),先读 Events 再看日志能省掉大量猜测。--previous 一定要加,容器重启后当前日志是“新的一生”,崩溃原因在上一份日志里。
exit code 判读
| exit code | 信号与含义 | 典型原因 | 下一步 |
|---|---|---|---|
| 137 | 128 + 9 = SIGKILL | 最常见是超过 memory limit 被 OOMKilled,也可能是删除超时被强杀 | 看 describe 的 Reason: OOMKilled 与 Limits 值 |
| 143 | 128 + 15 = SIGTERM | 优雅停机信号,滚动更新或被正常终止 | 确认 preStop 是否处理完,区别“正常”与“被误杀” |
| 1 | 应用自身异常退出 | 配置错误、端口被占用、依赖连不上 | 看日志首屏错误,通常是启动阶段失败 |
| 126 / 127 | 无法执行 / 命令不存在 | 镜像里没有该命令、无执行权限、脚本是 CRLF 换行 | 检查 entrypoint 与镜像内容 |
| 139 | SIGSEGV 段错误 | 本地库版本不匹配(CGO / JNI) | 保留 core dump,核对基础镜像 |
| 0 | 正常退出 | 主进程是前台运行的脚本,执行完就退出 | 容器主进程必须常驻,PID 1 不能提前退出 |
内存问题的本质:容器看不见限制
| 现象 | 原因 | 处置 |
|---|---|---|
| Java 容器被 OOMKilled 但堆没满 | JVM 未感知 cgroup 限制,按宿主机内存计算默认堆 | 设置 -XX:MaxRAMPercentage=70,或显式 -Xmx |
| Go 程序 RSS 远超预期 | GC 目标基于系统可用内存 | 设置 GOMEMLIMIT,并让 requests 接近 limits |
| Node.js 被 OOMKilled | 堆上限与容器 limit 无关 | 用 --max-old-space-size 按 limit 的 70% ~ 80% 设置 |
| 未设 limits 把节点拖垮 | 单 Pod 无上限,节点内存耗尽触发 Evicted | 所有工作负载必须设 limits,requests 与 limits 不要差太远 |
| requests 过小导致节点超卖 | 调度时按 requests 计算,实际用量远超 | requests 贴近真实用量,关键服务设为 Guaranteed |
| 侧车容器吃掉额度 | 整个 Pod 共享内存 limit | 把 limit 按所有容器之和来算 |
关键取舍:requests 决定调度与 QoS 等级,limits 决定被杀阈值。内存敏感的应用建议 requests == limits(Guaranteed),用一点资源浪费换稳定性;CPU 相反,limits 设得太死会造成 throttling,长尾延迟反而变差,通常只设 requests 或把 limits 放宽。
探针配置不当造成的“重启风暴”
initialDelaySeconds太短:应用要 60 秒启动,20 秒就开始探测 liveness,于是被反复杀掉重启。- 把依赖健康检查放进 liveness:下游一抖动,自己就被杀,故障从下游扩散到所有实例,规模反而变大。
- 正确分工:liveness 只判断“进程是否已经无法恢复”,readiness 才判断“能不能接流量”,慢启动应用另配
startupProbe。 - 阈值要满足
failureThreshold × periodSeconds大于应用的最大恢复时间,否则一次 GC 停顿就能触发重启。
startupProbe: # 慢启动应用必须先过这一关
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 5 # 最多给 150 秒启动时间
livenessProbe:
httpGet: { path: /livez, port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
failureThreshold: 2
摘流与保留现场的固定动作
# 1. 先让它不再接新流量:改掉标签使 Service 不再选中它,进程与现场都保留
kubectl -n prod label pod order-api-7d9c8f5b6-abcde serving=no --overwrite
# 2. 保存现场
kubectl -n prod describe pod order-api-7d9c8f5b6-abcde > /tmp/pod_describe.txt
kubectl -n prod logs order-api-7d9c8f5b6-abcde --previous --tail=500 > /tmp/pod_prev.log
kubectl -n prod get pod order-api-7d9c8f5b6-abcde -o yaml > /tmp/pod.yaml
# 3. 进容器看进程与真实的 cgroup 限制(无 cat 的精简镜像可改用 /proc/self/cgroup 反查)
kubectl -n prod exec -it order-api-7d9c8f5b6-abcde -- ps -o pid,rss,cmd
kubectl -n prod exec -it order-api-7d9c8f5b6-abcde -- cat /sys/fs/cgroup/memory.max
# 4. 证据齐全之后再重启或删除
kubectl -n prod delete pod order-api-7d9c8f5b6-abcde
cgroup v1 环境下的内存上限在 /sys/fs/cgroup/memory/memory.limit_in_bytes,v2 在 /sys/fs/cgroup/memory.max,读到的值应该与 resources.limits.memory 一致;不一致说明这个容器根本没拿到限制,OOM 迟早发生。
常见误判
| 误判 | 事实 | 正确做法 |
|---|---|---|
看到重启就 delete pod | 现场被清掉,根因无从查起,问题很快复现 | 先 describe 与 logs --previous 留证 |
| 只看当前容器日志 | 崩溃原因写在上一份日志里 | 必须加 --previous |
| 把依赖健康检查放进 liveness | 下游抖动会级联成自己全实例重启 | 依赖检查只放 readiness |
| 用 HPA 按 CPU 扩内存型故障 | 内存问题与 CPU 无关,扩容无效 | 调内存参数,或按内存指标扩容 |
| 探针超时设得过短 | 一次 GC 停顿就触发重启 | failureThreshold × periodSeconds 大于最大恢复时间 |
节点级问题
| 现象 | 自查命令 | 处置 |
|---|---|---|
| Pod 频繁 Evicted | kubectl describe node <node> 看 Conditions | 清理镜像与日志、扩容节点、给 Pod 设 limits |
| 节点 PID 打满(PIDPressure) | kubectl describe node 的 Allocated resources | 限制单节点 Pod 数量,排查僵尸进程 |
| 镜像拉取慢或失败 | kubectl describe pod 的 Failed to pull image 事件 | 用内网镜像仓库、节点预热镜像、配 imagePullSecrets |
| 磁盘 inode 耗尽 | df -i | 清理小文件与旧镜像,修正日志轮转 |
| 节点 NotReady | kubectl get nodes、节点上 systemctl status kubelet | 先排空(drain)再修节点,避免影响业务 |
处置与预防
- 处置顺序:先把 Pod 摘出流量(调 readiness 或缩容),再重启或迁移,避免重启期间请求继续打到坏实例;同时保留
describe输出与--previous日志作为现场。 - 节点维护用
kubectl drain优雅迁移,配合 PodDisruptionBudget 保证滚动更新时的最小可用副本数。 - HPA 按 CPU 扩容对内存型故障无效:内存型问题要么按自定义指标扩容,要么直接扩实例、调内存参数。
- 所有服务必须支持优雅停机:收到 SIGTERM → 从注册中心下线 → 等在途请求完成(preStop 加
terminationGracePeriodSeconds)→ 再退出。 - 告警覆盖:容器重启次数、OOMKilled 次数、探针失败次数、Pending 与 Evicted 的 Pod 数量。
小结:K8s 排查按 get pods -o wide → describe(先读 Events)→ logs --previous → exec 与 top pod → get events 的顺序推进;exit code 137 基本就是 OOMKilled,143 是优雅停机,1 是应用自己退出;内存类问题先解决“容器看不见限制”,探针类问题把 liveness、readiness、startup 的职责分清,再用 requests/limits、PDB 与优雅停机把稳定性补齐。