node.jsでメモリリークが起きたときの対応手順を実例を交えつつ紹介

要因がいくつかあるので結構コツが居る。
実際に起きたことを挙げながらつらつらと。




確実にリークしている(していない)ことを把握する



まず対応後どうなったら正しいのかそもそもメモリリークが起きているのかを知る必要がある。
Node.jsはメモリをギリギリまで溜めて開放するような仕様になっているため注意すること。

手動でガベージコレクションをおこなう


メモリリークが起きているか確認するためには手動でガベージコレクションしてメモリ使用量が増えていることを確認する。
起動時に --expose-gc オプションを扶養することで、global.gc()で手動でガベージコレクションできるようになる。

起動オプション例
node --max-old-space-size=2048 --expose-gc --inspect=28387 --debug-brk app.js -d
app.jsの後に--expose-gcを指定するとglobal.gc()が使用できない。

VSCodeのlaunchオプションはこんな感じ
{
  "type": "node",
  "request": "launch",
  "name": "Launch Program",
  "program": "${workspaceRoot}/app.js",
  "preLaunchTask": "tsc1",
  "protocol": "inspector",
  "runtimeArgs": [
    "--max-old-space-size=2048",
    "--expose-gc"
  ],
  "args": [
    "-d",
  ]
}

メモリ使用量を取得する


メモリの使用量はprocess.memoryUsage()で取得可能
console.log(process.memoryUsage())

external:142573
heapTotal:19955712
heapUsed:14169104
rss:58245120

のように出力される。
微妙に増減するのでそれなりに時間をかけて推移を見るのがいいかと。


external

外部記憶装置の容量
ストレージとか?

heapTotal

ヒープの合計サイズ

heapUsed

プロセスが使用しているヒープサイズ

rss

プロセスが実際に使用しているRAMの総メモリ量
プロセス + 共有ライブラリ らしい

正直イマイチわかっていない。
heapUsedの増減さえ見ておけばいいかと。



メモリリークの原因を探る




セッションについて


var session = require("express-session")

app.use(session({
  secret: "password",
  resave: false,
  saveUninitialized: false
}));


上記は警告が出る。
designed for a production environment, as it will leak
Warning: connect.session() MemoryStore is not


メモリストアだとリークが発生するのでexpress-sessionを使用する場合はDBなどに格納するようにすること。
未使用の場合でもリークが発生しているようなので使わない場合は外すこと。


リソースのクローズ処理


ファイル、DBなどリソースを取り扱い場合close処理が必要になる。
サンプルでたまに正しくCloseしていないケースがあるので注意すること。
ラッパーを作ってそこから呼び出しようにしておけば問題ないが処理がいろいろなところに散らばっていると結構骨。

実際にあったケースだとデータベースのClose処理にミスがあった。
具体的に言うと以下のようにDBアクセス時にCloseを複数回呼ぶ必要があった模様
// 修正前

  stmt.execute(
    param,
    (queryErr, result) => {
      if (queryErr) {
        console.log(queryErr)
      } else {
        const data = result.fetchAllSync()
        // console.log("Affected rows = " + data)
        resolve(data)
      }

      conn.close(() => {
        // console.log("done.")
      })
    })

// 修正後
  stmt.execute(
    param,
    (queryErr, result) => {
      if (queryErr) {
        console.log(queryErr)
        stmt.closeSync()
      } else {
        const data = result.fetchAllSync()
        // console.log("Affected rows = " + data)
        resolve(data)
        result.closeSync()
        stmt.closeSync()
      }

      conn.close(() => {
        // console.log("done.")
      })
    })


リークが発生する最小の構成を探る


処理A,B,C,Dと続くような処理がある場合。
C,Dの処理を除外してメモリの状態を見る。リークしていたらA,Bの処理に問題がある。リークしていない場合A,Bの処理に問題がある。
といった具合に少しずつ絞り込んでいく。
デバッグでも似たようなことをする人は多いのではないかと。



参考




上記のほかガベージコレクタの前後で差分を取るような方法がある。
https://postd.cc/simple-guide-to-finding-a-javascript-memory-leak-in-node-js/
https://www.yoheim.net/blog.php?q=20170205


Nodeそのものがリークしているケースもある模様。
リークに耐えうるような運用を考えるのもありか(定期再起動とか)

2018年3月15日木曜日