Nodeはリクエスト追跡にasync_hooksモジュールを使用します

Nodeはリクエスト追跡にasync_hooksモジュールを使用します

async_hooks モジュールは、Node.js バージョン 8.0.0 に正式に追加された実験的な API です。バージョン v8.xx で本番環境にも導入しました。

では、async_hooks とは何でしょうか?

async_hooks は、関連付けられたコールバックを持つオブジェクトである非同期リソースを追跡するための API を提供します。

つまり、async_hooks モジュールは非同期コールバックを追跡するために使用できます。では、この追跡機能はどのように使用すればいいのでしょうか。また、使用中にどのような問題が発生する可能性があるのでしょうか。

async_hooksを理解する

v8.xx バージョンの async_hooks は主に 2 つの部分で構成されます。1 つはライフサイクルを追跡するための createHook で、もう 1 つは非同期リソースを作成するための AsyncResource です。

const { createHook、AsyncResource、executionAsyncId } = require('async_hooks')

constフック = createHook({
 init (asyncId、タイプ、トリガーAsyncId、リソース) {},
 前 (asyncId) {},
 (asyncId) {} の後、
 破棄 (非同期 ID) {}
})
フックを有効にする()

関数fn(){
 コンソールログ(executionAsyncId())
}

const asyncResource = 新しい AsyncResource('demo')
asyncResource.run(fn)
asyncResource.run(fn)
asyncResource.emitDestroy()

上記コードの意味と実行結果は次のとおりです。

  1. 各非同期操作の init、before、after、destroy サイクル中に実行されるフック関数を含むフック インスタンスを作成します。
  2. このフックインスタンスを有効にします。
  3. デモ タイプの非同期リソースを手動で作成します。このとき、init フックがトリガーされ、非同期リソース ID は asyncId、タイプは type (つまり demo)、非同期リソースの作成コンテキスト ID は triggerAsyncId、非同期リソースは resource になります。
  4. この非同期リソースを使用して、fn 関数を 2 回実行します。これにより、before が 2 回、after が 2 回トリガーされます。非同期リソース ID は asyncId で、fn 関数の executeAsyncId を通じて取得された値と同じです。
  5. 破棄ライフサイクル フックを手動でトリガーします。

一般的に使用される async、await、promise 構文やリクエストなどの非同期操作の背後には、これらのライフサイクル フック関数もトリガーする非同期リソースがあります。

次に、init フック関数で、非同期リソース作成コンテキスト triggerAsyncId (親) から現在の非同期リソース asyncId (子) へのポイント関係を作成し、非同期呼び出しを直列に接続して完全な呼び出しツリーを取得し、コールバック関数 (上記のコードでは fn) の executeAsyncId() を通じて現在のコールバックを実行する非同期リソースの asyncId を取得し、呼び出しチェーンから呼び出し元をトレースします。

同時に、init は非同期リソース作成のフックであり、非同期コールバック関数作成のフックではないことにも注意する必要があります。これは、非同期リソースが作成された場合に 1 回だけ実行されます。実際の使用では、どのような問題が発生するでしょうか。

リクエストの追跡

例外のトラブルシューティングとデータ分析を目的として、クライアントから送信されたリクエストのリクエスト ヘッダー内の request-id を、Ada アーキテクチャ Node.js サービスのミッドエンド サービスとバックエンド サービスに送信される各リクエストのリクエスト ヘッダーに自動的に追加したいと考えています。

関数実装のシンプルな設計は次のとおりです。

  1. init フックを使用すると、同じ呼び出しチェーン上の非同期リソースがストレージ オブジェクトを共有できるようになります。
  2. リクエスト ヘッダー内のリクエスト ID を解析し、現在の非同期呼び出しチェーンに対応するストレージに追加します。
  3. リクエストが実行されたときに現在の呼び出しチェーンに格納されているリクエスト ID を取得するように、http および https モジュールのリクエスト メソッドを書き換えます。

サンプルコードは次のとおりです。

定数 http = require('http')
const { createHook, 実行AsyncId } = require('async_hooks')
定数 fs = require('fs')

