java中可通过thread local记录链路信息,每个请求进程都持有自己数据的副本,但node是单进程的,无法通过进程进行链路信息存储与追踪。但提供了AsyncLocalStorage实现异步资源状态共享,可以在整个异步操作中进行数据共享记录,通过此特性可以实现链路信息记录追踪。

基本使用

AsyncLocalStorage本身使用比较简单,提供了run方法用于入口设置,可以设置存储数据,并提供了get方法获取存储数据,存储数据在整个异步执行过程中(也就是包裹在run方法中的callback中的执行过程)是共享的,并与其它异步资源隔离,如下:

import { AsyncLocalStorage } from 'async_hooks';
import { v4 as uuidV4 } from 'uuid';

type TraceStore = { // 数据存储格式,这里只存储一个id
    traceId: string,
}

const localStorage = new AsyncLocalStorage<TraceStore>(); // 定义AsyncLocalStorage对象

const LocalStorageWrapper = {
    run: (traceStore: TraceStore, callback?: (args?: unknown) => any) => {
        // TODO 可以进行其它处理
        localStorage.run(traceStore, callback);
    },

    getTraceStore: () => {
        return localStorage.getStore();
    },
}

export default LocalStorageWrapper;

这样,将主函数传入run方法中,则在函数整个执行过程中都会维持一份数据记录,写一个打印函数,同时打印traceId与函数名,如下:

const COLORS = {
    NONE: '\x1B[0m',
    CYAN: '\x1B[36m', // 青色
}
const println = (message: any, ...optionParams: any[]) => {
    const traceStore = LocalStorageWrapper.getTraceStore();
    const err = new Error();
    const st = err.stack;
    const caller = st.split('\n')[2].trim().split(' ')[1]; // 函数调用的地 ...
    console.log(`${COLORS.CYAN}[${traceStore.traceId}, ${caller}]${COLORS.NONE}`, message, ...optionParams);
}

写一个测试函数如下:

function subFunc() {
    println("I'm sub func ...");
}

function testMain() {
    subFunc();
    println("I'm main func ...");
}

通过localStorage包裹调用,如下:

LocalStorageWrapper.run({traceId: new Date().getTime().toString()}, testMain);

则能看到输出如下:

image-20220129161739482

在整个过程中存储信息是共享的。

express等框架中使用

expressweb框架中使用时,可将run注册为中间件,在一个请求到来时执行中间件传入traceId,因为中间件是伴随路由整个过程的,所以在请求发出前整个异步过程都能共享存储,如下:

import * as express from "express";
import { Request, Response } from 'express';

import LocalStorageWrapper from "LocalStorageWrapper";

const app = express();
const port = 3000;
app.use((req: Request, res: Response, next: () => any) => { // 应用中间件
    const traceId = req.get('traceid') || new Date().getTime().toString();
    LocalStorageWrapper.run({ traceId }, next);
});

class FakeService { // 模拟一个其它服务类
    print() {
        println('service was called ...'); // 打印
    }
}

const fakeService = new FakeService();

app.get("/trace", (req: Request, res: Response) => {
    println("enter /trace"); // 打印
    fakeService.print();
    res.send("Hello World!");
});

app.listen(port, () => {
    console.log(`server listening on port ${port}`);
});

多次访问localhost:3000/trace,输出如下:

image-20220129164740905

性能

在引入异步状态存储后,由于每个异步执行都会触发响应hooks,因此性能会有所下降,虽说已有很大优化改善,但据Github上测试大约有8%的性能下降。