在 Node 中通过 Async Hooks 实现请求作用域

程序员成长指北

共 2933字,需浏览 6分钟

 ·

2021-08-10 06:22

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

作者:繁易

原文地址:https://mp.weixin.qq.com/s/I22TvmTqCKFClsp0YLDoZw

在日常的 Web 服务开发中,我们时常会遇到需要实现请求作用域的场景。

请求作用域在此指的是:该作用域的生命周期是会话级别的,在每一次会话过程中我们都会创建一个新的请求作用域,并在会话结束后进行销毁,且每个会话创建的请求作用域是相互隔离的。

应用场景

记录请求链路信息是请求作用域的一个经典场景,我们需要在请求开始前,为这个请求生成一条链路 ID。并将该次请求中访问的所有服务的请求信息、耗时、返回数据等数据附加在该链路上,最终生成一条完整的调用链路。

通过这种方式,我们得以将零散系统的调用信息实现了聚合。从而即使是在内部调用链路非常复杂的情况下,我们可以根据请求链路来快速的分析问题并进行定位。

如下图所示,我们可以方便的查看整体的请求调用链,也可以针对错误请求进行快速排错。

图源:打造立体化监控体系的最佳实践(https://cn.aliyun.com/aliware/news/monitoringsolution)

在 Node.js 中的实现

基于上述的场景,我们不难发现其实整体链路调用信息的生成,实际上是围绕着 TraceId 去做的。

这个 TraceId 在记录时有着如下的需求:

  • 全局可访问,在访问不同的服务之前都需要先获取到当前的 TraceId

  • TraceId 的生命周期为请求作用域级别,同一请求中获取到的 TraceId 是唯一的,不同请求的 TraceId 不一样

而这类需求在 Node.js 中实际上有着以下几种实现方式。

  • 手动传递:将 TraceId 作为函数的参数

  • 中间件挂载:利用 Midway/Koa 等 Web 框架提供的中间件能力,将 TraceId 挂载在请求 Context 上,每次调用前手动从 Context 获取

以上这两种方式实际上都可以实现,但也有着各自的缺陷。

  • 手动挂载:需要手动传递参数,较为繁琐

  • 中间件挂载:强依赖于 Web 框架提供的能力

因此在这儿,我们推荐使用一种全新的方式去实现请求作用域。那就是 Async Hooks。

Async Hooks

Async Hooks 是 Node.js 在 8.x 提供的原生模块,是为了用来追踪 Node.js 中异步资源的生命周期。在使用时,你可以根据给定的 Api,来创建一组 Hooks。

创建时的类型定义

其中参数解释如下:

  • asyncId:当前异步资源 Id,在生成时会自动递增,全局唯一。

  • type: 异步资源类型,例如:FSEVENTWRAP/FSREQCALLBACK/Timeout

  • triggerAsyncId:代表当前的异步资源 Id 是由哪个异步资源创建的

  • resource: 创建的具体资源

在这其中,asyncId 代表当前异步资源 Id,triggerAsyncId 代表创建该资源的异步资源 Id。

且 Async Hooks 为了调用便利,提供了直接的 Api 供开发者获取当前的 asyncId 与 triggerAsyncId。

使用 Demo

在使用时,我们只需要创建该 Hooks 并启用,即可开始监听异步事件。

如下图的 Demo 所示:

最终输出结果是:

不难看出,因为是在全局创建的 fs.open 事件,fs.open.triggerAsyncId 正是 global.asyncId。

实现请求作用域

基于以上的 Api 与 Demo,我们不难发现,Async Hooks 给我们提供了两个关键信息:

  • asyncId:自动递增,全局唯一

  • triggerAsyncId:代表当前的异步资源 Id 是由哪个异步资源 Id 创建的

根据  triggerAsyncId 的特性,我们可以知道轻松的推断出调用链。且由于 asyncId 是唯一的,所以即使是针对于同一个函数的调用,asyncId 也不同。

所以基于以上的推论,在 Async Hooks 中,我们在每一次异步调用过程中,都会生成一条独一无二的调用链。而这也是实现请求作用域的诀窍。

因此对于具体实现而言,我们需要做到如下几点:

  • 创建请求作用域,并往请求作用域存入我们需要数据

  • 函数在调用时可以访问到该数据

  • 该数据的生命周期为会话级别,不同会话之间互不干扰

关于代码侧的具体实现,实际上利用好 asyncId 与 triggerAsync 的特性,是不难做到的。

具体实现

以下是具体实现:

运行后的输出结果为:

通过这种方式,我们充分的利用了 Async Hooks 的特性,实现了我们自己的请求作用域。且在实际开发中使用起来也是非常简单的。

AsyncLocalStorage

在上面的 Demo 中,我采用了 AsyncLocalStorage 作为名称。
这个名称实际上是因为 Node.js 最近在 Async Hooks 上落地了一个 feature,名称就叫  AsyncLocalStorage。作用也是我们提到的实现异步请求作用作用域。

关于这部分,有兴趣的同学可以根据下方的参考资料,自行查阅 Node.js 官方文档(版本 V13.11.0)。

参考资料

本文参考资料如下:

  • Async Hooks 官方文档:https://nodejs.org/api/async_hooks.html

  • async-hooks: introduce async-storage API: https://github.com/nodejs/node/pull/26540

  • node-request-context:https://github.com/guyguyon/node-request-context

  • 学习使用 Node.js 中 async-hooks 模块:https://zhuanlan.zhihu.com/p/53036228

  • 打造立体化监控体系的最佳实践:https://cn.aliyun.com/aliware/news/monitoringsolution

如果觉得这篇文章还不错
点击下面卡片关注我
来个【分享、点赞、在看】三连支持一下吧

   “分享、点赞在看” 支持一波 

浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报