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 の一時テーブルと派生テーブルについての簡単な説明

推薦する

LinuxでのDNSサーバーの設定の詳細な説明

1. DNSサーバーの概念インターネットでの通信には IP アドレスの助けが必要ですが、数字に対する...

ab ツールを使用してサーバー上で API ストレス テストを実行します。

目次1 システムスループットの簡単な紹介2 試験方法2.1 クライアントテストツール2.1.1 GE...

WeChatアプレットコンポーネント開発:視覚的な映画座席選択機能

目次1. はじめに1. コンポーネントデータ2. コンポーネントページのレイアウト1. ロゴエリアの...

MySQL データベース監視ソフトウェア lepus の使用上の問題と解決策

lepus3.7 を使用して MySQL データベースを監視中に、次の問題が発生しました。このブログ...

jsはショッピングサイトの虫眼鏡機能を実現します

この記事では、ショッピングサイトの虫眼鏡機能を実現するためのjsの具体的なコードを紹介します。具体的...

MySQL のスローログ監視の誤報問題の分析と解決

以前は、さまざまな理由により、一部のアラームは真剣に受け止められませんでした。最近、休暇中に、すぐに...

Docker で ElasticSearch と Kibana をインストールするためのサンプル コード

1. はじめにElasticsearchは現在非常に人気があり、多くの企業が利用しているため、esを...

WindowsでMysql5.7.17のインストールと起動に失敗する問題を解決する

マシンに初めて MySQL をインストールします。オペレーティングシステムはwin7ですmysqlの...

mysql mycat ミドルウェアの簡単な紹介

1. mycatとはエンタープライズアプリケーション開発のための完全にオープンソースの大規模データベ...

純粋な CSS を使用してユーザーが Web ページのコンテンツをコピーするのを防ぐ方法

序文私自身の個人ブログを入力しているときに、ブログの詳細ページでさまざまなコンテンツをコピーするさま...

JS を使用して航空機戦争の小さなゲームを実装する

この記事の例では、参考のために航空機戦争ゲームを実装するためのJSの具体的なコードを共有しています。...

Linuxのip netnsコマンドを使用してネットワークポートを分離し、IPアドレスを設定します。

1. 分離マーカーを追加します。 ip netns add fd 2. 指定されたネットワーク カ...

MySQL で浮動小数点データを文字データに変換するときに起こりうる問題の詳細な説明

序文この記事は主に、MySQL で浮動小数点型を文字型に変換するときに発生する問題を紹介します。これ...

MySQLでストアドプロシージャをデバッグする最も簡単な方法

同僚から、一時テーブルを使用して変数データを挿入して表示する方法を教わったことがありますが、この方法...

Centos7 に mysql 8.0.13 (rpm) をインストールする詳細なチュートリアル

yum か rpm か? yum によるインストール方法は非常に便利ですが、公式サイトから MySQ...