メインコンテンツまでスキップ

第19章:Node特有の健康指標② イベントループ遅延 ⏳⚙️🧵✨

「なんか全体がモッサリ…😵‍💫 でもメモリは大丈夫そう…」みたいな時、イベントループ遅延(Event Loop Delay / Lag)が犯人のこと、めっちゃ多いです👀✨ この章では、“詰まり”を数値で検知して、原因に近づく道筋を作ります🧭💡


1) まずイメージ:イベントループは“1人レジ”🧍‍♀️🛒

画像を挿入予定

Node.jsのメインスレッド(イベントループ)は、ざっくり言うと **「順番に処理する1本のレーン」**です🎢

  • ふだん:I/O(DBやHTTP)は待ってる間に別の仕事できる🧵✨
  • でも:**重い同期処理(CPU計算 / 巨大JSON / sync I/O)**が来ると、レーンが詰まる🚧💥
  • 結果:タイマーが遅れる、レスポンスが遅れる、p99が跳ねる🏃‍♂️💨

この「タイマーが遅れて発火する遅れ」を観測するのが、monitorEventLoopDelayです。Node公式の perf_hooks.monitorEventLoopDelay() は、libuvのイベントループに紐づいたタイマー遅延でイベントループの遅れをサンプルし、ナノ秒で報告します⏱️(これがまさに狙い) (nodejs.org)


2) この章のゴール 🎯✨

できるようになればOKです😊🫶

  • ✅ **イベントループ遅延(p50/p95/p99)**を取って、ダッシュボードに出せる
  • 症状 → 原因候補をパターンで当てられる(詰まり探偵🕵️‍♀️)
  • ✅ “とりあえずCPU/メモリだけ見る”から卒業できる🎓✨

3) まず見るべき指標セット 🧰📈

A. イベントループ遅延(Delay / Lag)⏳

  • min / mean / max
  • p50 / p90 / p99(特に p99が超重要🔥)
  • Nodeの monitorEventLoopDelay()IntervalHistogram を返して、min/max/mean/stddevpercentile(99) みたいな値が取れます (nodejs.org)

B. イベントループ利用率(ELU)⚙️

「どれくらい忙しいか」を見る相棒です🤝✨ eventLoopUtilization() は、イベントループが idle / active だった累積時間と、計算済みの utilization(ELU) を返します (nodejs.org) しかも差分計算もできるので、5秒ごとみたいに区切って「今のELU」を作れます (nodejs.org)

Delay = 「遅れてる?」⏳ ELU = 「忙しすぎ?」⚙️ 2つセットで見ると、“なぜp99が悪いのか”が急に分かるようになります😊✨


4) “良い/危ない”の目安(最初の仮置き)🚦✨

最初はこのくらいの温度感でOKです(あとであなたのサービスに合わせて調整🎛️)

例として、イベントループ遅延の健康判定を

  • HEALTHY: p99 < 100ms
  • WARNING: p95 > 50ms or p99 100–250ms
  • CRITICAL: p99 > 250ms みたいに整理してる例があります🧪 (devcenter.upsun.com)

さらにSLOの“出発点”として

  • ELU(p95, 5分) < 0.75
  • イベントループ遅延(p99, 5分) < 200ms みたいな現場寄りの目安も提案されています(あくまでスタート地点ね🙏) (Medium)

5) 実装:イベントループ遅延をTSで計測する 🧪🧰✨

5-1. いちばん素直:monitorEventLoopDelay() を使う⏳

ポイントは2つだけ💡

  • 値はナノ秒(msに直す) (nodejs.org)
  • **resolution(ms)**はサンプリング間隔(細かいほど精密、でも多少コスト) (nodejs.org)
// src/observability/eventLoopDelay.ts
import { monitorEventLoopDelay } from "node:perf_hooks";

const histogram = monitorEventLoopDelay({ resolution: 20 }); // 20msごとにサンプル
histogram.enable();

const nsToMs = (ns: number) => ns / 1e6;

export type EventLoopDelaySnapshot = {
p50Ms: number;
p95Ms: number;
p99Ms: number;
maxMs: number;
meanMs: number;
};

export function sampleEventLoopDelayAndReset(): EventLoopDelaySnapshot {
// 取り方はシンプル:percentile() が使える✨ :contentReference[oaicite:8]{index=8}
const p50 = nsToMs(histogram.percentile(50));
const p95 = nsToMs(histogram.percentile(95));
const p99 = nsToMs(histogram.percentile(99));

const max = nsToMs(histogram.max);
const mean = nsToMs(histogram.mean);

// 1サンプル窓を作りたいのでリセット(運用の好みでOK)
histogram.reset();

return { p50Ms: p50, p95Ms: p95, p99Ms: p99, maxMs: max, meanMs: mean };
}

5-2. 相棒:ELU(Event Loop Utilization)も取る⚙️✨

ELUは「忙しさ」。Node公式いわく、CPU利用率っぽいけど イベントループがevent provider(例: epoll_wait)“の外”にいた割合です (nodejs.org) (つまり 同期で詰まるとELUが1に張り付くこともある…!😇)

// src/observability/elu.ts
import { eventLoopUtilization } from "node:perf_hooks";

let last = eventLoopUtilization();

export function sampleElu(): number {
// 差分が取れるのが便利✨ :contentReference[oaicite:10]{index=10}
const current = eventLoopUtilization(last);
last = eventLoopUtilization();
return current.utilization; // 0.0〜1.0 :contentReference[oaicite:11]{index=11}
}

6) まずは“ログで見える化”して感覚を掴む 👀🪵✨

メトリクス基盤に入れる前に、5秒ごとにログでも全然OKです😊 (このあと第22章でダッシュボードに載せると一気に強くなる💪📊)