// 呼び出しチェーンを追跡し、呼び出しチェーン ストレージ オブジェクトを作成します。const cache = {}
constフック = createHook({
 init (asyncId、タイプ、トリガーAsyncId、リソース) {
  if (type === 'TickObject') 戻り値
  // console.log も Node.js の非同期動作であるため、init フックがトリガーされ、同期メソッドを通じてのみログを記録できます。 fs.appendFileSync('log.out', `init ${type}(${asyncId}: trigger: ${triggerAsyncId})\n`);
  // 呼び出しチェーンストレージオブジェクトが初期化されているかどうかを判定します if (!cache[triggerAsyncId]) {
   キャッシュ[triggerAsyncId] = {}
  }
  // 親ノードのストレージを現在の非同期リソースと参照で共有します。cache[asyncId] = cache[triggerAsyncId]
 }
})
フックを有効にする()

// httpを書き換える
定数 httpRequest = http.request
http.request = (オプション、コールバック) => {
 const client = httpRequest(オプション、コールバック)
 // 現在のリクエストに対応する非同期リソースに格納されているリクエストIDを取得し、それをヘッダーに書き込みます
 定数 requestId = キャッシュ[executionAsyncId()].requestId
 console.log('キャッシュ', キャッシュ[executionAsyncId()])
 client.setHeader('リクエストID', リクエストID)

 顧客を返す
}

関数タイムアウト(){
 新しい Promise を返します ((resolve, reject) => {
  setTimeout(resolve, Math.random() * 1000)
 })
}
// サービス http を作成する
 .createServer(非同期(req, res) => {
  // 現在のリクエストのリクエスト ID を取得し、ストレージ キャッシュに書き込みます [executionAsyncId()].requestId = req.headers['request-id']
  //他の時間のかかる操作をシミュレートする await timeout()
  // リクエストを送信 http.request('http://www.baidu.com', (res) => {})
  res.write('hello\n')
  res.end()
 })
 .聞く(3000)

コードを実行して送信テストを実行すると、リクエスト ID が正しく取得できることがわかります。

トラップ

同時に、init は非同期コールバック関数作成のフックではなく、非同期リソース作成のフックであり、非同期リソースが作成された場合に 1 回だけ実行されることにも注意する必要があります。

しかし、上記のコードには問題があります。先ほど紹介した async_hooks モジュールのコードで示したように、非同期リソースは異なる関数を継続的に実行することができ、つまり、非同期リソースは再利用することができます。特に、C/C++ 部分で作成される TCP などの非同期リソースの場合、複数のリクエストが同じ TCP 非同期リソースを使用する可能性があります。その結果、この場合、複数のリクエストがサーバーに到着したときに、初期 init フック関数は 1 回だけ実行され、複数のリクエストの呼び出しチェーン トレースが同じ triggerAsyncId を追跡し、同じストレージを参照することになります。

検証を実行するために、前のコードを次のように変更します。 ストレージ初期化部分では、非同期呼び出しの追跡関係の監視を容易にするために、triggerAsyncId を保存します。

  if (!cache[triggerAsyncId]) {
   キャッシュ[トリガー同期ID] = {
    id: トリガー非同期Id
   }
  }

タイムアウト関数は、最初に長時間操作を実行し、次に短時間操作を実行するように変更されます。

関数タイムアウト(){
 新しい Promise を返します ((resolve, reject) => {
  setTimeout(解決、[1000, 5000].pop())
 })
}

サービスを再起動した後、postman を使用して (curl は各リクエストの後に接続を閉じるため、再現が不可能になるため、curl は使用しないでください)、2 つの連続したリクエストを送信します。次の出力を確認できます。

{ id: 1、requestId: '2 番目のリクエストの ID' }
{ id: 1、requestId: '2 番目のリクエストの ID' }

ストレージの書き込みと読み取りの間に異なる時間がかかる他の操作との複数の同時操作の場合、最初にサーバーに到着した要求に格納された値が、後でサーバーに到着した要求によって上書きされ、前の要求で間違った値が読み取られることがわかります。もちろん、書き込みと読み取りの間に他の時間のかかる操作が挿入されないようにすることはできますが、複雑なサービスでは、このような精神的なメンテナンス方法は明らかに信頼できません。この時点で、この再利用を避けるために、読み取りと書き込みの前に JS が新しい非同期リソース コンテキストに入るようにする必要があります。つまり、新しい asyncId を取得する必要があります。呼び出しチェーンのストレージ部分に次の変更を加える必要があります。

