Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: The Limits of Copy-on-write: How Ruby Allocates Memory 公開日: 2017/08/28 著者: Brandur サイト: https://brandur.org/ Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳) Unicorn(またはPumaやEinhorn)を実行していると、誰しもある奇妙な現象に気がつくでしょう。マスタからforkした(複数の)ワーカープロセスのメモリ使用量は開始当初は低いにもかかわらず、しばらくすると親と同じぐらいにまでメモリ量が増加します。大規模な本番インストールだと各ワーカーのサイズが100MB以上まで増加することもあり、やがてメモリはサーバーでもっとも制約の厳しいリソースになってしまい、しかもCPUはアイドル状態のままです。 現代的なOSの仮想メモリ管理システムは、まさにこの状況を防止するために設計されたcopy-on-write機能を提供します。プロセスの仮想メモリは4kページごとにセグメント化されます。プロセスがforkした当初の子プロセスは、すべてのページを親プロセスと共有します。子プロセスがページの変更を開始した場合にのみ、カーネルはその呼び出しをインターセプトし、ページをコピーして新しいプロセスに再度割り当てます。 子プロセスが時間とともに共有メモリ中心からコピー中心に移行する様子 だとすると、なぜUnicornのワーカーはもっと多くのメモリを共有しないのでしょうか。多くのソフトウェアが持つ(サイズを増減可能な)静的オブジェクトのコレクションは、一度だけ初期化された後、プログラムのライフタイム終了までメモリ上で変更されないままワーカー全体で共有され続ける筆頭候補になりえます。形式的にはそのとおりなのですが、実際には何も再利用されません。その理由を理解するには、Rubyのメモリ割り当て動作を詳しく調べる必要があります。 スラブとスロット まずはオブジェクト割り当ての概要をざっと押さえておきましょう。RubyがOSにリクエストするメモリはチャンクに分かれており、内部ではヒープページと呼ばれます。(Rubyの)ヒープページは、OSから渡される4kページ(以後「OSページ」と呼ぶことにします)とは同じではないため、ヒープページという名前は少々不運かもしれませんが、1つのヒープページは仮想メモリ上で多数のOSページにマッピングされます。Rubyは、(OSページが複数の場合も含め)OSページを専有することで、OSページを最大限利用できるようにヒープページのサイズを増減します(通常、4kのOSページ4つが16kのヒープページ1つに等しくなります)。 1つの(Ruby)ヒープ、そのヒープページ、各ページごとのスロット 1つのヒープページは「ヒープ(複数形はheaps)」と呼ばれることもあれば、「スラブ(slab)」や「アリーナ(arena)」と呼ばれることがあるのをご存知かもしれません。私としては曖昧さの少ない後者の2つが好みなのですが、Rubyのあらゆるソースで使われている呼び名に合わせて、以後1つのチャンクを「ヒープページ」、ヒープページが複数集まったコレクション1つを単に「ヒープ」と呼ぶことにします。 1つのヒープページは、1つのヘッダと多数の「スロット」でできています。各スロットにはRVALUEが1つずつあり、これはメモリ上のRubyオブジェクトです(詳しくは後述)。1つのヒープは1つのページを指し、そこから多数のヒープページが互いを指して、コレクション全体に繰り返される結合リスト(linked list)を1つ形成します。 ヒープを利用する Rubyのヒープは、ruby_setup(eval.c)から呼び出されるInit_heap(gc.c)によって初期化されます。ruby_setupは1つのRubyプロセスへの主要なエントリポイントです。ruby_setupはこのヒープに沿ってスタックとVMも初期化します。 void Init_heap(void) { heap_add_pages(objspace, heap_eden, gc_params.heap_init_slots / HEAP_PAGE_OBJ_LIMIT); … } Init_heapは、スロットのターゲット数を元に初期のページ数を決定します。デフォルト値は10,000ですが、設定や環境変数で調整可能です。 #define GC_HEAP_INIT_SLOTS 10000 設定に応じて、1ページあたりのスロット数が大まかに算出されます(gc.c)。ターゲットサイズは16k(2*14または1 << 14)から始まり、そこからmallocの予約(bookkeeping)1分の数バイトを引き、ヘッダー用の数バイトも引いてから、RVALUE構造体の既知のサイズで割ります。 /* default tiny heap size: 16KB */ #define HEAP_PAGE_ALIGN_LOG 14 enum { HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG), REQUIRED_SIZE_BY_MALLOC = (sizeof(size_t) * 5), HEAP_PAGE_SIZE = (HEAP_PAGE_ALIGN – REQUIRED_SIZE_BY_MALLOC), HEAP_PAGE_OBJ_LIMIT = (unsigned int)( (HEAP_PAGE_SIZE – sizeof(struct heap_page_header))/sizeof(struct RVALUE) ), } 64ビットシステムの場合、RVALUEは40バイトを占めます。デフォルトのRubyが最初に408スロット2ごとに24ページを割り当てていることを示すために、一部の計算を省略します。メモリがもっと必要になるとヒープは増大します。 RVALUE: メモリスロット上のオブジェクト 1つのヒープページ内にあるスロット1つにつきRVALUEが1つあり、メモリ上のRubyオブジェクトを表現します。定義は以下のとおりです(gc.cより)。 typedef struct RVALUE { union { struct RBasic basic; struct RObject object; struct RClass klass; struct RFloat flonum; struct RString string; struct RArray array; struct RRegexp regexp; struct RHash hash; struct RData data; struct RTypedData typeddata; struct RStruct rstruct; struct RBignum bignum; struct RFile file; struct RNode node; struct RMatch match; struct RRational rational; struct RComplex complex; } as; … } RVALUE; ここは私にとって、Rubyが任意の型を任意の変数に代入できるという神秘のベールが最初に剥がされる場所です。上から、RVALUEとは単にRubyがメモリ上に保持している、取り得るすべての型の巨大なリストにすぎないことが直ちにわかります。すべての型が同じメモリを共有できるようにCの共用体(union)で圧縮されています。共用体には一度に1つずつしか設定できませんが、その共用体全体のサイズは、最大でもリストの個別の型の最大サイズにしかなりません。 スロットを具体的に理解するため、そこに保持される可能性のある型の1つを見てみることにしましょう。以下は典型的なRubyの文字列です(ruby.hより)。 struct RString { struct RBasic basic; union { struct { long len; char *ptr; union { long capa; VALUE shared; } aux; } heap; char ary[RSTRING_EMBED_LEN_MAX + … Continue reading Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)