什么是 CLS?在浏览器和 Node.js 中实现 CLS

在写 Flutter 和 Serverless 查资料的时候,在某个博客里看到了 CLS 的相关内容,感受实际上是个很不错的软件工程的解耦想法,因而保存了下来。今天回过头来仔细研究了一下并决定给本身留下一些记录。javascript

场景

不管是在浏览器,仍是在服务端 Node.js,咱们常常会碰到打点上报,追踪错误这样的需求,即便不对特定用户进行追踪,咱们也会给某个 session 分配惟一 ID 以在 log / analytics 界面可以看到用户的完整行为,对于产品分析与错误再现是十分重要的。html

假设咱们须要写一个 error handling ,这个 error handling 会 hold 住全部的请求的异常,咱们如何分辨哪一个错误是哪一个请求形成的呢?java

log.error("Error occured", req);
复制代码

那么这个 error handling 就跟 req 耦合了node

假设咱们须要追踪某个错误,是哪一个 user 产生的,又或者是哪一个错误,user 干了什么致使的?git

log.info("User has done xxx", user);
log.error("Error occured by", user);
复制代码

因而跟 user 也深深的耦合了。github

单单这样的例子好像没有什么大问题,不过多两个参数嘛。但写过大型应用的同窗,后期不断增长功能的时候,你必定写过那种长长的参数列表的函数,又或者是好几百行的一个函数,实在是太不优雅,重构起来也太难。express

尝试解决

函数若是是同步的,那么咱们能够直接挂到全局变量(某个对象)下编程

const global = {};
$("button").click((event) => {
  global.event = event;
  log("button clicked");
});

function log(...args) {
  console.log(global.event, ...args); // { x: xxx, y: xxx, target: xxx } 'button clicked'
  // other logic
}
复制代码

显然这在异步中行不通json

const global = {};
$("button").click((event) => {
  global.event = event;
  setTimeout(() => {
    log("button clicked");
  }, 1000);
});

function log(...args) {
  console.log(global.event, ...args);
  // other logic
}
复制代码

你会发现打印的 global.event 全变成了同一个对象api

咱们须要可以从始至终在同一个异步调用链中一个持续化的存储, 又或者是咱们须要可以辨识当前的异步函数他的惟一辨识符,以和一样内容的异步函数但并非自己的运行的这个做区分。

CLS 登场

在其余语言中,有一个叫作 Thread-local storage 的东西,然而在 Javascript 中,并不存在多线程这种概念(相对而言,Web Worker 等与主进程并不冲突),因而 CLS ,Continuation-local Storage,一个相似于 TLS,得名于函数式编程中的 Continuation-passing style,旨在链式函数调用过程当中维护一个持久的数据。

浏览器的解决方案 Zone.js

先看看是怎么解决的

$('button').click(event => {
  Zone.current.fork({
    name: 'clickZone',
    properties: {
      event
    }
  }).run(
    setTimeout(() => {
      log('button clicked');
    }, 1000);
  );
});

function log(...args) {
  console.log(global.event, ...args);
  // other logic
}
复制代码

Zone.js 是 Angular 2.0 引入的,固然它的功能不仅是提供 CLS,他还有其余相关 API。

一个并不完美的解决方案

咱们试着思考一下, Zone.js 是如何作到这些的。若是浏览器没有提供异步函数运行环境的惟一标识,那么只剩下惟一的一条路,改写全部会产生异步的函数,包装了一层后也就能加入hook了。

我尝试本身写了一下 zone-simulate.js

const Zone = {
  _currentZone: {},
  get current() {
    return {
      ...this._currentZone,
      fork: (zone) => {
        this._currentZone = {
          ...this._currentZone,
          ...zone,
        };
        return this;
      },
      set: (key, value) => {
        this._currentZone[key] = value;
      },
    };
  },
};

(() => {
  const _setTimeout = global.setTimeout;
  global.setTimeout = (cb, timeout, ...args) => {
    const _currentZone = Zone._currentZone;
    _setTimeout(() => {
      const __after = Zone._currentZone;
      Zone._currentZone = _currentZone;
      cb(...args);
      Zone._currentZone = __after;
    }, timeout);
  };
})();

for (let i = 0; i < 10; i++) {
  const value = Math.floor(Math.random() * 100);
  console.log(i, value);
  Zone.current.fork({ i, value });
  setTimeout(() => {
    console.log(Zone.current.i, Zone.current.value);
  }, value);
}
复制代码

看似好像没什么问题,不过

angular with tsconfig target ES2017 async/await will not work with zone.js

