当前位置: 首页 > 科技观察

探索Node.js异步挂钩

时间:2023-03-19 15:37:31 科技观察

你听说过Node.js的`asynchooks`[1]模块吗?如果没有,那么你应该找出来。尽管它是Node.js9发布的新功能,但由于该模块仍处于测试阶段,我不建议将其用于生产,但您仍然应该熟悉它。简而言之,Node.js中的异步钩子,特别是async_hooks模块,提供了一个清晰易用的API来跟踪Node.js中的异步资源。使用此API最简单的方法是在JS中使用requireimport:constasync_hooks=require('async_hooks');我们这里讨论的异步特性是指Node.js创建的对象带有一个关联的回调,回调可以被调用多少次都无所谓。this有很多种,比如:Promises、创建服务的操作、超时等。记住大多数语言都可以关闭资源。其中一些由容器关闭,另一些由语言本身关闭。所以你的回调函数可能永远不会被调用。不过没关系,AsyncHook并不区分这些不同的情况。本文旨在深入探讨Hooks,并尝试通过一些示例来帮助您更深入地理解它们。你准备好了吗?👋在探索异步挂钩时,您可能还想查看AppSignalforNode.js[2]。我们为您提供对Node.jsCore、Express、Next.js、ApolloServer、node-postgres和node-redis[3]的开箱即用支持。API使用总觉得官方文档太复杂,要求太高。这就是为什么我通常选择传统的、友好的博客文章。我们先来看看AsyncHooksAPI提供的5个可用事件函数:init:顾名思义,在初始化特定异步资源时调用。仅作记录,在这一点上,我们有与异步资源相关联的钩子。beforeandafter:这与普通语言中函数执行前后非常相似。它们分别在资源执行之前和之后调用。destroy:很明显,无论资源的回调函数发生什么,只要资源被销毁就会被调用。promiseResolve:promiseResolve和Promise相关,当你的Promise调用它的resolve函数时,hook会触发这个函数。非常简单明了,让我们看一个基本的例子:constmyFirstAsyncHook=async_hooks.createHook({init,before,after,destroy,promiseResolve});是的,你必须先创建每个事件函数,然后将它赋值给CreateHook函数。此外,必须显式启用挂钩:myFirstAsyncHook.enable();我们继续看一个更完整的例子:constfs=require("fs");constasync_hooks=require("async_hooks");//SyncwritetotheconsoleconstwriteSomething=(phase,more)=>{fs.writeSync(1,`Phase:"${phase}",Exec.Id:${async_hooks.executionAsyncId()}${more?","+more:""}\n`);};//CreateandenablethehookconsttimeoutHook=async_hooks.createHook({init(asyncId,type,triggerAsyncId){writeSomething("Init",`asyncId:${asyncId},type:"${type}",triggerAsyncId:${triggerAsyncId}`);},before(asyncId){writeSomething("之前",`asyncId:${asyncId}`);},destroy(asyncId){writeSomething("Destroy",`asyncId:${asyncId}`);},after(asyncId){writeSomething("After",`asyncId:${asyncId}`);},});timeoutHook.enable();writeSomething("Beforecall");//设置timeoutsetTimeout(()=>{writeSomething("Exec.Timeout");},1000);此示例使用众所周知的本机函数setTimeout来跟踪超时的异步执行。在深入研究之前,让我们快速浏览一下第一个函数writeSomething。您可能想知道,当我们已经有一个打印到控制台的函数时,为什么我们要创建一个新函数来做同样的事情。原因是您不能使用任何控制台函数来测试异步挂钩,因为它们本质上是异步的。因此,当我们在下面提供一个init函数时,它会产生一个无限循环。这个函数会调用控制台的日志,这个日志又会触发初始化,如此往复,陷入死循环。这就是为什么我们需要重写一个“同步”日志记录函数。好了,现在让我们回过头来看看代码。我们的异步钩子提供了四个函数:init、before、after和destroy。此外,我们在超时之前和执行期间打印一条消息,因此您可以看到整个过程是如何线性进行的。在你的命令行中执行nodeindex.js,你会得到如下图所示的结果:观察hook是如何一步步执行trace的。似乎是一种有趣的跟踪方式,尤其是当您考虑将数据输入您已经使用的监控工具或日志跟踪工具时。Promise示例让我们看看我们的示例如何使用Promise。考虑这些代码片段:constcalcPow=async(n,exp)=>{writeSomething("Exec.Promise");returnMath.pow(n,exp);};(async()=>{awaitcalcPow(3,4);})();您也可以将此示例替换为前面的setTimeout示例。在此代码中,我们有一个执行求幂的异步函数。在异步块中还有一个相同的函数被调用。到目前为止,Node.js创建了两个Promise。下图是logging的结果:奇怪的是,我们有两个Promise,但是init函数被调用了三次。不用担心,这是因为Node.js团队最近在版本12中引入了异步执行性能方面的一些改进。您可以在此处找到更多信息[4]。尽管如此,执行过程符合我们的预期。分析:Hook函数性能和测量Node.js提供的另一个非常有趣的API是性能评估API[5]。既然我们在这里讨论测量,为什么不结合两者的功能来了解我们可以获得什么?该API可通过perf_hooks获得,使我们能够以类似于W3CWebPerformanceAPI[6]的方式获取性能/用户时间线指标。将其与异步挂钩相结合,我们可以做一些事情,比如跟踪异步函数完成所需的时间。让我们看另一个例子:constasync_hooks=require("async_hooks");const{performance,PerformanceObserver}=require("perf_hooks");consthook=async_hooks.createHook({init(asyncId){performance.mark(`init-${asyncId}`);},destroy(asyncId){performance.mark(`destroy-${asyncId}`);performance.measure(`entry-${asyncId}`,`init-${asyncId}`,`destroy-${asyncId}`);},});hook.enable();constobserver=newPerformanceObserver((data)=>console.log(data.getEntries()));observer.observe({entryTypes:["measure"],buffered:true});setTimeout(()=>{console.log("I'matimeout");},1200);由于我们只是跟踪记录执行时间,所以不需要使用在函数之前使用的中间事件。使用init和destroy就足够了。与异步挂钩一样,性能API通过创建观察者来工作。但是,无论何时开始或结束,您都必须使用其id显式标记每个事件。这样,当我们调用API的measure函数时,它会将收集到的数据聚合起来,并立即发送给观察者,观察者会为我们记录整个日志。请注意,这里我们使用了两次console.log函数。第一次不受影响,因为它包含在观察者的执行中。但是它第二次在setTimeout函数中执行,异步中的另一个异步,这意味着最后它会产生不同的输出。下图是一条日志记录:这个例子没有考虑事件类型的差异。在这里,我们在相同的测量场景中发生了超时和异步日志操作。但是,考虑到生产环境,建议大家建立一个更健壮的机制,在每次调用init时存储事件类型,稍后再调用销毁函数,不幸的是,当没有收到参数类型时,检查存储是否仍然存在.AsyncHooks中的另一个有用特性是`AsyncResource`[7]类。每当您为框架或库创建自己的资源时,它都会为您提供帮助。只需输入以下代码即可使用:constAsyncResource=require('async_hooks').AsyncResource;通过这种方式,您可以使用它来实例化一个新对象并在整个代码中手动定义它的每个阶段何时开始。例如:constresource=newAsyncResource('MyOwnResource');someFunction(functionsomeCallback(){resource.emitBefore();//doyourstuff...resource.emitAfter();});someOnClose(){resource.emitDestroy();这仍然是资源生命周期的示例,如果您要绑定本机C++代码,则更推荐使用。我将通过为您提供官方文档中的一个很好的示例[8]来简化它。结论正如我们所讨论的,异步挂钩仍处于试验阶段。因此,请谨慎使用它。由于hooks仅在Node.js8及更高版本中可用,您可以考虑迁移Node.js版本(很多时候这不是一种合适的方法)或使用社区的替代工具,例如async-tracer[9]。