Node.js AsyncHooks 与异步回调上下文

引子

我们都知道,Nodejs 最显著特点是单进程、异步、事件驱动。每当我们的代码碰到异步调用时,需要传入一个回调函数,等待异步调用结束时再被执行。一个典型的处理用户登录流程如下:

Node.js AsyncHooks 与异步回调上下文

图1注:图中数字标号表示事件发生时间顺序


但是,真实的线上环境,往往多个用户同时在登录,真实的场景如下:


Node.js AsyncHooks 与异步回调上下文

图2

上图假设数据库操作最为耗时,当用户 A 请求到达后,在等待数据库查询比对用户密码时,用户 B 发起了登录请求。让我们稍微修改下上图,加上时间轴:


Node.js AsyncHooks 与异步回调上下文

图3


假设用户的一次请求作为一个独立上下文,那么上图一共发生了三次上下文的切换:


  • 第一次:事件 2 和事件 3,从用户 A 切换到了用户 B
  • 第二次:事件 4 和事件 5,从用户 B 切换到了用户 A
  • 第三次:事件 5 和事件 6,从用户 A 再次切换到用户 B


在类似 Express、Egg 等 Web 框架,是如何帮助我们识别上下文切换的?


  • Express 所有的请求都封装为一个 Request 对象,作为回调函数的第一个参数
  • Egg 则是封装了一个 Context 对象,每次请求都是新的 Context。


这些方式的特点,都是先显式创建一个局部变量,后续的业务逻辑代码,都会在函数调用中层层传递这个变量,便于识别上下文信息。那有没有办法,隐式的传递上下文信息?


隐式传递上下文

结合前面的案例,在 Node.js 中同步代码不会导致用户上下文切换,只有当发生异步回调时,才有可能发生请求上下文的切换。所以要想隐式传递上下文,首先要做的就是能自动识别出异步回调。Node.js 8.1 版本开始支持监听异步调用:AsyncHooks API。


AsyncHooks


AsyncHooks 核心提供了四个钩子:init、before、after、destory

  • init 每次异步调用都会触发,执行时间点是异步请求的资源准备完毕时。
  • before 执行异步回调函数前调用
  • after 执行异步回调函数后调用
  • destory 异步调用关联的资源被销毁时调用

补充两个例子说明:


  • fs.open 打开文件操作,执行时间点为所请求的文件资源准备完毕时调用
  • net.createServer init 会在端口监听成功时执行,而 before 则会在每次有新请求,触发 createServer 中的回调函数执行前,调用


四个钩子覆盖了一次异步调用的整个生命周期。除此之外还提供了两个关键 ID:triggerAsyncIdasyncId 。这两个 ID 可以表达两次异步调用之间的"父子"关系。


追踪上下文

结合 AsyncHooks,将上文的案例,改成 triggerAsyncId 和 asyncId 的关系图,如下:

Node.js AsyncHooks 与异步回调上下文

每次异步调用,其 triggerAsyncId 和 asyncId 值类似如下(按上图事件顺序):

  • (1) 用户 A 发起 Http Request , triggerAsyncId: 0, asyncId 1
    • (2) 用户 A 发起 Database Query,triggerAsyncId: 1, asyncId 2
      • (5) 用户 A 发起 Write Session,triggerAsyncId 2, asyncId 5
  • (3) 用户 B 发起 Http Request,triggerAsyncId: 0, asyncId 3
    • (4) 用户 B 发起 Database Query, triggerAsyncId 3, asyncId 4
      • (6) 用户 B 发起 Write Sessiony, triggerAsyncId 4, asyncId 6

注:此处对系统发生真实异步调用进行了简化

通过 Hooks 以及 triggerAsyncId 和 asyncId 的关系,我们就可以找回每次异步调用发生时,该调用所属的上下文。相关的完整实现,可以参考 cls-hooked。


为什么没有普遍应用?

佛瑞德·布鲁克斯早就告诉我们软件工程没有银弹,之所以这种方式没有普及,个人理解主要以下几点:

  1. 隐式传递,代码可读性更差,不利于维护
  2. AsyncHooks 会有比较大的性能损耗,详见async-hooks-performance-impact,目前 API 稳定性还处于试验阶段

结束语

本文仅是从一个简单场景:异步上下文出发,引出了 AsyncHook 相关 API 的功能与基本使用。实际应用中,很多 APM 类程序都使用类似的能力。本文并没有对相关实现做深入讨论,希望通过此文,先介绍相关概念。

上一篇:3月招聘季 充电拿offer——达摩院趣味视觉AI训练营云上充电,限时免费报名中!


下一篇:手淘双11最新实践:PopLayer弹层领域研发模式升级