浏览器中如今并无完美的解决方案

咱们能够作个实验,在 console 里敲下以下代码

const _promise = Promise;
Promise = function () { console.log('rewrite by ourselves') };
new Promise(() => {}) instanceof Promise
// rewrite by ourselves
// true

async function test() {}
test() instanceof Promise
// false
test() instanceof _promise
// true

async function test() { return new Promise() }

test() instanceof Promise
// rewrite by ourselves
// false
test() instanceof _promise
// rewrite by ourselves
// true
复制代码

也就是说浏览器会把 async 函数的返回值用原生 Promise 包装一层,由于是原生语法,也就没法 hook async 函数。 固然咱们能够用 transpiler 把 async 函数改写成 generator 或者 Promise,不过这并不表明是完美的。

Node.js 的解决方案 async_hooks

Node.js 8后出现的 async_hook 模块,到了版本14仍然没有移去他身上的 Experimental 状态。以及在刚出现的时候是有性能问题的讨论(3年后的今天虽然不知道性能怎么样,不过既然没有移去 Experimental 的标签,若是追求高性能的话仍是应该保持观望)

虽然没有移去 Experimental 的状态,可是稳定性应该没有什么太大问题,大量的 Node.js 的追踪库 / APM 依赖着 async_hooks 模块,若是有重大问题,应该会及时上报并修复

对于性能问题,不展开篇幅讨论,取决于你是否愿意花一点点的性能降低来换取代码的低耦合。

如何使用

async_hooks 提供了一个 createHook 的函数,他能够帮助你监听异步函数的运行时建立以及退出等状态,而且附带了这个运行时的惟一辨识id,咱们能够简单地用它来建立一个 CLS。

cls.js

const {
  executionAsyncId,
  createHook,
} = require("async_hooks");

const { writeSync: fsWrite } = require("fs");
const log = (...args) => fsWrite(1, `${args.join(" ")}\n`);

const Storage = {};
Storage[executionAsyncId()] = {};

createHook({
  init(asyncId, _type, triggerId, _resource) {
    // log(asyncId, Storage[asyncId]);
    Storage[asyncId] = {};
    if (Storage[triggerId]) {
      Storage[asyncId] = { ...Storage[triggerId] };
    }
  },
  after(asyncId) {
    delete Storage[asyncId];
  },
  destroy(asyncId) {
    delete Storage[asyncId];
  },
}).enable();

class CLS {
  static get(key) {
    return Storage[executionAsyncId()][key];
  }
  static set(key, value) {
    Storage[executionAsyncId()][key] = value;
  }
}


// --- seperate line ---

function timeout(id) {
  CLS.set('a', id)
  setTimeout(() => {
    const a = CLS.get('a')
    console.log(a)
  }, Math.random() * 1000);
}

timeout(1)
timeout(2)
timeout(3)
复制代码

Node.js 13 后的官方实现

在社区中已经有了那么多优秀实现的前提下,Node.js 13.10 后新增了一个 AsyncLocalStorage 的 API

nodejs.org/api/async_h…

实际上他已是开箱可用的 CLS 了

const {
  AsyncLocalStorage,
} = require("async_hooks");

const express = require("express");
const app = express();

const session = new AsyncLocalStorage();

app.use((_req, _res, next) => {
  let userId = Math.random() * 1000;
  console.log(userId);
  session.enterWith({ userId });
  setTimeout(() => {
    next();
  }, userId);
});

app.use((_req, res, next) => {
  const { userId } = session.getStore();
  res.json({ userId });
});

app.listen(3000, () => {
  console.log("Listen 3000");
});


const fetch = require('node-fetch')

new Array(10).fill(0).forEach((_, i) => fetch('http://localhost:3000/test', {
  method: 'GET',
}).then(res => res.json()).then(console.log))

// Output:
// Listen 3000
// 355.9573987560112
// 548.3773445851497
// 716.2437886469793
// 109.84756385607896
// 907.6261832949347
// 308.34659685842513
// 407.0145853469649
// 525.820449114568
// 76.91502437038133
// 997.8611964598299
// { userId: 76.91502437038133 }
// { userId: 109.84756385607896 }
// { userId: 308.34659685842513 }
// { userId: 355.9573987560112 }
// { userId: 407.0145853469649 }
// { userId: 525.820449114568 }
// { userId: 548.3773445851497 }
// { userId: 716.2437886469793 }
// { userId: 907.6261832949347 }
// { userId: 997.8611964598299 }
复制代码

参考

相关文章
相关标签/搜索