Node绑定全局TraceID的实现方法

问题描述

由于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 官方正努力将其变得生产可用。因此, 在机器资源足够的情况下,使用本解决方案,牺牲部分性能,换取开发体验。

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

(0)

相关推荐

  • 解决linux下node.js全局模块找不到的问题

    今天在在linux上用npm安装了pm2准备部署node项目,结果通过pm2命令启动项目的时候报pm2找不到,这很伤,以为pm2没有安装成功,但是在node安装目录下面的bin文件夹里面调用pm2却没问题. /usr/local/node/node-v7.3.0-linux-x64/bin/pm2 list 类似这样调用pm2 list命令查看pm2启动的项目没有任何问题. 原来linux系统上通过npm全局安装了模块以后,不能像windos下面那样自动配置环境变量,需要自己动手配置...好吧,

  • Windows 系统下设置Nodejs NPM全局路径

    在开发过程中我们可能希望重新设置npm的全局路径,具体方法如下: npm config set cache "D:\nodejs\node_cache" npm config set prefix "D:\nodejs\node_global"

  • nodejs中全局变量的实例解析

    1.global 类似于客户端javascript运行环境中的window module1.js: module.exports={}; //耻辱的使用了全局变量 global.varA = "abc"; 关于global对象,实际使用中可以省去global. 他是默认的根作用域,相当于web环境中的window对象. global 对象有几个挺有用的属性: console.log(__dirname);//当前路径 console.log(__filename);//当前在执行的js

  • 详解Node全局变量global模块

    环境:Node v8.2.1; Npm v5.3.0; OS Windows10 在浏览器中我们有window对象用于挂载全局的变量,在Node中我们有global对象可以进行挂载,很多共用的属性就可以挂载到global对象上了,本身它自己也拥有很多的属性. 1.API结构图 2.一些例子 下面是部分API的例子,对部分API的一个简单代码实现 2.1 global 全局命名空间,通过 global 定义的变量,在任何地方都可以使用,类似于浏览器端定义在全局范围中的变量. // foo.js g

  • Node.js模块全局安装路径配置方法

    下载安装NodeJS后,在自己选择的路径下会有如下的文件: 默认情况下NodeJS安装会同时安装npm(模块管理器:用于管理用户require的模块,有全局和本地两种). 注: 全局:执行npm install <模块的名字> -g 就会将模块装在全局路径下,当用户在程序中require(<模块的名字>)的时候不用考虑模块在哪,如果不修改全局路径,用户下载的模块会默认在C:\Users\Administrator\AppData\Roaming\npm这个路径下. 局部:执行npm

  • Node绑定全局TraceID的实现方法

    问题描述 由于Node.js的 单线程模型 的限制,我们无法设置全局 traceid 来聚合请求,即 实现输出日志与请求的绑定 .如果不实现日志和请求的绑定,我们难以判断日志输出与对应用户请求的对应关系,这对 线上问题排查 带来了困难. 例如,在用户访问 retrieveOne API 时,其会调用 retrieveOneSub 函数,如果我们想在 retrieveOneSub 函数中输出当前请求对应的学生信息,是繁琐的.在 course-se 现有实现下,我们针对此问题的解决方法是: 方案1:

  • Vue数据双向绑定原理及简单实现方法

    Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. 一.示例 var vm = new Vue({ data: { obj: { a: 1 } }, created: function () { console.log(this.obj); } }); 二.实现原理 vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的. 1)数据劫持.vue是通过Object.defineProperty()来实现数据劫持

  • Node.js静态服务器的实现方法

    当你输入一个url时,这个url可能对应服务器上的一个资源(文件)也可能对应一个目录. So服务器会对这个url进行分析,针对不同的情况做不同的事. 如果这个url对应的是一个文件,那么服务器就会返回这个文件. 如果这个url对应的是一个文件夹,那么服务器会返回这个文件夹下包含的所有子文件/子文件夹的列表. 以上,就是一个静态服务器所主要干的事. 但真实的情况不会像这么简单, 我们所拿到的url可能是错误的,它所对应的文件或则文件夹或许根本不存在, 又或则有些文件和文件夹是被系统保护起来的是隐藏

  • Node端异常捕获的实现方法

    目录 常见Node报错处理机制 try catch Node原生错误处理机制 Promise async/await + try catch unhandledRejection 特殊情况如何捕获异常 uncaughtException Express错误处理 常见Node报错处理机制 try catch try...catch是大家最常用的错误处理机制,Javascript语言内置的错误处理机制可以在检测到代码异常的时候直接进行捕获并处理. function test() { try { th

  • node版本快速切换及管理方法

    目录 如何下载node 1.使用n切换node版本 2.使用nvm切换node版本 nvm和n的区别 注意事项 如何下载node 不要下载最新版,推荐先下载稳定版的nodejs.org/zh-cn/downl…,下载完一键式安装即可,安装完成后,命令行查看是否成功. 1.使用n切换node版本 node有一个模块n,是专门用来管理node.js的版本的.npm是用来安装n的包管理工具,前端很多工具都可以通过npm来发布,而n是专门用来对node进行版本管理的. 安装模块和指定node版本 # 安

  • Node.js全局处理响应并进行异常管理

    目录 中间件设计模式--"洋葱模型" Node.js中全局处理响应 使用Koa创建应用 中间件设计模式--"洋葱模型" 在Node.js中,可以使用中间件来处理响应和异常. 中间件是在请求处理流程中插入的函数,可以在请求到达处理程序之前或之后执行操作. Node.js中的“洋葱模型”是一种中间件设计模式,该模式允许开发人员在请求的生命周期中使用各种中间件.这种模式被称为“洋葱模型”,因为在这个模型中,请求和响应都被认为是在一个不断层层剥离的“洋葱”中传递的,每一层都

  • jQuery使用bind函数实现绑定多个事件的方法

    本文实例讲述了jQuery使用bind函数实现绑定多个事件的方法.分享给大家供大家参考,具体如下: 在jQuery中绑定多个事件名称是,使用空格隔开,举例如下: $("#foo").bind("mouseenter mouseleave", function() { $(this).toggleClass("entered"); }); 在1.4版本以后,也可以如下绑定 $("#foo").bind({ click: func

  • AngularJS全局警告框实现方法示例

    本文实例讲述了AngularJS全局警告框实现方法.分享给大家供大家参考,具体如下: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="vi

  • jQuery支持动态参数将函数绑定到事件上的方法

    本文实例讲述了jQuery支持动态参数将函数绑定到事件上的方法.分享给大家供大家参考.具体分析如下: 下面的js代码提供了两种方法用于绑定函数到事件,其中方法二可以传递动态参数,非常实用 //方法一 $('#foo').click(function(event) { alert('User clicked on "foo."'); }); //方法二, 支持动态传参 $('#foo').bind('click', {test1:"abc", test2:"

随机推荐