先の記事でGenkaiのマルチスレッド化について話したが、この時に困ったこと が起きた。過剰なメモリ消費である。

限界鯖はメモリ1GBのサーバーなのだが、Genkaiのサーバープロセスの RSS(Resident Set Size)が300-500MB程度まで増加し、メモリが足りなくなっ てカーネルに強制終了させられるのだ。

一体どうして……。.dat ファイルは合計しても数MBにしかならず、リクエスト を処理するのに確保されたメモリはリクエストハンドラを抜ける時には全て要 らなくなっているはずだというのに。

調べてみたところ、Rubyで書かれたサーバープログラムが法外なメモリを要求 するというのは、よくある話らしい。「Malloc Can Double Multi-threaded Ruby Program Memory Usage 」。まとめると:

  1. メモリ断片化が原因でメモリを無駄に消費することがある。
  2. 文字列など、Ruby値から間接的に参照されるメモリ領域の malloc で大部分の断片化が起こる。
  3. glibc のスレッド毎メモリアリーナが多いと断片化が増大する。
  4. IOを行うRubyアプリケーションは global VM lock を放すので、この時に 新しいメモリアリーナが作成され得る。
  5. 対策としては glibc のメモリアリーナ数を MALLOC_ARENA_MAX 環境変数を 用いて制限するか、別の malloc 実装である jemalloc を使う。

結果から言うと、MALLOC_ARENA_MAX の設定では対した効果は確認できなかっ た1が、jemalloc (5.2.0) を使った結果、プロセスのRSSを数十MBにすること ができた。

ただし、jemalloc を使っても特定の種類のリクエストに対してはメモリ使用 量が不都合に増加した。スレッド一覧(subject.txt)の生成だ。

subject.txt の内容は動的に生成され、これには板の全ての .dat ファイルを 読んでその投稿数を数える必要がある。.dat ファイルをパースして個々の投 稿オブジェクト(それぞれ数個の文字列をインスタンス変数として持つ)を作成 して、その数を数えるコードになっていたのだが、例によって使い終わったオ ブジェクトのメモリがOSに返っていないようだった。

これはコードを修正して 単に .dat ファイルの改行コードの数を数えるよう にした。

結論

メモリアロケータの置き換えとコードの修正で実用的パフォーマンスを引き出 すことができたが、シングルスレッドの場合に比べて相当にコードに気を遣わ なければならないことが判明した。

自分が浪費的なコードを書いていたことも事実だが、このまま開発を続けると 不自然な設計になることが考えられる。たとえば、メモリが確実に開放される ようにサブプロセスで実質的なタスクを行ったり、一定量のオブジェクトが不 要になった時点で GC.start する、新しいオブジェクトを作る代わりに既存の オブジェクトの状態を変更することでプログラミングする等……。

弱ったなぁ。

  1. 仮想メモリ空間の量については確かに減る。アリーナ1つにつき数百MBがコミットされるようだ。実メモリが割り当てられる(RSSが増加する)わけではない。