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 动画卷页 返回顶部 动画特效(兼容Chrome)
Feb 15 Javascript
js中的eventType事件及其浏览器支持性介绍
Nov 29 Javascript
判断访客终端类型集锦
Jun 05 Javascript
JavaScript中Window对象的属性及事件
Dec 25 Javascript
jQuery实现的无缝广告图片左右滚动功能详解
Dec 24 Javascript
Move.js入门
Feb 08 Javascript
微信小程序 支付功能开发错误总结
Feb 21 Javascript
微信小程序实现页面跳转传值以及获取值的方法分析
Dec 18 Javascript
详解js类型判断
May 22 Javascript
详解处理bootstrap4不支持远程静态框问题
Jul 20 Javascript
node省市区三级数据性能测评实例分析
Nov 06 Javascript
Js类的构建与继承案例详解
Sep 15 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-fpm的两种进程管理模式详解
2013/06/03 PHP
php 微信开发获取用户信息如何实现
2016/12/13 PHP
PHP实现的Redis多库选择功能单例类
2017/07/27 PHP
PHP实现的数据对象映射模式详解
2019/03/20 PHP
ExtJS DOM元素操作经验分享
2013/08/28 Javascript
JavaScript队列函数和异步执行详解
2017/06/19 Javascript
详解利用jsx写vue组件的方法示例
2017/07/17 Javascript
客户端(vue框架)与服务器(koa框架)通信及服务器跨域配置详解
2017/08/26 Javascript
webpack3之loader全解析
2017/10/26 Javascript
Node.js使用supervisor进行开发中调试的方法
2019/03/26 Javascript
微信小程序实现点击空白隐藏的方法示例
2019/08/13 Javascript
vue解决花括号数据绑定不成功的问题
2019/10/30 Javascript
ES2020让代码更优美的运算符 (?.) (??)
2021/01/04 Javascript
js加减乘除精确运算方法实例代码
2021/01/17 Javascript
[01:01:04]2018DOTA2亚洲邀请赛 4.5 淘汰赛 OpTic vs TNC 第一场
2018/04/06 DOTA
Centos5.x下升级python到python2.7版本教程
2015/02/14 Python
致Python初学者 Anaconda入门使用指南完整版
2018/04/05 Python
pyqt 多窗口之间的相互调用方法
2019/06/19 Python
python中seaborn包常用图形使用详解
2019/11/25 Python
keras 权重保存和权重载入方式
2020/05/21 Python
Python使用configparser读取ini配置文件
2020/05/25 Python
matplotlib部件之矩形选区(RectangleSelector)的实现
2021/02/01 Python
css3实现3D色子翻转特效
2014/12/23 HTML / CSS
努比亚手机官网:nubia
2016/10/06 全球购物
联想马亚西亚官方网站:Lenovo Malaysia
2018/09/19 全球购物
NYX Professional Makeup英国官网:美国平价专业彩妆品牌
2019/11/13 全球购物
Marc O’Polo俄罗斯官方在线商店:德国高端时尚品牌
2019/12/26 全球购物
高级3D打印市场:Gambody
2019/12/26 全球购物
妇产科护士自我鉴定
2013/10/15 职场文书
给海归自荐信的建议
2013/12/13 职场文书
英语专业学生个人求职信
2014/01/28 职场文书
廉洁自律承诺书
2014/03/27 职场文书
母校寄语大全
2014/04/10 职场文书
部队反四风对照检查材料
2014/09/26 职场文书
2014离婚协议书范文(3篇)
2014/11/29 职场文书
2016新春团拜会致辞
2015/08/01 职场文书