Ruby をマスターする:メモリ リークを効率的に検出して修正する
メモリ リークとは、メモリ使用量が意図せず、制御されておらず、際限なく増加することです。どんなに小さなものであっても、最終的にはリークが原因でプロセスがメモリ不足になりクラッシュします。このクラッシュを回避するためにアプリを定期的に再起動したとしても (判断はできません。実際にそうしました!)、メモリ リークによるパフォーマンスへの影響は依然として発生します。
メモリ リークに関する 2 部構成のシリーズの 1 つ目であるこの投稿では、まず Ruby がメモリを管理する方法、ガベージ コレクション (GC) がどのように機能するか、そしてリークを見つける方法について見ていきます。
2 番目のパートでは、リークの追跡についてさらに詳しく説明します。
始めましょう!
Ruby のメモリ管理
Ruby オブジェクトはヒープに保存され、各オブジェクトはヒープ上の 1 つのスロットを埋めます。
Ruby 3.1 より前は、ヒープ上のすべてのスロットは同じサイズ (正確には 40 バイト) でした。スロットに収まらないほど大きすぎるオブジェクトは、ヒープの外側に保存されました。各スロットには、オブジェクトが移動された場所への参照が含まれていました。
Ruby 3.1では、Stringの可変幅割り当て オブジェクトが結合されました。間もなく、可変幅の割り当てがすべてのオブジェクト タイプで標準になるでしょう。
可変幅の割り当ては、キャッシュの局所性を改善することでパフォーマンスを向上させることを目的としています。オブジェクトのすべての情報は、2 つのメモリ位置にまたがるのではなく、1 つの場所に保存されます。
また、メモリ管理 (一部) も簡素化されるはずです。現時点では、2 つの「ヒープ」があります。
- より小さな Ruby オブジェクトを格納する Ruby ヒープ (または GC ヒープ)
- より大きなオブジェクトを格納する C ヒープ (または malloc/transient ヒープ)
可変幅の割り当てが標準になると、後者のヒープは必要なくなります。
ヒープは指定されたサイズ (デフォルトでは 10,000 スロット) で開始され、オブジェクトは作成時に空きスロットに割り当てられます。 Ruby がオブジェクトを作成しようとして、使用可能な空きスロットがない場合、ガベージ コレクション (GC) が発生して、いくつかの空きスロットが使用可能になります。
GC 後の空きスロットが少なすぎる場合は、ヒープが拡張されます (これについては後で詳しく説明します)。
以下に、制御できる要素とその環境変数を示します。
- ヒープの初期サイズ -
RUBY_GC_HEAP_INIT_SLOTS - GC の発生後に使用可能になる空きスロットの数 -
RUBY_GC_HEAP_FREE_SLOTS - ヒープの拡張量 -
RUBY_GC_HEAP_GROWTH_FACTOR
Ruby のガベージ コレクション
Ruby のガベージ コレクションは「世界を停止」します。GC が発生すると他のプロセスは発生しません。 Ruby のガベージ コレクション (2.1 以降) も世代別です。 これは、ガベージ コレクターには 2 つのモードがあることを意味します。
- マイナー GC - 「若い」オブジェクト (最近作成されたオブジェクト) を検査します
- メジャー GC - 「古い」オブジェクトと「若い」オブジェクト (すべて) を検査します。 オブジェクト)
メモ :「古い」オブジェクトは 3 以降も存続しています。 メジャーまたはマイナーに関わらず、GC が実行されます。
ヒープがいっぱいになると、マイナー GC が最初に呼び出されます。制限を下回るほど十分なスロットを解放できない場合は、メジャー GC が呼び出されます。その後、まだ十分な空きスロットがない場合にのみ、ヒープが拡張されます。
メジャー GC はより多くのオブジェクトを参照するため、マイナー GC よりも高価です。
世代別 GC のパフォーマンスが高い理由の背後にある理論は、オブジェクトが通常 2 つのカテゴリに分類されるということです。
<オル>
十分な空きスロットがある場合でも、古いオブジェクトの数が特定のしきい値を超えている場合、メジャー GC はマイナー GC の後に実行されます。この制限はヒープのサイズが大きくなるにつれて増加し、RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR によって制御できます。 環境変数。
リークが発生すると、クリーンアップできないオブジェクトが作成され、ますます old が増えます。 オブジェクト。これは、主要な (高価な) GC が必要以上に頻繁に実行されることを意味します。 GC の実行中は他に何も実行されないため、時間の無駄になります。
Ruby のメモリレイアウトとガベージ コレクターについてさらに詳しく読むために、この記事の最後にいくつかのリンクを残しました。
Ruby におけるメモリ リークはどのようなものですか?
AnyUnix システムで利用できる簡単なツールを使用すると、メモリ リークを確認できます。次のコードを例として取り上げます。
このコードが「漏洩する」と言うのは少し不公平です。コードが漏洩しているだけです。 —しかし、それは私たちの目的には役立ちます。
1 つの端末でこのプログラムを実行し、watch を実行すると、コマンド ラインからリークを非常に簡単に観察できます。 -ps を使用して時間の経過とともにメモリを増加させます .
pgrep -f "ruby ./leaky.rb" ps を制限できるようにプロセス ID を見つけます。 関心のあるプロセスのみに出力します。お察しの通り、これは grep のようなものです。 プロセスの場合。
watch このツールを使用すると、特定のコマンドの出力をポーリングしてその場で更新できるため、ターミナル内にライブ ダッシュボードが表示されます。
このような出力が得られ、数秒ごとに更新されます。
%MEM が表示されるはずです。 および RSS 増加しています。それらは次のとおりです:
%MEM- プロセスが使用するメモリ量(ホスト マシン上のメモリの割合)RSS(常駐セット サイズ) - プロセスが使用する RAM の量 (バイト単位)。
この基本的な OS のみの情報は、リークがあるかどうかを特定するのに十分です。メモリが増加し続ける場合は、メモリリークが発生していることを意味します。
ガベージ コレクター モジュールを使用して Ruby のリークを見つける
GC を使用して、Ruby コード自体内のリークを検出することもできます。 モジュール。
GC.stat メソッドは、多くの有用な情報を含むハッシュを返します。ここで、:heap_live_slots に興味があります。 、これはヒープ上で使用されているスロットの数です。これは :heap_free_slots の逆です。 ループの最後で、メジャー GC を強制し、使用されたスロットの数、つまり GC 後に残るオブジェクトの数を出力します。
小さなプログラムを実行すると、これが無限に増加することがわかります。リークが発生しました。 GC.stat(:old_objects) を使用することもできました。 同じ効果が得られます。
GC は モジュールを使用してifを確認できます。 リークがあり、(puts を賢明に扱えるなら) ステートメント) リークが発生している可能性がある場所では、ObjectSpace を使用してリークしている可能性のあるオブジェクトのタイプを確認できます。 モジュール。
ObjectSpace.count_objects メソッドは、ライブ オブジェクトの数を含むハッシュを返します。 T_STRING たとえば、 はメモリ内に存在する文字列の数です。私たちのかなりリークの多いプログラムでは、GC の後であっても、この値はループごとに増加します。文字列オブジェクトがリークしていることがわかります。
AppSignal を使用した本番環境でのアプリケーション パフォーマンスの監視
ps で遊んでいるとき および GC おもちゃのプロジェクトにとって賢明なルートになる可能性があります。また、楽しくて有益です。 — 私はしません 実稼働アプリのメモリ リーク検出ソリューションとしてこれらを推奨します。
ここで、アプリケーション パフォーマンス監視 (APM) ツールを使用します。非常に大規模な企業であれば、これらを自分で構築することができます。ただし、小規模な服装の場合は、既製の APM を選択するのが最適です。月額料金を支払う必要がありますが、提供される情報はそれを補って余りあるものです。
メモリ リークを検出するには、サーバーまたはプロセスのメモリ使用量 (RSS とも呼ばれます) の経時的なグラフを見つける必要があります。以下は、デプロイ直後の正常なアプリの AppSignal の「プロセス メモリ使用量」ダッシュボードのスクリーンショットの例です。

