Ruby Ruby 的好朋友 -- jemalloc

early · 2018年10月29日 · 最后由 charleszhang 回复于 2018年11月15日 · 3190 次阅读
本帖已被设为精华帖!

几个月前翻译了Malloc会加倍Ruby多线程应用的内存消耗,从这篇文章中得知了jemalloc,以及它在减少内存碎片方面的作用。直到最近在生产环境中看到了它真实的表现,更加惊叹其神奇。它是如何做到让Ruby进程减少了数倍的内存消耗? 对于这个点,得花点精力搞明白才行。

Ruby进程特别是在多线程环境下,其内存消耗令人震惊,生产中一个SIdekiq进程轻轻松松可以消耗3个G以上。Ruby垃圾回收这些年已经做了很多优化,变得相对成熟,但是其内存消耗为何如此高?

原因是有内存碎片。而且碎片同时存在于两个隔离的层级:

  • Ruby堆空间,也就是ObjectSpace
  • 动态内存分配器管理的堆空间

Ruby内存模型

ObjectSpace中以页为单位管理堆,每页中有固定数量的插槽,每个槽中可以放一个ruby对象(RVALUE)。随着应用程序的运行,GC会清除垃圾对象,同时也会申请新的页,慢慢地页中的槽就会有大量空闲,也就造成了内存碎片。

在Ruby中,ObjectSpace中放不下的数据(使RVALUE整体大于40bytes)都会通过动态内存分配器单独申请内存存放,只在RVALUE中保留对象的指针。而在多线程环境下,由于常用的Malloc实现上的缺陷,导致其在Arena中有严重的内存碎片,具体的可见上面的文章

使用jemalloc,其实就是让jemalloc来扮演动态内存分配器的角色,替换malloc的工作。由于jemalloc的优异表现,多线程Ruby应用的内存消耗骤降数倍,这说明了内存消耗上的锅,glibc malloc占大头,ObjectSpace中的内存碎片造成的消耗要远小于malloc所产生的。

那jemalloc为何如此优秀?,终于来到本文的主题,简单了解一下jemalloc的设计概要

  • 按照对象的大小隔离存放。 相同大小等级的对象放在一起,不同大小等级的对象分开。始终优先复用低地址的空间,这是降低内存碎片关键。
  • 谨慎划分对象的等级 。如果等级间差距过大,档次过少。容易增加对象尾部不可用空间的数量,会造成内部碎片。如果档次过多,可以预见专用于对象各等级的内存消耗会增加,这会造成外部碎片。
  • 缩紧元数据的内存消耗 。jemalloc限制元数据内存消耗比例,不超过总内存的消耗的2%。
  • 最小化活跃数据页的数量。操作系统按照页的方式管理虚拟内存,将数据集中到尽可能少的页上有重要意义。
  • 最小化锁竞争。jemalloc实现了多个独立的arenas,同时还实现了各线程独立的缓存,使得内存申请/释放过程可以无干扰并行进行。

jemalloc将对象等级按大小分为三大级别,每个级别中有数十个小等级:

  • Small: [8], [16, 32, 48, ..., 128], [192, 256, 320, ..., 512], [768, 1024, 1280, ..., 3840]
  • Large: [4 KiB, 8 KiB, 12 KiB, ..., 4072 KiB]
  • Huge: [4 MiB, 8 MiB, 12 MiB, ...]

小于8字节的,分配8字节;大于4KB小于8KB的,分配8KB···

像这样根据对象的大小进行严格分级,并将同一级别的对象存放在一起,不同级别的对象分开存放。通过紧凑的组织进行分级存放。 jemalloc from facebook

上图是jemalloc的核心结构图。可以看到有以下几个部分:

  • 图左上角,有多个arenas。arenas是管理内存的最大单位,每个arena彼此独立,数量一般为CPU核心数的四倍。
  • 每个arena包含多个chunk,每个chunk默认为4M(和环境有关),这也是jemalloc每次从操作系统申请内存的单位。实际的数据都在chunk内。
  • chunk又进一步被分为多个run(图左下角),run对应到具体的小单位内存等级,其大小为页(4KB)的整数倍。同等级的run由相应的bin来管理,每个bin负责管理一个对象等级,当应用要申请某个等级的内存时,就到对应的bin中获取(bin通过栈和树来管理run)。
  • 小对象中,run会被进一步分成整数个大小相等的region,region是最小的内存单位,具体大小和其等级保持一致。每一个region就是实际分配给应用的内存单位。例如,如果应用申请8字节的内存,jemalloc返回给应用的就是一个对应大小的region。 来自 http://tinylab.org

