容器与 K8s 排查:OOMKilled 与重启

K8s Pod 异常排查:一条命令链走到底

容器里的排查有两个额外难点:进程看到的“内存”不等于节点的内存,而重启往往连现场一起带走了。本章按 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信号与含义典型原因下一步
137128 + 9 = SIGKILL最常见是超过 memory limit 被 OOMKilled,也可能是删除超时被强杀describeReason: OOMKilled 与 Limits 值
143128 + 15 = SIGTERM优雅停机信号,滚动更新或被正常终止确认 preStop 是否处理完,区别“正常”与“被误杀”
1应用自身异常退出配置错误、端口被占用、依赖连不上看日志首屏错误,通常是启动阶段失败
126 / 127无法执行 / 命令不存在镜像里没有该命令、无执行权限、脚本是 CRLF 换行检查 entrypoint 与镜像内容
139SIGSEGV 段错误本地库版本不匹配(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现场被清掉,根因无从查起,问题很快复现describelogs --previous 留证
只看当前容器日志崩溃原因写在上一份日志里必须加 --previous
把依赖健康检查放进 liveness下游抖动会级联成自己全实例重启依赖检查只放 readiness
用 HPA 按 CPU 扩内存型故障内存问题与 CPU 无关,扩容无效调内存参数,或按内存指标扩容
探针超时设得过短一次 GC 停顿就触发重启failureThreshold × periodSeconds 大于最大恢复时间

节点级问题

现象自查命令处置
Pod 频繁 Evictedkubectl 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清理小文件与旧镜像,修正日志轮转
节点 NotReadykubectl 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 widedescribe(先读 Events)→ logs --previousexectop podget events 的顺序推进;exit code 137 基本就是 OOMKilled,143 是优雅停机,1 是应用自己退出;内存类问题先解决“容器看不见限制”,探针类问题把 liveness、readiness、startup 的职责分清,再用 requests/limits、PDB 与优雅停机把稳定性补齐。

笔记加载中…