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(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。