记录一次不太寻常的 OOM 事件

作为一名 go 后端开发,除了业务逻辑 bug,基本上就只剩 panic 和内存泄露(包括协程泄露)问题了。其中内存泄露可以说是所有后端开发都可能遇到的问题,这也是发生 OOM 的主要原因。

所以一般情况下,排查 OOM 的切入点就是内存泄露。在 Go 中,主要就是查协程泄露,其他还有切片引用大数组未释放等情况。得益于 Go 自带的 pprof 工具,倒也不用瞎猜,看下正在运行的协程和活跃对象基本就能知道原因了。

不过,事情如果都是这么简单,也就不会有这篇文章了。在我遇到的一次 OOM 中,我发现并没有发生协程泄露,活跃对象占用空间也远低于观察到到的内存占用,这就比较奇怪了。

当时这个问题是折腾好久,一直没彻底解决问题,后面看到一篇文章才怀疑到 GC 头上。
文章地址:GOMEMLIMIT is a game changer for high-memory applications | Weaviate - Vector Database

这里简单放个图片:

这个图片意思是,如果堆上有 4GB 长期生存的对象,并且限制 6GB 内存,那么就可能发生 OOM。

这是因为 Go GC 触发时机是内存增长一倍的时候。大部分情况下这样是合理的,GC 的频率并不是越快越好,太快 GC 会增加 CPU 占用和毛刺。但是总有情况是例外的,下面就说说我是这么踩到这个坑的。

案发过程

我出问题的那个程序是处理多媒体的,这其中就包括视频。大概逻辑是根据 url 获取视频数据,然后做编码发送给 后面的服务,请求格式是 json,也就需要做 json 序列化。

因为这些都是在内存中操作的,为了避免频繁申请内存,所以用了一个池来存放视频数据。

这就满足了上图中的两个条件,一是内存池相当于占用了大量空间的长期存活对象(图中绿色部分),二是编码和序列化操作会产生很多短期存活对象(图中黄色部分),加上不是非常充裕的内存限制,所以产生了 OOM。

本质上来说,是 Go 为了降低 GC 消耗而损失了一定的吞吐率。

在使用了 GOMEMLIMIT 限制后,可以观察到内存在增长到限制值后趋于平稳,同时CPU占用变高。这是符合预期的。

总结

相信很多 Go 开发都没调过 GC 参数,并且也没啥问题。我觉得这也是 Go 的优点之一。但也不能迷信 Go GC 能默默的摆平一切问题。

还有吐槽一下,Go 简单是真的,Go 团队懒也是真的。虽然应该少让开发人员调 GC 参数,但有句话说,我可以不用,但是你不能不给啊。在 Go 增加 GOMEMLIMIT 的很长一段时间里,只能调比例而不是根据硬件调绝对值。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 2

这种记录,分析定位的过程值得写详细一些,这里一笔带过了

2个月前 评论
symphony09 (楼主) 2个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!