定数 http = require('http')
const { createHook, 実行AsyncId } = require('async_hooks')
定数 fs = require('fs')
定数キャッシュ = {}

定数 httpRequest = http.request
http.request = (オプション、コールバック) => {
 const client = httpRequest(オプション、コールバック)
 定数 requestId = キャッシュ[executionAsyncId()].requestId
 console.log('キャッシュ', キャッシュ[executionAsyncId()])
 client.setHeader('リクエストID', リクエストID)

 リターンクライアント
}

// ストレージの初期化を独立したメソッドに抽出します。async function cacheInit (callback) {
 // await 操作を使用して、await 後のコードが新しい非同期コンテキストに入るようにします。await Promise.resolve()
 キャッシュ[executionAsyncId()] = {}
 // 後続の操作がこの新しい非同期コンテキストに属するようにコールバック実行を使用します。callback() を返します。
}

constフック = createHook({
 init (asyncId、タイプ、トリガーAsyncId、リソース) {
  if (!cache[triggerAsyncId]) {
   // init フックはもはや初期化しません return fs.appendFileSync('log.out', `cacheInit メソッドを使用して初期化されていません`)
  }
  キャッシュ[非同期ID] = キャッシュ[トリガー非同期ID]
 }
})
フックを有効にする()

関数タイムアウト(){
 新しい Promise を返します ((resolve, reject) => {
  setTimeout(解決、[1000, 5000].pop())
 })
}

http
.createServer(非同期(req, res) => {
 // 後続の操作をcacheInitへのコールバックとして渡す
 cacheInitを待機します(非同期関数fn() {
  cache[executionAsyncId()].requestId = req.headers['リクエストID']
  タイムアウトを待つ()
  http.request('http://www.baidu.com', (res) => {})
  res.write('hello\n')
  res.end()
 })
})
.聞く(3000)

コールバックを使用したこの編成方法は、koajs ミドルウェア モデルと非常に一致していることは注目に値します。

非同期関数ミドルウェア (ctx, next) {
 Promise.resolve() を待つ
 キャッシュ[executionAsyncId()] = {}
 次を返す()
}

Node.js v14 の

await Promise.resolve() を使用して新しい非同期コンテキストを作成するこの方法は、常に少し「異端」のように思えます。幸いなことに、NodeJs v9.xx では、非同期コンテキストを作成するための公式実装である asyncResource.runInAsyncScope が提供されています。さらに、NodeJs v14.xx は、非同期呼び出しチェーン データ ストレージの公式実装を直接提供しており、非同期呼び出し関係の追跡、新しい非同期起動ドキュメントの作成、データの管理という 3 つのタスクを直接完了するのに役立ちます。 APIについては詳しくは紹介しません。新しいAPIを直接使用して、以前の実装を変換します。

const { AsyncLocalStorage } = require('async_hooks')
// asyncLocalStorage ストレージインスタンスを直接作成します。非同期ライフサイクルフックを管理する必要がなくなります。const asyncLocalStorage = new AsyncLocalStorage()
定数ストレージ = {
 有効にする(コールバック){
  // 新しいストレージを作成するには run メソッドを使用し、その後の操作は新しい非同期リソース コンテキストを使用するために run メソッドのコールバックとして実行する必要があります asyncLocalStorage.run({}, callback)
 },
 取得 (キー) {
  asyncLocalStorage.getStore()[キー]を返す
 },
 (キー、値) を設定する {
  asyncLocalStorage.getStore()[キー] = 値
 }
}

// httpを書き換える
定数 httpRequest = http.request
http.request = (オプション、コールバック) => {
 const client = httpRequest(オプション、コールバック)
 // 非同期リソースストレージのリクエストIDを取得し、ヘッダーに書き込みます
 client.setHeader('リクエストID', storage.get('リクエストId'))

 リターンクライアント
}

