Node.js v8.x 新特性 Async Hook 簡介
Async Hook 的出現簡單來說有兩個目的,一是提供了一個處理非同步任務機制的抽象;二是暴露了方便追蹤 handle objects 生命周期的 Hook。本文主要從以下幾個方面來討論:
- Hook 的起因
- Overview
- Handle Objects
- 一些意外n
Hook 的起因
Node.js 是因非同步的特性而流行,然而非同步的特性本身卻有一定的缺陷。從筆者的角度看來簡單來說有三個:1、思路差異;2、複雜的場景控制困難;3、難以調試/監控。其中的 1、2 在這幾年層出不窮的輪子以及最終 async/await 特性的發布下已經日趨穩定。而第三個問題則是 Node.js 最近兩個大版本中努力的方向。Async Hook 的出現在筆者看來一定程度上完善了非同步的監控機制。
早期 domain 出現的時候就有同學想要按照 domain 的方式設計一個監聽非同步行為的 hook (參見 issue: implement domain-like hooks (asynclistener) for userland),基於這個出現了一個叫做 async-listener 的模塊。該模塊在 process 對象上 pollyfill 了幾個監控非同步行為的介面。而 8.0 中出現的 Async Hook 則可以說是 Node.js 對 async-listener 的官方支持。n
const async_hooks = require(async_hooks);nnsetTimeout(() => {n console.log(first setTimeout id, async_hooks.currentId()); // 2n});nnsetTimeout(() => {n console.log(second setTimeout id, async_hooks.currentId()); // 4n});n
Async hook 對每一個函數(不論非同步還是同步)提供了一個 Async scope,你可以通過 async_hooks.currentId() 獲取當前函數的 Async ID。
const async_hooks = require(async_hooks);nnconsole.log(default Async Id, async_hooks.currentId()); // 1nnprocess.nextTick(() => {n console.log(nextTick Async Id, async_hooks.currentId()); // 5n test();n});nnfunction test () {n console.log(nextTick Async Id, async_hooks.currentId()); // 5n}n
在同一個 Async scope 中,你會拿到相同的 Async ID。n
Overview
const async_hooks = require(async_hooks);nn// 獲取當前執行上下文的非同步的 Async IDnconst cid = async_hooks.currentId();nn// 獲取調用當前非同步的非同步的 Async IDnconst tid = async_hooks.triggerId();nn// 創建一個新的 AsyncHook 實例. 所有回調都是可選項nconst asyncHook = async_hooks.createHook({ init, before, after, destroy });nn// 允許該實例中非同步函數啟用 hook 不會自動生效需要手動啟用。nasyncHook.enable();nn// 關閉監聽非同步事件nasyncHook.disable();nn//n// 以下是可以監控的幾個事件的 callbackn//nn// 對象構造時會觸發 init 事件(此時資源可能還沒初始化完 )n// 因此 asyncId 引用的資源 (resource) 的所有欄位都可能還未填充nfunction init(asyncId, type, triggerId, resource) { }nn// before is called just before the resources callback is called. It can ben// called 0-N times for handles (e.g. TCPWrap), and will be called exactly 1n// time for requests (e.g. FSReqWrap).nfunction before(asyncId) { }nn// after is called just after the resources callback has finished.nfunction after(asyncId) { }nn// destroy is called when an AsyncWrap instance is destroyed.nfunction destroy(asyncId) { }n
Handle Objects
Async Hooks 可以觸發事件來通知我們關於 handle object 的變化。所以要了解 Async Hook 同時也需要了解 handle objects。
Node.js 的核心 API 大部分是用 JavaScript 定義的,然而 ECMAScript 標準卻並沒有規範 JavaScript 要如何使用 TCP socket、讀取文件等等的一些列操作。這些操作是通過 C++ 調用 libuv 和 V8 來實現的,而 node 內部與這些 C++ 層交互的對象被稱之為 handle objects。
8.0 之後使用 Async hook 可以對 handle object 的生命周期進行追蹤:
const fs = require(fs);nconst async_hooks = require(async_hooks);nnlet indent = 0;nnasync_hooks.createHook({n init(asyncId, type, triggerId) {n const cId = async_hooks.currentId();n print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`);n },n before(asyncId) {n print(`${getIndent(indent)}before: ${asyncId}`);n indent += 2;n },n after(asyncId) {n indent -= 2;n print(`${getIndent(indent)}after: ${asyncId}`);n },n destroy(asyncId) {n print(`${getIndent(indent)}destroy: ${asyncId}`);n },n}).enable();nnlet server = require(net).createServer((sock) => {n sock.end(hello worldn);n server.close();n});nnserver.listen(8080, () => print(server started));nnfunction print(str) {n fs.writeSync(1, str + n);n}nnfunction getIndent(n) {n return .repeat(n);n}n
使用 nc localhost 8080 來調用。可以看到 server 端的輸出:
TCPWRAP(2): trigger: 1 scope: 1nTickObject(3): trigger: 2 scope: 1nbefore: 3nserver startednafter: 3ndestroy: 3nTCPWRAP(4): trigger: 2 scope: 0nbefore: 2n TickObject(5): trigger: 2 scope: 2nafter: 2nbefore: 5n SHUTDOWNWRAP(6): trigger: 4 scope: 5nafter: 5ndestroy: 5ndestroy: 2nbefore: 6nafter: 6ndestroy: 6nbefore: 4n TTYWRAP(7): trigger: 4 scope: 4n SIGNALWRAP(8): trigger: 4 scope: 4n TickObject(9): trigger: 4 scope: 4n TickObject(10): trigger: 4 scope: 4nafter: 4nbefore: 9nafter: 9nbefore: 10nafter: 10nbefore: 4nafter: 4ndestroy: 9ndestroy: 10ndestroy: 4n
其中形如 TCPWRAP 的描述則是 handle object,而 scope 即當前非同步操作的 Async ID,如果在當前非同步中又觸發了非同步,那麼新的非同步的 Trigger ID 即當前的 Async ID。當某個非同步即將被執行 before hook 會被觸發,當某個非同步執行完成 after hook 會被觸發,當非同步流程結束則會觸發 destroy hook。
在 Node.js 中一些如 socket 之類的 handle objects 原本並不能準確的確認其釋放,而現在則可以通過 destroy hook 來確認其釋放了。
一些意外
雖然看起來是有前景,然而這個特性還處於試驗中。可能會出現意外或者一些神奇的情況。
比如在 Async Hook 監控的非同步中,console.log 也是基於非同步實現的,所以如果在 Hook 中使用 console.log 來列印信息就會出現死循環。只能使用非常原始的方式在 Hook 中輸出信息,即類似上文中大家看到的直接 fs.writeSync 的方式:n
const fs = require(fs);nconst util = require(util);nnfunction print(...args) {n // use a function like this one when debugging inside an AsyncHooks callbackn fs.writeSync(1, `${util.format(...args)}n`);n}n
Async Hook 是對目前的非同步進行 Hook,如果 Hook 中的代碼如果出現異常, node 的進程會打出 stack trace 然後退出進程。所以不建議在這些 hook 中加入邏輯,如果加入 try/catch 可能會增加正常代碼的維護成本,各位也最好謹慎使用這個試驗性質的黑魔法。n
最後附上一些引用鏈接:
nodejs/diagnostics
context: core module to manage generic contexts for async call chains · Issue #5243 · nodejs/node-v0.x-archive
Node.js v8.1.1 Documentation
推薦閱讀:
※如何使用koa2+es6/7打造高質量Restful API
※Nodejs系列課程,從入門到進階幫你打通全棧
※Node快閃:發布你自己的npm模塊
※基於 Node.js 的聲明式可監控爬蟲網路
