Kras本記事は、原著者の許諾のもとに翻訳・掲載しております。2016注釈:お気に入りに登録してください。http://www.alexkras.com/simple-guide-t...初めに数カ月前、私は、Node.jsでメモリリークのデバッグをしなければなりませんでした。このテーマに役立つ数多くの記事を見つけましたが、その一部をじっくり読み終えた後でさえ、問題のデバッグのために具体的に何をすれば良いのかまだよく分かりませんでした。この記事の目的は、Node.jsでメモリリークを発見するための簡単ガイドを提供することです。分かりやすい方法を1つに絞って説明することが、(私が思うに)Node.jsでのあらゆるメモリリークデバッグの第一歩となるはずです。場合によっては、この方法は十分ではないかもしれません。参考になるかもしれない他のリソースへのリンクも張っておきます。最小限の理論JavaScriptは、ガベージコレクションを行う言語です。JavaScriptエンジンによって、自動的に割り当てられたり、割り当てを解除されたりします。V8は、どのようにしてメモリの割り当てを解除すべきタイミングを知るのでしょうか?V8は、プログラムの中のルートノードから始まる全ての変数について、変数のグラフを保持します。JavaScriptには、論理値(Boolean)、文字列(String)、数値(Number)、オブジェクト(Object)という4つのデータ型があります。最初の3つは基本型で、割り当てられたデータを持ち続けるだけですが(例えばテキストの文字列)、オブジェクト(Object)とJavaScriptにあるその他全てのものは、オブジェクトで(例えば配列も)、他のオブジェクトへの参照(ポインタ)を保持できます。定期的に、V8はメモリグラフを確認し、ルートノードから到達できないデータグループを特定しようとします。ルートノードから到達できなければ、V8はそのデータはもう使われることがないと判断し、メモリを解放します。このプロセスがガベージコレクションと呼ばれるものです。いつメモリリークは発生するのかJavaScriptでメモリリークが発生するのは、既に必要とされていないデータがまだルートノードから到達可能な場合です。この状態では、V8はそのデータがまだ使われていると判断し、メモリを解放しないのです。メモリリークをデバッグするためには、誤って保持されているデータを特定し、V8がクリーンアップできるようにする必要があります。ガベージコレクションは、常に稼働しているものではないことに注意しなければなりません。通常、V8は、適切な時期にガベージコレクションを開始します。定期的に、あるいは、空きメモリが減ってきたことを検知した時に、ガベージコレクションを開始します。Node.jsでは、各プロセスに使えるメモリ量が限られているため、V8は、メモリを効率的に使う必要があるのです。注釈:Node.jsは、メモリ不足です。alexkras.comにアクセスして、メモリリークを確認してください。後者の緊急ガベージコレクションは、重大なパフォーマンス低下の原因になり得ます。多くのメモリリークを起こしているアプリケーションがあるとしましょう。Node.jsのプロセスは、メモリが不足し始めます。これにより、V8は、緊急ガベージコレクションを開始します。ほとんどのデータはまだルートノードから到達できる状態で、メモリ解放できる余地が少なく、ほぼそのままとなります。遅かれ早かれ、Node.jsのプロセスは再びメモリ不足に陥り、再度ガベージコレクションを開始します。気付かないうちに、アプリケーションは、連続ガベージコレクションサイクルに入り、何とかプロセスを機能させようとし続けます。V8は、多くの時間をガベージコレクションに費やし、実際のプログラム実行に使われるリソースがほとんど残りません。JavaScriptエンジンは、ガベージコレクション実行のタイミングを決定する複雑なロジックを持っています。その点を考慮すると、たとえNode.jsのプロセスでメモリの使用量が増加し続けるのを目にしたとしても、それがメモリリークのせいだと分かるのは、ガベージコレクションが実行されて、使われていないメモリが解放された後です。ありがたいことに、Node.jsでは、手動でもガベージコレクションを開始することができますので、メモリリークを確認する際には、まずこれを実行します。index.jsです。Node.jsがこのモードで実行されていれば、プログラムからglobal.gc()を呼び出すことによって、いつでもガベージコレクションをプログラムで開始することができます。process.memoryUsage().heapUsedを呼び出すことによって、プロセスで使われているメモリ量をチェックすることもできます。手動でガベージコレクションを開始し、使用中のヒープを確認することによって、実際にプログラム内でメモリリークが起こっているかどうかを判断することができるのです。サンプルプログラム簡単なメモリリークプログラムを作りました。こちらでご覧いただけます。index.jsを実行すると、実際の動作を確認することができます。secondsこのプログラムは、以下のような動きをします。5ミリ秒ごとに、ランダムにオブジェクトを生成し、leakyDataとnonLeakyDataという名前の2つの配列に格納します。nonLeakyData配列は5ミリ秒ごとにクリーンアップしますが、leakyData配列のクリーンアップを”忘れます“。2秒ごとに、プログラムがメモリの使用量を出力します。start)でプログラムを実行すると、メモリに関するデータの出力が始まります。メモリが急速に増加しているのが分かるでしょう。2秒ごと、それもメモリの状態を得る直前にガベージコレクションをしているにもかかわらずです。Heap.")このデータの出力は、このような感じです。Heap.このデータをプロットすると、メモリの使用量の増大はもっと明確になります。注意:データプロットの方法を知りたい場合は、このまま読み進めてください。不要の場合は、次のセクションに進んでください。出力されたデータは、JSONファイルに保存し、それを読み込んでPythonの数行のプログラムでプロットしています。混乱を避けるために別のブランチに入れていますが、ここで確認していただけます。https://github.com/akras14/memory-leak-example/tree/plot関連する箇所は、こちらです。Size')plt.show()plotブランチは、チェックアウトして、通常通りにプログラムを実行することもできます。plot.pyを実行して、プロットを生成してください。実行の前にはコンピュータに、Matplotlibライブラリをインストールしておく必要があります。他にも、エクセルを使ってデータをプロットすることができます。最低3回のヒープダンプ採取さあ、これで問題が再現できました。次は何をするのでしょうか。今度は、問題がどこにあるのかを突き止め、手直ししなければいけません。前出のサンプルプログラムの以下の部分にお気付きかもしれませんね。モジュールを使っています。https://github.com/bnoordhuis/node-heapdumpnode-heapdumpを使用するための準備は以下の通りです。{{pid}}を呼び出すもしkillという要素を初めて見たのであれば、これはUNIXのコマンドで、あらゆる実行プロセスに対し、(特に)カスタムシグナル(ユーザ定義シグナルともいう)を送ることができるものです。Node.jsのheapdumpは、ユーザシグナル2、すなわち-USR2とプロセスIDを受け取ると、そのプロセスのヒープダンプを採取するように設定されています。{{pid}}コマンドを自動化しています。ここでprocess.killはkillコマンドのためのNodeラッパーです。SIGUSR2はNode.jsでの-USR2の呼び方で、process.pidは現在のNode.jsのプロセスIDを得ます。完璧なヒープダンプを採取するために、ガベージコレクションの後、その都度このコマンドを実行します。'SIGUSR2');はWindows系プラットホーム上で動きませんので、代わりにheapdump.writeSnapshot()を実行してください。この例はheapdump.writeSnapshot()があれば、少しばかり簡単なのかもしれません。{{pid}}シグナルを使えば、ヒープダンプを開始することができ、役に立つということをお伝えしたかったのです。次のセクションでは、メモリリークを分離するために、生成されたヒープダンプを活用する方法についてお話しします。問題の発見ステップ2では、頻繁にヒープダンプを生成しましたが、必要なのは少なくとも3カ所です。理由はすぐに分かるでしょう。開発者ツールのナビゲータの”Profiles”タブを選び、画面の一番下にある”Load”ボタンを押します。採取した最初のヒープダンプを探して、選択します。以下の手順に従って、ヒープダンプをChromeのビューにロードします。続けて、残りの2つのファイルもビューにロードします。採取したヒープダンプの最後の2つを使っても構いません。重要なのは、採取した順番通りにヒープダンプをロードすることです。”Profiles”タブは以下のようになっていると思います。ヒープの容量が時間の経過とともに少しずつ増えていることが、上の図からお分かりになるでしょう。ヒープダンプ法ヒープダンプをロードしたら、”Profiles”タブにサブビューがたくさん増えているのを目にして、途方に暮れてしまうでしょう。その中で特に役立つと思ったビューが1つあります。最後に採取したヒープダンプをクリックするとすぐに、”Summary”ビューに移動します。”Summary”ドロップダウンの左側に、”All”と書かれた別のドロップダウンがあるのに気付くはずです。そのドロップダウンをクリックします。heapdump-(最後から2番目のヒープマップ)”を選んでください。これで、最初のヒープダンプと最後から2番目のヒープダンプの間のどこかで割り当てられた全てのオブジェクトが表示されます。ガベージコレクションによって収集されているべきオブジェクトが、最後のヒープダンプの中にいまだに留まっているというこの事実は、問題の原因であり、調査しなければなりません。実際はなかなか見事な機能ですが、あまり直感的な見つけ方ではないし、いとも簡単に見落としてしまいます。少なくとも最初は、”(string)”の様にカッコの中にあるものは無視するサンプルアプリケーションのアウトラインステップが完了した後は、以下のようなビューが作られて終了です。sizeサイズがオブジェクトとその子オブジェクト全体の大きさを表すことにご注意ください。あるべきではない5つのエントリが、私の最新のスナップショットに保たれているようです。code)、(string)、(system)、そしてSimpleClassです。この5つの中で、SimpleClassだけは見覚えがあるでしょう。サンプルアプリケーションの、以下のコードから発生したものだからです。SimpleClass(randomData);最初に(array)や(string)のエントリを調べたいという誘惑にかられるかもしれません。Summaryにある全てのオブジェクトは、コンストラクタの名前でグループ化されています。配列や文字列の場合はJavaScriptエンジン内部のコンストラクタです。あなたのプログラムが、明らかにこれらのコンストラクタで作られたデータを保持し続けていますが、そこに踏み込んで調査しようとすると多くのノイズも得てしまうため、メモリリークの原因を見つけるのがさらに難しくなってしまうのです。飛ばしてしまう方が賢明です。その代わり、サンプルアプリケーションのSimpleClassのコンストラクタのように、明らかに怪しいものがないか調べてみましょう。SimpleClassコンストラクタのドロップダウンアローをクリックし、結果リストから作られたオブジェクトをどれか選んでみます。するとウィンドウの下の方に保持のためのパスが現れます(上の図を見てください)。そこから、leakyData配列がデータを離さないことがとても簡単に検出できます。あなたの使っているアプリケーションが、サンプルほど恵まれていなかったら、内部のコンストラクタ(例えば文字列)を調べ、そこからメモリリークの原因を明らかにする必要があるかもしれません。その場合のコツは、しばしばいくつかの内部のコンストラクタグループに現れる値のグループを特定し、メモリリークの疑いを示す手がかりとして使うことです。サンプルアプリケーションの場合は、ランダムな数字を文字列にしただけに見えるような文字列をいくつも目にするでしょう。それらの保持用のパスを調べれば、Chromeの開発者ツールがleakyData配列を示してくれるでしょう。怪しいメモリリークを特定し、それを修正したら、ヒープの使い方に大きな違いが感じられるはずです。サンプルアプリケーションで、次の行のコメント化を解除したとします。upそしてステップ1で説明したアプリケーションをもう一度実行すると、以下のような結果が出力されます。Heap.もしもこのデータをプロットしたら、以下のようになります。やりました!メモリリークはなくなりました。メモリ使用の最初のスパイクが残っていることに注意してください。これは、プログラムが安定するのを待つ間は普通のことです。分析の際に、このスパイクをメモリリークと解釈しないように注意してください。他のリソースへのリンクChromeの開発者ツールを使ったメモリのプロファイリングこの記事に書いたことのほとんどは、上のビデオから引用しました。この記事が存在する唯一の理由は、私がこのビデオを2週間のコースの間3回繰り返し見て、キーポイント(だと私が信じているもの)を特定しなくてはならなかったからで、他の人たちにはこの発見のプロセスを簡単にしてもらいたいと思ったからです。この記事の補足として、上のビデオを見ることをお勧めします。その他のお役立ちツール-memwatch-nextこれは、また別のステキなツールで、ここで挙げておく価値があると思います。このための論法は、こちらで読むことができます(短い記事ですし、時間を割く価値はあります)。こちらのリポジトリをご覧ください。memwatch-nextを使ってインストールすることができます。2つのイベントと一緒に使います。hd.end();console.log(diff);最後のコンソールログは次のようなものを出力し、どんなタイプのオブジェクトがメモリの中で増えたのかを知らせてくれます。}}すばらしいですね。Developer.chrome.comの、JavaScriptのメモリのプロファイリングhttps://developer.chrome.com/devtools/docs/javascript-memory-profilingこれは必読です。ここで触れた全てのテーマとそれ以上のものをカバーしていますし、より詳しく、より正確に書かれています。Osmaniによるトークをお見逃しなく。デバッグのコツやリソースをたくさん紹介しています。スライドは、こちらから入手できます。サンプルコードは、こちらです。まとめメモリリークを再現し特定しようとする時は、ガベージコレクションを手動で開始させます。--expose-gcフラグを使ってNode.jsを実行し、プログラムからglobal.gc()を呼び出してください。https://github.com/bnoordhuis/node-heapdumpを使って、少なくとも3回、ヒープダンプを採取しましょう。メモリリークを分離させるために、3点ヒープダンプ法を使います。メモリリークがなくなったことを確認します。ツイート翻訳に対するフィードバックがございましたら、メールまたはGitHubのIssueよりお寄せください。Ltd.リクルートグループサイトへ。