一、前言¶
某个周四下午,线上反馈说某个微服务的接口时不时返回 502。看了一眼监控面板——Pod 在频繁重启,一个小时内重启了 8 次。登录 Kibana 一看日志,啥都没有,Pod 挂了日志还没来得及刷盘。
这种"时好时坏、没有日志"的故障最烦人。本文把这次排错的完整过程记录下来,从现象到根因,每一步做什么、为什么这么做,都写清楚。
二、现象¶
2.1 第一眼¶
1 | 查看 Pod 状态 |
两个副本,一个有 8 次重启,一个只有 2 次。健康的那个重启 2 次也是异常的(正常情况应该是 0)。
1 | kubectl describe pod api-service-7d4f8c6b9-2kx9j -n production |
重点看 Last State 部分:
1 | Containers: |
OOMKilled + Exit Code 137,明确的信号——进程被 cgroup OOM Killer 干掉了。
2.2 第一反应¶
看到 OOMKilled,本能反应就是"内存 limit 设小了"。查了一下 Deployment 的资源限制:
1 | resources: |
512Mi 的 limit。看了下正常运行的 Pod 的内存占用:
1 | kubectl top pod api-service-7d4f8c6b9-ht3ws -n production |
内存已经 487Mi,接近 512Mi 的 limit 了。这个副本还在运行但离上限只有 25Mi 的余量。另一个副本可能因为流量分布不均刚好触到了 OOM。
第一轮修复:把 memory limit 从 512Mi 加到 1024Mi,request 也提到 512Mi。等待观察。
三、加完 limit 没解决¶
3.1 仍然重启¶
加完资源半小时后,重启次数还在涨:
1 | kubectl get pods -n production -w |
新 Pod 上线后还是会挂,只是间隔时间拉长了一些。
3.2 看事件¶
1 | kubectl get events -n production --sort-by='.lastTimestamp' | tail -20 |
1 | 40m Warning BackOff pod/api-service-7d4f8c6b9-2kx9j Back-off restarting failed container |
Readiness probe 和 Liveness probe 都在报错。这说明容器虽然没被 OOM Kill,但已经无法正常响应请求了。等到探针连续失败几次,kubelet 就会重启容器。
3.3 查 Java 进程内部¶
这个服务是 Java 写的,看了一眼启动参数:
1 | # 容器启动命令 |
-Xmx512m 限制了 JVM 堆最大 512MB。但容器 limit 已经加到 1024Mi 了,这里只用了 512Mi,说明不是堆的问题。
问题在堆外内存(Off-Heap Memory):NIO Direct Buffer、Metaspace、线程栈、JNI 开销等等。Java 的 -Xmx 只管堆,堆外内存它不管。
1 | 进到 Pod 里看看 |
VmRSS 812MB,容器 limit 1024Mi,看起来还有余量,但为什么不稳定?
3.4 看 GC 日志¶
找运维把 GC 日志拉出来:
1 | 2024-04-18T14:23:11.123+0800: [GC (Allocation Failure) ... |
Full GC 几乎每秒触发一次,老年代几乎填满无法回收。这不是正常状态。
四、根因¶
4.1 抓线程 dump¶
1 | kubectl exec -it api-service-7d4f8c6b9-ht3ws -n production -- \ |
分析 dump 文件发现大量线程卡在同一个地方:
1 | "http-nio-8080-exec-172" #172 daemon prio=5 os_prio=0 tid=0x00007f... |
大量 HTTP 线程都在等数据库连接。HikariCP 连接池满了,所有请求都在排队。
4.2 查数据库侧¶
连接池配置:
1 | spring: |
20 个连接,30 秒超时。一旦某个慢查询把连接占住不放,后面的请求就全部阻塞。
翻数据库的 processlist:
1 | SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep' ORDER BY TIME DESC; |
发现一条 SQL 跑了 37 秒:
1 | SELECT * FROM order_detail WHERE order_no IN ('...', '...', ...) |
order_no 没有索引,IN 子句里传了 500 多个值,每次查询走全表扫描。这张表 200 万行,平均每次扫描耗时 20-30 秒。
4.3 完整链路¶
1 | 慢查询 (30s+) |
五、修复¶
5.1 紧急止血¶
临时加索引,先让慢查询停下来:
1 | ALTER TABLE order_detail ADD INDEX idx_order_no (order_no); |
加完索引后,那条 SQL 从 30 秒降到 2 毫秒。连接池不再被占满,Pod 重启立即停止。
5.2 后续改进¶
| 问题 | 修复 |
|---|---|
order_no 无索引 |
已加索引,并纳入 DDL Review 流程 |
| HikariCP 没有连接超时检测 | 加 max-lifetime: 600000,超时连接强制回收 |
| 慢查询无告警 | 开启 MySQL slow_query_log,超过 1 秒的 SQL 推送钉钉 |
| 容器内存余量不足 | 调大 limit 到 2048Mi,JVM -Xmx 保持 512Mi,留足堆外内存 |
| 监控盲区 | 接入 Pod 重启事件告警 + GC 耗时监控 |
5.3 一个有用的配置¶
HikariCP 加这个参数可以检测慢连接:
1 | spring: |
leak-detection-threshold 不会杀死连接,但会在日志里打印出栈信息——下次定位问题就不用 dump 线程了。
六、总结¶
| 排查步骤 | 用的命令/工具 | 发现了什么 |
|---|---|---|
| 看 Pod 状态 | kubectl get pods |
频繁重启 |
| 看容器退出原因 | kubectl describe pod |
OOMKilled, Exit 137 |
| 看内存占用 | kubectl top pod, /proc/status |
RSS 接近 limit |
| 看 GC 日志 | -XX:+PrintGCDetails |
连续 Full GC |
| 线程 dump | jstack |
卡在 HikariCP 拿连接 |
| 数据库 processlist | SHOW PROCESSLIST |
慢查询无索引 |
| 加索引 | ALTER TABLE ... ADD INDEX |
问题解决 |
回过头看,这次排错花了半天,但真正的根因其实很简单——一张表的字段没加索引。OOMKilled 只是表面现象,慢查询占满连接池导致线程堆积才是真凶。
这次之后我养成一个习惯:线上 Java 服务出 OOM,第一反应不是加内存 limit,而是先看线程 dump。内存不够往往是结果,不是原因。