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 动态参数判空操作
Dec 22 Javascript
javascript 禁用IE工具栏,导航栏等等实现代码
Apr 01 Javascript
用表格输出1-1000之间的数字实现代码(附特效)
Apr 21 Javascript
jquery取子节点及当前节点属性值的方法
Sep 09 Javascript
加载列表时jquery获取ul中第一个li的属性
Nov 02 Javascript
jquery使用正则表达式验证email地址的方法
Jan 22 Javascript
详解JavaScript 中getElementsByName在IE中的注意事项
Feb 21 Javascript
jQuery插件FusionCharts绘制的3D双柱状图效果示例【附demo源码】
Apr 20 jQuery
js实现添加删除表格(两种方法)
Apr 27 Javascript
vue2.0+vue-router构建一个简单的列表页的示例代码
Feb 13 Javascript
微信小程序实现下拉框功能
Jul 16 Javascript
vant中的toast轻提示实现代码
Nov 04 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
IIS6.0+PHP5.x+MySQL5.x+Zend3.0x+GD+phpMyAdmin2.8x通用安装实例(已经完成)
2006/12/06 PHP
封装一个PDO数据库操作类代码
2009/09/09 PHP
PHP数组无限分级数据的层级化处理代码
2012/12/29 PHP
PHP中常用的转义函数
2014/02/28 PHP
smarty高级特性之对象的使用方法
2015/12/25 PHP
php实现的PDO异常处理操作分析
2018/12/27 PHP
JavaScript 基于原型的对象(创建、调用)
2009/10/16 Javascript
jQuery1.6 使用方法一
2011/11/23 Javascript
分享33个jQuery与CSS3实现的绚丽鼠标悬停效果
2014/12/15 Javascript
JSON 对象未定义错误的解决方法
2016/09/29 Javascript
Javascript+CSS3实现进度条效果
2016/10/28 Javascript
QQ跳转支付宝并自动领红包脚本(最新)
2018/06/22 Javascript
解决jquery的ajax调取后端数据成功却渲染失败的问题
2018/08/08 jQuery
Vue.js 父子组件通信的十种方式
2018/10/30 Javascript
Vue中inheritAttrs的使用实例详解
2020/12/31 Vue.js
[01:07:11]Secret vs Newbee 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
[00:58]PWL开团时刻DAY5——十人开雾0换5
2020/11/04 DOTA
python调用短信猫控件实现发短信功能实例
2014/07/04 Python
Python实现的堆排序算法示例
2018/04/29 Python
基于Pandas读取csv文件Error的总结
2018/06/15 Python
python获取服务器响应cookie的实例
2018/12/28 Python
对Pyhon实现静态变量全局变量的方法详解
2019/01/11 Python
简单了解Python生成器是什么
2019/07/02 Python
Tensorflow中tf.ConfigProto()的用法详解
2020/02/06 Python
在python中修改.properties文件的操作
2020/04/08 Python
详解python如何引用包package
2020/06/07 Python
利用CSS3实现自定义滚动条代码分享
2016/08/18 HTML / CSS
英国香水店:The Perfume Shop
2017/03/27 全球购物
CK加拿大官网:Calvin Klein加拿大
2020/03/14 全球购物
牵手50新加坡:专为黄金岁月的单身人士而设的交友网站
2020/08/16 全球购物
社区安全检查制度
2014/02/03 职场文书
公开承诺书格式
2014/05/21 职场文书
不同意离婚代理词
2015/05/23 职场文书
李白经典诗之一:全文无一“月”字,却句句有月
2019/07/12 职场文书
ThinkPHP5和ThinkPHP6的区别
2021/03/31 PHP
宫崎骏十大动画电影,宫崎骏好看的动画电影排名
2022/03/22 日漫