node使用async_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()

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

  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请求追踪内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 在 Node.js 中使用 async 函数的方法

    借助于新版 V8 引擎,Node.js 从 7.6 开始支持 async 函数特性.今年 10 月 31 日,Node.js 8 也开始成为新的长期支持版本,因此你完全可以放心大胆地在你的代码中使用 async 函数了.在这边文章里,我会简要地介绍一下什么是 async 函数,以及它会如何改变我们编写 Node.js 应用的方式. 1 什么是 async 函数 利用 async 函数,你可以把基于 Promise 的异步代码写得就像同步代码一样.一旦你使用 async 关键字来定义了一个函数,那

  • nodejs async异步常用函数总结(推荐)

    nodejs框架类库很多,"流程控制"本来是件比较简单的事,但是对于nodejs的异步架构的实现方法,对于需要同步的业务逻辑,实现起来比较麻烦. async的集合控制: async.map(arr, function(item, callback){},callback(err, results){});async.mapSeries app.get('/testMap',function(req, res){ var arr = [0,1,2,3,4,5,6]; async.mapS

  • Node.js 中使用 async 函数的方法

    借助于新版 V8 引擎,Node.js 从 7.6 开始支持 async 函数特性.今年 10 月 31 日,Node.js 8 也开始成为新的长期支持版本,因此你完全可以放心大胆地在你的代码中使用 async 函数了.在这边文章里,我会简要地介绍一下什么是 async 函数,以及它会如何改变我们编写 Node.js 应用的方式. 1 什么是 async 函数 利用 async 函数,你可以把基于 Promise 的异步代码写得就像同步代码一样.一旦你使用 async 关键字来定义了一个函数,那

  • 浅谈node.js中async异步编程

    1.什么是异步编程? 异步编程是指由于异步I/O等因素,无法同步获得执行结果时, 在回调函数中进行下一步操作的代码编写风格,常见的如setTimeout函数.ajax请求等等. 示例: for (var i = 1; i <= 3; i++) { setTimeout(function(){ console.log(i); }, 0); }; 这里大部分人会认为输出123,或者333.其实它会输出 444 这里就是我们要说的异步编程了. 高级函数的定义 这里为什么会说到高级函数,因为高级函数是异

  • node 使用 async 控制并发的方法

    目标 建立一个 lesson5 项目,在其中编写代码. 代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode(https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式. 注意:与上节课不同,并发连接数需要控制在 5 个. 输出示例: [ { "title": "[公告]发招聘帖的同学留意一下这里", "href": "http://cnodejs.or

  • Node.js如何对SQLite的async/await封装详解

    前言 本文主要给大家介绍的是关于Node.js对SQLite的async/await封装的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 用于将每个SQLite函数同步化,并可以用await的接口. 注意:需要SQLite for Node模块和Node.js 8.0+,并支持async / await. SQLite最常用作本地或移动应用程序的存储单元,当需要从程序的各个部分访问数据时,回调不是最佳解决方案. 为了在程序程序中更自然地访问数据,我编写了一个将回调转换为

  • 浅析node Async异步处理模块用例分析及常用方法介绍

    最近在研究nodejs,令我感受比较深的是--熟悉js代码的地球人都知道,js的加载顺序很重要!很重要!!那么问题来了,在编写node的时候,会在后台去请求很多接口(我们公司是与java后台交接数据的),接口就会有个回调,这么多回调怎么办呢--每个回调回来的加载顺序怎么去处理是个问题--总不可能去嵌套--嵌套--这样吧,那会显的多麻烦!!这么大的一个问题怎么没有处理方法呢,那是不可能的--对吧! Async异步处理模块! 以下是小sam的理解: 安装很简单,就跟普通的安装模块一样就行了 这样就安

  • node基于async/await对mysql进行封装

    前言 node是基于异步的,因此在进行数据库查询操作的通常是通过回调来操作查询结果.但是在有了es7的async/await,基本不再需要回调了,所以本篇是基于async/await对mysql进行一次操作封装,让查询更加方便.(node版本需>=7.0). 原理 简单来说,async/await的实现原理是基于promise,根据promise的状态来判断是否真正返回,因此我们可以在mysql真正查询到结果后将promise状态切换为resolve,返回结果.如出现错误通过reject返回错误

  • 详解node Async/Await 更好的异步编程解决方案

    一.异步编程的终极解决方案 前几天写过关于 javascript 异步操作的文章<Javascript Promise 详解>. 最近在学习 Puppeteer的时候又发现另一种异步编程解决方案:Async/Await. 异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题. 从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底. 它们都有额外的复杂性,都需要理解抽象的底层运行机制. 在 A

  • Nodejs异步流程框架async的方法

    Async的简单介绍: Async是一个流程控制工具包,提供了直接而强大的异步功能.基于Javascript为Node.js设计,同时也可以直接在浏览器中使用.Async提供了大约20个函数,包括常用的map, reduce, filter, forEach等,异步流程控制模式包括,串行(series),并行(parallel),瀑布(waterfall)等. https://github.com/caolan/async 我们常用的是以下四种: 串行无关联 串行有关联 并行无关联 智能控制 1

  • 深入学习nodejs中的async模块的使用方法

    最近在学习nodejs,这两天学习了async模块这个地方知识点挺多的,所以,今天添加一点小笔记. async模块是为了解决嵌套金字塔,和异步流程控制而生.常用的方法介绍 npm 安装好async模块,然后引入就可以使用 var async = require('async'); 1. series(tasks,[callback]) 多个函数从上到下依次执行,相互之间没有数据交互 var task1 =function(callback){ console.log("task1");

随机推荐