Node.jsパフォーマンスの`perf_hooks`と`AsyncLocalStorage`による解明
Min-jun Kim
Dev Intern · Leapcell

はじめに
ウェブ開発のペースの速い世界では、Node.jsアプリケーションのパフォーマンスはユーザーエクスペリエンスとビジネスの成功に直接影響します。応答時間の遅延、メモリリーク、または非効率的なコードパスは、ユーザーの不満や収益の損失につながる可能性があります。一般的な監視のためのさまざまなツールが存在しますが、パフォーマンスのボトルネックの背後にある「なぜ」を真に理解するには、多くの場合、特定のコード実行とそれらの操作を取り巻くコンテキスト情報に対する詳細な洞察が必要です。ここでNode.jsの組み込みモジュールであるperf_hooks
とAsyncLocalStorage
が登場し、アプリケーションの動作を計測および監視するための強力で軽量なソリューションを提供します。この記事では、これら2つのモジュールを組み合わせて活用し、深いパフォーマンスの可視性を提供し、開発者がNode.jsアプリケーションの重要な領域を特定して最適化するのを支援する方法を掘り下げます。
パフォーマンス監視の詳細
実践的なアプリケーションに入る前に、使用するコアツールの明確な理解を確立しましょう。
perf_hooks
: このNode.jsモジュールは、Web Performance APIの実装を提供します。performance.mark()
、performance.measure()
、performance.now()
などのメソッドを使用してJavaScriptコードのパフォーマンスを測定できます。カスタムパフォーマンスメトリックの作成や特定の操作の遅延の監視に非常に役立ちます。AsyncLocalStorage
: Node.js 12で導入されたAsyncLocalStorage
は、同じ論理リクエストまたは実行コンテキスト内の非同期操作全体でデータを保存および取得する方法を提供します。非同期コールスタックの代わりにスレッドローカルストレージと考えてください。これは、操作のトレースや、操作がさまざまな非同期コールバックやプロミスにまたがっていても、パフォーマンス測定にコンテキストメタデータ(リクエストIDやユーザーIDなど)を添付するのに重要です。
perf_hooks
とAsyncLocalStorage
を組み合わせる力の根源はその補完的な性質にあります。perf_hooks
はどれだけ長くかかったかを伝え、AsyncLocalStorage
はそれにどれだけ長くかかったかのコンテキストを提供します。これにより、「この特定のリクエストから発信されたあのユーザーのためにgetUserData
を実行するのにどれだけ時間がかかったか?」といった質問に、内部アプリケーションの計測のみに基づいて答えることができます。
perf_hooks
を使用した関数実行の測定
関数の実行時間を測定するためにperf_hooks
を使用する基本的な例から始めましょう。
const { performance, PerformanceObserver } = require('perf_hooks'); // 'measure'イベントをリッスンするPerformanceObserverを作成 const obs = new PerformanceObserver((items) => { items.getEntries().forEach((entry) => { console.log(`Measurement: ${entry.name} - Duration: ${entry.duration.toFixed(2)}ms`); }); // obs.disconnect(); // 一度だけ監視したい場合は切断します }); obs.observe({ entryTypes: ['measure'], buffered: true }); function expensiveOperation(iterations) { let sum = 0; for (let i = 0; i < iterations; i++) { sum += Math.sqrt(i); } return sum; } // 操作の開始をマーク performance.mark('startExpensiveOperation'); // 関数を実行 const result = expensiveOperation(10000000); // 操作の終了をマーク performance.mark('endExpensiveOperation'); // 2つのマーク間の期間を測定 performance.measure('expensiveOperationDuration', 'startExpensiveOperation', 'endExpensiveOperation'); console.log('Operation complete. Result:', result);
このコードを実行すると、PerformanceObserver
はexpensiveOperationDuration
の期間をログに記録します。これはパフォーマンスのボトルネックを理解するための基本的なステップです。
AsyncLocalStorage
でコンテキストを追加する
次に、AsyncLocalStorage
を統合して、パフォーマンス測定にコンテキスト情報を追加しましょう。一般的なシナリオは、非同期フロー全体でrequestId
を追跡することです。
const { AsyncLocalStorage } = require('async_hooks'); const { performance, PerformanceObserver } = require('perf_hooks'); const crypto = require('crypto'); // リクエストID生成用 const asyncLocalStorage = new AsyncLocalStorage(); // PerformanceObserverはそのまま const obs = new PerformanceObserver((items) => { items.getEntries().forEach((entry) => { const context = asyncLocalStorage.getStore(); const requestId = context ? context.requestId : 'N/A'; console.log(`[Request ID: ${requestId}] Measurement: ${entry.name} - Duration: ${entry.duration.toFixed(2)}ms`); }); }); obs.observe({ entryTypes: ['measure'], buffered: true }); function simulateDatabaseCall(delay) { return new Promise(resolve => setTimeout(resolve, delay)); } async function processUserRequest(userId) { // リクエスト固有のデータを保存 const requestId = crypto.randomUUID(); asyncLocalStorage.enterWith({ requestId, userId }); performance.mark('startProcessUserRequest'); console.log(`[Request ID: ${requestId}] Processing request for user: ${userId}`); // 複数の非同期ステップをシミュレート performance.mark('startDatabaseRead'); await simulateDatabaseCall(Math.random() * 100); // DBからの読み取りをシミュレート performance.mark('endDatabaseRead'); performance.measure('DatabaseReadDuration', 'startDatabaseRead', 'endDatabaseRead'); performance.mark('startBusinessLogic'); // 同期または非同期のビジネスロジック await simulateDatabaseCall(Math.random() * 50); // 別の非同期操作 // AsyncLocalStorageからのコンテキストはここでまだ利用可能です const currentContext = asyncLocalStorage.getStore(); console.log(`[Request ID: ${currentContext.requestId}] Executing business logic.`); performance.mark('endBusinessLogic'); performance.measure('BusinessLogicDuration', 'startBusinessLogic', 'endBusinessLogic'); performance.mark('endProcessUserRequest'); performance.measure('TotalRequestProcessing', 'startProcessUserRequest', 'endProcessUserRequest'); console.log(`[Request ID: ${requestId}] Request processed.`); } // 並行リクエストをシミュレート processUserRequest('user-123'); setTimeout(() => processUserRequest('user-456'), 50); setTimeout(() => processUserRequest('user-789'), 100);
この強化された例では:
asyncLocalStorage
を初期化します。processUserRequest
内で、一意のrequestId
を生成し、asyncLocalStorage.enterWith()
を使用してuserId
とともに保存します。このコンテキストは、このenterWith
ブロック内で開始されたすべての後続の非同期操作全体で暗黙的に利用可能になります。PerformanceObserver
コールバックは、measure
イベントが発生したときにasyncLocalStorage.getStore()
からrequestId
を取得し、パフォーマンスメトリックを特定の要求に直接リンクします。await
呼び出しが複数回行われた後でも、asyncLocalStorage.getStore()
を介して利用可能なコンテキストに注意してください。これは、非同期境界を越えて状態を維持する機能を示しています。
このパターンは、デバッグ、A/Bテストのパフォーマンス、マイクロサービス(requestId
を渡す場合)へのリクエストのトレース、およびリクエストタイプまたはユーザーセグメントごとの詳細なパフォーマンスレポートの生成に非常に強力です。
アプリケーションシナリオ
- APIエンドポイントレイテンシ追跡: 各受信APIリクエストにかかる合計時間を測定し、それをリクエストパス、ユーザーID、およびその他の関連リクエストパラメータにリンクします。
- データベースクエリパフォーマンス: 特定のデータベースクエリまたはORM操作を計測して、遅いクエリを特定し、それを発信元リクエストに添付します。
- マイクロサービス間通信: サービスがリクエストIDを持つメッセージを交換する場合、
AsyncLocalStorage
を使用して、メッセージの処理中にそのIDを維持し、パフォーマンスのエンドツーエンドのトレーサビリティを保証できます。 - バックグラウンドジョブ監視: 長時間実行されるバックグラウンドタスクの実行時間とコンテキストデータ(例:ジョブID、ジョブを開始したユーザー)を追跡します。
- A/Bテストパフォーマンス: リクエストコンテキストを知ることで、異なる機能フラグまたはユーザーグループに基づいてパフォーマンスメトリックを分析でき、新機能のパフォーマンスへの影響を評価するのに役立ちます。
結論
perf_hooks
とAsyncLocalStorage
を組み合わせることで、Node.js開発者は、詳細なパフォーマンス監視のための堅牢でネイティブなツールキットを備えます。perf_hooks
はコード実行時間の正確な測定を可能にし、AsyncLocalStorage
は複雑な非同期フロー全体でコンテキストをインテリジェントに維持します。これらを組み合わせることで、高レベルのメトリックを超えて、アプリケーションのパフォーマンスの背後にある「誰が」、「何を」、「いつ」を理解できるようになり、よりターゲットを絞った効果的な最適化につながります。最終的には、より高速で信頼性の高いNode.jsアプリケーションが実現します。このデュオは、高性能なNode.jsサービスを構築することに真剣に取り組んでいる誰にとっても不可欠です。