首页 > 解决方案 > Sidekiq 在 Rails 应用程序中导致内存膨胀

问题描述

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

标签: ruby-on-railssidekiq

解决方案


从一个快速的谷歌听起来你正在经历内存碎片,这对于 Sidekiq 来说是很正常的。你在使用类变量吗?您的代码在执行期间是否需要类?您正在执行多少个 AR 查询?许多 AR 查询会创建数千个甚至数百万个对象并将它们丢弃。你的代码是线程安全的吗?根据Sidekiq 作者的这篇文章,我们可以看到内存膨胀发生在多线程应用程序中的大量内存领域。该文章中有一些解决方案的详细信息,甚至 Sidekiq 存储库的自述文件都非常有帮助,但可能值得概述原因以了解为什么在“rails/ruby”中会发生内存膨胀。

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

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

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

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

就个人而言,我不会仅仅依靠GC.compact你可以做这个简单的MALLOC_ARENA_MAX技巧,而是阅读源文档,你应该找到一个合适的解决方案。


推荐阅读