通过将内存分成多个档次,且大小以整数倍增长,各自分别管理。同时优先复用低地址的可用区域,使得内存内存块被高效利用,不会被遗忘,同时避免了某些内存区尾部不够大(要申请32字节,却只剩8字节了),而不能被使用的情况,这在很大程度上减少了碎片。(glibc malloc就是将对象混在一起存放的)

为了提高并发能力,jemalloc实现了多级缓冲机制,为每个线程实现了独立的tcache(图右上)。 线程在申请内存时,先到自己的tcache缓冲中去取,当缓冲为空或者满了,才去arena进货或将内存刷回(此时会有锁,粒度为每个小档)。这种设计可以极大地避开并发中的竞争,减少锁的产生。同时,可以让同一线程中的数据尽量在同一块相邻的内存中,这对于cpu高效的利用cache line有极大帮助。

同时,jemalloc用自己特地改进过的左倾红黑树(红节点在左边),作为元数据来追踪可用的内存区,提高搜索速度。

jemalloc会限制被使用页(dirty page)的数量,当数量超过一定比例,就会启动GC对数据页进行清洗(purge),并尝试进行合并,挤出未被使用的空间,将其释放回操作系统或返回可用区域,进一步减少了碎片。

以上。

本文内容参考了以下资料,想要准确详细的细节,请进一步查阅:

共收到 21 条回复
apt-get update
apt-get install libjemalloc-dev
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 2.5.3

嗯 我就是来贴个 Code

huacnlee 将本帖设为了精华贴 10月30日 13:38

@so_zengtao的风,来个完整的

Ubuntu MAC centos 安装jemalloc

sudo apt-get install libjemalloc-dev
brew install jemalloc
sudo yum install -y jemalloc jemalloc-devel

rbenv

RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 2.5.0

rvm

rvm reinstall 2.5.0 --disable-binary --with-jemalloc

源码安装

./configure --with-jemalloc
make
make install

检查安装是否正确

ruby -r rbconfig -e "puts RbConfig::CONFIG['LIBS']"
# 应该输出:  -lpthread -ljemalloc -ldl -lobjc

ruby-install

ruby-install ruby 2.5.3 -- --with-jemalloc

使用会带来啥风险呢?

jicheng1014 回复

已经在facebook上正常运行多年了,获得了广泛的认可。 这儿有个讨论帖 https://ruby-china.org/topics/35515

拿空间换时间,大量的内存整理,必然带来性能的下降。。

early 回复

不科学啊,,,,

pynix 回复

它没做内存整理,只是防止碎片

https://bugs.ruby-lang.org/issues/9113#note-12

sam saffron说libc的allocator很垃圾,默认就不应该用这个,然后后面一堆人各种反对。

pynix 回复

清洗是对于脏页(dirty page)进行的回收、合并等操作,是增加可用内存和减少碎片的操作,并不会有大量的内存移动之类的操作。 具体的过程有点复杂,可以参考下附录中的这篇文章

adamshen 回复

https://bugs.ruby-lang.org/issues/14718 在这个里由后续。结论是malloc的问题是在glibc在某个版本开始某个默认参数发生了变化,如果调回去就跟jemalloc差不多了。而jemalloc不同版本表现也不尽相同,无非是时间和空间的取舍。最后大家一致认为最佳解决方案是调整malloc的那个参数,因为对于ruby的场景更为适合。

Xenofex 回复

等等...... 为什么这个跟我看的解释完全不一样......

glibc malloc 的问题不是从某个版本开始而是一直也是这样. 更改参数可以有大幅改善.

