当前位置: 首页 > 后端技术 > Node.js

Node绑定全局TraceID

时间:2023-04-03 17:31:43 Node.js

问题描述由于Node.js单线程模型的限制,我们无法设置全局traceid聚合请求,即实现输出日志与请求的绑定。如果没有实现日志与请求的绑定,我们就很难判断日志输出与对应用户请求的对应关系,给线上排查带来困难。例如,当用户访问retrieveOneAPI时,会调用retrieveOneSub函数。如果我们要在retrieveOneSub函数中输出当前请求对应的学生信息,比较麻烦。在course-se现有的实现下,我们针对该问题的解决方案是:方案一:在调用retrieveOneSub函数的parent函数中,即retrieveOne,解构paramData,输出学生相关信息,但该方案无法提炼log输出粒度。方案二:修改retrieveOneSub函数签名,接收paramData作为参数。这种方案可以保证日志输出的粒度,但是在调用链很深的情况下,需要修改每个函数的函数签名,使其接收paramData,工作量很大。不太可行。/***返回一个函数来检索提交*@param{ParamData}paramData*@param{Context}ctx*@param{string}id*/exportasyncfunctionretrieveOne(paramData,ctx,id){const{subModel}=参数数据.ce;constsub_asgn_id=Number(id);//通过paramData.user获取用户相关信息,比如user_id,//但日志输出粒度无法细化,除非修改retrieveOneSub的签名,//添加paramData作为其参数。const{user_id}=paramData.user;console.log(`${user_id}正在尝试检索一个提交。`);//调用了retrieveOneSub函数。constsub=awaitretrieveOneSub(sub_asgn_id,subModel);常量提交=子;分配(子,{sub_asgn_id});分配(参数数据,{提交,子});returnsub;}/***从数据库获取一个提交*@param{number}sub_asgn_id*@param{SubModel}model*/asyncfunctionretrieveOneSub(sub_asgn_id,model){const[sub]=awaitmodel.findById(sub_asgn_id);if(!sub){thrownewME.SoftError(ME.NOT_FOUND,'找不到提交');}returnsub;}AsyncHooks其实针对以上问题,我们也可以从Node的AsyncHooks实验性API入手。Node.jsv8.x之后,官方提供了对AsyncHooks(异步钩子)API的支持,可以用来监控异步行为。AsyncScopeAsyncHooks为每个(同步或异步)函数提供了一个AsyncScope,我们可以调用executionAsyncId方法获取当前函数的AsyncID,调用triggerAsyncId获取当前函数调用者的AsyncID。constasyncHooks=require("async_hooks");const{executionAsyncId,triggerAsyncId}=asyncHooks;安慰。日志(`顶级:${executionAsyncId()}${triggerAsyncId()}`);constf=()=>{console.log(`f:${executionAsyncId()}${triggerAsyncId()}`);};f();constg=()=>{console.log(`setTimeout:${executionAsyncId()}${triggerAsyncId()}`);setTimeout(()=>{console.log(`innersetTimeout:${executionAsyncId()}${triggerAsyncId()}`);},0);};setTimeout(g,0);setTimeout(g,0);在上面的代码中,我们使用setTimeout模拟了一个异步调用过程,在这个异步过程中我们调用了handler同步函数,输出了它对应的AsyncID和TriggerAsyncID。执行以上代码后,运行结果如下。toplevel:10f:10setTimeout:71setTimeout:91innersetTimeout:117innersetTimeout:139通过上面的日志输出,我们得到如下信息:调用一个同步函数不会改变它的AsyncID,比如里面函数f调用者的AsyncID与其调用者的AsyncID相同。同一个函数,在不同的时间被异步调用,会被分配不同的AsyncID,比如上面代码中的g函数。跟踪异步资源前面我们说过,AsyncHooks可以用来跟踪异步资源。为了达到这个目的,我们需要了解AsyncHooks的相关API。具体说明参见下面代码中的注释。constasyncHooks=require("async_hooks");//创建一个AsyncHooks实例。consthooks=asyncHooks.createHook({//构造对象时会触发init事件init:function(asyncId,type,triggerId,resource){},//在执行回调之前会触发before事件.before:function(asyncId){},//回调执行后会触发after事件after:function(asyncId){},//对象销毁后会触发destroy事件destroy:function(asyncId){}});//允许实例为异步函数启用钩子。hooks.enable();//关闭对异步资源的跟踪。钩子。禁用();当我们调用createHook时,我们可以注入init、before、after、destroy函数来跟踪异步资源的不同生命周期。新的方案是基于AsyncHooksAPI,我们可以设计如下方案来实现日志和请求记录的绑定,即TraceID的全局绑定。constasyncHooks=require("async_hooks");const{executionAsyncId}=asyncHooks;//保存异步调用的上下文。const上下文={};consthooks=asyncHooks.createHook({//构造对象时会触发init事件init:function(asyncId,type,triggerId,resource){//triggerId为当前函数调用者的asyncIdif(contexts[triggerId]){//设置当前函数的异步上下文与调用者的异步上下文一致。contexts[asyncId]=contexts[triggerId];}},//对象被销毁后会触发destroy事件。destroy:function(asyncId){if(!contexts[asyncId])返回;//销毁当前的异步上下文。删除上下文[asyncId];}});//钥匙!允许在此实例中为异步函数启用挂钩。hooks.enable();//模拟业务处理功能。functionhandler(params){//设置上下文,这可以在中间件(如Logger中间件)中完成。上下文[executionAsyncId()]=参数;//下面是业务逻辑。console.log(`handler${JSON.stringify(params)}`);f();}functionf(){setTimeout(()=>{//输出异步进程的参数console.log(`setTimeout${JSON.stringify(contexts[executionAsyncId()])}`);});}//模拟两个异步过程(两个请求)。setTimeout(handler,0,{id:0});setTimeout(handler,0,{id:1});在上面的代码中,我们首先声明了contexts来存储每个异步进程中的context数据(比如TraceID),然后我们创建了一个AsyncHooks的实例。在异步资源初始化时,我们设置当前AsyncID对应的上下文数据,使其数据为调用者的上下文数据;当异步资源被销毁时,我们删除相应的上下文数据。这样一开始我们只需要设置上下文数据,然后就可以获取到它引起的各个进程(同步进程和异步进程)中的上下文数据,从而解决问题。执行上面的代码,结果如下。根据输出日志,我们的方案是可行的。handler{"id":0}handler{"id":1}setTimeout{"id":0}setTimeout{"id":1}不过需要注意的是AsyncHooks是一个实验性的API,有一定的性能损失,但Node官方正在努力使其生产就绪。因此,在机器资源充足的情况下,使用该方案牺牲一些性能换取开发经验。