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

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

引子

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

image.png

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


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


image.png

图2

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


image.png

图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 的关系图,如下:

image.png

每次异步调用,其 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 类程序都使用类似的能力。本文并没有对相关实现做深入讨论,希望通过此文,先介绍相关概念。

相关文章
|
1月前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
|
4月前
|
存储 前端开发 JavaScript
javascript 异常问题之为自定义异常提供丰富的上下文信息如何实现
javascript 异常问题之为自定义异常提供丰富的上下文信息如何实现
|
3月前
|
前端开发 JavaScript 数据库连接
掌握 JavaScript 异步编程:从回调到 Async/Await
在现代 JavaScript 开发中,异步编程是处理非阻塞操作的关键技术。本文从早期的回调函数讲起,逐步过渡到 Promise 和 ES2017 的 async/await 语法,展示了异步编程如何变得更加简洁和强大。通过实用的技巧和最佳实践,帮助开发者避免常见陷阱,提升代码效率和可靠性。
|
2月前
|
JavaScript 前端开发 开发者
掌握Node.js中的异步编程:从回调到async/await
Node.js的异步编程模型是其核心特性之一,它使得开发者能够构建高性能和高并发的应用程序。本文将带你从Node.js的异步编程基础开始,逐步深入到回调函数、Promises、以及最新的async/await语法。我们将探讨这些异步模式的原理、使用场景和最佳实践,并通过实例代码展示如何在实际项目中应用这些概念。
|
2月前
|
前端开发 JavaScript 开发者
JS 异步解决方案的发展历程以及优缺点
本文介绍了JS异步解决方案的发展历程,从回调函数到Promise,再到Async/Await,每种方案的优缺点及应用场景,帮助开发者更好地理解和选择合适的异步处理方式。
|
2月前
|
移动开发 JavaScript 前端开发
【JavaScript】JS执行机制--同步与异步
【JavaScript】JS执行机制--同步与异步
28 1
|
2月前
|
前端开发 JavaScript UED
深入了解JavaScript异步编程:回调、Promise与async/await
【10月更文挑战第11天】深入了解JavaScript异步编程:回调、Promise与async/await
24 0
|
3月前
|
JavaScript 前端开发
一个js里可以有多少个async function,如何用最少的async function实现多个异步操作
在 JavaScript 中,可以通过多种方法实现多个异步操作并减少 `async` 函数的数量。
|
3月前
|
JSON 前端开发 JavaScript
一文看懂 JavaScript 异步相关知识
一文看懂 JavaScript 异步相关知识
43 4
|
3月前
|
存储 前端开发 JavaScript
node中循环异步的问题[‘解决方案‘]_源于map循环和for循环对异步事件配合async、await的支持
本文探讨了在Node.js中处理循环异步操作的问题,比较了使用map和for循环结合async/await处理异步事件的差异,并提供了解决方案。
46 0