Node.js 应用全链路追踪
全链路追踪技术的两个核心要素分别是全链路信息获取
和全链路信息存储展示
。
本文一共分为三个篇章进行介绍;
- 第一章介绍Nodejs应用全链路信息获取。
- 第二章介绍Node.js 应用全链路追踪实战。
Node.js 应用全链路追踪系统
一、简介
目前主流的 Node.js 架构设计主要有以下两种方案:
- 通用架构:只做 ssr 和 bff,不做服务器和微服务;
- 全场景架构:包含 ssr、bff、服务器、微服务。
上述两种方案对应的架构说明图如下图所示:
在上述两种通用架构中,nodejs 都会面临一个问题,那就是:
在请求链路越来越长,调用服务越来越多,其中还包含各种微服务调用的情况下,出现了以下诉求:
- 如何在请求发生异常时快速定义问题所在;
- 如何在请求响应慢的时候快速找出慢的原因;
- 如何通过日志文件快速定位问题的根本原因。
我们要解决上述诉求,就需要有一种技术,将每个请求的关键信息聚合起来,并且将所有请求链路串联起来。让我们可以知道一个请求中包含了几次服务、微服务请求的调用,某次服务、微服务调用在哪个请求的上下文。
这种技术,就是Node.js应用全链路追踪。它是 Node.js 在涉及到复杂服务端业务场景中,必不可少的技术保障。
综上,我们需要Node.js应用全链路追踪,说完为什么需要后,下面将介绍如何做Node.js应用的全链路信息获取。
二、全链路信息获取
全链路信息获取,是全链路追踪技术中最重要的一环。只有打通了全链路信息获取,才会有后续的存储展示流程。
对于多线程语言如 Java 、 Python 来说,做全链路信息获取有线程上下文如 ThreadLocal 这种利器相助。而对于Node.js来说,由于单线程和基于IO回调的方式来完成异步操作,所以在全链路信息获取上存在天然获取难度大的问题。那么如何解决这个问题呢?
三、业界方案
由于 Node.js 单线程,非阻塞 IO 的设计思想。在全链路信息获取上,到目前为止,主要有以下 4 种方案:
- domain: node api;
- zone.js: Angular 社区产物;
- 显式传递:手动传递、中间件挂载;
- Async Hooks:node api;
而上述 4 个方案中, domain 由于存在严重的内存泄漏,已经被废弃了;zone.js 实现方式非常暴力、API比较晦涩、最关键的缺点是 monkey patch 只能 mock api ,不能 mock language;显式传递又过于繁琐和具有侵入性;综合比较下来,效果最好的方案就是第四种方案,这种方案有如下优点:
- node 8.x 新加的一个核心模块,Node 官方维护者也在使用,不存在内存泄漏;
- 非常适合实现隐式的链路跟踪,入侵小,目前隐式跟踪的最优解;
- 提供了 API 来追踪 node 中异步资源的生命周期;
- 借助 async_hook 实现上下文的关联关系;
优点说完了,下面我们就来介绍如何通过 Async Hooks 来获取全链路信息。
四、async hooks
官方文档描述 async_hooks
: 它被用来追踪异步资源,也就是监听异步资源的生命周期。
The async_hooks module provides an API to track asynchronous resources.
既然它被用来追踪异步资源,则在每个异步资源中,都有两个 ID:
asyncId
: 异步资源当前生命周期的 IDtrigerAsyncId
: 可理解为父级异步资源的 ID,即parentAsyncId
通过以下 API 调取
1 | ini复制代码const async_hooks = require('async_hooks'); |
更多详情参考官方文档: async_hooks API
既然谈到了 async_hooks
用以监听异步资源,那会有那些异步资源呢?我们日常项目中经常用到的也无非以下集中:
Promise
setTimeout
fs
/net
/process
等基于底层的API
然而,在官网中 async_hooks
列出的竟有如此之多。除了上述提到的几个,连 console.log
也属于异步资源: TickObject
。
1 | 复制代码FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE, |
async_hooks.createHook
我们可以通过 asyncId
来监听某一异步资源。
通过 async_hooks.createHook
创建一个钩子,事例代码:
1 | less复制代码const asyncHook = async_hooks.createHook({ |
我们只需要关注最重要的四个 API:
init
: 监听异步资源的创建,在该函数中我们可以获取异步资源的调用链,也可以获取异步资源的类型,这两点很重要。destory
: 监听异步资源的销毁。要注意setTimeout
可以销毁,而Promise
无法销毁,如果通过 async_hooks 实现 CLS(Continuation-local Storage) 可能会在这里造成内存泄漏!before
after
1 | scss复制代码setTimeout(() => { |
async_hooks 调试及测试
1 | javascript复制代码const fs = require('fs') |
async_hooks.createHook 可以注册 4 个方法来跟踪所有异步资源的初始化(init)、回调之前(before)、回调之后(after)、销毁后(destroy)事件,并通过调用 .enable() 启用,调用 .disable() 关闭。
这里我们只关心异步资源的初始化和销毁的事件,并使用 fs.writeSync(1, msg)
打印到标准输出,writeSync 的第 1 个参数接收文件描述符,1 表示标准输出。为什么不使用 console.log 呢?因为 console.log 是一个异步操作,如果在 init、before、after 和 destroy 事件处理函数中出现,就会导致无限循环,同理也不能使用任何其他的异步操作。
运行该程序,打印如下:
本文转载自: 掘金