[NodeJS] 3点ヒープダンプ法を用いたメモリリークの調査を行う
こんにちは、@yoheiMuneです。
最近仕事でNodeJSのメモリリーク調査を行うことがあったので、その手順をブログに残しておきたいと思います。
メモリリークの調査では「増え続ける変数(=メモリ)は何かを特定する」ことが目的です。
https://github.com/yoheiMune/node-playground/tree/master/008_memory_leak
上記のオプションを付与して、アプリを実行します。
今回のプログラム(
そうすると以下のように1つ目が取得できます。
続いてまた「Profiles」から2回目以降も取得して合計3つを取得します。
そうすると、「1回目と2回目の間にメモリが生成されたもののうち、3回目にもまだ残っている(GCされていない)もの」が表示されます。
まず目に付くのは
そのほかにもポチポチと開いていくと、ランダム文字列も目につきます。
このように気になる変数や値を見つけて、コードを見返してみると、以下のようにメモリリークの箇所が見えてきます。
それらの情報から、今回は
http://postd.cc/simple-guide-to-finding-a-javascript-memory-leak-in-node-js
最後になりますが本ブログでは、Node.js・Python・Linux・フロントエンド・インフラ・Go言語・開発関連・Swift・Java・機械学習など雑多に情報発信をしていきます。自分の第2の脳にすべく、情報をブログに貯めています。気になった方は、本ブログのRSSやTwitterをフォローして頂けると幸いです ^ ^。
最後までご覧頂きましてありがとうございました!
最近仕事でNodeJSのメモリリーク調査を行うことがあったので、その手順をブログに残しておきたいと思います。
目次
メモリリークとは
メモリリークとは、プログラム稼働(サーバー稼働)していて、どんどんとメモリを使ってしまう不具合です。原因としては、ルート(NodeJSの場合はglobal
変数、クライアントJSだとwindow
変数)から参照できる変数が増えて残り続けることで、GC(ガベージコレクション)でメモリを解放できないことが原因です。詳しくはこちらのブログを参照ください。メモリリークの調査では「増え続ける変数(=メモリ)は何かを特定する」ことが目的です。
事前準備
メモリリーク調査のために、いくつかの事前準備を行います。NodeJSのv6.3以上を準備する
この記事では、NodeJSの--inspect
オプションを利用するために、v.6.3.0
以上のバージョンが必要です。nvm
などでインストールしてください(執筆時点だと7.5.0
が最新のよう)。$ nvm install 7.5.0nvmの導入や使い方は「nvmをインストールや利用など」をご覧ください。
(任意)Pythonの準備
後ほどグラフ表示を行いますが、もしやりたい場合にはPythonをインストールします。またmatplotlib
というグラフ描画ライブラリを使うので導入しておきます。$ pip install matplotlibMacの方で
matplotlib
のインストールでエラーが発生する場合はこちらの記事が役立つかもしれません。メモリリーク用のサンプルアプリ
以下におきましたので、適宜ご利用ください。https://github.com/yoheiMune/node-playground/tree/master/008_memory_leak
// app_memory_leak.js のアプリ部分 // どこかにメモリリークがある. const leak = []; class MyClass { constructor(str) { this.str = str; } } function func1(val) { leak.push(val); } setInterval(() => { let randomData = Math.random().toString(); func1(new MyClass(randomData)); }, 10);この記事では、上記の内容を対象に調査を行います。
ガベージコレクションを行うコードの追加
メモリリークの調査では、ヒープダンプ(=メモリの使用状況)を複数回取得して、その差分を調査します(詳細は後続の「3点ヒープダンプ法」を参照)。ガベージコレクションで綺麗になったメモリからヒープダンプを取得したいので、GCを定期的に実行するコードを追加します。function checkMemory() { // gc. try { global.gc(); } catch (e) { console.log('You have to run this program as `node --expose-gc app_memory_leak.js`'); process.exit(); } // Check heap memory. const heapUsed = process.memoryUsage().heapUsed; console.log('heapSize: ', heapUsed); } // 1秒ごとに実施 setInterval(checkMemory, 1000);これで綺麗さっぱりしたヒープダンプを取得できるようになり、調査が捗ります。
NodeJSの起動
メモリリークの調査のために、以下のオプションを付与してNodeJSを実行します。オプション | 内容 |
---|---|
--expose-gc | コード内でGCを利用可能にする |
--inspect | ChromeなどでNodeJSのインスペクトできるようにする |
$ node --expose-gc --inspect app_memory_leak.jsそうすると、以下のような出力が行われます。
Debugger listening on port 9229. Warning: This is an experimental feature and could change at any time. To start debugging, open the following URL in Chrome: chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/c9bfcc23-fc0e-47bf-af5a-37158bede690 heapSize: 3045952 heapSize: 3595736 heapSize: 3617984 heapSize: 3395800 heapSize: 3333936 heapSize: 3326776 heapSize: 3335752 heapSize: 3342784 heapSize: 3350088インスペクトのURL(
chrome-devtools://devtools/...
)が表示され、その後に1秒おきに利用中メモリサイズが表示されます。起動しておくと、どんどんとメモリが増えていくことがわかります。(オプション)Pythonでのグラフ表示
上記のプログラムを実行すると、同ディレクトリにstats.json
が表示されます。中身は1秒ごとのメモリ利用サイズです。それを以下のコードで読み込むとグラフを表示できます。import matplotlib.pyplot as plt import json statsFile = open('stats.json', 'r') heapSizes = json.load(statsFile) print('Plotting %s' % ', '.join(map(str, heapSizes))) plt.plot(heapSizes) plt.ylabel('Heap Size') plt.show()グラフ表示のイメージは以下です。
メモリリークの調査
さてここからが本題です。アプリを起動した時に出たChromeへのURL(以下例)を、Chromeで開きます。chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/c9bfcc23-fc0e-47bf-af5a-37158bede690そうすると以下のような画面が表示されます。
3点ヒープダンプ法
今回のメモリリーク調査では3点ヒープダンプ法を用います。ヒープダンプ(=メモリの使用状況)を時間差で3回取得します。そして1回目と2回目の間にメモリが生成されたもののうち、3回目にもまだ残っている(GCされていない)ものを特定します(それがメモリリークの候補です)。今回のプログラム(
app_memory_leak.js
)では、1秒ごとにGCを行なっているので、3回目のヒープダンプでも残っているメモリは、メモリリークの可能性が高いという算段です。ヒープダンプの取得
ヒープダンプを3回取得します。Profilesタブに移動して、「Take Heap Snapshot」を選び、「Take Snapshot」で取得します(Chromeのバージョンによっては違うかも)。そうすると以下のように1つ目が取得できます。
続いてまた「Profiles」から2回目以降も取得して合計3つを取得します。
3点ヒープダンプ法を実施
3つ取得できたら、「Snapshot 3」を選択したのちに、下記のように「Object allocated between Snapshot 1 and Snapshot 2」を選びます。そうすると、「1回目と2回目の間にメモリが生成されたもののうち、3回目にもまだ残っている(GCされていない)もの」が表示されます。
まず目に付くのは
MyClass
という自分で定義したクラスです。いっぱいメモリに残っていることがわかります。そのほかにもポチポチと開いていくと、ランダム文字列も目につきます。
このように気になる変数や値を見つけて、コードを見返してみると、以下のようにメモリリークの箇所が見えてきます。
const leak = []; class MyClass { // ***** メモリリーク候補:MyClass ***** constructor(str) { this.str = str; } } function func1(val) { leak.push(val); } setInterval(() => { let randomData = Math.random().toString(); // ***** メモリリーク候補:ランダムな文字列 ***** func1(new MyClass(randomData)); }, 10);また、インスペクタのMyClassのところを展開してみると、MyClassがランダム文字列を持っていることがわかったり、MyClassは
leak
というグローバル変数から参照されていることもわかります。それらの情報から、今回は
leak = []
でメモリリークしていることがわかります。const leak = []; class MyClass { constructor(str) { this.str = str; } } function func1(val) { // leak.push(val); // ***** コメントアウトした ****** } setInterval(() => { let randomData = Math.random().toString(); func1(new MyClass(randomData)); }, 10);
# メモリ使用量が一定になった $ node --expose-gc app_memory_leak.js heapSize: 3267056 heapSize: 3267208 heapSize: 3267056 heapSize: 3267208 heapSize: 3267056 heapSize: 3267208 heapSize: 3267056 heapSize: 3267208 heapSize: 3267056 heapSize: 3267208 heapSize: 3267248
参考資料
メモリリーク調査については、以下記事を参照しました。ありがとうございます。http://postd.cc/simple-guide-to-finding-a-javascript-memory-leak-in-node-js
最後に
3点ヒープダンプ法はいいですね。実戦で使ったのは初めてでしたが無事に出来て良かったです。上記の参考記事には、非常に助けられました。上記の記事に加えてもう少し書きたいことがあり、今回はブログを書いた次第でした。最後になりますが本ブログでは、Node.js・Python・Linux・フロントエンド・インフラ・Go言語・開発関連・Swift・Java・機械学習など雑多に情報発信をしていきます。自分の第2の脳にすべく、情報をブログに貯めています。気になった方は、本ブログのRSSやTwitterをフォローして頂けると幸いです ^ ^。
最後までご覧頂きましてありがとうございました!