Sidekiq 导致 rails 应用内存膨胀

Sidekiq causing memory bloat in rails app

我有一个 rails 应用程序,sidekiq 工作人员在后台执行进程,最初有大约 30 个线程来执行任务。我们发现这会导致内存使用率过高,减少工作人员的线程数会减少内存膨胀,但我不明白为什么。谁能解释一下?

从快速 google 来看,您似乎遇到了内存碎片,这对于 Sidekiq 来说是很正常的。您是否使用 class 变量?您的代码在执行期间是否需要 classes?您正在执行多少个 AR 查询?许多 AR 查询会创建数千(如果不是数百万)对象并将它们丢弃。 你的代码 thread-safe? 根据 Sidekiq 作者的 this post ,我们可以看到内存膨胀发生在多线程应用程序中的大量内存区域。那篇文章甚至 Sidekiq 存储库的自述文件中有一些解决方案的细节非常有用,但可能值得概述因果关系以了解为什么内存膨胀发生在 'rails/ruby'.

Ruby 中的内存分配涉及三层:解释器、OS 内存分配器库和内核。 Ruby 在称为 Ruby heap pages 的内存区域中组织对象,并且 ruby 堆页面被划分为 equal-sized 个槽,其中一个对象占用一个槽。这些槽要么被占用要么空闲,当 Ruby 分配一个新对象时,它会尝试占用一个空闲槽。如果没有空闲槽,它将分配一个新的堆页面。每个插槽都有一个字节限制,如果一个对象高于字节限制,就会在堆页面中放置一个指向该对象的指针。

内存碎片是在这些分配发生时发生的,并且在高线程应用程序中非常频繁。当垃圾收集发生时,堆页面将已清除的插槽标记为空闲并允许重新使用该插槽。如果堆页面中的所有对象都被标记为空闲,那么堆页面将被释放回内存分配器并可能返回内核。 Ruby 不承诺对所有对象进行垃圾回收,那么当不是所有空闲槽都被释放并且有大量堆页面被部分填充时会发生什么?堆页面有可供 Ruby 分配的可用插槽,但内存分配器仍然认为它们已分配内存。内存分配器不会立即释放整个 OS 堆,并且可以释放任何单个 OS 页面,只要为该页面释放所有分配。

因此线程会产生一个问题,因为每个线程都试图同时从同一个 OS 堆分配内存,并且它们争用访问权限。一次只能有一个线程执行分配,这降低了多线程的内存分配性能。内存分配器试图通过创建多个 OS 堆来优化性能,并尝试将不同的线程分配给它自己的 OS 堆。

如果你可以访问 ruby 2.7,你可以调用 GC.compact 来解决这个问题。它提供了一种方法来查找可以在 Ruby 中移动的对象并压缩它们并减少使用的堆页面数量。通过 GC in-between 消耗的插槽释放的空插槽现在可以压缩。比如说,你有一个有四个槽的堆页面,只有槽一、二和四分配了一个对象。紧凑调用将评估对象 4 是否为可移动对象,并将其分配给槽 3 以及与该对象关联的任何引用,并重定向到槽 3。插槽四现在放置了一个 T_MOVED 对象,最终 GC 将 T_MOVED 对象替换为 T_EMPTY,准备分配。

就个人而言,我不会完全依赖 GC.compact,您可以使用简单的 MALLOC_ARENA_MAX 技巧,但阅读源文档后您应该会找到合适的解决方案。