目录

服务 OOM 排查指南:从现象到根因的系统分析与解决

作为开发者,相信大家最不愿意见到的场景之一就是:OOM(Out of Memory,直译便是超过最大内存)

这种问题通常事发突然,影响面广,而且排查起来往往像大海捞针。

本文将记录分享我在实际工作中遇到 OOM 是如何进行排查分析寻找问题的,希望能帮助你在遇到这类问题时,能够沉着应对,快速定位并解决问题。

一、OOM 的情况

OOM 发生时,最直接的表现就是:

  • 服务进程消失: pstop 命令看不到对应的进程了。
  • 日志中断: 应用日志在某个时间点突然停止,没有任何异常堆栈或错误信息(这是最常见的情况)。
  • 系统日志 (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 发生后,最重要的是第一时间收集尽可能多的信息,这对于后续分析至关重要。

  1. 检查系统日志(如果是容器部署,此步骤可忽略):

    • dmesg -T | grep -E 'oom|Out of memory': 这是首要步骤,确认是否是内核触发的 OOM Killer。记录下被杀死的进程 ID (PID)、时间、以及当时系统的内存状况。
    • journalctl -u <your-service-name> -f: 查看系统服务管理器(如 systemd)记录的日志,可能会有一些线索。
  2. 检查应用日志:

    直接部署应用:

    • 仔细查看 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>
  3. 检查监控数据(如果有监控平台的话,比如):

    • 内存使用率:观察 OOM 发生前内存使用率的变化趋势是突然飙升还是缓慢增长

      • 突然飙升: 通常与某个特定的用户请求、定时任务或外部事件(如大量数据导入、缓存失效)有关。
      • 缓慢增长: 更可能是内存泄漏(Memory Leak),即程序在运行过程中分配的内存无法被回收,导致内存占用持续上升。
    • 其他指标:CPU 使用率、磁盘 I/O、网络 I/O、线程数、活跃连接数等,这些指标异常也可能间接导致内存问题。

  4. 分析服务的情况:

(二)分析根因

根据以上的步骤可以分析具体产生 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/