node使用async_hooks模块进行请求追踪

时间:2021-05-26

async_hooks 模块是在 v8.0.0 版本正式加入 Node.js 的实验性 API。我们也是在 v8.x.x 版本下投入生产环境进行使用。

那么什么是 async_hooks 呢?

async_hooks 提供了追踪异步资源的 API,这种异步资源是具有关联回调的对象。

简而言之,async_hooks 模块可以用来追踪异步回调。那么如何使用这种追踪能力,使用的过程中又有什么问题呢?

认识 async_hooks

v8.x.x 版本下的 async_hooks 主要有两部分组成,一个是 createHook 用以追踪生命周期,一个是 AsyncResource 用于创建异步资源。

const { createHook, AsyncResource, executionAsyncId } = require('async_hooks')const hook = createHook({ init (asyncId, type, triggerAsyncId, resource) {}, before (asyncId) {}, after (asyncId) {}, destroy (asyncId) {}})hook.enable()function fn () { console.log(executionAsyncId())}const asyncResource = new AsyncResource('demo')asyncResource.run(fn)asyncResource.run(fn)asyncResource.emitDestroy()

上面这段代码的含义和执行结果是:

  • 创建一个包含在每个异步操作的 init、before、after、destroy 声明周期执行的钩子函数的 hooks 实例。
  • 启用这个 hooks 实例。
  • 手动创建一个类型为 demo 的异步资源。此时触发了 init 钩子,异步资源 id 为 asyncId,类型为 type(即 demo),异步资源的创建上下文 id 为 triggerAsyncId,异步资源为 resource。
  • 使用此异步资源执行 fn 函数两次,此时会触发 before 两次、after 两次,异步资源 id 为 asyncId,此 asyncId 与 fn 函数内通过 executionAsyncId 取到的值相同。
  • 手动触发 destroy 生命周期钩子。
  • 像我们常用的 async、await、promise 语法或请求这些异步操作的背后都是一个个的异步资源,也会触发这些生命周期钩子函数。

    那么,我们就可以在 init 钩子函数中,通过异步资源创建上下文 triggerAsyncId(父)到当前异步资源 asyncId(子)这种指向关系,将异步调用串联起来,拿到一棵完整的调用树,通过回调函数(即上述代码的 fn)中 executionAsyncId() 获取到执行当前回调的异步资源的 asyncId,从调用链上追查到调用的源头。

    同时,我们也需要注意到一点,init 是异步资源创建的钩子,不是异步回调函数创建的钩子,只会在异步资源创建的时候执行一次,这会在实际使用的时候带来什么问题呢?

    请求追踪

    出于异常排查和数据分析的目的,希望在我们 Ada 架构的 Node.js 服务中,将服务器收到的由客户端发来请求的请求头中的 request-id 自动添加到发往中后台服务的每个请求的请求头中。

    功能实现的简单设计如下:

  • 通过 init 钩子使得在同一条调用链上的异步资源共用一个存储对象。
  • 解析请求头中 request-id,添加到当前异步调用链对应的存储上。
  • 改写 http、https 模块的 request 方法,在请求执行时获取当前当前的调用链对应存储中的 request-id。
  • 示例代码如下:

    const http = require('http')const { createHook, executionAsyncId } = require('async_hooks')const fs = require('fs')// 追踪调用链并创建调用链存储对象const cache = {}const hook = createHook({ init (asyncId, type, triggerAsyncId, resource) { if (type === 'TickObject') return // 由于在 Node.js 中 console.log 也是异步行为,会导致触发 init 钩子,所以我们只能通过同步方法记录日志 fs.appendFileSync('log.out', `init ${type}(${asyncId}: trigger: ${triggerAsyncId})\n`); // 判断调用链存储对象是否已经初始化 if (!cache[triggerAsyncId]) { cache[triggerAsyncId] = {} } // 将父节点的存储与当前异步资源通过引用共享 cache[asyncId] = cache[triggerAsyncId] }})hook.enable()// 改写 httpconst httpRequest = http.requesthttp.request = (options, callback) => { const client = httpRequest(options, callback) // 获取当前请求所属异步资源对应存储的 request-id 写入 header const requestId = cache[executionAsyncId()].requestId console.log('cache', cache[executionAsyncId()]) client.setHeader('request-id', requestId) return client}function timeout () { return new Promise((resolve, reject) => { setTimeout(resolve, Math.random() * 1000) })}// 创建服务http .createServer(async (req, res) => { // 获取当前请求的 request-id 写入存储 cache[executionAsyncId()].requestId = req.headers['request-id'] // 模拟一些其他耗时操作 await timeout() // 发送一个请求 http.request('http://', (res) => {}) res.write('hello\n') res.end() }) }) .listen(3000)

    可以看到,官方实现的 asyncLocalStorage.run API 和我们的第二版实现在结构上也很一致。

    于是,在 Node.js v14.x.x 版本下,使用 async_hooks 模块进行请求追踪的功能很轻易的就实现了。

    到此这篇关于node使用async_hooks模块进行请求追踪的文章就介绍到这了,更多相关node async_hooks请求追踪内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

    声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

    相关文章