メモリ使用量増大のトラブルシューティング (Node.js)

問題

New Relic Node.js エージェントをインストール後、Node.js アプリのメモリ使用量が増大した。

対策

このメモリ増加にはいくつかの原因が考えられ、それぞれの解決方法が考えられます。

Node.js エージェントのバージョンを 1.18.0 以上にアップグレードする
Node.js エージェントのバージョン 1.18.0 では、TLS 接続中に発生するNode.js のメモリリークの緩和策を導入しました。

Node.js Core には TLS 接続を含む著しいメモリリークがあります。証明書を指定しているクライアントは、New Relic エージェントを追加すると、このリークについてすぐに分かるでしょう。1.18.0 以降のバージョンでは、利用できればデフォルトのクライアント証明書を使ってこの問題を緩和します。 (HTTPS プロキシでカスタム証明書を使用する場合など) TLS のメモリリーク回避策を適用できない場合は、新しいログメッセージが出力されます。

SSL が原因による増加
Node.js Core の TLS によるメモリリークを減らすには、SSL を無効にします。

SSL を向こうにする場合は、手動で、port80 に設定してください。

TLS メモリバッファ割り当てによる増加
Node.js アプリが SSL や HTTPS などの暗号化方式を初めて使うと、スラブバッファが作成されます。このバッファのデフォルトサイズは 10 MB です。

別のルーターレイヤーでインバウンドリクエストの SSL Termination が発生する環境で実行されているアプリでは、通常、このオーバーヘッドは発生しません。Heroku や AWS のようなクラウドサービスでは、このようなことがしばしば起こります。Node.js エージェントは、HTTPS 経由で New Relic サービスに送信データを送信します。よって、スラブバッファの割り当てがトリガーされます。

対策:

場合によっては、スラブバッファをデフォルト値の 10 MB よりも小さくできます。

スラブバッファのサイズを調整するには、tls.SLAB_BUFFER_SIZE を変更します。

New Relic エージェントを使う場合は、スラブバッファサイズを 128 KB 未満に設定しないでください。 SSL、HTTPS、その他の形式の暗号を使ってサービスやクライアントと通信するアプリケーションでは、スラブバッファの割り当てを減らすべきではありません。

クラスタワーカーのスラブ割り当てによる増加
Node.js はCluster モジュールを提供しています。これにより、サーバー上で使用可能な全プロセッサコアを使って、リクエストを並行処理できます。ただし、各クラスタワーカーは SSL トランザクション用に自身のスラブバッファを割り当て、Node.js エージェントのデータのコピーを各自保持します。これにより、メモリのオーバーヘッドに、使用済みクラスタワーカーの数が乗算されます。

また、サーバーが複数の Node.js アプリを同時に実行する場合も同様です。
対策:
クラウドサービスプロバイダの中には、ある時点で、実際に使用できるよりも多くのプロセッサコアを表示するサーバー環境を使っています。クラスタワーカーの数を減らすか、Cluster のサポートなしで実行すると、パフォーマンスに影響を与えることなくメモリ使用量が減少することがあります。

ディスクに保存されたログメッセージの増加
ログメッセージはデフォルトでディスクに記録されます。メッセージがどのように処理されるかにより、メッセージオブジェクトはガベージコレクション用に Old 領域に移動することがあります。これは、たとえオブジェクトへの参照がなくなっても、オブジェクトがしばらくの間メモリ内に留まることを意味します。これにより、プロセスによって消費されるメモリ量が増加します。追加でかかる処理時間は、ガベージコレクションにも使用されます。

対策:

Node.js のバージョンに応じて、エージェントは traceinfo ログレベルをデフォルトに設定することがあります。ガベージコレクションに費やされたメモリ使用量と時間を大幅に削減するには、冗長なログレベルを infowarn レベルに変更します。

MongoDB カーソルのリークに起因する増加
データベースドライバー多くは、cursor と呼ばれる抽象化されたインタフェースを使っています。カーソルは、クエリーの結果を反復処理できる機能を提供しています。たとえば、mongodb ドライバはー find クエリの実行時にカーソルを提供します。

カーソルは、Node.js ランタイムのオブジェクトとしても MongoDB サーバーのエンティティとしても存在します。アプリがカーソルの使用を終了すると、アプリはサーバーとクライアントアプリの両方のリソースを解放するためにカーソルを閉じる必要があります。

Node.js では、サーバー内のカーソルを閉じることなく、カーソルをガベージコレクトしてアプリ内のリソースを解放できます。これはアプリは気づかないかもしれませんが、New Relic の Node.js エージェントは、オープンカーソルを追跡し、結果の反復処理時間を測定します。アプリが使用する全カーソルを閉じないと、エージェントは古いカーソルを追跡しつづけるため、メモリをリークし続けます。

対策:

アプリがクエリーの結果処理を終了後、cursor.close() を呼び出すことによって、アプリが作成した全カーソルを確実に閉じられることができます。

エージェントのデータストレージによる増加
Node.js エージェントはアプリが処理する各トランザクションのデータを記録します。データは通常、トランザクション名でグループ化されます。毎分1回の収穫サイクルごとに、記録された(収集した)トランザクションの種類が増えるほど、エージェントが使用するメモリー量は増加します。

さらに、各トランザクションには大量のデータが保持されます。ただし、トランザクションが完了すると最終的に破棄されます。アプリが処理する同時トランザクション数が増えると、エージェントが使用するメモリー量は増加します。

対策:

メモリ使用量の増加の原因が、エージェントのデータストレージであると判明している場合は、サーバーにメモリーを追加するか、ハイスペックなサーバーインスタンスに切り替えてください。

関連情報

関連する情報は以下のとおりです。