jemalloc 的问题是源于 THP ( Transparent Huge Pages ), Ruby 2.6 已经停止使用. ( https://bugs.ruby-lang.org/issues/14705 ),

ksec 回复

我看到的解释和你接近。 对于jemalloc持谨慎态度的人一般是两个原因:

对于glibc malloc,它为了减少多线程环境下,申请内存所产生的锁竞争,将可分配的arena数量放的比较大,这导致了严重的内存碎片,也是因为它本身不是为多线程环境而设计的。 将MALLOC_ARENA_MAX调小,可以减轻这种情况,但是会使得线程间的竞争变得严重,内存碎片是降低了,但是性能会受影响。

ksec 回复

我这个帖子里提到了M_ARENA_MAX默认值的变化(从2到 2 * CPU核数),有人怀疑是red hat为了讨好大用户(通常拥有足够的配置)所以用空间换性能。最后页面下方sidekiq作者和Sam(一开始jemalloc)的倡导者也都达成了一致,认为malloc和jemalloc的区别的原因是他们默认参数的不同。换句话说malloc如果设置了M_ARENA_MAX = 2也可以达到jemalloc的评测性能。因此两者并没有所谓的性能上的明显区别。

持谨慎态度的人并不仅仅是认为jemalloc会带来问题,而是如果不能证明jemalloc确实更适合ruby,那么就没必要作为默认选项(本帖的讨论内容)。毕竟这不是一个小的改动。

@early

Our Sidekiq servers use Ubuntu 16.04, so we started by installing jemalloc: From there, we configured the LD_PRELOAD environment variable by adding the following to

Note: The location of jemalloc may vary depending on version and/or Linux distribution.

sudo apt-get install libjemalloc-dev
vim /etc/environment
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1  # add this

原文地址

和再重新编译使用有差异?

awking 回复

这是用动态配置的方式让应用使用jemalloc吧,不想用时可以动态修改环境变量来控制。重新编译就没这个灵活性了。https://github.com/jemalloc/jemalloc/wiki/Getting-Started

so_zengtao 回复

在mac上遇到了如下错误,请问是什么原因?

Undefined symbols for architecture x86_64:
  "_je_calloc", referenced from:
      _rb_objspace_alloc in gc.o
      _ruby_xcalloc in gc.o
      _heap_assign_page in gc.o
      _Init_Method in vm.o
      _ruby_init_setproctitle in setproctitle.o
  "_je_free", referenced from:
      _ruby_glob0 in dir.o
      _ruby_brace_expand in dir.o
      _glob_helper in dir.o
      _dln_find_exe_r in dln_find.o
      _rb_objspace_free in gc.o
      _ruby_xfree in gc.o
      _free_const_entry_i in gc.o
      ...
  "_je_malloc", referenced from:
      _ruby_glob0 in dir.o
      _ruby_brace_expand in dir.o
      _glob_helper in dir.o
      _Init_heap in gc.o
      _gc_writebarrier_incremental in gc.o
      _rb_gc_writebarrier_remember in gc.o
      _gc_set_initial_pages in gc.o
      ...
  "_je_malloc_conf", referenced from:
      _ruby_show_version in version.o
  "_je_malloc_usable_size", referenced from:
      _rb_objspace_free in gc.o
      _ruby_xfree in gc.o
      _ruby_xcalloc in gc.o
      _free_const_entry_i in gc.o
      _rb_gc_call_finalizer_at_exit in gc.o
      _rb_gc_unregister_address in gc.o
      _objspace_xmalloc0 in gc.o
      ...
  "_je_posix_memalign", referenced from:
      _heap_assign_page in gc.o
  "_je_realloc", referenced from:
      _replace_real_basename in dir.o
      _rb_enc_register in encoding.o
      _rb_encdb_declare in encoding.o
      _rb_enc_replicate in encoding.o
      _rb_encdb_replicate in encoding.o
      _rb_encdb_dummy in encoding.o
      _rb_enc_init in encoding.o
      ...
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [miniruby] Error 1
charleszhang 回复

Mac 版本是 ? Clang 版本是 ? 用的 C++ 标准库版本是 ?

但没有尝试 jemalloc ,前几天试了 Sidekiq 作者说的设置 MALLOC_ARENA_MAX=2 ,内存上涨是慢下来了,但性能受影响非常明显。因为现在的执行效率其实完全可以接受,暂时也就没再折腾了。

blacklee 回复

找到原因了,自己编译的jemalloc有问题😂

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