集合知プラットフォーム事業部・エンジニアの榎本です。コロナ禍の運動不足を解消すべく筋肉体操で筋トレを続けてますが、上腕三頭筋がいい感じに成長しており継続の大切さを身に沁みて実感しております。
目次
- TL;DR(三行要約)
- Rubyアプリケーションのメモリ肥大化問題
- jemalloc を使ってみる
- jemalloc とは?
- jemalloc で改善するのか?
- jemalloc の設定方法
- jemalloc をプロダクション導入してみた結果
- まとめ
- おまけ:jemalloc についてMatzに聞いてみた
TL;DR(三行要約)
- jemalloc でRubyアプリのメモリ効率改善
- jemalloc でRubyアプリのパフォーマンス改善
- jemalloc の導入も簡単
Rubyアプリケーションのメモリ肥大化問題
Ruby on RailsなどのRubyアプリケーションを運用する上で、メモリ使用量の肥大化に頭を悩ませた方は多くいらっしゃるのではないでしょうか。
下記は典型的なRailsアプリケーションのメモリ使用量のグラフです。メモリの使用量が対数関数のグラフのように時間とともに100%に近づいていく様子が見て取れます。
この問題の素朴な対処法としては、しきい値を定義して定期的にworkerプロセスを再起動してやることです。実際にそれを実現するためのgemがいくつか存在します。
しかし puma_worker_killerの README 冒頭で注意喚起されているとおり、頻繁な再起動はCPUリソースを消費させパフォーマンス劣化の要因にもなることから、あくまで応急処置であるべきです。メモリ肥大化の根本的な原因となっているコードがあるのであれば、それをきちんと調査し修正・対応すべきでしょう。
jemalloc を使ってみる
こういったメモリの肥大化・断片化がなぜ起こるのか、それにどう対処すべきかについては書籍・『Complete Guide to Rails Performance』(ちなみに本書は弊社で行っている分科会で取り上げた書籍の1冊です)の中で詳しく解説されています1。
細かい話は本を読んでいただくとして、本書の中ではRubyアプリケーションのメモリ使用を効率化する方法として、メモリアロケーターを jemallocに切り替える方法が紹介されています。
前置きが少し長くなってしまいましたが、本記事では jemalloc を実際にプロダクション投入してみた結果とともに jemalloc について紹介したいと思います。
jemalloc とは?
jemallocとは Meta(旧・Facebook)社が中心となって開発されているメモリアロケーターです2。その特徴として、メモリの断片化の回避とスケーラブルな並行性サポートを謳っています。
jemalloc で改善するのか?
気になるのは jemalloc 導入によって実際にメモリ使用率は改善するのか?というところです。
検索してみるとすぐに jemalloc 導入で実際にメモリの使用量が改善したという事例がいくつか見つかりました。
- Rubyアプリケーションのメモリ使用量上昇問題をjemallocを使うことで解決しました - Studyplus Engineering Blog
- Reducing Sidekiq Memory Usage with Jemalloc | Brandon Hilkert
- How jemalloc improved memory usage of our Rails application | by Bernat Rafales | Code & Wild | Medium
- How we decreased our memory usage with jemalloc - DEV Community
またこちらのベンチマークによると、メモリだけではなくパフォーマンスも10%程度 jemalloc によって向上することが示されています。
CRuby 2.5.0 jemalloc tcmalloc increase w/ tcmalloc increase w/ jemalloc Median Throughput 175.13 req/sec 197.49 req/sec 183.33 req/sec 4.68% 12.77%
jemalloc の設定方法
Rubyはデフォルトのメモリアロケーターとして、 glibc malloc を使います。ではどのようにメモリアロケーターをデフォルトから jemalloc に切り替えることができるのでしょうか?
--with-jemalloc
オプションをつけてRubyをコンパイルする方法もありますが、一番手軽な方法は 環境変数 LD_PRELOAD
を設定することです。このLD_PRELOAD
に jemallocのsoファイルのパスを指定してやればOKです。
具体例を示しましょう。下記は alpineベースのRuby dockerイメージにおける jemalloc 設定方法になります(マルチステージビルドを使って jemalloc のインストールを行っていることに注意してください)。
FROM ruby:X.X.X-alpine as base ... FROM base as jemalloc RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.2.1/jemalloc-5.2.1.tar.bz2 | tar -xj && \ cd jemalloc-5.2.1 && \ ./configure && \ make && \ make install ... COPY --from=jemalloc /usr/local/lib/libjemalloc.so.2 /usr/local/lib/ ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 ...
jemalloc インストール後、ENV
命令にてLD_PRELOAD
にsoファイルのパスを指定しています。この状態でRubyアプリケーションを起動すれば、Rubyのメモリアロケーターは jemalloc に切り替わります。
ただ、この環境変数はDockerイメージ全体のソフトウェアに影響するグローバルな設定なので、設定して問題ないかはきちんとステージング環境などで動作確認・検証しましょう。
jemalloc をプロダクション導入してみた結果
実際に本番環境で稼働するRailsアプリケーションに jemalloc を適用させてみました。その結果をご紹介します。
下記は sidekiq (v6.3)の jemalloc 導入前後一週間のメモリ使用率の比較です。青い線が導入後のメモリ使用率、点線が導入前のメモリ使用率となっております。
少しわかりにくいのですが、平均して5%程度メモリの使用率が改善したことが確認できました。しかし、先に紹介した改善事例のようにグラフにはっきりと改善が現れると期待していたので、正直なところ少し期待外れ感は否めませんでした。
パフォーマンスについてはどうでしょうか? 下記はRailsアプリケーション(Rails v6.1, Webサーバーはunicorn)のレイテンシーのグラフです。上から順に p99, p95, p90のレイテンシーとなっています。
こちらはグラフにはっきりとした改善(10-20%程度のレイテンシーの改善)が現れました。Dockerfile
を少しイジっただけでこれだけのパフォーマンスが改善したのは、期待以上の結果と言えます。
まとめ
Rubyのメモリアロケーターを jemalloc に切り替えることで、アプリケーションコードの変更なしにメモリ使用およびパフォーマンスを改善できました。
導入もさほど難しくないので、Rubyアプリケーションのメモリおよびパフォーマンスにお困りの方は一度試してみてはいかがでしょうか。
おまけ:jemalloc についてMatzに聞いてみた
弊社の技術アドバイザリーとしてMatzさんがおりますので、Matzさんにもjemalloc について見解を伺ってみました。
Q. jemalloc コアチーム的にどう考えている?
- どのアロケーターでパフォーマンス向上するかは、アプリケーションの特性次第。一概にどれがいいとは言えないと思う
- jemalloc に変える提案も来た3が、Rubyとして取り込む予定はない
LD_PRELOAD
でメモリアロケーターを変更できる口は用意してあるので、変更したい場合はそちらを使ってほしい
- Ruby として jemalloc を推奨することはしないが、Railsアプリケーションでメモリがボトルネックになりやすいということは理解しているので、改善は進めている。すでに入れた変更だと下記のようなもの。
- 世代別GC
- インクリメンタルGC
- Object Compaction
- Rubyとして推奨はしないがコミュニティの中で jemalloc のほうが良さそうだといった知見が公開されるのは大歓迎である
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら