Ruby ruby 内存持续增加,无法垃圾回收

tomanderson · August 11, 2021 · Last by heroyct replied at August 12, 2021 · 962 hits

我的每个 rails 项目,都发现有长期运行或内存逐渐增加、只增不减的问题。本着严于律己、宽于待人的原则,我一直以为是自己代码写得太烂,出现了内存泄露。

直到今天做了一个实验,我怀疑这个问题和 ruby 本身有关……

环境是 ruby3.0.1 + rails6.1.4。

实验 1:

100.times do
  long_str = "a" * 10_000_000
end

render json: 'OK'

代码简单至极,就是创建一个长字符串,运行 100 次只是为了效果更明显。

这段代码的效果是:每次运行大约增加 1g 内存,且不会释放。

按理说,render 之后整个请求已经结束,long_str 已经没有继续使用的可能,又不是什么实例变量、全局变量,ruby 应该把 long_str 垃圾回收,但是并没有。

我曾经以为 long_str 已经被回收,只是 ruby 占着内存的坑不想还给系统而已,除非系统内存不足才会还。但是我冒着死机的风险运行到系统内存全部吃光,ruby 进程的内存并没有分毫减少。

实验 2:

long_str = "a" * 10_000_000
long_str = nil

如果之前是因为 ruby 不知道 long_str 已经没用了所以没回收,那现在用 long_str = nil 告诉 ruby 总可以了吧?

结果:和实验 1 没有什么区别。

实验 3:

long_str = "a" * 10_000_000
long_str = nil
GC.start

现在每一轮循环结束后手动执行垃圾回收。确实有效果,ruby 不会内存增加 1g 了,但是每次会增长 10m,而且永远不会释放。

我就想知道,这个问题到底怎么解决,到底怎么写才不会“内存泄露?即使手动执行垃圾回收(在我看来这些代码是多余的),ruby 内存仍然会永久性增长?

代码已经简单到极点了,如果连这么简单的代码都有内存问题,那真正的项目中怎么可能不出现所谓“内存泄露”?

运行的 ruby 进程无法释放内存。 一般是 2 种解决方案。

  1. 对于需要每日运行,或者持续运行的脚本程序,使用 crontab 来管理,运行完就关闭。
  2. 对于 web 应用,使用 puma , passenger 等 web server, 这类 server 的特点是:有一个 spawn thread, 管理多个 worker thread, 每次 worker 运行完之后,就会 干掉自己,达到释放内存的目的。一般这样的 thread 会持续 3~5 分钟左右。

另外,就是能拆分的尽量拆分。例如,一个 web 应用中,找到最吃内存的那个功能,把它独立出来,也能好很多。

我见过一个 ruby 进程跑到 17G 内存的。用户量太大了。对于这样的 api, 要么修改逻辑,要么使用其他语言来实现,例如 rust ( 几十 MB 内存占用... )

所以,说到底,就看你的问题有多大,希望解决问题的预算有多少。

Reply to sg552sg552

我用的是 puma,但是没有发现 worker 会自动 kill 啊?有一个 gem puma_worker_killer 可以自动 kill,但是实测发现会影响请求所以没敢用。

https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html

ruby 的 GC 和 malloc 的 GC 是两层东西

先换成 jemalloc 试试。

再看看是不是哪个你项目不同于其他项目的常用的 gem 有问题。正常情况下内存增长是一个对数曲线

Reply to tomanderson

理论上,当一个 worker process 没有请求的时候,会自动被干掉。 如果你的没有出现这个情况的话,可能是:

  1. 设置 worker 数量的问题。是不是使用了固定的数量
  2. 是不是 客户端一起在请求。
Reply to sg552sg552

worker 是固定的数量,设置类似:workers ENV.fetch("WEB_CONCURRENCY") { 4 } 我不知道这个怎么能设置成动态的,请教一下

似乎可以设置成 0?试一下。也可以看看官方文档。

Reply to sg552sg552

设成 0,1 个 worker 都没有启动。。。官方文档也看了,好像没有你说的“worker process 没有请求的时候,会自动被干掉”

100.times do
  long_str = "a" * 10_000_000
  sleep 1
end

仅仅执行这个内存使用是很稳定的,我觉得如果有问题的话应该是 rails。还有就是之前用 development 来跑一个小项目,大概几天之后就会非常卡,production 没这个问题,当时没细究但是也有可能是这个环境差异

puma 的话试试 jemalloc

You need to Sign in before reply, if you don't have an account, please Sign up first.