Node绑定全局TraceID的实现方法
问题描述
由于Node.js的 单线程模型的限制,我们无法设置全局traceid来聚合请求,即 实现输出日志与请求的绑定。如果不实现日志和请求的绑定,我们难以判断日志输出与对应用户请求的对应关系,这对 线上问题排查带来了困难。
例如,在用户访问 retrieveOneAPI时,其会调用 retrieveOneSub函数,如果我们想在 retrieveOneSub函数中输出当前请求对应的学生信息,是繁琐的。在course-se现有实现下,我们针对此问题的解决方法是:
- 方案1:在调用 retrieveOneSub函数的父函数,即 retrieveOne内,对 paramData进行 解构,输出学生相关信息,但该方案 无法细化日志输出粒度。
- 方案2:修改 retrieveOneSub函数签名,接收 paramData为其参数,该方案 能确保日志输出粒度,但 在调用链很深的情况下,需要给各函数修改函数签名,使其接收 paramData,颇具工作量,并不太可行。
/** *返回获取一份提交的函数 *@param{ParamData}paramData *@param{Context}ctx *@param{string}id */ exportasyncfunctionretrieveOne(paramData,ctx,id){ const{subModel}=paramData.ce; constsub_asgn_id=Number(id); //通过paramData.user获取user相关信息,如user_id, //但无法细化日志输出粒度,除非修改retrieveOneSub的签名, //添加paramData为其参数。 const{user_id}=paramData.user; console.log(`${user_id}istryingtoretreiveonesubmission.`); //调用了retrieveOneSub函数。 constsub=awaitretrieveOneSub(sub_asgn_id,subModel); constsubmission=sub; assign(sub,{sub_asgn_id}); assign(paramData,{submission,sub}); 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的支持。
AsyncScope
AsyncHooks对每一个(同步或异步)函数提供了一个AsyncScope,我们可调用 executionAsyncId方法获取当前函数的AsyncID,调用 triggerAsyncId获取当前函数调用者的AsyncID。
constasyncHooks=require("async_hooks"); const{executionAsyncId,triggerAsyncId}=asyncHooks; console.log(`toplevel:${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:10 f:10 setTimeout:71 setTimeout:91 innersetTimeout:117 innersetTimeout: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。 hooks.enable(); //关闭对异步资源的追踪。 hooks.disable();
我们在调用 createHook时,可注入 init、 before、 after和 destroy函数,用于 追踪异步资源的不同生命周期。
全新解决方案
基于AsyncHooksAPI,我们即可设计以下解决方案,实现日志与请求记录的绑定,即TraceID的全局绑定。
constasyncHooks=require("async_hooks"); const{executionAsyncId}=asyncHooks; //保存异步调用的上下文。 constcontexts={}; consthooks=asyncHooks.createHook({ //对象构造时会触发init事件。 init:function(asyncId,type,triggerId,resource){ //triggerId即为当前函数的调用者的asyncId。 if(contexts[triggerId]){ //设置当前函数的异步上下文与调用者的异步上下文一致。 contexts[asyncId]=contexts[triggerId]; } }, //在销毁对象后会触发destroy事件。 destroy:function(asyncId){ if(!contexts[asyncId])return; //销毁当前异步上下文。 deletecontexts[asyncId]; } }); //关键!允许该实例中对异步函数启用hooks。 hooks.enable(); //模拟业务处理函数。 functionhandler(params){ //设置context,可在中间件中完成此操作(如LoggerMiddleware)。 contexts[executionAsyncId()]=params; //以下是业务逻辑。 console.log(`handler${JSON.stringify(params)}`); f(); } functionf(){ setTimeout(()=>{ //输出所属异步过程的params。 console.log(`setTimeout${JSON.stringify(contexts[executionAsyncId()])}`); }); } //模拟两个异步过程(两个请求)。 setTimeout(handler,0,{id:0}); setTimeout(handler,0,{id:1});
在上述代码中,我们先声明了 contexts用于存储每个异步过程中的上下文数据(如TraceID),随后我们创建了一个AsyncHooks实例。我们在异步资源初始化时,设置当前AsyncID对应的上下文数据,使得其数据为调用者的上下文数据;我们在异步资源被销毁时,删除其对应的上下文数据。
通过这种方式,我们只需在一开始设置上下文数据,即可在其引发的各个过程(同步和异步过程)中,获得上下文数据,从而解决了问题。
执行上述代码,其运行结果如下。根据输出日志可知,我们的解决方案是可行的。
handler{"id":0} handler{"id":1} setTimeout{"id":0} setTimeout{"id":1}
不过需要注意的是,AsyncHooks是 实验性API, 存在一定的性能损耗,但Node官方正努力将其变得生产可用。因此, 在机器资源足够的情况下,使用本解决方案,牺牲部分性能,换取开发体验。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。