爲何須要監聽異步資源?html
在一個 Node 應用中,異步資源監聽使用場景最多的地方在於:node
下圖爲 zipkin 根據 traceId 定位的全鏈路追蹤:git
咱們來看一個在異常處理中配置用戶信息的示例:github
const session = new Map()
app.use((ctx, next) => {
try {
await next()
} catch (e) {
const user = session.get('user')
// 把 user 上報給異常監控系統
}
})
app.use((ctx, next) => {
// 設置用戶信息
const user = getUserById()
session.set('user', user)
})
複製代碼
當在後端服務全局配置用戶信息,以便異常及日誌追蹤。因爲此時採用的 session 是異步的,用戶信息極其容易被隨後而來的請求而覆蓋,那如何正確獲取用戶信息呢?數據庫
官方文檔如此描述 async_hooks
: 它被用來追蹤異步資源,也就是監聽異步資源的生命週期。後端
The async_hooks module provides an API to track asynchronous resources.api
既然它被用來追蹤異步資源,則在每一個異步資源中,都有兩個 ID:bash
asyncId
: 異步資源當前生命週期的 IDtrigerAsyncId
: 可理解爲父級異步資源的 ID,即 parentAsyncId
經過如下 API 調取session
const async_hooks = require('async_hooks');
const asyncId = async_hooks.executionAsyncId();
const trigerAsyncId = async_hooks.triggerAsyncId();
複製代碼
更多詳情參考官方文檔: async_hooks APIapp
既然談到了 async_hooks
用以監聽異步資源,那會有那些異步資源呢?咱們平常項目中常常用到的也無非如下集中:
Promise
setTimeout
fs
/net
/process
等基於底層的API然而,在官網中 async_hooks
列出的竟有如此之多。除了上述提到的幾個,連 console.log
也屬於異步資源: TickObject
。
FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject
複製代碼
咱們能夠經過 asyncId
來監聽某一異步資源,那如何監聽到該異步資源的建立及銷燬呢?
答案是經過 async_hooks.createHook
建立一個鉤子,API 及釋義見代碼:
const asyncHook = async_hooks.createHook({
// asyncId: 異步資源Id
// type: 異步資源類型
// triggerAsyncId: 父級異步資源 Id
init (asyncId, type, triggerAsyncId, resource) {},
before (asyncId) {},
after (asyncId) {},
destroy(asyncId) {}
})
複製代碼
咱們只須要關注最重要的四個 API:
init
: 監聽異步資源的建立,在該函數中咱們能夠獲取異步資源的調用鏈,也能夠獲取異步資源的類型,這兩點很重要。destory
: 監聽異步資源的銷燬。要注意 setTimeout
能夠銷燬,而 Promise
沒法銷燬,若是經過 async_hooks 實現 CLS 可能會在這裏形成內存泄漏!before
after
setTimeout(() => {
// after 生命週期在回調函數最前邊
console.log('Async Before')
op()
op()
op()
op()
// after 生命週期在回調函數最後邊
console.log('Async After')
})
複製代碼
調試大法最重要的是調試工具,而且不停地打斷點與 Step In 嗎?
不,調試大法是 console.log
。
但若是調試 async_hooks
時使用 console.log
就會出現問題,由於 console.log
也屬於異步資源: TickObject
。那 console.log
有沒有替代品呢?
此時可利用 write
系統調用,用它向標準輸出(STDOUT
)中打印字符,而標準輸出的文件描述符是 1。由此也可見,操做系統知識對於服務端開發的重要性不言而喻。
node 中調用 API 以下:
fs.writeSync(1, 'hello, world')
複製代碼
完整的調試代碼以下:
function log (...args) {
fs.writeSync(1, args.join(' ') + '\n')
}
async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
log('Init: ', `${type}(asyncId=${asyncId}, parentAsyncId: ${triggerAsyncId})`)
},
before(asyncId) {
log('Before: ', asyncId)
},
after(asyncId) {
log('After: ', asyncId)
},
destroy(asyncId) {
log('Destory: ', asyncId);
}
}).enable()
複製代碼
Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads.
CLS
是存在於異步資源生命週期的一個鍵值對存儲,對於在同一異步資源中將會維護一份數據,而不會被其它異步資源所修改。社區中有許多優秀的實現,而在高版本的 Node (>=8.2.1) 可直接使用 async_hooks
實現。
而我本身使用 async_hooks
也實現了一個 CLS: [cls-session](github.com/shfshanyue/…]
const Session = require('cls-session')
const session = new Session()
function timeout (id) {
session.scope(() => {
session.set('a', id)
setTimeout(() => {
const a = session.get('a')
console.log(a)
})
})
}
timeout(1)
timeout(2)
timeout(3)
// Output:
// 1
// 2
// 3
複製代碼
本篇文章講解了異步資源監聽的使用場景及實現方式,可總結爲如下三點: