使用 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 经常使用场景在异常监控及全链路式日志处理中
相关文章
相关标签/搜索