この記事は、先月ペルソナの最初のベータ版をリリースした Mozilla の Identity チームによって提供された一連の Node.JS ホリデー シーズン記事の最初の記事です。ペルソナを開発するとき、私たちはデバッグからローカリゼーション、依存関係管理などに至る一連のツールを構築しました。この一連の記事では、Node.js を使用して高可用性サービスを構築したい人にとって役立つ、私たちの経験とこれらのツールをコミュニティと共有します。これらの記事をお楽しみいただき、ご意見やご投稿をお待ちしております。
Node.js の重大な問題、つまりメモリ リークに関する話題の記事から始めます。 Node-memwatch を紹介します。これは、Node のメモリ リークを見つけて分離するのに役立つライブラリです。
なぜトラブルを求めるのですか?
メモリ リークの追跡に関して最もよくある質問は、「なぜわざわざ自分自身を悩ませるのですか?」です。まず取り組むべき、もっと差し迫った問題があるのではないだろうか?サービスを時々再起動するか、より多くの RAM をサービスに割り当てることを選択してみてはいかがでしょうか?これらの質問に答えるために、次の 3 つの提案を提案します:
1.メモリ使用量の増加は気にしないかもしれませんが、V8 は気にします (V8 は Node ランタイムのエンジンです)。メモリ リークが増加すると、V8 はガベージ コレクターの使用をより積極的に行うようになり、アプリケーションの実行が遅くなる可能性があります。したがって、ノード上でメモリ リークが発生すると、プログラムのパフォーマンスに悪影響を及ぼします。
2.メモリ リークは、他の種類の障害を引き起こす可能性があります。メモリをリークするコードは、限られたリソースを継続的に参照する可能性があります。ファイル記述子が不足する可能性があり、また、突然新しいデータベース接続を確立できなくなる可能性があります。この種の問題は、アプリのメモリが不足するずっと前に表面化する可能性がありますが、それでも問題が発生する可能性があります。
3.最終的に、アプリは遅かれ早かれクラッシュします。アプリの人気が高まるにつれて、それは必ず起こります。 Hacker News では誰もがあなたを笑い、嘲笑するでしょう、そしてそれはあなたを悲劇にするでしょう。
何千マイルも続く堤防を決壊したアリの巣はどこにあるのでしょうか?
複雑なアプリケーションを構築する場合、さまざまな場所でメモリ リークが発生する可能性があります。 クロージャはおそらく最もよく知られており、悪名高いものです。クロージャはスコープ内のものへの参照を保持するため、メモリ リークは通常ここから発生します。
クロージャーの漏れは、多くの場合、誰かが探したときに初めて発見されます。しかし、Node の非同期の世界では、いつでもどこでもコールバック関数を通じてクロージャを常に生成します。これらのコールバック関数が作成直後に使用されないと、割り当てられたメモリが増加し続け、メモリ リークがないように見えるコードでもリークが発生します。そして、この種の問題は見つけるのがさらに困難です。
アプリケーションでは、上流のコードの問題が原因でメモリ リークが発生する可能性もあります。メモリ リークが発生したコードを見つけることはできるかもしれませんが、完璧なコードを見つめて、どのようにしてメモリ リークが発生したのか疑問に思うだけかもしれません。
こうしたメモリ リークの場所を特定するのが難しいため、node-memwatch のようなツールが必要になります。伝説によると、数か月前、私たちのロイド・ヒライエルはストレステストで明らかになったメモリリークを追跡しようとして、2日間小さな部屋に閉じ込められました。 (ちなみに、負荷テストに関するロイドの今後の記事にご期待ください)
2 日間の懸命な作業の後、彼は最終的に Node カーネルの原因を発見しました。http.ClientRequest のイベント リスナーが解放されていなかったということです。 (最終的に問題を修正したパッチはわずか 2 文字でしたが、重要な文字でした)。このつらい経験が、ロイドにメモリ リークの発見に役立つツールを作成するきっかけを与えました。
メモリリーク特定ツール
Node.js アプリケーションのメモリ リークを特定するための、継続的に改良されている便利なツールが多数あります。以下にその一部を示します:
上記のツールは誰もが気に入っていますが、私たちのシナリオにはどれも当てはまりません。 Web Inspector はアプリケーションの開発には最適ですが、ホット デプロイメント シナリオ、特に複数のサーバーとサブプロセスが関係する場合には使用が困難です。同様に、長期間の高負荷操作中に発生するメモリ リークも再現が困難です。 dtrace や libumem などのツールは優れていますが、すべてのオペレーティング システムで利用できるわけではありません。
Enternode-memwatch
プログラムにメモリ リークが発生した可能性があるときにデバイスに通知する必要がなく、リークが存在する場所を見つけるのに役立つクロスプラットフォーム デバッグ ライブラリが必要です。そこで、node-memwatchを実装しました。
それは次の 3 つのことを提供します:
「リーク」イベントエミッター
memwatch.on('leak', function(info) { // look at info to find out about what might be leaking });
「ステータス イベント エミッター
」
var memwatch = require('memwatch'); memwatch.on('stats', function(stats) { // do something with post-gc memory usage stats });
ヒープメモリ領域の分類
var hd = new memwatch.HeapDiff(); // your code here ... var diff = hd.end();
また、テスト中に非常に役立つガベージ コレクターをトリガーできる関数もあります。はい、合計4つです。
var stats = memwatch.gc();
memwatch.on('stats', ...): GC 後のヒープ統計
node-memwatch は、JS オブジェクトが割り当てられる前に、完全なガベージ コレクションとメモリ圧縮の後にメモリ使用量のサンプルを生成できます。 (V8 の post-gc フック V8::AddGCEpilogueCallback を使用して、ガベージ コレクションがトリガーされるたびにヒープ使用量情報を収集します)
統計には次のものが含まれます:
メモリ リークのあるアプリケーションのデータの例を次に示します。以下のグラフは、メモリ使用量を経時的に追跡します。おかしな緑色の線は、 process.memoryUsage() がレポートする内容を示しています。赤い線は、node_memwatch によって報告された current_base を示します。左下のボックスには追加情報が表示されます。
Incr GC が非常に高いことに注意してください。つまり、V8 は必死にメモリをクリアしようとしているということになります。
memwatch.on('leak', ...): ヒープ割り当ての傾向
アプリケーションにメモリ リークがある可能性があることを警告する簡単な検出アルゴリズムを定義しました。つまり、5 回連続した GC の後でもメモリが割り当てられているものの解放されていない場合、node-memwatch はリーク イベントを発行します。イベントの具体的な情報形式は次のように明確で読みやすいです:
{ start: Fri, 29 Jun 2012 14:12:13 GMT, end: Fri, 29 Jun 2012 14:12:33 GMT, growth: 67984, reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr' }
memwatch.HeapDiff(): 查找泄漏元凶
最后,node-memwatch能比较堆上对象的名称和分配数量的快照,其对比前后的差异可以帮助找出导致内存泄漏的元凶。
var hd = new memwatch.HeapDiff(); // Your code here ... var diff = hd.end();
对比产生的内容就像这样:
{ "before": { "nodes": 11625, "size_bytes": 1869904, "size": "1.78 mb" }, "after": { "nodes": 21435, "size_bytes": 2119136, "size": "2.02 mb" }, "change": { "size_bytes": 249232, "size": "243.39 kb", "freed_nodes": 197, "allocated_nodes": 10007, "details": [ { "what": "Array", "size_bytes": 66688, "size": "65.13 kb", "+": 4, "-": 78 }, { "what": "Code", "size_bytes": -55296, "size": "-54 kb", "+": 1, "-": 57 }, { "what": "LeakingClass", "size_bytes": 239952, "size": "234.33 kb", "+": 9998, "-": 0 }, { "what": "String", "size_bytes": -2120, "size": "-2.07 kb", "+": 3, "-": 62 } ] } }
HeapDiff方法在进行数据采样前会先进行一次完整的垃圾回收,以使得到的数据不会充满太多无用的信息。memwatch的事件处理会忽略掉由HeapDiff触发的垃圾回收事件,所以在stats事件的监听回调函数中你可以安全地调用HeapDiff方法。