// src/observability/loopHealthLogger.ts
import { sampleEventLoopDelayAndReset } from "./eventLoopDelay";
import { sampleElu } from "./elu";

export function startLoopHealthLogger() {
setInterval(() => {
const d = sampleEventLoopDelayAndReset();
const elu = sampleElu();

console.log(
JSON.stringify({
msg: "loop_health",
elu,
eventLoopDelayMs: {
p50: d.p50Ms,
p95: d.p95Ms,
p99: d.p99Ms,
max: d.maxMs,
mean: d.meanMs,
},
})
);
}, 5000);
}

7) “メトリクスとして出す”なら:Prometheusが最短 🏁📈✨

prom-clientcollectDefaultMetrics() を使うと、イベントループ遅延系のメトリクスが最初から揃います🎁✨ 例:nodejs_eventloop_lag_p99_seconds など(秒単位) (tessl.io) サンプリング精度は eventLoopMonitoringPrecision(ms)で調整できます (tessl.io)

// src/metrics/prometheus.ts
import express from "express";
import { collectDefaultMetrics, register } from "prom-client";

collectDefaultMetrics({
eventLoopMonitoringPrecision: 10, // ms(細かいほど精密、コスト増) :contentReference[oaicite:14]{index=14}
});

export function mountMetricsEndpoint(app: express.Express) {
app.get("/metrics", async (_, res) => {
res.setHeader("Content-Type", register.contentType);
res.end(await register.metrics());
});
}

👉 これで nodejs_eventloop_lag_p99_seconds とかが取れます(まずは“あるだけで勝ち”🥳) (tessl.io)


8) OTel派なら:Nodeランタイム計測を自動で生やせる 🌱🧵✨

OpenTelemetry側でも、Nodeランタイム指標の“意味づけ”が進んでいて、イベントループ遅延は nodejs.eventloop.delay.p99(単位s)みたいに 分割されたGaugeとして扱う設計になっています(Nodeがヒストグラム全体を返さない前提) (OpenTelemetry)

また、@opentelemetry/instrumentation-runtime-nodemonitoringPrecision(ms)でELU集計の粒度を決められます (app.unpkg.com)


9) ミニ演習:わざと詰まらせてグラフを動かす 🧪💥📈

「体験」すると一気に腹落ちします😊✨ **“わざとCPUを塞ぐ”**エンドポイントを作って、p99が跳ねるのを見るよ👀🔥

// src/routes/block.ts
import type { Request, Response } from "express";

function busyWait(ms: number) {
const end = Date.now() + ms;
while (Date.now() < end) {
// 何もしない(=イベントループを塞ぐ😱)
}
}

export function blockRoute(req: Request, res: Response) {
const ms = Number(req.query.ms ?? 200);
busyWait(ms);
res.json({ ok: true, blockedMs: ms });
}

観察ポイント👀✨

  • /block?ms=200 を連打すると…

    • eventLoopDelay p99 が上がる
    • ELUが上がる
    • ✅ リクエストレイテンシも悪化(第17章のREDと繋がる)🚦⏱️

10) 症状 → 原因候補(すぐ使える早見表)🧾🔍✨

観測の形👀ありがちな原因😇次の一手🧭
p99だけ尖る🏔️特定リクエストで重い同期処理“遅いリクエスト”をログ/トレースで特定(第28章へ)
p50もp99も全体的に高い😵‍💫恒常的にCPU過負荷、並列やりすぎ仕事量を減らす/並列数を制限/重い処理をオフロード
ELUが高いのにCPUが低め🤔同期ブロック(spawnSync等)でイベントループが止まる同期APIを排除(Node公式の例でもブロックでELU=1になる) (nodejs.org)
maxがたまにドカン💥“たまに来る巨大入力”でJSON/検証/圧縮が重い入力サイズ制限、分割、Worker threads検討(例あり) (devcenter.upsun.com)

11) ちょい上級:uvの内部指標もヒントになる🧠🧪

Nodeには performance.nodeTiming.uvMetricsInfoloopCount / events / eventsWaiting を取れる機能もあります(比較的新しめ) (nodejs.org) 「イベントが溜まってる?」みたいな当たりをつける材料になります🎯✨ (集計タイミングは setImmediate の中が推奨、という注意もあります) (nodejs.org)


12) AI活用(この章向け)🤖💖

そのまま貼って使えるお願い文🎀

  • monitorEventLoopDelayeventLoopUtilization を5秒ごとにサンプルして、JSONログに出すモジュールをTSで作って。p50/p95/p99/max/meanをmsで。」
  • 「イベントループ遅延が悪化した時の原因候補を、“同期処理”観点で優先度つきで10個出して。対策も1行ずつ。」
  • 「/block エンドポイントで詰まりを再現したので、観測ログ例から“どう読めばいいか”を説明して。」

まとめ 🎉✨(この章で一番大事なこと)

  • イベントループ遅延 = Nodeの“詰まり”メーター
  • p99を見れば「ユーザーが痛い瞬間」が見える🔥
  • ELUとセットにすると「忙しすぎ?止まってる?」が判別できる⚙️
  • 実装は perf_hooks だけで始められる(しかも公式が意図を明言してる) (nodejs.org)
  • Prometheusなら prom-client のデフォルトメトリクスで最短導入できる📈 (tessl.io)
  • OTelでも意味づけ(メトリクス名)が整理されてきてる🧵 (OpenTelemetry)

次の第20章(ビジネスメトリクス)に行く前に、もしよければ😊 あなたの題材API(/work /slow /fail)に合わせて、「詰まりが起きやすい処理」候補をこちらで“教材用にいい感じ”に仕込む案も出せますよ🧪💥✨