Rubyアプリのメモリ肥大を防ぐ方法 ― 断片化とスロー解放への対策
Rubyアプリケーションにおけるメモリ肥大(メモリブロート)は、開発者の間で頻繁に議論されるテーマです。本記事では、Rubyのメモリ管理がどのように問題を引き起こすのかを解説し、本番環境でアプリがメモリを大量消費してしまうのを防ぐための具体的な対策を紹介します。
まずは、「メモリ肥大」とは何を意味するのかを正しく理解することから始めましょう。それでは、詳しく見ていきます!
Rubyにおけるメモリ肥大とは?
メモリ肥大とは、アプリケーションのメモリ使用量が明確な理由もなく急激に増加する現象のことです。この増加は緩やかな場合もありますが、多くの場合は継続的に進行します。一定期間にわたる複数回の実行によって発生する「メモリリーク」とは異なる現象である点に注意してください。
メモリ肥大は、本番環境のRubyアプリケーションにとって最悪の事態の一つとなり得ます。しかし、適切な対策を講じれば回避可能です。例えば、アプリのメモリ使用量に急激なスパイクを検知した場合は、他の問題を切り分ける前に、まずメモリ肥大の兆候がないかを確認するのが良いでしょう。
診断と修正の方法に入る前に、Rubyのメモリアーキテクチャについて簡単におさらいしておきましょう。
Rubyのメモリ構造
Rubyのメモリ使用は、利用可能なシステムリソースを賢く管理するためのいくつかの要素によって成り立っています。具体的には、Ruby言語そのもの、ホストOS、そしてシステムカーネルがこれに該当します。
さらに、ガベージコレクション(GC)のプロセスも、メモリがどのように管理・再利用されるかを左右する重要な役割を担っています。
ヒープページとメモリスロット
Rubyはオブジェクトを「ヒープページ」と呼ばれるセグメント単位で整理しています。ヒープ空間全体(利用可能なメモリ)は「使用中」と「空き」の領域に分割され、各ヒープページはさらに均等サイズのスロットに細分化されます。1つのスロットには、1単位サイズのオブジェクトが格納されます。
新しいオブジェクトにメモリを割り当てる際、Rubyはまず使用中のヒープ空間から空きスロットを探します。見つからなければ、空き領域から新しいヒープページを確保します。
メモリスロットは約40バイト程度の小さなメモリ領域です。スロットに収まりきらないデータは、ヒープページ外の別領域に保存され、各スロットにはその外部データへのポインタが保持されます。
システムのメモリアロケータは、ヒープページや外部データポインタを含む、Rubyランタイム環境でのすべての割り当てを行います。
RubyにおけるOSのメモリ割り当て
Ruby言語が発行するメモリ割り当て要求は、ホストOSのメモリアロケータによって処理・応答されます。
通常、メモリアロケータは malloc、calloc、realloc、free といったC言語の関数群で構成されています。それぞれ簡単に見ていきましょう。
- malloc:「memory allocation(メモリ割り当て)」の略で、オブジェクトに空きメモリを割り当てるために使われます。割り当てるメモリサイズを受け取り、割り当てられたメモリブロックの先頭アドレスへのポインタを返します。
- calloc:「contiguous allocation(連続割り当て)」の略で、連続したメモリブロックの割り当てを可能にします。既知の長さのオブジェクト配列を確保する際に特に有効です。
- realloc:「re-allocation(再割り当て)」の略で、新しいサイズでメモリを再割り当てできます。
- free:事前に確保されたメモリ領域を解放するために使われます。解放すべきメモリブロックの先頭アドレスへのポインタを受け取ります。
Rubyのガベージコレクション
言語ランタイムのガベージコレクションは、利用可能なメモリをどれだけ効率的に活用できるかに大きな影響を与えます。
Rubyはかなり高度なGCを備えており、前述のAPIメソッドをすべて活用して、常時アプリケーションのメモリ消費を最適化しています。
RubyのGCに関する興味深い事実として、GC実行中はアプリケーション全体が停止するという点が挙げられます。これにより、GC中に新しいオブジェクトが割り当てられることはありません。そのため、GCルーチンはなるべく頻度を抑え、素早く完了するように設計すべきです。
Rubyでメモリ肥大が起こる2つの主な原因
ここからは、Rubyでメモリ肥大が発生する最も重要な2つの原因――「断片化(フラグメンテーション)」と「スローなメモリ解放」について掘り下げます。
メモリの断片化
メモリの断片化とは、オブジェクトの割り当てがメモリ上に散在し、連続した空きメモリブロックの数が減少する状態を指します。ディスク上に十分な空きメモリがあっても、連続したブロックがなければオブジェクトに割り当てることができません。この問題はどのプログラミング言語や環境でも起こり得るもので、各言語ごとに独自の解決策を持っています。
断片化は2つのレベルで発生します。言語レベルとメモリアロケータレベルです。順番に詳しく見ていきましょう。
Rubyレベルでの断片化
言語レベルの断片化は、GCプロセスの設計に起因します。GCはRubyのヒープページ内のスロットを「空き」としてマークし、そのスロットを別のオブジェクトの割り当てに再利用できるようにします。もしヒープページ全体が空きスロットだけで構成されていれば、そのページはメモリアロケータへ返却され、再利用されます。
しかし、ごく一部のスロットだけが空きとしてマークされていない場合、そのヒープページはアロケータに返却されません。さまざまなヒープページの多数のスロットが同時に割り当て・解放されていることを考えると、ヒープページ全体が一度に解放される可能性は極めて低いのです。結果として、GCがメモリを解放しても、部分的に占有されたメモリブロックがあるため、アロケータ側では再利用できません。
メモリアロケータレベルでの断片化
メモリアロケータ自身も同様の問題に直面します。OSヒープは、完全に空になった時点で初めて解放しなければなりません。しかし、GCのランダムな動作を考えると、OSヒープ全体を一度に解放できる可能性はほぼありません。
さらに、メモリアロケータはシステムメモリからOSヒープを確保してアプリケーションに提供しますが、既存のヒープに十分な空きメモリがあるにもかかわらず、新たなOSヒープの確保に移ってしまうことがあります。これは、アプリのメモリ指標が急上昇する典型的なパターンです。
スローなメモリ解放
Rubyにおけるメモリ肥大のもう一つの重要な原因が、解放済みメモリのシステムへの返却が遅いことです。この状況では、新しいメモリブロックがオブジェクトに割り当てられる速度に対して、メモリが解放される速度が大幅に遅くなります。これは初心者が陥りやすい定番の問題ではありませんが、断片化以上にメモリ肥大に深刻な影響を与えます。
メモリアロケータのソースコードを調査すると、アロケータはOSページをOSヒープの末尾でのみ、しかもごく稀にしか解放しないよう設計されていることが分かります。おそらくパフォーマンス上の理由によるものですが、裏目に出て逆効果になることもあります。
Rubyのメモリ肥大を解決する方法
メモリ肥大の原因が分かったところで、次はデフラグメンテーション(断片化解消)とトリミングによって、これらの問題を解決しアプリのパフォーマンスを改善する方法を見ていきましょう。
デフラグメンテーションによる対策
断片化はGCの設計に起因するため、根本的に修正することは困難です。しかし、以下のステップを実践することで、メモリが断片化するリスクを減らせます。
- 大量のメモリを使用するオブジェクトへの参照を宣言した場合は、役目を終えたら必ず手動で解放しましょう。
- 静的なオブジェクトの割り当ては、できるだけ1つの大きなブロックでまとめて宣言します。こうすることで、永続的なクラスやオブジェクト、その他のデータが同じヒープページに配置され、後から動的な割り当てを行う際に静的なヒープページを気にする必要がなくなります。
- 可能であれば、大きな動的割り当てはコードの冒頭で行いましょう。大きな静的割り当てブロックの近くに配置され、残りのメモリ領域を清潔に保てます。
- 小さくてあまりクリアされないキャッシュを使っている場合は、冒頭の永続的な静的割り当てとグループ化するのが得策です。思い切ってキャッシュ自体を削除すれば、メモリ管理はさらに改善します。
- 標準のglibcメモリアロケータの代わりにjemallocを使いましょう。この小さな変更だけで、Rubyのメモリ消費量を最大4分の1まで削減できることがあります。ただし、すべての環境で互換性があるとは限らないため、本番投入前に必ず入念なテストを行ってください。
トリミングによる対策
スローなメモリ解放を解決するには、GCプロセスを上書きして、より頻繁にメモリを解放する必要があります。これを実現するAPIが malloc_trim です。必要なのは、GCプロセス中にこの関数を呼び出すようRubyを修正することだけです。
以下は、gc.c の gc_start 関数内で malloc_trim を呼び出すように変更したRuby 2.6のコードです。
gc_prof_timer_start(objspace);
{
gc_marks(objspace, do_full_mark);
// BEGIN MODIFICATION
if (do_full_mark)
{
malloc_trim(0);
}
// END MODIFICATION
}
gc_prof_timer_stop(objspace);注意: この手法はアプリを不安定にする可能性があるため、本番アプリケーションでの使用は推奨されません。ただし、スローなメモリ解放がパフォーマンスに深刻な打撃を与えており、あらゆる解決策を試す覚悟がある場合には非常に有用です。
まとめと次のステップ
メモリ肥大は特定が難しく、修正はさらに困難です。
本記事では、Rubyアプリにおけるメモリ肥大の2つの主要な原因――「断片化」と「スローなメモリ解放」――と、それに対する2つの解決策――「デフラグメンテーション」と「トリミング」――を紹介しました。
アプリのメトリクスを常に監視し、メモリ肥大の兆候を早期に発見して、アプリがダウンする前に対応することが重要です。本記事が、あなたのRubyアプリケーションのメモリ肥大問題を解決する一助となれば幸いです。
P.S. Ruby Magicの最新記事をいち早く読みたい方は、ぜひRuby Magicニュースレターをご購読ください。記事を見逃すことは一切ありません!
ゲスト執筆者のKumar Harshは、新進気鋭のソフトウェア開発者です。RubyやJavaScriptなどの人気Web技術を題材にしたコンテンツを手掛ける、情熱的なライターでもあります。詳細は彼のウェブサイトをご覧いただくか、Twitterでフォローしてください。
-
RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法
Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この
-
Windows 10でGoogle Chromeのメモリ使用量が高いときの対処法|RAM消費を減らす9つの方法
Google Chromeは、シンプルでクリーンな操作性により快適で安全なブラウジング体験を提供することで人気を集める、世界で最も利用されているWebブラウザの一つです。しかし、Chromeに対する最大の不満として挙げられるのが「システムメモリ(RAM)を大量に消費する」という点です。メモリを大量に消費すると、Chrome本体だけでなくWindows 10パソコン全体の動作が遅くなり、ユーザー体験全体に悪影響を及ぼします。この記事では、Chromeのメモリ使用量を削減し、Windows 10でのRAM消費を抑えるための実用的な対処法を紹介します。 Chromeが大量のメモリを使用する理由は?