Node绑定全局TraceID的实现方法


Posted in Javascript onNovember 14, 2019

问题描述

由于Node.js的 单线程模型 的限制,我们无法设置全局 traceid 来聚合请求,即 实现输出日志与请求的绑定 。如果不实现日志和请求的绑定,我们难以判断日志输出与对应用户请求的对应关系,这对 线上问题排查 带来了困难。

例如,在用户访问 retrieveOne API 时,其会调用 retrieveOneSub 函数,如果我们想在 retrieveOneSub 函数中输出当前请求对应的学生信息,是繁琐的。在 course-se 现有实现下,我们针对此问题的解决方法是:

  1. 方案1:在调用 retrieveOneSub 函数的父函数,即 retrieveOne 内,对 paramData 进行 解构 ,输出学生相关信息,但该方案 无法细化日志输出粒度 。
  2. 方案2:修改 retrieveOneSub 函数签名,接收 paramData 为其参数,该方案 能确保日志输出粒度 ,但 在调用链很深的情况下,需要给各函数修改函数签名 ,使其接收 paramData ,颇具工作量,并不太可行。
/**
 * 返回获取一份提交的函数
 * @param {ParamData}  paramData
 * @param {Context}   ctx
 * @param {string}   id
 */
export async function retrieveOne(paramData, ctx, id) {
 const { subModel } = paramData.ce;
 const sub_asgn_id = Number(id);

 // 通过 paramData.user 获取 user 相关信息,如 user_id ,
 // 但无法细化日志输出粒度,除非修改 retrieveOneSub 的签名,
 // 添加 paramData 为其参数。
 const { user_id } = paramData.user;
 console.log(`${user_id} is trying to retreive one submission.`);
 // 调用了 retrieveOneSub 函数。
 const sub = await retrieveOneSub(sub_asgn_id, subModel);
 const submission = sub;
 assign(sub, { sub_asgn_id });
 assign(paramData, { submission, sub });
 return sub;
}

/**
 * 从数据库获取一份提交
 * @param {number}   sub_asgn_id
 * @param {SubModel}   model
 */
async function retrieveOneSub(sub_asgn_id, model) {
 const [sub] = await model.findById(sub_asgn_id);
 if (!sub) {
  throw new ME.SoftError(ME.NOT_FOUND, '找不到该提交');
 }
 return sub;
}

Async Hooks

其实,针对以上的问题,我们还可以从 Node 的 Async Hooks 实验性 API 方面入手。在 Node.js v8.x 后,官方提供了可用于 监听异步行为 的 Async Hooks(异步钩子)API 的支持。

Async Scope

Async Hooks 对每一个(同步或异步)函数提供了一个 Async Scope ,我们可调用 executionAsyncId 方法获取当前函数的 Async ID ,调用 triggerAsyncId 获取当前函数调用者的 Async ID。

const asyncHooks = require("async_hooks");
const { executionAsyncId, triggerAsyncId } = asyncHooks;

console.log(`top level: ${executionAsyncId()} ${triggerAsyncId()}`);

const f = () => {
 console.log(`f: ${executionAsyncId()} ${triggerAsyncId()}`);
};

f();

const g = () => {
 console.log(`setTimeout: ${executionAsyncId()} ${triggerAsyncId()}`);
 setTimeout(() => {
  console.log(`inner setTimeout: ${executionAsyncId()} ${triggerAsyncId()}`);
 }, 0);
};

setTimeout(g, 0);
setTimeout(g, 0);

在上述代码中,我们使用 setTimeout 模拟一个异步调用过程,且在该异步过程中我们调用了 handler 同步函数,我们在每个函数内都输出其对应的 Async ID 和 Trigger Async ID 。执行上述代码后,其运行结果如下。

top level: 1 0
f: 1 0
setTimeout: 7 1    
setTimeout: 9 1    
inner setTimeout: 11 7
inner setTimeout: 13 9

通过上述日志输出,我们得出以下信息:

  • 调用同步函数,不会改变其 Async ID ,如函数 f 内的 Async ID 和其调用者的 Async ID 相同。
  • 同一个函数,被不同时刻进行异步调用,会分配至不同的 Async ID ,如上述代码中的 g 函数。

追踪异步资源

正如我们前面所说的,Async Hooks 可用于追踪异步资源。为了实现此目的,我们需要了解 Async Hooks 的相关 API ,具体说明参照以下代码中的注释。

const asyncHooks = require("async_hooks");

// 创建一个 AsyncHooks 实例。
const hooks = 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 函数,用于 追踪异步资源的不同生命周期 。

全新解决方案

基于 Async Hooks API ,我们即可设计以下解决方案,实现日志与请求记录的绑定,即 Trace ID 的全局绑定。

const asyncHooks = require("async_hooks");
const { executionAsyncId } = asyncHooks;

// 保存异步调用的上下文。
const contexts = {};

const hooks = 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;
  // 销毁当前异步上下文。
  delete contexts[asyncId];
 }
});

// 关键!允许该实例中对异步函数启用 hooks 。
hooks.enable();

// 模拟业务处理函数。
function handler(params) {
 // 设置 context ,可在中间件中完成此操作(如 Logger Middleware)。
 contexts[executionAsyncId()] = params;
 
 // 以下是业务逻辑。
 console.log(`handler ${JSON.stringify(params)}`);
 f();
}

