メモリリーク。一言でプログラマを死に追いやる恐怖の言葉。C/C++の世界ではmallocしたのにfreeしないとかのケアレスミスでよく起きていた問題です。その後、ガベージコレクタが掃除してくれるプログラミング言語が増え、一部の言語で循環参照に気をつけるぐらいであまり気にしなくても良い的な風潮になっています。
というものの、そうとも言ってられなくない状況も増えてきています。クラウドのスケールアウトブームも一段落というかコモディティ化し、go言語で再び性能向上方面に関心が寄せられたり、日本でErlangの勉強会が満席になったり、スケールアウトから再びスケールアップ方面に話題が移りつつあるのを感じます。長時間稼働のサーバで、スケールアップしてさらに数多くのリクエストを大量に受けるようになると、少しのリークでも大問題になります。
僕は現在はnode.jsを頻繁に使っています。node.jsはV8のおかげで、コンパイル言語から比べると遅いかもしれないけど、他のスクリプト言語の中では高速な部類に入りますし、今後もさまざまな場所で使われていくと思います。このエントリーではnode.jsのメモリリークの発見の方法について紹介します。
node.jsの基本的なメモリリークの発見方法
メモリリークの発見を難しくしてしている要因は、クロージャと無名関数です。
ガベージコレクタを搭載した言語のメモリリークは、「変数を持ち続けているために、リンクが残り続けてしまう」のが原因です。Chromeの開発者ツールを使うと、その時のオブジェクトのスナップショットを取得できてクラス情報も得られます。クラス名が得られますし、定義したクラスを使っている箇所は何箇所かに絞れることが多いので、比較的原因箇所を見つけるのは難しくないと思います。
やっかいなのは、クロージャです。クロージャは、構文的なつながりはなくとも、内部から参照していればその外の変数をキャプチャします。クロージャが残っていれば、そのキャプチャされている変数も残り続けます。たとえその変数がローカル変数であってもです。実際にそれが無名関数として定義されていれば(そのケースが多いはず)、難易度は上がります。具体的に見て行きましょう。
node-webkit-agentで増加したオブジェクト一覧を見る
node.jsでクロージャを含めたメモリリークを発見する方法や、補助ツールなどはいくつもあります。その中で、いろいろ比較してみて、僕が一番便利に感じたのが、node-webkit-agentです。$ npm install node-webkit-agentしたら、実際にデータを取りたいアプリに以下の行を追加します。
var agent = require('webkit-devtools-agent');
次にそのアプリを起動します。アプリが起動したら、Macなら$ kill -USR2 プロセスID、Linuxなら$ kill -SIGUSR2 プロセスIDを実行し、Chromeで、このURLを開きます。 こんな感じのページが開くはずです。
負荷試験などを実行してヒープのスナップショットを一度とって、再度負荷試験をかけ、その後またスナップショットを撮ります。
下部のSammaryと書かれている箇所をクリックしてComparisonに変更すると、他のスナップショットと比較した情報が見られます。結果はクラスごとに整理して出してくれますし、どれだけ増えて、どれだけ減って、トータルの増減がどれくらいかも記録されています。これさえあればメモリリークの確認はできるはず!
そうは問屋がおろしません。
(closure)と書かれた項目を見てみると、みんな無名関数です。開いてみると、この中でキャプチャしている変数が見られますが、よっぽど他にないローカル変数名を使っていない限り、実際にどこで実行されている文かをひと目で確認することができません。share/scriptと見ていくと、定義されたファイルはわかりますが、無名関数なんてものは1ファイル中に数十個使われていることもざらなので、特定は難しいです。基本的には同じクロージャが何個も残っていれば「これはリークだ」と判断しやすいのですが、場合によっては数千を超える増加量になるのに、ツリーを毎回開いて比較してみないと分からないし、開いても分からないことあるので、リークしてものがどれかを特定するには時間がかかってしまいます。
無名関数にはadd-func-name
ここで登場するのが、add-func-nameというツールです。名前はださいですが、きちんと結果を出す奴です。このツールを、$ add-func-name --exclude=test --output=src2 srcのように実行します。そうすると、src以下のJSファイルをパースして、無名関数を見つけては名前を追加します。このコマンドラインでは、出力ファイル群を保存するために別フォルダを指定しています。上書きもできますが、簡単に戻せるように別フォルダにしておいて使うのがオススメです。
このツールは、即時実行関数とか、オブジェクト表記内部の関数や、変数に代入されている無名関数とか、すべての関数を探しだします。evalとか、new Functionで動的に作っていない限りはすべて変換されます。それではこのツールを適用し、先ほどのステップを再度繰り返してみます。こんどはどうでしょう?
関数名に、プリフィックスと数字がついています。数字は重複なく設定されるため、一意に特定できます。ディスク上のソースコードを変更する方式のため、ソースコードをagとか何らかのツールで調べればその関数が定義されている箇所がすぐに見つかります。ツリーを開いて探していくのに比べたら、10倍以上の早さでリークしているクロージャを発見できるでしょう。
