はじめに
Node.jsのパフォーマンス調査は、Inspectorやプロファイラで「どの関数が重いか」を追える反面、OS側の事情で遅くなっているケースに弱いことがあります。
たとえば、こんな状況です。
- アプリのCPUプロファイルでは特に重い関数が見えないのに、全体が遅い
- Pod/VMのCPU使用率は高くないのに、なぜか待たされる
- DBや外部APIが遅いように見えるが、実はホスト側のネットワークやディスクが詰まっている
- 「たまに数秒固まる」が、アプリ内ログでは説明できない
ここで効いてくるのが、OS側から観測するというアプローチです。
Linuxのperfや、eBPF(bpftrace / BCC / libbpf系ツール)を使うと、アプリの中身を大きく改造せずに次のような事実をつかめます。
- どのプロセス/スレッドがオンCPU(本当にCPU上で実行)していたか
- どれだけコンテキストスイッチ(実行の切り替え)が発生しているか
- パケットやソケット、キューの観点でネットワーク遅延がどこで増えているか
- システムコールやスケジューラの動きから、待ちの正体を推定できるか
このブログは「eBPFの理論を深く」よりも、Node.jsの遅さを“外さず”に切り分けるための、実務寄りの入り口をまとめます。
前提:Linux環境での話が中心です(Ubuntu/Debian/AlmaLinux/RHEL系など)。Mac/Windowsはこのままではできません(VMやWSL2、Linuxサーバーで実施)。
座学
1) Node.jsの“遅い”はOSの言葉で言い換えられる
アプリ内で「遅い」と感じる現象は、OS視点だとだいたい次のどれかに分類できます。
- オンCPU時間が長い:CPU上で実行し続けている(計算が重い/GC/暗号化など)
- ランキュー待ちが長い:CPUは空いていそうでも、スケジューリング都合で待たされている(コンテナのCPU制限・他プロセスの干渉など)
- ブロッキングが多い:I/O待ち(ディスク/ネットワーク/ロック/ページフォールト)
- コンテキストスイッチ過多:スレッド切り替えが頻繁で、実行効率が落ちる(過剰な並行、スレッドプール飽和、ロック競合など)
- ネットワーク遅延:TCP再送、輻輳、キュー詰まり、DNS、TLSなど(アプリから見えづらい)
Node.jsの“イベントループが詰まっている”という話も、OS的には「オンCPUが長い」「ランキュー待ち」「ページフォールト」「GC」「他プロセス干渉」など、複数の理由に分解できます。
2) perf と eBPF のざっくり役割分担
- perf:カーネルが提供する性能計測機能(PMU)を使い、CPUサンプルやスタックを取る“王道”。
- 強い:CPUサンプル、オンCPU解析、フレームグラフ
- 便利:導入が比較的簡単、どのディストリにもだいたいある
- 注意:シンボル解決や権限、コンテナ内実行などでつまずきやすい
- eBPF:カーネル内部で安全に小さなプログラムを動かし、イベントを捕まえる“観測の拡張”。
- 強い:スケジューラ、ネットワーク、システムコール、遅延のヒストグラム化
- 便利:特定条件だけを狙って観測できる(低オーバーヘッドにしやすい)
- 注意:カーネル機能・権限・ツールチェーンの差が大きい(環境依存)
入門のコツは、いきなり難しいことをしないで、まず perf でオンCPU、次に eBPFで“待ちの正体”へ進むことです。
3) Node.jsをOS観測するときの実務的な落とし穴
- コンテナのCPU制限:CPU使用率が低く見えても、cgroup制限で実行できる時間が少ない場合がある
- Nodeのシンボル:JITや最適化の影響でスタックが読みにくいことがある
- 権限問題:perf/eBPFはroot権限やcapabilityが必要なことが多い
- 本番での安全性:計測は短時間・対象限定・オーバーヘッド管理が必須
「取れるからとりあえず全部取る」ではなく、最小の計測で答えに近づく設計が重要です。
ハンズオン
ここでは、以下の3本立てで実際に“OS側から”Node.jsを観測します。
- perfでオンCPU時間(どこでCPUを食っているか)
- perf / eBPFでコンテキストスイッチ(切り替え過多か)
- eBPFでネットワーク遅延(TCPの遅延・再送の気配)
前提環境(目安)
- Linux(できればカーネル 5.x 以降)
- Node.jsで動く何か(HTTPサーバーでもバッチでも)
- ツール:
perf(パッケージ:linux-tools/perfなど)bpftrace(導入できるなら)- (環境によっては)BCCツール群(
bcc-tools/bpfcc-tools)
ここでのコマンドはディストリ差があります。まずは「入るもの」から使うのが現実解です。
0) 観測対象のPIDを固定する
まず対象のNodeプロセスPIDを取ります。
pidof node
# もしくは
ps aux | grep node
以降、$PIDとして進めます。
1) perfでオンCPU時間を掴む(最初にやるべき)
1-1) まずは軽く:perf top
いまCPUを食っている箇所を“その場で”眺めます。
sudo perf top -p $PID
ここで見たいのは、
- Node/V8由来っぽいシンボルが上位にいるか
libcryptoやzlibなどネイティブライブラリが支配していないか- カーネル側(
tcp_*やcopy_user_*など)が妙に多くないか
「CPUが原因かどうか」の一次判定に便利です。
1-2) 証拠を残す:perf record → report
短時間だけサンプリングして、後で落ち着いて解析します。
sudo perf record -F 99 -p $PID -g -- sleep 15
sudo perf report
-F 99:サンプル頻度(高すぎると負荷が上がる)-g:コールスタック取得
ここでオンCPUの“犯人”が見えたら、Node側の改善(同期処理を減らす、重い変換を避ける、ワーカー化など)へ繋げられます。
1-3) フレームグラフ(可能なら)
環境により手順が増えますが、フレームグラフは“誰が時間を食っているか”が直感的です。
導入が重い場合は無理にやらず、perf reportで十分戦えます。
2) コンテキストスイッチを観測する(“CPUは空いてるのに遅い”の正体)
2-1) まずは全体傾向:perf stat
コンテキストスイッチの発生量を見ます。
sudo perf stat -p $PID -- sleep 10
出力に context-switches が出ます。ここが異常に多い場合、
- スレッド切り替え過多
- ロック競合や待ちが多い
- ワーカー/スレッドプールの使い方が悪い
などが疑えます。
2-2) もう少し踏み込む:スケジューリングの気配を見る
環境によっては、perf sched が使えます。
sudo perf sched record -p $PID -- sleep 10
sudo perf sched timehist
ここで「走りたいのに走れない(待たされている)」が見えるなら、
- コンテナのCPU quota
- 同居プロセスの干渉(ノイジーネイバー)
- IRQ/softirqの偏り
など、Node内だけを見ても分からない理由に当たることが多いです。
実務のありがちパターン:アプリは軽いのに、ホストの別プロセスが暴れていてランキュー待ちが伸びる。
2-3) eBPFで“切り替えイベント”を数える(bpftraceがある場合)
bpftraceが入るなら、Nodeプロセスに対してスケジューリングを観測できます(環境差が大きいので、入門では“できたらラッキー”くらいでOK)。
例(イメージ):対象PIDのスイッチイン/アウトをカウント
sudo bpftrace -e 'tracepoint:sched:sched_switch { @[comm] = count(); }'
これをそのまま使うより、実務では「Nodeだけ」「特定条件だけ」に絞り込みます。入門段階では、まず “スイッチが多い”という事実を掴むのが目的です。
3) ネットワーク遅延をOS側から見る(TCP再送・輻輳・キュー詰まり)
3-1) まずはTCPの健康診断:ss
TCPの状態はssでざっくり把握できます。
ss -ti
接続ごとの rtt(往復遅延)や再送の気配、輻輳ウィンドウなどが見えることがあります。
「外部APIが遅い」と言われているのに、実は rtt が跳ねていた、というような切り分けに使えます。
3-2) 再送が増えていないか(パケットロスの疑い)
ツールが許せば nstat や netstat -s で統計を見るのも手です。
nstat
# または
netstat -s | grep -i retrans
再送が多いなら、アプリ最適化ではなく、ネットワークの問題(経路、MTU、NIC、キュー、帯域、輻輳)が本命になってきます。
3-3) eBPFで“遅延イベント”を拾う(可能なら)
eBPFが強いのは、カーネル内部のネットワークイベント(TCP再送や遅延)を直接拾える点です。
環境によっては bcc-tools に「TCP遅延」「再送」系のツールが同梱されています(ディストリにより名称が違います)。
ここは環境差が非常に大きいので、入門の現実解としては:
- まず
ss/nstat/netstat -sで統計を見る - さらに必要なら、運用環境に合う eBPFツール(bcc-tools / bpftraceスクリプト / libbpf系)を選定する
という順に進めるのが事故りにくいです。
実務での勝ちパターン:
「アプリが遅い」→ OS統計で再送が増えてる → ルータ/回線/MTU/輻輳を疑う → アプリ改修を無駄にしない
4) どの結果が出たら何を疑うか(解釈の早見)
- perfでオンCPUが支配的
- Nodeの同期処理、重い変換、暗号、正規表現、GCを疑う
- 対策:処理分割、ワーカー化、アルゴリズム改善、キャッシュ、JITに優しい形へ
- context-switchesが多い / schedで待ちが長い
- CPU quota、同居干渉、スレッド/ワーカープール、ロック競合を疑う
- 対策:CPU割当見直し、プロセス分離、スレッド数の調整、同期設計の見直し
- ネットワーク統計で再送・RTT悪化
- アプリよりネットワーク(輻輳、経路、MTU、DNS、TLS、LB設定)を疑う
- 対策:経路/帯域/MTU、LB/keepalive、接続再利用、タイムアウト設計、リトライ設計
「アプリを速くする」の前に、「何が支配しているか」をOS側で確定させるのがポイントです。
まとめ
Node.jsの診断はアプリ内ツールだけでもできますが、OS側の事情が原因のときにハマりやすいです。perfとeBPF系ツールを入り口として持っておくと、
- CPUが本当に足りないのか(オンCPU)
- 実行したいのに待たされているのか(スケジューリング/コンテキストスイッチ)
- ネットワークが悪いのか(RTT/再送/輻輳)
を、コード変更ほぼなしで切り分けられます。
入門の実務的なおすすめ手順はこれです。
perf top→perf record/reportでオンCPUの犯人を掴むperf stat/perf schedで“待ち”の正体に当たりをつけるss/nstat/netstat -sでネットワークの健康診断- 必要に応じて、環境に合うeBPFツールを選定して深掘り
“遅いから最適化”ではなく、OS観測で原因を外さない。
これが、Node.js運用を一段強くする近道です。
コメントを残す