hooks模块进行请求追踪

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() // 改写 http const httpRequest = http.request http.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://www.baidu.com', (res) => {}) res.write('hello\n') res.end() }) .listen(3000)

执行代码并进行一次发送测试,发现已经可以正确获取到 request-id。

陷阱

同时,我们也需要注意到一点,init 是异步资源创建的钩子,不是异步回调函数创建的钩子,只会在异步资源创建的时候执行一次。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wszpjg.html