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 相关文章推荐
运用JQuery的toggle实现网页加载完成自动弹窗
Mar 18 Javascript
JavaScript清空数组元素的两种方法简单比较
Jul 10 Javascript
jQuery实现图片左右滚动特效
Apr 20 Javascript
jQuery解决$符号命名冲突
Jun 18 Javascript
jQuery加密密码到cookie的实现代码
Apr 18 jQuery
详解vue-router2.0动态路由获取参数
Jun 14 Javascript
bootstrap multiselect下拉列表功能
Aug 22 Javascript
vue将单页面改造成多页面应用的方法
Nov 25 Javascript
react脚手架如何配置less和ant按需加载的方法步骤
Nov 28 Javascript
如何为你的JavaScript代码日志着色详解
Apr 08 Javascript
深入浅析vue中cross-env的使用
Sep 12 Javascript
vue实现登录、注册、退出、跳转等功能
Dec 23 Vue.js
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
BBS(php & mysql)完整版(六)
2006/10/09 PHP
phpQuery占用内存过多的处理方法
2013/11/13 PHP
php通过PHPExcel导入Excel表格到MySQL数据库的简单实例
2016/10/29 PHP
深入理解PHP的远程多会话调试
2017/09/21 PHP
PHP基于面向对象实现的留言本功能实例
2018/04/04 PHP
js获取当前select 元素值的代码
2010/04/19 Javascript
13 个JavaScript 性能提升技巧分享
2012/07/26 Javascript
JavaScript中“基本类型”之争小结
2013/01/03 Javascript
Jquery倒数计时按钮setTimeout的实例代码
2013/07/04 Javascript
键盘KeyCode值列表汇总
2013/11/26 Javascript
浅析Node.js中使用依赖注入的相关问题及解决方法
2015/06/24 Javascript
javascript实现图片延迟加载方法汇总(三种方法)
2015/08/27 Javascript
AngularJS入门教程之AngularJS表达式
2016/04/18 Javascript
使用jQuery制作基础的Web图片轮播效果
2016/04/22 Javascript
javascript实现一个网页加载进度loading
2017/01/04 Javascript
angularjs2 ng2 密码隐藏显示的实例代码
2017/08/01 Javascript
限时抢购-倒计时的完整实例(分享)
2017/09/17 Javascript
详解Js中的模块化是如何实现的
2017/10/18 Javascript
使用vue-cli(vue脚手架)快速搭建项目的方法
2018/05/21 Javascript
Webpack之tree-starking 解析
2018/09/11 Javascript
vue+axios 前端实现登录拦截的两种方式(路由拦截、http拦截)
2018/10/24 Javascript
对layui数据表格动态cols(字段)动态变化详解
2019/10/25 Javascript
js实现弹幕飞机效果
2020/08/27 Javascript
socket + select 完成伪并发操作的实例
2017/08/15 Python
Python Xml文件添加字节属性的方法
2018/03/31 Python
Python批量修改图片分辨率的实例代码
2019/07/04 Python
python实现人脸签到系统
2020/04/13 Python
加拿大时尚少女服装品牌:Garage
2016/10/10 全球购物
纽约通行卡:The New York Pass(免费游览纽约90多个景点)
2017/07/29 全球购物
IdealFit官方网站:女性蛋白质、补充剂和运动服装
2019/03/24 全球购物
美国尼曼百货官网:Neiman Marcus
2019/09/05 全球购物
Farfetch澳大利亚官网:Farfetch Australia
2020/04/26 全球购物
委托证明范本
2014/11/25 职场文书
廉洁自律承诺书2015
2015/01/22 职场文书
详解 TypeScript 枚举类型
2021/11/02 Javascript
ssh服务器拒绝了密码 请再试一次已解决(亲测有效)
2022/08/14 Servers