The ultimate jemalloc guide

Одна из основных performance issue с которой сталкивается типичное Rails приложение на определённом этапе развития — это нехватка памяти. Первое и самое «дешевое», с точки затраченного времени на решение, это подключить альтернативный аллокатор памяти jemalloc. Он заменяет стандартную реализацию malloc из С более эффективной реализацией лучше подходящей для веб-приложений.
Как установить и использовать?

Обычный сервер или локальная машина

Необходимо установить пакет libjemalloc-dev.
При установке Ruby через RVM необходимо указать флаг -C —with-jemalloc.
Если через rbenv то поставить флаг в переменную окружения RUBY_CONFIGURE_OPTS.

# На Ubuntu
sudo apt-get update 
sudo apt-get install libjemalloc-dev

# На Mac
brew install jemalloc

rvm install 2.6.4 -C --with-jemalloc

# or 
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 2.6.4

Как проверить что Ruby использует jemalloc? В интернете везде написано смотрите значения в RbConfig::CONFIG[‘LIBS’] но у меня там было упорно пусто, а ведь я раньше это уже всё ставил и работало. Оказалось что с Ruby 2.6 нужно смотреть RbConfig::CONFIG[‘MAINLIBS’]. Итого

# Ruby < 2.6
ruby -r rbconfig -e "puts RbConfig::CONFIG['LIBS']"

# Ruby >= 2.6
ruby -r rbconfig -e "puts RbConfig::CONFIG['MAINLIBS']"

В выводе этой команды должен быть ключ -ljemalloc значит всё ок и Ruby использует наш аллокатор.

Docker

FROM ruby:2.6

RUN apt-get update &amp;&amp; \
  apt-get install libjemalloc1 &amp;&amp; \
  rm -rf /var/lib/apt/lists/*

ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1

В принципе такой метод можно использовать и локально или на сервере, но если LD_PRELOAD установить глобально то все процессы будут использовать jemalloc неявно, а мы этого возможно не хотим. Так как внутри докера мы запускаем только один процесс, то это норм. В случае использования переменной окружения LD_PRELOAD мы не увидим флага -ljemalloc в RbConfig::CONFIG[‘MAINLIBS’] потому что в данном случае jemalloc не вкомпилирован внутрь, но при этом стандартная функция malloc всё же будет заменена. В этом можно убедится выполнив команду

$ MALLOC_CONF=stats_print:true ruby -e "exit"

___ Begin jemalloc statistics ___
Version: "5.1.0-0-g61efbda7098de6fe64c362d309824864308c36d4"
Build-time option settings
  config.cache_oblivious: true
  config.debug: false
  config.fill: true
  config.lazy_lock: false
  config.malloc_conf: ""
  config.prof: false
  config.prof_libgcc: false
  config.prof_libunwind: false
  config.stats: true
  config.utrace: false
  config.xmalloc: false
Run-time option settings
# ...

Которая выведет информацию от текущего аллокатора, если jemalloc не подключен, то там будет пусто.

Heroku

Для Heroku проблема памяти стоит особенно остро, так как инстансы там маленькие и дорогие.
Здесь достаточно подключить buildpack https://github.com/brian-kephart/heroku-buildpack-jemalloc. Проверить можно так же с помощью MALLOC_CONF=stats_print:true ruby -e «exit»

Результаты

Вот графики из NewRelic до:

Puma process before
Sidekiq processes before

И после

Puma process after
Sidekiq processes after

Как видим наиболее значительный выигрыш получили для процесса веб-приложения, потребление памяти уменьшилось на 50%. Sidekiq процесс стал потреблять на 20% меньше. Это прекрасный результат который позволит меньше платить за сервера. Ну либо платить столько же, но писать менее оптимальный код 🙂