华为云优惠活动, 三年4核16G服务器仅需不到2000块钱 截止到五月底
你可以跟着我的笔记 当我有一台服务器时,我做了什么 来开始维护服务器并搭建应用,将引导你使用 docker 和 k8s 搭建一个自己的服务器开发集群。

# 使用 async_hooks 监听异步资源的生命周期

为什么需要监听异步资源?

在一个 Node 应用中,异步资源监听使用场景最多的地方在于:

  • 异常捕捉时需要提供用户信息,在每次客户端请求中保持一致的用户信息
  • 全链路式日志追踪,设计每次请求的第三方服务、数据库、Redis携带一致的 traceId

下图为 zipkin 根据 traceId 定位的全链路追踪:

zipkin 全链路追踪

我们来看一个在异常处理中配置用户信息的示例:

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.

既然它被用来追踪异步资源,则在每个异步资源中,都有两个 ID:

  • asyncId: 异步资源当前生命周期的 ID
  • trigerAsyncId: 可理解为父级异步资源的 ID,即 parentAsyncId

通过以下 API 调取

const async_hooks = require('async_hooks');

const asyncId = async_hooks.executionAsyncId();

const trigerAsyncId = async_hooks.triggerAsyncId();

更多详情参考官方文档: async_hooks API

# 异步资源

既然谈到了 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: 异步资源回调函数执行后

从以下代码可看出 beforeafter 的位置

setTimeout(() => {
  // after 生命周期在回调函数最前边
  console.log('Async Before')
  op()
  op()
  op()
  op()
  // after 生命周期在回调函数最后边
  console.log('Async After')
})

注意: Promise 无 destory 的生命周期,要注意由此造成的内存泄漏。另外,如果使用 await prom,Promise 也不会有 before/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](https://github.com/shfshanyue/cls-session]

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 常用场景在异常监控及全链路式日志处理中

扫码关注公众号全栈成长之路,并发送

即可在关注期间无限制浏览本站全部文章内容

点击关闭

你也可以在文章关于回复公众号扫码解锁全站的技术实现中获得解锁代码,永久解锁本站全部文章

Last Updated: 3/30/2020, 1:08:07 PM