Одна из основных 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 && \ apt-get install libjemalloc1 && \ 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 до:
И после
Как видим наиболее значительный выигрыш получили для процесса веб-приложения, потребление памяти уменьшилось на 50%. Sidekiq процесс стал потреблять на 20% меньше. Это прекрасный результат который позволит меньше платить за сервера. Ну либо платить столько же, но писать менее оптимальный код 🙂