🦖

Part1 「Rubyのメモリ肥大化はfragmentation(断片化)が原因(かもしれない)」

2022/04/10に公開約4,000字

3回に分けて記事を作成する。
本記事はそのPart1である。

Part1 「Rubyのメモリ肥大化はfragmentation(断片化)が原因(かもしれない)」
Part2 「Rubyのメモリ仕様 -ブラックボックスを覗く-」
Part3 「jemallocがRubyアプリケーションのメモリ利用効率を向上させるのか...?」

目次

  • Rubyのメモリ肥大化というテーマ
  • メモリ使用量が上昇していく...
  • マルチスレッドプロググラムとfragmentation(断片化)
  • 次回に続く...

Rubyのメモリ肥大化というテーマ

Rubyアプリケーションのメモリ使用量の上昇に対処する手段として、puma_worker_killerの導入が考えられる。puma_worker_killerは、PumaのWorkerを再起動することで、上昇したメモリを元の低いレベルに戻す。しかし、Workerの再起動によって解決するとは、すなわち原因そのものを解決していることを意味しない。

...it would act as a temporary band-aid to buy you time to allow you to fix your issues.(中略)If you are frequently restarting your workers then you're killing your performance.
問題を修正するための時間を与える一時的なバンドエイドの機能である。(中略)もし頻繁にworkerを再起動するならば、パフォーマンスを殺してしまっている。
Puma Worker Killer」※訳は筆者による

メモリが肥大化した時は、まずはメモリを大きく使用する実装をしていないか、それを探ることが最も優先される。だがもしかすると、その原因はRubyのメモリ管理とmallocの仕様が引き起こすfragmentation(断片化)にあるかもしれない。本記事では、Rubyのfragmentation(断片化)に関する話題を取り上げる。

メモリ使用量が上昇していく...

Rubyプログラマー (Rubiest)は、普段メモリを意識することは多くない。malloc()で割り当てたメモリを、ガーベージコレクション(以下GC)がよしなにfree()で返却してくれるからだ。だが何らかの理由により、メモリが開放されないバグをmemory leakと呼ぶ。

memory leakでは直線的にメモリが増加する。

しかしメモリが増加しているからといって、すぐさまmemory leakと判断するのは飛躍している。メモリの上昇がpltaua(頭打ち)になっていないかを観察したい。

Do not panic if you see a sudden rise in the memory usage of your app. Apps can run out of memory for all sorts of reasons - not just memory leaks.
アプリケーションのメモリ使用量が突然上昇してもパニックにはならないで欲しい。様々な要因でメモリが不足しているからだ。- memory leakだけが理由ではない。
「Hunting Down Memory Issues In Ruby: A Definitive Guide」 ※訳は引用者による

pltaua(頭打ち)した後、次第に減少していく。

メモリの割当と開放(malloc()とfree())を繰り返すとfragmentation(断片化)が発生する。これはRubyのメモリ管理方法によるものである為、回避することは難しい。では、fragmentation(断片化)を発生させる Rubyの仕様とは何だろうか..?

fragmentation(断片化)によるメモリの肥大化は、対数的なグラフを描く。

マルチスレッドプロググラムとfragmentation(断片化)

Nate Berkopec氏の「Malloc Can Double Multi-threaded Ruby Program Memory Usage」は、Rubyとfragmentation(断片化)のテーマにおいて、インターネット上の多くのサイトから参照されている記事だ。そしてBPS株式会社が運営するメディア「TechRacho」が、同記事を丁寧に翻訳してくれている。(「Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)」ただし、記事が執筆された2017年からRubyもバージョンアップを重ねており、Rubyのメモリ管理も進化している点には注意したい。ここ数年の動向については、別章で取り上げるつもりだ。まずここでは、本記事において説明されているマルチスレッドプログラムが引き起こすメモリの上昇についてまとめてみよう。

  1. malloc呼び出しは、メモリ上で実際に配置される場所については定義しない
  2. 1個のRVALUEに収まりきれないオブジェクトに空き(メモリ)を割り当てるときに発生
  3. 「スレッド単位メモリアリーナ」がロックの奪い合いを軽減する目的で、アリーナを拡張する
  4. 「スレッド単位メモリアリーナ」は、アリーナの結合ができない
    ※詳しくはやはり引用元を参照して頂きたい。

1と2がfragmentation(断片化)の原因である。mallocの仕様上、メモリ間の隙間が発生する。 3と4がメモリの増加を意味している。アリーナとは、メモリプールとメモリプール管理部をひとつのまとまりとした管理単位を指す。(参考:malloc(3)のメモリ管理構造

そして同記事では、fragmentation(断片化)の解決策が3点提示されている。

  1. メモリアリーナを削減する
  2. jemallocを使用する
  3. GCのコンパクション

1. メモリアリーナを削減する

MALLOC_ARENA_MAX環境変数を設定することである。記事によればMALLOC_ARENA_MAX=2を設定したところ、メモリ使用量を2〜3倍節約するケースも報告されているとのことだ。然乍、これはメモリの使用量の上限を設定するように思える。従ってパフォーマンスとのトレードオフの関係にある為、運用するアプリケーションに合わせた適切な導入を検討するべきだろう。

2. jemallocを使用する

jemallocについては、別記事で詳しく検討したい。Rubyのデフォルトであるmallocをjemallocに置き換えることで、メモリとパフォーマンスの向上が期待できる。

3. GCのコンパクション

コンパクションは、記事では今後導入を期待するものとされている。そして2017年の記事公開以降、2019年のRuby2.7.0ではGC.compactionメソッド、2020年のRuby3.0ではGC.auto_compact=trueが導入された。この点についても、後で取り上げことにしたい。

次回に続く...

Part1はここまでにしておこう。Rubyには、メモリ肥大化というテーマがある。どうやらこれはRubyがマルチスレッドプログラムであることに起因するようだ。そして3つの解決策を提示しているが、詳しくは次回以降で確認する。Part2では、 Rubyのメモリ仕様について若干の深堀りを試みる。世代インクリメンタルGCといった、RubyのGC仕様等も取り上げるつもりだ。

Part2 「Rubyのメモリ仕様 -ブラックボックスを覗く-」

Discussion

ログインするとコメントできます