node使用async_hooks模塊進(jìn)行請(qǐng)求追蹤
async_hooks 模塊是在 v8.0.0 版本正式加入 Node.js 的實(shí)驗(yàn)性 API。我們也是在 v8.x.x 版本下投入生產(chǎn)環(huán)境進(jìn)行使用。
那么什么是 async_hooks 呢?
async_hooks 提供了追蹤異步資源的 API,這種異步資源是具有關(guān)聯(lián)回調(diào)的對(duì)象。
簡(jiǎn)而言之,async_hooks 模塊可以用來(lái)追蹤異步回調(diào)。那么如何使用這種追蹤能力,使用的過(guò)程中又有什么問(wèn)題呢?
認(rèn)識(shí) async_hooks
v8.x.x 版本下的 async_hooks 主要有兩部分組成,一個(gè)是 createHook 用以追蹤生命周期,一個(gè)是 AsyncResource 用于創(chuàng)建異步資源。
const { createHook, AsyncResource, executionAsyncId } = require('async_hooks')
const hook = createHook({
init (asyncId, type, triggerAsyncId, resource) {},
before (asyncId) {},
after (asyncId) {},
destroy (asyncId) {}
})
hook.enable()
function fn () {
console.log(executionAsyncId())
}
const asyncResource = new AsyncResource('demo')
asyncResource.run(fn)
asyncResource.run(fn)
asyncResource.emitDestroy()
上面這段代碼的含義和執(zhí)行結(jié)果是:
- 創(chuàng)建一個(gè)包含在每個(gè)異步操作的 init、before、after、destroy 聲明周期執(zhí)行的鉤子函數(shù)的 hooks 實(shí)例。
- 啟用這個(gè) hooks 實(shí)例。
- 手動(dòng)創(chuàng)建一個(gè)類型為 demo 的異步資源。此時(shí)觸發(fā)了 init 鉤子,異步資源 id 為 asyncId,類型為 type(即 demo),異步資源的創(chuàng)建上下文 id 為 triggerAsyncId,異步資源為 resource。
- 使用此異步資源執(zhí)行 fn 函數(shù)兩次,此時(shí)會(huì)觸發(fā) before 兩次、after 兩次,異步資源 id 為 asyncId,此 asyncId 與 fn 函數(shù)內(nèi)通過(guò) executionAsyncId 取到的值相同。
- 手動(dòng)觸發(fā) destroy 生命周期鉤子。
像我們常用的 async、await、promise 語(yǔ)法或請(qǐng)求這些異步操作的背后都是一個(gè)個(gè)的異步資源,也會(huì)觸發(fā)這些生命周期鉤子函數(shù)。
那么,我們就可以在 init 鉤子函數(shù)中,通過(guò)異步資源創(chuàng)建上下文 triggerAsyncId(父)到當(dāng)前異步資源 asyncId(子)這種指向關(guān)系,將異步調(diào)用串聯(lián)起來(lái),拿到一棵完整的調(diào)用樹(shù),通過(guò)回調(diào)函數(shù)(即上述代碼的 fn)中 executionAsyncId() 獲取到執(zhí)行當(dāng)前回調(diào)的異步資源的 asyncId,從調(diào)用鏈上追查到調(diào)用的源頭。
同時(shí),我們也需要注意到一點(diǎn),init 是異步資源創(chuàng)建的鉤子,不是異步回調(diào)函數(shù)創(chuàng)建的鉤子,只會(huì)在異步資源創(chuàng)建的時(shí)候執(zhí)行一次,這會(huì)在實(shí)際使用的時(shí)候帶來(lái)什么問(wèn)題呢?
請(qǐng)求追蹤
出于異常排查和數(shù)據(jù)分析的目的,希望在我們 Ada 架構(gòu)的 Node.js 服務(wù)中,將服務(wù)器收到的由客戶端發(fā)來(lái)請(qǐng)求的請(qǐng)求頭中的 request-id 自動(dòng)添加到發(fā)往中后臺(tái)服務(wù)的每個(gè)請(qǐng)求的請(qǐng)求頭中。
功能實(shí)現(xiàn)的簡(jiǎn)單設(shè)計(jì)如下:
- 通過(guò) init 鉤子使得在同一條調(diào)用鏈上的異步資源共用一個(gè)存儲(chǔ)對(duì)象。
- 解析請(qǐng)求頭中 request-id,添加到當(dāng)前異步調(diào)用鏈對(duì)應(yīng)的存儲(chǔ)上。
- 改寫(xiě) http、https 模塊的 request 方法,在請(qǐng)求執(zhí)行時(shí)獲取當(dāng)前當(dāng)前的調(diào)用鏈對(duì)應(yīng)存儲(chǔ)中的 request-id。
示例代碼如下:
const http = require('http')
const { createHook, executionAsyncId } = require('async_hooks')
const fs = require('fs')
// 追蹤調(diào)用鏈并創(chuàng)建調(diào)用鏈存儲(chǔ)對(duì)象
const cache = {}
const hook = createHook({
init (asyncId, type, triggerAsyncId, resource) {
if (type === 'TickObject') return
// 由于在 Node.js 中 console.log 也是異步行為,會(huì)導(dǎo)致觸發(fā) init 鉤子,所以我們只能通過(guò)同步方法記錄日志
fs.appendFileSync('log.out', `init ${type}(${asyncId}: trigger: ${triggerAsyncId})\n`);
// 判斷調(diào)用鏈存儲(chǔ)對(duì)象是否已經(jīng)初始化
if (!cache[triggerAsyncId]) {
cache[triggerAsyncId] = {}
}
// 將父節(jié)點(diǎn)的存儲(chǔ)與當(dāng)前異步資源通過(guò)引用共享
cache[asyncId] = cache[triggerAsyncId]
}
})
hook.enable()
// 改寫(xiě) http
const httpRequest = http.request
http.request = (options, callback) => {
const client = httpRequest(options, callback)
// 獲取當(dāng)前請(qǐng)求所屬異步資源對(duì)應(yīng)存儲(chǔ)的 request-id 寫(xiě)入 header
const requestId = cache[executionAsyncId()].requestId
console.log('cache', cache[executionAsyncId()])
client.setHeader('request-id', requestId)
return client
}
function timeout () {
return new Promise((resolve, reject) => {
setTimeout(resolve, Math.random() * 1000)
})
}
// 創(chuàng)建服務(wù)
http
.createServer(async (req, res) => {
// 獲取當(dāng)前請(qǐng)求的 request-id 寫(xiě)入存儲(chǔ)
cache[executionAsyncId()].requestId = req.headers['request-id']
// 模擬一些其他耗時(shí)操作
await timeout()
// 發(fā)送一個(gè)請(qǐng)求
http.request('http://www.baidu.com', (res) => {})
res.write('hello\n')
res.end()
})
.listen(3000)
執(zhí)行代碼并進(jìn)行一次發(fā)送測(cè)試,發(fā)現(xiàn)已經(jīng)可以正確獲取到 request-id。
陷阱
同時(shí),我們也需要注意到一點(diǎn),init 是異步資源創(chuàng)建的鉤子,不是異步回調(diào)函數(shù)創(chuàng)建的鉤子,只會(huì)在異步資源創(chuàng)建的時(shí)候執(zhí)行一次。
但是上面的代碼是有問(wèn)題的,像前面介紹 async_hooks 模塊時(shí)的代碼演示的那樣,一個(gè)異步資源可以不斷的執(zhí)行不同的函數(shù),即異步資源有復(fù)用的可能。特別是對(duì)類似于 TCP 這種由 C/C++ 部分創(chuàng)建的異步資源,多次請(qǐng)求可能會(huì)使用同一個(gè) TCP 異步資源,從而使得這種情況下,多次請(qǐng)求到達(dá)服務(wù)器時(shí)初始的 init 鉤子函數(shù)只會(huì)執(zhí)行一次,導(dǎo)致多次請(qǐng)求的調(diào)用鏈追蹤會(huì)追蹤到同一個(gè) triggerAsyncId,從而引用同一個(gè)存儲(chǔ)。
我們將前面的代碼做如下修改,來(lái)進(jìn)行一次驗(yàn)證。 存儲(chǔ)初始化部分將 triggerAsyncId 保存下來(lái),方便觀察異步調(diào)用的追蹤關(guān)系:
if (!cache[triggerAsyncId]) {
cache[triggerAsyncId] = {
id: triggerAsyncId
}
}
timeout 函數(shù)改為先進(jìn)行一次長(zhǎng)耗時(shí)再進(jìn)行一次短耗時(shí)操作:
function timeout () {
return new Promise((resolve, reject) => {
setTimeout(resolve, [1000, 5000].pop())
})
}
重啟服務(wù)后,使用 postman (不用 curl 是因?yàn)?curl 每次請(qǐng)求結(jié)束會(huì)關(guān)閉連接,導(dǎo)致不能復(fù)現(xiàn))連續(xù)的發(fā)送兩次請(qǐng)求,可以觀察到以下輸出:
{ id: 1, requestId: '第二次請(qǐng)求的id' }
{ id: 1, requestId: '第二次請(qǐng)求的id' }
即可發(fā)現(xiàn)在多并發(fā)且寫(xiě)讀存儲(chǔ)的操作之間有耗時(shí)不固定的其他操作情況下,先到達(dá)服務(wù)器的請(qǐng)求存儲(chǔ)的值會(huì)被后到達(dá)服務(wù)器的請(qǐng)求執(zhí)行復(fù)寫(xiě)掉,使得前一次請(qǐng)求讀取到錯(cuò)誤的值。當(dāng)然,你可以保證在寫(xiě)和讀之間不插入其他的耗時(shí)操作,但在復(fù)雜的服務(wù)中這種靠腦力維護(hù)的保障方式明顯是不可靠的。此時(shí),我們就需要使每次讀寫(xiě)前,JS 都能進(jìn)入一個(gè)全新的異步資源上下文,即獲得一個(gè)全新的 asyncId,避免這種復(fù)用。需要將調(diào)用鏈存儲(chǔ)的部分做以下幾方面修改:
const http = require('http')
const { createHook, executionAsyncId } = require('async_hooks')
const fs = require('fs')
const cache = {}
const httpRequest = http.request
http.request = (options, callback) => {
const client = httpRequest(options, callback)
const requestId = cache[executionAsyncId()].requestId
console.log('cache', cache[executionAsyncId()])
client.setHeader('request-id', requestId)
return client
}
// 將存儲(chǔ)的初始化提取為一個(gè)獨(dú)立的方法
async function cacheInit (callback) {
// 利用 await 操作使得 await 后的代碼進(jìn)入一個(gè)全新的異步上下文
await Promise.resolve()
cache[executionAsyncId()] = {}
// 使用 callback 執(zhí)行的方式,使得后續(xù)操作都屬于這個(gè)新的異步上下文
return callback()
}
const hook = createHook({
init (asyncId, type, triggerAsyncId, resource) {
if (!cache[triggerAsyncId]) {
// init hook 不再進(jìn)行初始化
return fs.appendFileSync('log.out', `未使用 cacheInit 方法進(jìn)行初始化`)
}
cache[asyncId] = cache[triggerAsyncId]
}
})
hook.enable()
function timeout () {
return new Promise((resolve, reject) => {
setTimeout(resolve, [1000, 5000].pop())
})
}
http
.createServer(async (req, res) => {
// 將后續(xù)操作作為 callback 傳入 cacheInit
await cacheInit(async function fn() {
cache[executionAsyncId()].requestId = req.headers['request-id']
await timeout()
http.request('http://www.baidu.com', (res) => {})
res.write('hello\n')
res.end()
})
})
.listen(3000)
值得一提的是,這種使用 callback 的組織方式與 koajs 的中間件的模式十分一致。
async function middleware (ctx, next) {
await Promise.resolve()
cache[executionAsyncId()] = {}
return next()
}
NodeJs v14
這種使用 await Promise.resolve() 創(chuàng)建全新異步上下文的方式看起來(lái)總有些 “歪門(mén)邪道” 的感覺(jué)。好在 NodeJs v9.x.x 版本中提供了創(chuàng)建異步上下文的官方實(shí)現(xiàn)方式 asyncResource.runInAsyncScope。更好的是,NodeJs v14.x.x 版本直接提供了異步調(diào)用鏈數(shù)據(jù)存儲(chǔ)的官方實(shí)現(xiàn),它會(huì)直接幫你完成異步調(diào)用關(guān)系追蹤、創(chuàng)建新的異步上線文、管理數(shù)據(jù)這三項(xiàng)工作!API 就不再詳細(xì)介紹,我們直接使用新 API 改造之前的實(shí)現(xiàn)
const { AsyncLocalStorage } = require('async_hooks')
// 直接創(chuàng)建一個(gè) asyncLocalStorage 存儲(chǔ)實(shí)例,不再需要管理 async 生命周期鉤子
const asyncLocalStorage = new AsyncLocalStorage()
const storage = {
enable (callback) {
// 使用 run 方法創(chuàng)建全新的存儲(chǔ),且需要讓后續(xù)操作作為 run 方法的回調(diào)執(zhí)行,以使用全新的異步資源上下文
asyncLocalStorage.run({}, callback)
},
get (key) {
return asyncLocalStorage.getStore()[key]
},
set (key, value) {
asyncLocalStorage.getStore()[key] = value
}
}
// 改寫(xiě) http
const httpRequest = http.request
http.request = (options, callback) => {
const client = httpRequest(options, callback)
// 獲取異步資源存儲(chǔ)的 request-id 寫(xiě)入 header
client.setHeader('request-id', storage.get('requestId'))
return client
}
// 使用
http
.createServer((req, res) => {
storage.enable(async function () {
// 獲取當(dāng)前請(qǐng)求的 request-id 寫(xiě)入存儲(chǔ)
storage.set('requestId', req.headers['request-id'])
http.request('http://www.baidu.com', (res) => {})
res.write('hello\n')
res.end()
})
})
.listen(3000)
可以看到,官方實(shí)現(xiàn)的 asyncLocalStorage.run API 和我們的第二版實(shí)現(xiàn)在結(jié)構(gòu)上也很一致。
于是,在 Node.js v14.x.x 版本下,使用 async_hooks 模塊進(jìn)行請(qǐng)求追蹤的功能很輕易的就實(shí)現(xiàn)了。
到此這篇關(guān)于node使用async_hooks模塊進(jìn)行請(qǐng)求追蹤的文章就介紹到這了,更多相關(guān)node async_hooks請(qǐng)求追蹤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 深入學(xué)習(xí)nodejs中的async模塊的使用方法
- 在 Node.js 中使用 async 函數(shù)的方法
- nodejs async異步常用函數(shù)總結(jié)(推薦)
- node 使用 async 控制并發(fā)的方法
- Nodejs異步流程框架async的方法
- node基于async/await對(duì)mysql進(jìn)行封裝
- 淺談node.js中async異步編程
- 詳解node Async/Await 更好的異步編程解決方案
- Node.js 中使用 async 函數(shù)的方法
- Node.js如何對(duì)SQLite的async/await封裝詳解
- 淺析node Async異步處理模塊用例分析及常用方法介紹
相關(guān)文章
用node.js寫(xiě)一個(gè)jenkins發(fā)版腳本
這篇文章主要介紹了用node.js寫(xiě)一個(gè)jenkins發(fā)版腳本,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
Node.js實(shí)現(xiàn)分片上傳斷點(diǎn)續(xù)傳示例詳解
這篇文章主要為大家介紹了Node.js實(shí)現(xiàn)分片上傳斷點(diǎn)續(xù)傳示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
NodeJS?GRPC?多個(gè)?.proto?文件的處理步驟
本文教程詳細(xì)介紹了在NodeJS環(huán)境中如何使用gRPC框架處理多個(gè).proto文件,步驟包括安裝依賴、定義.proto文件、生成gRPC代碼、實(shí)現(xiàn)服務(wù)器和客戶端以及運(yùn)行,適用于開(kāi)發(fā)者在構(gòu)建分布式應(yīng)用時(shí)進(jìn)行接口定義和服務(wù)實(shí)現(xiàn)2024-10-10
npm安裝依賴報(bào)錯(cuò)ERESOLVE?unable?to?resolve?dependency?tree的解決方
當(dāng)我們拿到一個(gè)前端項(xiàng)目的時(shí)候,想要把它運(yùn)行起來(lái),首先是要給它安裝依賴,下面這篇文章主要給大家介紹了關(guān)于npm安裝依賴報(bào)錯(cuò)ERESOLVE?unable?to?resolve?dependency?tree的解決方法,需要的朋友可以參考下2023-04-04
nodejs 全局變量和全局對(duì)象知識(shí)點(diǎn)及用法詳解
在本篇文章里小編給大家整理的是一篇關(guān)于nodejs 全局變量和全局對(duì)象知識(shí)點(diǎn)及用法等內(nèi)容,對(duì)此有興趣的朋友們可以學(xué)習(xí)參考下。2021-12-12

