使用 async_hooks 監聽異步資源的生命週期

更多文章: Node 進階

爲何須要監聽異步資源?html

在一個 Node 應用中,異步資源監聽使用場景最多的地方在於:node

  • 異常捕捉時須要提供用戶信息,在每次客戶端請求中保持一致的用戶信息
  • 全鏈路式日誌追蹤,設計每次請求的第三方服務、數據庫、Redis攜帶一致的 traceId

下圖爲 zipkin 根據 traceId 定位的全鏈路追蹤:git

zipkin 全鏈路追蹤

咱們來看一個在異常處理中配置用戶信息的示例: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

官方文檔如此描述 async_hooks: 它被用來追蹤異步資源,也就是監聽異步資源的生命週期。後端

The async_hooks module provides an API to track asynchronous resources.api

既然它被用來追蹤異步資源,則在每一個異步資源中,都有兩個 ID:bash

  • asyncId: 異步資源當前生命週期的 ID
  • trigerAsyncId: 可理解爲父級異步資源的 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
複製代碼

async_hooks.createHook

咱們能夠經過 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')
})
複製代碼

async_hooks 調試及測試

調試大法最重要的是調試工具,而且不停地打斷點與 Step In 嗎?

不,調試大法是 console.log

但若是調試 async_hooks 時使用 console.log 就會出現問題,由於 console.log 也屬於異步資源: TickObject。那 console.log 有沒有替代品呢?

此時可利用 write 系統調用,用它向標準輸出(STDOUT)中打印字符,而標準輸出的文件描述符是 1。由此也可見,操做系統知識對於服務端開發的重要性不言而喻。

node 中調用 API 以下:

fs.writeSync(1, 'hello, world')
複製代碼

什麼是文件描述符 (file descriptor)

完整的調試代碼以下:

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 實現

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
複製代碼

小結

本篇文章講解了異步資源監聽的使用場景及實現方式,可總結爲如下三點:

  1. CLS 是基於異步資源生命週期的存儲,可經過 async_hooks 實現
  2. 開啓 async_hooks 後,每個異步資源都有一個 asyncId 與 trigerAsyncId,經過兩者可查知異步調用關係
  3. CLS 經常使用場景在異常監控及全鏈路式日誌處理中
相關文章
相關標籤/搜索