服务 OOM 排查指南:从现象到根因的系统分析与解决
作为开发者,相信大家最不愿意见到的场景之一就是:OOM(Out of Memory,直译便是超过最大内存)
这种问题通常事发突然,影响面广,而且排查起来往往像大海捞针。
本文将记录分享我在实际工作中遇到 OOM 是如何进行排查分析寻找问题的,希望能帮助你在遇到这类问题时,能够沉着应对,快速定位并解决问题。
一、OOM 的情况
OOM 发生时,最直接的表现就是:
- 服务进程消失:
ps或top命令看不到对应的进程了。 - 日志中断: 应用日志在某个时间点突然停止,没有任何异常堆栈或错误信息(这是最常见的情况)。
- 系统日志 (dmesg): 在 Linux 系统中,使用
dmesg | grep -i 'out of memory'通常能找到线索,内核会打印出哪个进程因为内存不足而被杀死。例如:[12345.678901] Out of memory: Kill process 1234 (my-service) score 456 or sacrifice child - 容器环境 (Docker/K8s): 容器会直接退出,状态变为
OOMKilled。在 Kubernetes 中,kubectl describe pod <pod-name>会显示Reason: OOMKilled - 监控告警: 如果有完善的监控系统(如 Prometheus + Grafana, Zabbix 等),会收到内存使用率 100% 或接近 100% 的告警,随后是服务可用性下降或端口监听消失的告警
二、OOM 问题排查方法
面对 OOM,切忌上来就漫无目的地修改代码,建议按照以下步骤排查寻找问题的根源:
(一)确认现场,收集信息
OOM 发生后,最重要的是第一时间收集尽可能多的信息,这对于后续分析至关重要。
-
检查系统日志(如果是容器部署,此步骤可忽略):
dmesg -T | grep -E 'oom|Out of memory': 这是首要步骤,确认是否是内核触发的 OOM Killer。记录下被杀死的进程 ID (PID)、时间、以及当时系统的内存状况。journalctl -u <your-service-name> -f: 查看系统服务管理器(如 systemd)记录的日志,可能会有一些线索。
-
检查应用日志:
直接部署应用:
- 仔细查看 OOM 发生前后的应用日志。虽然 OOM 本身不会产生堆栈,但在 OOM 发生前,应用可能已经出现了一些异常,如大量的
GC overhead limit exceeded(JVM)、数据库连接池耗尽、第三方服务超时等,这些都可能是内存暴增的诱因。 - 注意日志中是否有大量重复的错误、异常,或者处理大批次数据的记录。
容器部署应用:
-
k8s:
# 查看之前崩溃的 Pod 的日志 kubectl logs -p <pod_name> -n <namespace> # 查看 pod 重启的原因情况 kubectl describe pod <pod_name> -n <namespace> -
docker:
# 查看容器停止前的日志(包括所有历史日志): docker logs <container_id_or_name> # 查看容器停止前最后几行日志(例如,最后100行): docker logs --tail 100 <container_id_or_name>
- 仔细查看 OOM 发生前后的应用日志。虽然 OOM 本身不会产生堆栈,但在 OOM 发生前,应用可能已经出现了一些异常,如大量的
-
检查监控数据(如果有监控平台的话,比如):
-
内存使用率:观察 OOM 发生前内存使用率的变化趋势是突然飙升还是缓慢增长?
- 突然飙升: 通常与某个特定的用户请求、定时任务或外部事件(如大量数据导入、缓存失效)有关。
- 缓慢增长: 更可能是内存泄漏(Memory Leak),即程序在运行过程中分配的内存无法被回收,导致内存占用持续上升。
-
其他指标:CPU 使用率、磁盘 I/O、网络 I/O、线程数、活跃连接数等,这些指标异常也可能间接导致内存问题。
-
-
分析服务的情况:
-
Go 服务: Go 可以使用
pprof。通过go tool pprof http://<service-ip>:<pprof-port>/debug/pprof/heap可以交互式地分析内存使用情况,或者在服务退出前通过代码触发内存采样并保存。具体请移步:
-
(二)分析根因
根据以上的步骤可以分析具体产生 OOM 的原因,首先你要理解产生 OOM 的情况不一定是纯粹的"内存使用量过多",或者说有很多种情况都会导致内存使用量过多:
- 大量数据加载到内存
- CPU 使用率巨高不下
- 并发链接不释放也会导致内存飙高
那么针对这几种情况进行分析:
1. 代码问题 OOM
代码存储的内存量过大,比如,map、slice存了很多数据,没释放,最常见就是【数据列表查询】,一次性查询的量过多、未做分页查询
想一想:一次接口查询 10 M,并发多次请求,内存能不爆吗?
解决:可以通过【最后的日志】判断服务停止前在执行那些请求,找到代码,然后进行优化
2. 接口并发阻塞
一个请求耗时很长,导致rpc/tcp的链接池爆满,每个链接没释放,那内存也不会释放,多个链接就多个内存累加,直到内存oom
同样,可以通过【最后的日志】判断服务停止前在执行那些请求,找到代码,然后进行优化,分析为什么耗时长
可以使用单元测试、压力测试、pprof 对该接口进行分析,一般存在以下几种常见情况:
- 存在性能问题函数/方法(比如最常见的使用 + 拼接字符串,slice 没有提前分配容量)(可通过 pprof 分析)
- 协程泄露:是否开启协程但是没有关闭措施,导致协程出现空转(可通过 pprof 分析)
- 锁的粒度过大/死锁,导致接口并发耗时过长(可通过 pprof 分析)
- 数据库存在问题,比如慢查询 sql 拖垮了耗时,sql 返回数据量过大,数据库 CPU 飙高(可通过云数据库的监控进行分析)
3. 机器内存不足
怎么判断是机器内存不足还是服务的问题呢?这就需要看服务 OOM 的情况是否是内存突然飙升,突然飙高的情况一般是服务存在问题,如果是较为平稳的内存增长,那也许是业务所需(比如数据导入服务),用户量增加
如果是机器内存不足,那只能做临时扩容操作,后续再排查机器的内存使用量是否合理,再进行长期扩容
总结
Go 服务的 OOM 排查虽然有挑战,但借助 Go 强大的工具链(尤其是 pprof)和对 Go 内存模型、并发模型的深入理解,我们总能找到问题的根源。排查 OOM 的过程(针对服务问题):
- 收集 OOM 信息
- 排查问题代码所在(如代码问题、数据库问题等等…)
- 可借助一些工具进行排查解决(如 pprof、云监控、数据库监控、服务监控等等….)
- 确认问题所在,解决问题(具体性能调优可移步本站搜索相关的性能调优文章,包括 pprof 的使用指南)
OOM 排查只是提供一个个人的解决思路,具体因情况而异,希望本文能够对大家有所帮助!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-server-oom-guide/
备用原文链接: https://blog.fiveyoboy.com/articles/go-server-oom-guide/