eBPF/perfでNode.jsを“OS側から”観測する入門|オンCPU時間・コンテキストスイッチ・ネットワーク遅延でボトルネックを外さない

はじめに

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を観測します。

  1. perfでオンCPU時間(どこでCPUを食っているか)
  2. perf / eBPFでコンテキストスイッチ(切り替え過多か)
  3. 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由来っぽいシンボルが上位にいるか
  • libcryptozlib などネイティブライブラリが支配していないか
  • カーネル側(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) 再送が増えていないか(パケットロスの疑い)

ツールが許せば nstatnetstat -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/再送/輻輳)

を、コード変更ほぼなしで切り分けられます。

入門の実務的なおすすめ手順はこれです。

  1. perf topperf record/report でオンCPUの犯人を掴む
  2. perf stat / perf sched で“待ち”の正体に当たりをつける
  3. ss / nstat / netstat -s でネットワークの健康診断
  4. 必要に応じて、環境に合うeBPFツールを選定して深掘り

“遅いから最適化”ではなく、OS観測で原因を外さない
これが、Node.js運用を一段強くする近道です。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です