デプロイ後の不健全なアプリは次のとおりです。

AppSignal では、GC やヒープ スロットなどの Ruby VM の統計情報も表示されるため、メモリ リークに関するより明確な信号が得られます。ライブスロットの数が増え続ける場合は、リークが発生しています!

AppSignal for Ruby について詳しくは、こちらをご覧ください。
まとめと続きの読み物
この投稿では、Ruby のメモリ管理とガベージ コレクターについて簡単に説明しました。次に、Unix ツールと Ruby の GC モジュールを使用してメモリ リークを発見する方法を診断しました。
次回は、memory_profiler の使用方法を見ていきます。 および derailed_benchmarks 漏れを見つけて修正します。
それまでの間、使用したツールについて詳しく読むことができます。
watchpspgrep
追加の詳細情報:
GCモジュールのドキュメントObjectSpaceモジュールのドキュメント- ガベージ コレクションの詳細
- 可変幅の割り当て
コーディングを楽しんでください。また次回お会いしましょう!
追記Ruby Magic の投稿を報道後すぐに読みたい場合は、Ruby Magic ニュースレターを購読して、投稿を 1 つも見逃さないようにしてください。
-
既存のRailsアプリケーションのコンテナ化
コンテナ化ソフトウェアは、開発と展開を容易にするために、標準化されたユニットにパッケージ化しています。コンテナは、アプリケーションのコードとそのすべての依存関係をバンドルします。コンテナは完全にスタンドアロンにすることができます。これには、ソフトウェア、ランタイム環境、およびシステムライブラリを含むパッケージが含まれています。コンテナは、開発者と運用チームが、環境に関係なく、ソフトウェアが同じように実行されることを保証するのに役立ちます。コードをインフラストラクチャから分離することで、「コンテナ化」されたアプリは、ローカル環境、テスト環境、本番環境で同じように実行されます。 Dockerは、
-
すべてのWeb開発者が機械学習を検討する必要がある理由
私にはまだ子供がいませんが、子供がいるときは、2つのことを子供に学んでもらいたいです。 パーソナルファイナンス 機械学習 特異点が近いと信じているかどうかにかかわらず、世界がデータで実行されていることは否定できません。そのデータがどのように知識に変換されるかを理解することは、最近の成人にとって重要であり、開発者にとってはさらに重要です。 これは、フルスタックのRuby開発者が機械学習(ML)にアクセスできるようにするシリーズの最初の記事です。自由に使えるMLツールを理解することで、利害関係者がより良い意思決定を行えるように支援できます。今後の記事では、個々のテクニックと実際の例に焦点