文章

OMM

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

一、OOM 典型现象

OOM(Out of Memory)即内存溢出,指服务进程所需内存超过系统或容器分配的最大限制,被内核的 OOM Killer 强制终止,或因内存耗尽主动崩溃。

  • 服务进程消失: 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

  • 监控告警: 如果有完善的监控系统(如 Prometheus + Grafana, Zabbix 等),会收到内存使用率 100% 或接近 100% 的告警,随后是服务可用性下降或端口监听消失的告警

图示

image-20260131205726325

二、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:

      1
      2
      3
      4
      5
      
      # 查看之前崩溃的 Pod 的日志
      kubectl logs -p <pod_name> -n <namespace>
           
      # 查看 pod 重启的原因情况
      kubectl describe pod <pod_name> -n <namespace>
      
    • docker:

      1
      2
      3
      4
      5
      
      # 查看容器停止前的日志(包括所有历史日志):
      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. 分析服务的情况:

    • Go 服务: Go 可以使用 pprof。通过 go tool pprof http://<service-ip>:<pprof-port>/debug/pprof/heap 可以交互式地分析内存使用情况,或者在服务退出前通过代码触发内存采样并保存。

(二)分析根因

产生 OOM 的情况不一定是纯粹的”内存使用量过多”,或者说有很多种情况都会导致内存使用量过多

  • 大量数据加载到内存(临时变量)
  • CPU 使用率巨高不下
  • 并发链接不释放也会导致内存飙高

1. 代码问题 OOM

代码存储的内存量过大,比如,map、slice存了很多数据,没释放,最常见就是【数据列表查询】,一次性查询的量过多、未做分页查询

1
2
3
4
5
6
7
8
9
10
11
// 典型的内存问题代码示例
func getUserList() {
    // ❌ 一次性查询全表,数据量巨大
    users := db.Query("SELECT * FROM users") // 假设返回 100万条数据 = 几十MB
    
    // 或者大 map/slice 一直追加,从不清理
    var cache []User
    for {
        cache = append(cache, newData...) // 无限增长,永不释放
    }
}

解决:可以通过【最后的日志】判断服务停止前在执行那些请求,找到代码,然后进行优化

2. 接口并发阻塞

一个请求耗时很长,导致rpc/tcp的链接池爆满,每个链接没释放,那内存也不会释放,多个链接就多个内存累加,直到内存oom

可以通过【最后的日志】判断服务停止前在执行那些请求,找到代码,然后进行优化,分析为什么耗时长

可以使用单元测试、压力测试、pprof 对该接口进行分析,一般存在以下几种常见情况:

  • 存在性能问题函数/方法(比如最常见的使用 + 拼接字符串,slice 没有提前分配容量)(可通过 pprof 分析)
  • 协程泄露:是否开启协程但是没有关闭措施,导致协程出现空转(可通过 pprof 分析)
  • 锁的粒度过大/死锁,导致接口并发耗时过长(可通过 pprof 分析)
  • 数据库存在问题,比如慢查询 sql 拖垮了耗时,sql 返回数据量过大,数据库 CPU 飙高(可通过云数据库的监控进行分析)

img

3. 机器内存不足

资源配额不足:

  • 物理机 / 虚拟机:系统总内存不足,服务内存配置超过实际可用资源;
  • 容器环境:Docker/K8s 资源限制过低(如 K8s Pod 配置 resources.limits.memory=512Mi,但服务实际需要 1Gi);
  • 第三方依赖占用:其他进程 / 容器占用大量内存,导致服务可用内存不足。

定位方法:

  • 物理机执行 free -h top 查看总内存和其他进程占用;
  • K8s 执行 kubectl describe pod 名称 查看资源限制和实际使用情况;
  • 对比服务正常运行和 OOM 时的资源占用差异,判断配额是否合理。

判断是机器内存不足还是服务的问题呢?

  • 如果内存突然飙升,突然飙高的情况一般是服务存在问题
  • 如果是较为平稳的内存增长,那也许是业务所需(比如数据导入服务),用户量增加。

常见问题

Q1. Go 服务 OOM 无堆栈日志,如何快速定位?

优先通过 dmesg 确认 OOM 触发,再结合 pprof 内存快照(提前集成 pprof)和监控趋势:若内存缓慢增长,重点查 goroutine 泄漏和全局容器;

若突然飙升,重点查大任务、突发流量和缓存失效。

Q2. 生产环境 Go 服务如何安全使用 pprof?

  1. 限制 pprof 端口的访问 IP(如通过防火墙只允许内网访问);
  2. 使用快照导出(curl 下载 heap.pprof)替代实时分析,减少性能影响;
  3. 避开业务高峰时段采集数据。

Q3. Docker/K8s 中 OOM,如何判断是资源不足还是服务泄漏?

  1. 查看 Pod 内存使用率趋势:若长期接近 limits.memory 且缓慢增长,是泄漏;
  2. 临时调高 limits.memory 后,若内存仍持续增长,是泄漏;
  3. 若调高后稳定,是资源配额不足。

Q4. Go 服务中,大切片 / Map 如何避免内存占用过高?

  1. 分页处理大数据(如数据库查询 LIMIT/OFFSET);
  2. 使用 sync.Pool 缓存临时大对象,避免频繁分配;
  3. 大对象使用后手动置为 nil,帮助 GC 回收。

Q5. goroutine 泄漏除了通道阻塞,还有哪些常见场景?

  1. 无退出条件的 for 循环(如缺少 break 或退出信号);
  2. sync.WaitGroup 未正确调用 Done(),导致 Wait() 阻塞;
  3. 第三方库接口调用阻塞(如无超时的 HTTP 请求)。

Golang 内存泄漏深度分析与实战:从检测到修复的完整指南 - 五岁博客

Go 避免协程 Goroutine 泄露的措施 - 五岁博客

Go 踩过的坑之协程参数不能过大 - 五岁博客

Go 内存分配逃逸分析指南 - 五岁博客

Go 性能调优全攻略:从分析到优化 - 五岁博客

[pprof 性能分析Go 语言高性能编程极客兔兔](https://geektutu.com/post/hpg-pprof.html#3-内存性能分析)

© 2024- lfj