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


Posted in Javascript onJanuary 28, 2021

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()

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

  1. 创建一个包含在每个异步操作的 init、before、after、destroy 声明周期执行的钩子函数的 hooks 实例。
  2. 启用这个 hooks 实例。
  3. 手动创建一个类型为 demo 的异步资源。此时触发了 init 钩子,异步资源 id 为 asyncId,类型为 type(即 demo),异步资源的创建上下文 id 为 triggerAsyncId,异步资源为 resource。
  4. 使用此异步资源执行 fn 函数两次,此时会触发 before 两次、after 两次,异步资源 id 为 asyncId,此 asyncId 与 fn 函数内通过 executionAsyncId 取到的值相同。
  5. 手动触发 destroy 生命周期钩子。

像我们常用的 async、await、promise 语法或请求这些异步操作的背后都是一个个的异步资源,也会触发这些生命周期钩子函数。

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

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

请求追踪

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

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

  1. 通过 init 钩子使得在同一条调用链上的异步资源共用一个存储对象。
  2. 解析请求头中 request-id,添加到当前异步调用链对应的存储上。
  3. 改写 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 是异步资源创建的钩子,不是异步回调函数创建的钩子,只会在异步资源创建的时候执行一次。

但是上面的代码是有问题的,像前面介绍 async_hooks 模块时的代码演示的那样,一个异步资源可以不断的执行不同的函数,即异步资源有复用的可能。特别是对类似于 TCP 这种由 C/C++ 部分创建的异步资源,多次请求可能会使用同一个 TCP 异步资源,从而使得这种情况下,多次请求到达服务器时初始的 init 钩子函数只会执行一次,导致多次请求的调用链追踪会追踪到同一个 triggerAsyncId,从而引用同一个存储。

我们将前面的代码做如下修改,来进行一次验证。 存储初始化部分将 triggerAsyncId 保存下来,方便观察异步调用的追踪关系:

if (!cache[triggerAsyncId]) {
   cache[triggerAsyncId] = {
    id: triggerAsyncId
   }
  }

timeout 函数改为先进行一次长耗时再进行一次短耗时操作:

function timeout () {
 return new Promise((resolve, reject) => {
  setTimeout(resolve, [1000, 5000].pop())
 })
}

重启服务后,使用 postman (不用 curl 是因为 curl 每次请求结束会关闭连接,导致不能复现)连续的发送两次请求,可以观察到以下输出:

{ id: 1, requestId: '第二次请求的id' }
{ id: 1, requestId: '第二次请求的id' }

即可发现在多并发且写读存储的操作之间有耗时不固定的其他操作情况下,先到达服务器的请求存储的值会被后到达服务器的请求执行复写掉,使得前一次请求读取到错误的值。当然,你可以保证在写和读之间不插入其他的耗时操作,但在复杂的服务中这种靠脑力维护的保障方式明显是不可靠的。此时,我们就需要使每次读写前,JS 都能进入一个全新的异步资源上下文,即获得一个全新的 asyncId,避免这种复用。需要将调用链存储的部分做以下几方面修改:

const http = require('http')
const { createHook, executionAsyncId } = require('async_hooks')
const fs = require('fs')
const cache = {}

const httpRequest = http.request
http.request = (options, callback) => {
 const client = httpRequest(options, callback)
 const requestId = cache[executionAsyncId()].requestId
 console.log('cache', cache[executionAsyncId()])
 client.setHeader('request-id', requestId)

 return client
}

// 将存储的初始化提取为一个独立的方法
async function cacheInit (callback) {
 // 利用 await 操作使得 await 后的代码进入一个全新的异步上下文
 await Promise.resolve()
 cache[executionAsyncId()] = {}
 // 使用 callback 执行的方式,使得后续操作都属于这个新的异步上下文
 return callback()
}

const hook = createHook({
 init (asyncId, type, triggerAsyncId, resource) {
  if (!cache[triggerAsyncId]) {
   // init hook 不再进行初始化
   return fs.appendFileSync('log.out', `未使用 cacheInit 方法进行初始化`)
  }
  cache[asyncId] = cache[triggerAsyncId]
 }
})
hook.enable()

function timeout () {
 return new Promise((resolve, reject) => {
  setTimeout(resolve, [1000, 5000].pop())
 })
}

http
.createServer(async (req, res) => {
 // 将后续操作作为 callback 传入 cacheInit
 await cacheInit(async function fn() {
  cache[executionAsyncId()].requestId = req.headers['request-id']
  await timeout()
  http.request('http://www.baidu.com', (res) => {})
  res.write('hello\n')
  res.end()
 })
})
.listen(3000)

值得一提的是,这种使用 callback 的组织方式与 koajs 的中间件的模式十分一致。

async function middleware (ctx, next) {
 await Promise.resolve()
 cache[executionAsyncId()] = {}
 return next()
}

NodeJs v14

这种使用 await Promise.resolve() 创建全新异步上下文的方式看起来总有些 “歪门邪道” 的感觉。好在 NodeJs v9.x.x 版本中提供了创建异步上下文的官方实现方式 asyncResource.runInAsyncScope。更好的是,NodeJs v14.x.x 版本直接提供了异步调用链数据存储的官方实现,它会直接帮你完成异步调用关系追踪、创建新的异步上线文、管理数据这三项工作!API 就不再详细介绍,我们直接使用新 API 改造之前的实现

