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 相关文章推荐
避免回车键导致的页面无意义刷新的解决方法
Apr 12 Javascript
javascript和jquery修改a标签的href属性
Dec 16 Javascript
利用js实现在浏览器状态栏显示访问者在本页停留的时间
Dec 29 Javascript
angularjs学习笔记之三大模块(modal,controller,view)
Sep 26 Javascript
js与jQuery实现checkbox复选框全选/全不选的方法
Jan 05 Javascript
vue.js入门(3)——详解组件通信
Dec 02 Javascript
利用node.js爬取指定排名网站的JS引用库详解
Jul 25 Javascript
慕课网题目之js实现抽奖系统功能
Sep 19 Javascript
Vue SPA单页应用首屏优化实践
Jun 28 Javascript
vue axios基于常见业务场景的二次封装的实现
Sep 21 Javascript
js实现拖拽与碰撞检测
Sep 18 Javascript
springboot+VUE实现登录注册
May 27 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
咖啡冲泡指南 咖啡有哪些制作方式 单品咖啡 意式咖啡
2021/03/06 冲泡冲煮
Php做的端口嗅探器--可以指定网站和端口
2006/10/09 PHP
解析php下载远程图片函数 可伪造来路
2013/06/25 PHP
页面乱码问题的根源及其分析
2013/08/09 PHP
php获取服务器端mac和客户端mac的地址支持WIN/LINUX
2014/05/15 PHP
ThinkPHP视图查询详解
2014/06/30 PHP
PHP文件操作详解
2016/12/30 PHP
php中file_get_contents()函数用法实例
2019/02/21 PHP
PHP使用PDO创建MySQL数据库、表及插入多条数据操作示例
2019/05/30 PHP
js日期、星座的级联显示代码
2014/01/23 Javascript
$.each与$().each的区别示例介绍
2014/03/20 Javascript
编写高效jQuery代码的4个原则和5个技巧
2014/04/24 Javascript
javascript获取系统当前时间的方法
2015/11/19 Javascript
Jquery技巧(必须掌握)
2016/03/16 Javascript
JavaScript实现类似拉勾网的鼠标移入移出效果
2016/10/27 Javascript
使用 jQuery.ajax 上传带文件的表单遇到的问题
2016/10/31 Javascript
简单三步实现报表页面集成天气
2016/12/15 Javascript
JavaScript函数节流和函数防抖之间的区别
2017/02/15 Javascript
JQ图片文件上传之前预览功能的简单实例(分享)
2017/11/12 Javascript
Vuex 使用及简单实例(计数器)
2018/08/29 Javascript
微信小程序实现蓝牙打印
2019/09/23 Javascript
[01:19]DOTA2城市挑战赛报名开始 开启你的城市传奇
2018/03/23 DOTA
跟老齐学Python之赋值,简单也不简单
2014/09/24 Python
Python实现基于PIL和tesseract的验证码识别功能示例
2018/07/11 Python
Python操作Excel插入删除行的方法
2018/12/10 Python
python利用多种方式来统计词频(单词个数)
2019/05/27 Python
Python3.9.1中使用match方法详解
2021/02/08 Python
英国领先的品牌珠宝和配件供应商:Acotis Jewellery
2018/03/07 全球购物
类、抽象类、接口的差异
2016/06/13 面试题
酒店led欢迎词
2014/01/09 职场文书
2014全国两会学习心得体会2000字
2014/03/10 职场文书
先进班集体事迹材料
2014/12/25 职场文书
党员个人自我评价
2015/03/03 职场文书
道歉情书大全
2015/05/12 职场文书
建筑工程挂靠协议书
2016/03/23 职场文书
GitHub上77.9K的Axios项目有哪些值得借鉴的地方详析
2021/06/15 Javascript