function f() {
 setTimeout(() => {
  // 输出所属异步过程的 params 。
  console.log(`setTimeout ${JSON.stringify(contexts[executionAsyncId()])}`);
 });
}

// 模拟两个异步过程(两个请求)。
setTimeout(handler, 0, { id: 0 });
setTimeout(handler, 0, { id: 1 });

在上述代码中,我们先声明了 contexts 用于存储每个异步过程中的上下文数据(如 Trace ID),随后我们创建了一个 Async Hooks 实例。我们在异步资源初始化时,设置当前 Async ID 对应的上下文数据,使得其数据为调用者的上下文数据;我们在异步资源被销毁时,删除其对应的上下文数据。

通过这种方式,我们只需在一开始设置上下文数据,即可在其引发的各个过程(同步和异步过程)中,获得上下文数据,从而解决了问题。

执行上述代码,其运行结果如下。根据输出日志可知,我们的解决方案是可行的。

handler {"id":0}
handler {"id":1}
setTimeout {"id":0}
setTimeout {"id":1}

不过需要注意的是,Async Hooks 是 实验性 API , 存在一定的性能损耗 ,但 Node 官方正努力将其变得生产可用。因此, 在机器资源足够的情况下,使用本解决方案,牺牲部分性能,换取开发体验。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
一些不错的js函数ajax
Aug 20 Javascript
node.js中的url.resolve方法使用说明
Dec 10 Javascript
JS检测移动端横竖屏的代码
May 30 Javascript
js实现刷新页面后回到记录时滚动条的位置【两种方案可选】
Dec 12 Javascript
JavaScript上传文件时不用刷新页面方法总结(推荐)
Aug 15 Javascript
QQ跳转支付宝并自动领红包脚本(最新)
Jun 22 Javascript
angularjs结合html5实现拖拽功能
Jun 25 Javascript
JavaScript的词法结构精华篇
Oct 17 Javascript
小程序二次贝塞尔曲线实现购物车商品曲线飞入效果
Jan 07 Javascript
更优雅的微信小程序骨架屏实现详解
Aug 07 Javascript
小程序实现上传视频功能
Aug 18 Javascript
Node使用koa2实现一个简单JWT鉴权的方法
Jan 26 Javascript
vue-router结合vuex实现用户权限控制功能
Nov 14 #Javascript
vue router 传参获取不到的解决方式
Nov 13 #Javascript
Vue解析带html标签的字符串为dom的实例
Nov 13 #Javascript
vue props对象validator自定义函数实例
Nov 13 #Javascript
微信小程序获取当前位置和城市名
Nov 13 #Javascript
使用Promise封装小程序wx.request的实现方法
Nov 13 #Javascript
微信小程序wx.request的简单封装
Nov 13 #Javascript
You might like
用PHP ob_start()控制浏览器cache、生成html实现代码
2010/02/16 PHP
PHP中is_file不能替代file_exists的理由
2014/03/04 PHP
php提示Failed to write session data错误的解决方法
2014/12/17 PHP
PHP使用curl_multi实现并发请求的方法示例
2018/04/29 PHP
Nigma vs Liquid BO3 第二场2.13
2021/03/10 DOTA
JavaScript window.setTimeout() 的详细用法
2009/11/04 Javascript
jQuery获得子元素个数的方法
2015/04/14 Javascript
使用AngularJS和PHP的Laravel实现单页评论的方法
2015/06/19 Javascript
详解JavaScript的表达式与运算符
2015/11/30 Javascript
基于AngularJs + Bootstrap + AngularStrap相结合实现省市区联动代码
2016/05/30 Javascript
JS中script标签defer和async属性的区别详解
2016/08/12 Javascript
解决JS内存泄露之js对象和dom对象互相引用问题
2017/06/25 Javascript
JS实现浏览上传文件的代码
2017/08/23 Javascript
js禁止Backspace键使浏览器后退的实现方法
2017/09/01 Javascript
Promise.all中对于reject的处理方法
2018/08/01 Javascript
JS实现使用POST方式发送请求
2019/08/30 Javascript
WEEX环境搭建与入门详解
2019/10/16 Javascript
JS出现404错误原理及解决方案
2020/07/01 Javascript
JS相册图片抖动放大展示效果的示例代码
2021/01/29 Javascript
Python中协程用法代码详解
2018/02/10 Python
高效使用Python字典的清单
2018/04/04 Python
对numpy下的轴交换transpose和swapaxes的示例解读
2019/06/26 Python
Python 实现Numpy中找出array中最大值所对应的行和列
2019/11/26 Python
python中安装django模块的方法
2020/03/12 Python
你应该知道的Python3.6、3.7、3.8新特性小结
2020/05/12 Python
Python常用库Numpy进行矩阵运算详解
2020/07/21 Python
解决pycharm不能自动保存在远程linux中的问题
2021/02/06 Python
html5理解head_动力节点Java学院整理
2017/07/13 HTML / CSS
Canvas 文本转粒子效果的实现代码
2019/02/14 HTML / CSS
自考生自我鉴定范文
2013/10/01 职场文书
机械专业求职信
2014/05/25 职场文书
罗马假日观后感
2015/06/08 职场文书
医院见习总结
2015/06/24 职场文书
手把手教你怎么用Python实现zip文件密码的破解
2021/05/27 Python
使用Python+OpenCV进行卡类型及16位卡号数字的OCR功能
2021/08/30 Python
万能密码的SQL注入漏洞其PHP环境搭建及防御手段
2021/09/04 SQL Server