Node.jsサーバーが「悲鳴」を上げるとき
Node.jsエンジニアの皆さんなら、CPU使用率は低いのに、サーバーが突然クラッシュしたり、レスポンスが極端に遅くなったりする状況に遭遇したことがあるのではないでしょうか。私は以前、約5万行のコードベースを持つリアルタイムデータ処理システムを管理していました。本番環境で起動してから4時間経つごとに、RAMが500MBから4GBまで急上昇し、最終的にOOM(Out of Memory)エラーが発生していました。当時は、コードを一行ずつ調べたり、至る所にconsole.logを仕込んだりしていましたが、それはまさに悪夢でした。
Node.jsの「アキレス腱」は、Event Loopのシングルスレッド機構にあります。同期処理の関数が長く走りすぎたり、データ配列が解放されなかったりするだけで、アプリケーション全体が停滞してしまいます。デフォルトの--inspectやChrome DevToolsも優れたツールですが、初心者にとってはグラフが非常に複雑で視認性が低いのが難点です。そこで私が選んだのがClinic.jsです。
このツールセットは、Doctor、Bubbleprof, Flame、Heapprofilerという4つの専門モジュールで構成されています。これらはパフォーマンスを直感的なチャートとして可視化してくれます。半年間運用してみた結果、Clinic.jsのおかげで、厄介なパフォーマンス問題の根本原因を特定する時間を最大70%短縮できました。
クイックインストール
始めるには、Node.js環境が必要です。どのプロジェクトからでもこの「医師」を呼び出せるように、グローバルにインストールすることをお勧めします。
npm install -g clinic
エラーを顕在化させるための負荷テスト(load test)には、autocannonが欠かせません。これを使えば、数千のリクエストを擬似的に生成し、サーバーの耐久性を確認できます。
npm install -g autocannon
実践的な「診断」プロセス
Clinic.jsは、アプリケーションの症状に応じて複数のツールを使い分けます。ここでは、私が日々のデバッグでどのように活用しているかを紹介します。
1. Clinic Doctor:総合診断
原因不明でアプリが遅いと感じたときは、いつもclinic doctorから始めます。これはCPU、Event Loop Lag、Memory、およびActive Handlesの数を詳細に監視します。
clinic doctor -- node server.js
次に、別のターミナルを開き、autocannonを使ってトラフィックを送り込みます。
autocannon -c 100 -d 20 http://localhost:3000
サーバーを停止(Ctrl+C)すると、Clinic.jsが自動的にブラウザタブを開き、結果を表示します。もしEvent Loopのグラフに赤い高い棒(通常100ms以上)が表示されていれば、メインスレッドをブロックしている重い計算関数が存在している証拠です。
2. Clinic Flame:CPUの「ホットスポット」を特定する
DoctorがCPUのボトルネックを報告した場合は、clinic flameを使ってフレームグラフ(Flamegraph)を作成します。このグラフは、どの関数が最も処理時間を「消費」しているかを明確に示してくれます。
clinic flame -- node server.js
ポイントは、バーの幅に注目することです。バーが広いほど、その関数がCPUを長時間占有していることを意味します。以前、このグラフを見ただけで、ループ内で非常に複雑な正規表現(Regex)関数が繰り返し呼び出されているのを発見したことがあります。
3. Clinic Bubbleprof:非同期処理の遅延をチェックする
Node.jsの強みは非同期I/Oにありますが、Bubbleprofはデータベースや外部API呼び出し時の遅延を調査するのに最適なツールです。
clinic bubbleprof -- node server.js
これは「バブル(泡)」を描画します。バブルが大きいほど、そこでの待ち時間(レイテンシ)が長いことを示します。データベースクエリの部分に巨大なバブルがある場合は、インデックスの確認やクエリの最適化をすぐに行うべきです。
メモリリークを完全に解消する
冒頭で触れたメモリリーク(RAMの溢れ)を修正するために、私はclinic heapprofilerを使用しました。このツールは、Garbage Collectorによってクリーンアップされずにメモリ内に「居座り続けている」オブジェクトを特定します。
実際のケーススタディ: グローバルに宣言されたconst cache = []という配列を発見しました。リクエストごとにオブジェクトがプッシュされる一方で、削除されることがありませんでした。Heapprofilerの結果、この配列がヒープ容量の80%を占めていることが判明しました。1000要素制限のLRU Cacheに変更したところ、RAMは200MB程度で安定しました。
本番環境で使用する際の重要な注意点
非常に強力なツールですが、ユーザーが利用中の本番サーバーで直接 Clinic.jsを実行することはお勧めしません. プロファイリングによって15〜30%のオーバーヘッド(追加のリソース消費)が発生する可能性があるためです。最善の方法は以下の通りです:
- 本番環境と同じ構成のステージング(Staging)環境を構築する。
- 匿名化(sanitize)された実データを使用してエラーを再現する。
autocannonを使用して、障害発生時と同等のトラフィックをシミュレートする。
勘でバグを推測するのはやめましょう. コードを修正する前に、必ず実際の数値を確認してください。Clinic.jsによる視覚的なグラフは、エラーを素早く見つけるだけでなく、チームや上司にシステムの問題を説明する際の「揺るぎない証拠」にもなります。