// http を使用する
 .createServer((req, res) => {
  ストレージを有効にする(非同期関数() {
   // 現在のリクエストのリクエスト ID を取得し、ストレージに書き込みます。storage.set('requestId', req.headers['request-id'])
   http.request('http://www.baidu.com', (res) => {})
   res.write('hello\n')
   res.end()
  })
 })
 .聞く(3000)

ご覧のとおり、asyncLocalStorage.run API の公式実装も、第 2 バージョンの実装と構造的に一貫しています。

そのため、Node.js v14.xx では、async_hooks モジュールを使用してリクエスト追跡機能を簡単に実装できます。

ノード リクエスト トラッキングに async_hooks モジュールを使用する方法については、これで終わりです。ノード async_hooks リクエスト トラッキングの詳細については、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Node.js の async モジュールの使用に関する詳細な研究
  • Node.js で非同期関数を使用する方法
  • 一般的な Node.js 非同期関数の概要 (推奨)
  • Nodeは非同期を使用して同時実行を制御する
  • Nodejs 非同期プロセス フレームワーク 非同期メソッド
  • Nodeはasync/awaitに基づいてMySQLをカプセル化します
  • node.js における非同期プログラミングについての簡単な説明
  • ノード Async/Await の詳細な説明: より優れた非同期プログラミング ソリューション
  • Node.js で非同期関数を使用する方法
  • SQLite の Node.js で async/await を使用する方法
  • ノードの簡単な分析、非同期処理モジュールのユースケース分析と一般的な方法の紹介

<<:  Nginx ソースコード調査における nginx 電流制限モジュールの詳細な説明

>>:  MySQL の一時テーブルと派生テーブルについての簡単な説明

推薦する

Docker データ ストレージ ボリュームの詳細な説明

デフォルトでは、コンテナ データの読み取りと書き込みはコンテナのストレージ レイヤーで行われます。コ...

Dockerプライベートウェアハウスの構築とインターフェース管理の詳細な説明

1. レジストリについて公式 Docker ハブは、パブリックイメージを管理するのに適した場所です。...

JavaScript DOMContentLoaded イベントのケーススタディ

DOMContentLoaded イベント文字通り、DOM がロードされた後に実行されます。 win...

Linuxコマンド履歴の調整方法の詳細な説明

Linux システムの bash history コマンドは、以前に実行したコマンドを記憶し、再入力...

VueのSSRサーバーサイドレンダリング例の詳細な説明

サーバーサイドレンダリング (SSR) を使用する理由検索エンジンのクローラーが完全にレンダリングさ...

Springboot アプリケーションを迅速にデプロイするために Docker とアイデアを統合する詳細なプロセス

目次1. はじめに2. 環境とツール3. Dockerをインストールし、リモート接続を構成する4. ...

CSS クリアフロートクリア:both サンプルコード

今日はフロートのクリアについてお話します。フロートのクリアについてお話する前に、フロートとは何かを理...

CSS3 テキストシャドウ text-shadow プロパティの詳細な説明

テキストシャドウ text-shadow プロパティの効果: 1. 右下隅の影、左下隅の影、左上隅の...

CSS でコンテンツが長すぎる問題を解決する方法の詳細な説明

CSS を記述するときに、デザインに存在する重要なケースを忘れてしまうことがあります。たとえば、コン...

ウェブメッセージボード機能を実現するjs

この記事の例では、Webメッセージボードを実装するためのjsの具体的なコードを参考までに共有していま...

Vueはvueメタ情報を使用して各ページのタイトルとメタ情報を設定します。

title: vue は vue-meta-info を使用して各ページのタイトルとメタ情報を設定...

Mac での MySQL と Squel Pro の設定

Node.js の人気に応えて、最近、いくつかのサーバー側機能を実装するために Node.js を使...

JavaプログラミングでJavaScriptの超実用的なテーブルプラグインを書く

目次効果ドキュメント最初のステップステップ2ステップ3ソースコード効果ドキュメント最初のステップta...

Dockerコンテナに入る方法と出る方法

1 Dockerサービスを開始するまず、docker サービスを開始する方法を知っておく必要がありま...

MySQL Innodb ストレージ構造と Null 値の保存の詳細な説明

背景:テーブルスペース: すべての INNODB データはテーブルスペース (共有テーブルスペース)...