问题描述
由于Node.js的 单线程模型 的限制,我们无法设置全局 traceid 来聚合请求,即 实现输出日志与请求的绑定 。如果不实现日志和请求的绑定,我们难以判断日志输出与对应用户请求的对应关系,这对 线上问题排查 带来了困难。
例如,在用户访问 retrieveOne API 时,其会调用 retrieveOneSub 函数,如果我们想在 retrieveOneSub 函数中输出当前请求对应的学生信息,是繁琐的。在 course-se 现有实现下,我们针对此问题的解决方法是:
- 方案1:在调用 retrieveOneSub 函数的父函数,即 retrieveOne 内,对 paramData 进行 解构 ,输出学生相关信息,但该方案 无法细化日志输出粒度 。
- 方案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 官方正努力将其变得生产可用。因此, 在机器资源足够的情况下,使用本解决方案,牺牲部分性能,换取开发体验。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 中国武警男声合唱团《辉煌之声1天路》[DTS-WAV分轨]
- 紫薇《旧曲新韵》[320K/MP3][175.29MB]
- 紫薇《旧曲新韵》[FLAC/分轨][550.18MB]
- 周深《反深代词》[先听版][320K/MP3][72.71MB]
- 李佳薇.2024-会发光的【黑籁音乐】【FLAC分轨】
- 后弦.2012-很有爱【天浩盛世】【WAV+CUE】
- 林俊吉.2012-将你惜命命【美华】【WAV+CUE】
- 晓雅《分享》DTS-WAV
- 黑鸭子2008-飞歌[首版][WAV+CUE]
- 黄乙玲1989-水泼落地难收回[日本天龙版][WAV+CUE]
- 周深《反深代词》[先听版][FLAC/分轨][310.97MB]
- 姜育恒1984《什么时候·串起又散落》台湾复刻版[WAV+CUE][1G]
- 那英《如今》引进版[WAV+CUE][1G]
- 蔡幸娟.1991-真的让我爱你吗【飞碟】【WAV+CUE】
- 群星.2024-好团圆电视剧原声带【TME】【FLAC分轨】