Rails 关于 RoR 内存问题的讨论

Terry.Shi · 2018年03月14日 · 最后由 summer_wind 回复于 2022年06月20日 · 4202 次阅读

背景情况

看到 https://ruby-china.org/topics/35236
想到我们也遇到了这样的问题。感觉可以拿出来讨论一下。
下图是一个 rails 项目服务器 14 天的内存增长情况

8 核 16G 阿里云服务器 puma 3.10.0 rails 5.05 可以看到在没有部署或者重启的操作下,呈现内存持续增长。
内存持续增长对系统的危害:在内存接近满值的时候会触发操作系统 out of memory 机制,可能导致关键进程被系统杀死 (redis postgresql sidekiq 等) 造成系统异常,此外也会让程序花费大量的时间在 GC 上从而导致程序响应速度降低。

关于 ruby 的内存机制

  • Ruby 有自己的内存管理机制,叫做 Ruby Heaps。他独立于操作系统的 System Heap,包含很多 Slots,其中每一个 Slots 指向一个对象。Slots 本身存储于 Ruby Heaps 中,但其指向的对象存储于 System Heap 中。例如一个 Ruby 程序创建了一个 50M 的字符串,这时 Ruby Heaps 中就有一个指向该字符串的 Slot,而真正的字符串存储于 System Heap 中。当这一字符串不再被引用时,Slot 会在下一次内存清理(GC iteration )中被回收,同时存储 50M 字符串的内存也会返回给操作系统。Ruby 会在最开始创建一个最小的 Ruby Heap,此后再必要的时候进行 Ruby Heaps 的创建或销毁。 每次创建 Ruby Heap 会是上一个的 1.8 倍(由 RUBY_GC_HEAP_GROWTH_FACTOR 控制,这个环境变量值设置越低,意味着我们越要频繁的运行 GC 和请求分配内存。该数值越大,意味着更少的 GC,以及超过我们程序运行所需要的内存。),而当一个 Ruby Heap 中的 Slot 均为空(free Slot)时,该 Ruby Heap 会被释放(内存被操作系统回收)。即 ruby 是通过多个 heaps 来管理内存的,每个 heap 有许多 slot 用来存放对象,只有当一个 heap 的 slot 全部是 free 的时候 ruby 才会把这个 heap 释放掉,把内存交还 os.

  • Ruby 中的常量是永远不会被垃圾回收的,所以如果常量引用了一个对象,那么这个对象也永远不会被垃圾回收,是由于将来 可能 会使用它们。在 Ruby 中一个对象一旦被全局对象引用,它就不会被垃圾回收。 这一原则也适用于常量,全局变量,模块 (modules) 和类 (class)。因此,在全局可访问的任何地方引用对象都要注意这一点。

  • ruby 会在内存不够时根据 RUBY_GC_HEAP_GROWTH_FACTOR 申请内存,因为分配内存的操作开销很大,Ruby 会把这些分配的内存保持住一段时间。一旦进程将这些内存用尽,那么就再次申请内存。内存会逐渐释放,这一过程很慢。

综合上述几点:ruby 的程序会释放内存给操作系统,但是如果一个 heap 里存在不会被垃圾回收的对象,那这个 heap 的空间就不会释放给操作系统,同时内存释放很慢,容易发生内存持续上涨的情况。

处理方向

首先明确的是内存占用的多少跟并发量是有直接关系的。但是排除这个因素,对接口内存占用分析能发现,不合理的代码会让内存上升的速度大大加快,整个趋势就是一个持续上涨的趋势。

  • 添加 puma_worker_killer。这个是最简单有效但是不治本的方法。
  • 优化代码。不合理的代码会对内存造成极大的负担,如 N+1 的查询,不加 limit 的查询,返回大量无用字段等。

定位问题

这里主要用了三个工具(当然不止这些 newrelic 等也都可以)

  • oink
  • memory_profiler
  • scoutapp 前两个是内存检测的 gem,最后一个是一个三方服务(缺点就是贵,我们用的试用版。。。)
    能检测到 n+1 查询
    以及内存占用大的接口

    看哪些接口创建了大量对象

优化方式

  • 减少全局对象
  • 减少创建的对象
  • 数据库查询尽量使用 limit 限制查询结果条数
  • 避免 n+1 查询
  • 冻结一个字符串,解释器会认为你不会修改该字符串,并保留它以便重复使用。(在 Ruby 3 中,字符串字面量在所有文件中默认被冻结。)
    其他更多优化可以看下面这篇博客
    http://blog.csdn.net/tianxingjian111222/article/details/53906140

这些权当做抛砖引玉,主要想听听各位大佬的看法。比如 ruby 内存机制上有哪些理解不到位,比如还有什么样的写法会占用大量内存,比如还有哪些能够优化代码的方式,比如 ruby3 在内存优化上会带来哪些惊喜。

其他关于 ruby 内存的讨论贴

引入一些相关的讨论,Sidekiq 的作者的分析:

https://github.com/puma/puma/issues/1047#issuecomment-257904428

项目中遇到 sidekiq 占用内存持续增长,高的时候达到 2G,最后解决方法是切换成了 jemalloc,有同样问题的同学可以试试看 https://engineering.binti.com/jemalloc-with-ruby-and-docker/

需要 登录 后方可回复, 如果你还没有账号请 注册新账号