const { AsyncLocalStorage } = require('async_hooks')
// 直接创建一个 asyncLocalStorage 存储实例,不再需要管理 async 生命周期钩子
const asyncLocalStorage = new AsyncLocalStorage()
const storage = {
 enable (callback) {
  // 使用 run 方法创建全新的存储,且需要让后续操作作为 run 方法的回调执行,以使用全新的异步资源上下文
  asyncLocalStorage.run({}, callback)
 },
 get (key) {
  return asyncLocalStorage.getStore()[key]
 },
 set (key, value) {
  asyncLocalStorage.getStore()[key] = value
 }
}

// 改写 http
const httpRequest = http.request
http.request = (options, callback) => {
 const client = httpRequest(options, callback)
 // 获取异步资源存储的 request-id 写入 header
 client.setHeader('request-id', storage.get('requestId'))

 return client
}

// 使用
http
 .createServer((req, res) => {
  storage.enable(async function () {
   // 获取当前请求的 request-id 写入存储
   storage.set('requestId', req.headers['request-id'])
   http.request('http://www.baidu.com', (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请求追踪内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript中的new的使用方法与注意事项
May 16 Javascript
javascript支持firefox,ie7页面布局拖拽效果代码
Dec 20 Javascript
javascript实现的在当前窗口中漂浮框的代码
Mar 15 Javascript
jQuery之网页换肤实现代码
Apr 30 Javascript
浅析showModalDialog数据缓存问题(用禁止浏览器缓存解决)
Jul 09 Javascript
倒记时60刷新网页的js代码
Feb 18 Javascript
纯JS前端实现分页代码
Jun 21 Javascript
基于ajax与msmq技术的消息推送功能实现代码
Dec 26 Javascript
vue.js+Echarts开发图表放大缩小功能实例
Jun 09 Javascript
JavaScript实现简单的文本逐字打印效果示例
Apr 12 Javascript
Vue中使用ElementUI使用第三方图标库iconfont的示例
Oct 11 Javascript
JavaScript实现简单计算器功能
Dec 19 Javascript
JavaScript如何实现防止重复的网络请求的示例
Jan 28 #Javascript
JavaScript实现跟随鼠标移动的盒子
Jan 28 #Javascript
vue.js实现点击图标放大离开时缩小的代码
Jan 27 #Vue.js
使用JS实现鼠标放上图片进行放大离开实现缩小功能
Jan 27 #Javascript
vscode自定义vue模板的实现
Jan 27 #Vue.js
vue+echarts实现中国地图流动效果(步骤详解)
Jan 27 #Vue.js
js实现鼠标切换图片(无定时器)
Jan 27 #Javascript
You might like
PHP中for循环语句的几种变型
2006/11/26 PHP
手把手教你打印出PDF(关于fpdf的简单应用)
2013/06/25 PHP
PHP大批量插入数据库的3种方法和速度对比
2014/07/08 PHP
PHP生成指定随机字符串的简单实现方法
2015/04/01 PHP
php实现连接access数据库并转txt写入的方法
2017/02/08 PHP
javascript 面向对象编程 聊聊对象的事
2009/09/17 Javascript
如何使用JS获取IE上传文件路径(IE7,8)
2013/07/08 Javascript
验证码在IE中不刷新而谷歌等浏览器正常的解决方案
2014/03/18 Javascript
js键盘事件的keyCode
2014/07/29 Javascript
jquery+css实现的红色线条横向二级菜单效果
2015/08/22 Javascript
浅谈jquery上下滑动的注意事项
2016/10/13 Javascript
使用prop解决一个checkbox选中后再次选中失效的问题
2017/07/05 Javascript
Angular.js中$resource高大上的数据交互详解
2017/07/30 Javascript
使用nodejs+express实现简单的文件上传功能
2017/12/27 NodeJs
简单了解vue中父子组件如何相互传递值(基础向)
2019/07/12 Javascript
基于Angular 8和Bootstrap 4实现动态主题切换的示例代码
2020/02/11 Javascript
mpvue实现微信小程序快递单号查询代码
2020/04/03 Javascript
vue实现移动端input上传视频、音频
2020/08/18 Javascript
[01:05:29]DOTA2-DPC中国联赛 正赛 PSG.LGD vs Aster BO3 第二场 1月24日
2021/03/11 DOTA
让Python代码更快运行的5种方法
2015/06/21 Python
python itchat实现微信自动回复的示例代码
2017/08/14 Python
Python读取Json字典写入Excel表格的方法
2018/01/03 Python
TensorFlow的权值更新方法
2018/06/14 Python
Python的UTC时间转换讲解
2019/02/26 Python
python3.6生成器yield用法实例分析
2019/08/23 Python
python批量修改xml属性的实现方式
2020/03/05 Python
Python如何在单元测试中给对象打补丁
2020/08/03 Python
浅谈利用缓存来优化HTML5 Canvas程序的性能
2015/05/12 HTML / CSS
教育局长自荐信范文
2013/12/22 职场文书
项目计划书范文
2014/01/09 职场文书
交通违章检讨书
2014/09/21 职场文书
体育教师个人工作总结
2015/02/09 职场文书
给老师的保证书怎么写
2015/05/09 职场文书
2015中秋节晚会开场白
2015/07/30 职场文书
成本低的5个创业项目:投资小、赚钱快
2019/08/20 职场文书
Python绘制地图神器folium的新人入门指南
2021/05/23 Python