Socket.IOのメモリリーク調査
春頃にSocket.IOを使ったアプリケーションを作ったのだけれど、時間経過とともにメモリ消費が増大するのに悩まされて、結局定期的に再起動という強引な手段で乗り切った。
落ち着いたのでメモリリークの調査でもしようかと思ったところでSocket.IO 1.0がリリースされたので、メモリ消費量について調査したメモ。
tl; dr
- メモリリークなんてなかったので、世界は平和だった
- Socket.IO 1.0では切断後にも回収されないヒープがあるように見える
メモリ問題で悩んでいた頃は0.9系を使っていたのだけれど、基本的な機能を使っている限りメモリリークは確認できなかった。実際には複数台構成での運用だったり、他にもいろいろと機能実装していたので、どこかで書き方にまずい部分があったのだと思う。
0.9.16から0.9.17でのメモリ消費量推移
Socket.IO 1.0でのメモリ消費量調査の前に、0.9系の末期でメモリ消費量がどのように変わっていったかを調査する。
当時、npmでのリリースバージョンは0.9.16で、既に開発の中心は1.0系へと移っていたため積極的なメンテナンスは行われていなかった。2013年10月に、tico8氏による修正が取り込まれてメモリリークが大きく改善されたものの、新しいバージョンとしてリリースされることがなかったため、実運用では直接commitハッシュを指定するなどの小技が求められていた。
参考:Node.js Cluster+Socket.IO+Redisによるリアルタイム通知システム|サイバーエージェント 公式エンジニアブログ
テストは問題切り分けのためいくつかのパターンで実施した。検証用コードはgithubに、結果はGoogle Spreadsheetでそれぞれ公開されている。
- 接続してメッセージを送信し続けるだけのテスト → メモリリークは確認されず
- 接続/切断/再接続を繰り返すテスト → メモリリークは確認されず
- 接続/切断/再接続を繰り返すテスト(RedisStore使用) → パッチ未適用では消費量が増加
0.9.16では、RedisStoreを利用した場合に切断/再接続を繰り返すと、時間経過とともにメモリ消費量が膨れ上がることが確認できる。patchedは、0.9.16リリース後にmergeのみされていたcommitを適用したバージョン(47b06c0)、0.9.17にはこれに複数ノード間でのpub/subで接続数が膨れ上がる問題への対処が含まれている(0.9.17)。
0.9.17から1.0.6でのメモリ消費量推移
1.0系では、通信周りの実装をengine.ioとして分離して、内部構造が大きく変更されている。メモリリークの原因になっていたStore周りもAdapterとして設計変更されているため、不具合については修正されたかどうかというよりも新設計に問題があるかどうかという話になるのかもしれない。
結果は以下の通りとなった。RedisStoreを使った場合のメモリ消費量推移グラフを貼り付ける。
- 接続してメッセージを送信し続けるだけのテスト → メモリリークは確認されず
- 接続/切断/再接続を繰り返すテスト → メモリリークは確認されず
- 接続/切断/再接続を繰り返すテスト(RedisStore使用) → メモリリークは確認されず
全体的にメモリリークの傾向はほとんど確認されなかった。若干ではあるものの、1.0.6ではheapUsedが増加傾向にあるので、逆に新しい問題が内在しているのかもしれない。
1.0系では0.9系と比較してheapUsedはやや多くなっているものの、RSSは半分程度となっており、メモリ消費量としては大きく抑えられているように思われる。
heapUsedの増加についてnode-heapdumpの出力結果を眺めてみたものの、あからさまに残っているようなデータはさすがになかったので、もう少し内部構造の理解が進んでから再度調査をしてみたい。
テストコードについて
テストに用いたコード一式は、以下のリポジトリで公開した。テスト内容ごとにディレクトリが分かれているが、ほぼ同様のコードとなっている。実行方法などはREADME.mdを参照のこと。
castor4bit/socketio-memoryleak-test · GitHub
- 001_default … 接続してメッセージ送信を繰り返すテスト
- 002_reconnect … 接続/メッセージ送信/切断(再接続)を繰り返すテスト
- 003_with_redis … 002_reconnectテストをRedisStore/Adapterで実行する
- 004_with_mongodb … 002_reconnectテストでメッセージ送信の都度MongoDBへの書き込みを行う
0.9系のバージョン別検証は、0.9.17のpackage.jsonを適宜書き換えて実行した。
"socket.io": "0.9.16",
"socket.io": "git://github.com/Automattic/socket.io.git#47b06c0fcfaf95acc07eb8e6cfc6cbeeabea5a00",
グラフ化は、出力されたmemoryUsage()の値をawkで切り出して、Google Spreadsheetにimport→Chart生成を手動でやった。
$ node --expose_gc server.js | tee /tmp/log.txt
$ node client.js
$ awk '{ print $4,$6,$8 }' /tmp/log.txt > /tmp/result.csv