ThreadLocal 变量的说法来自于 Java, 这是在多线程模型下出现并发问题的一种解决方案.
ThreadLocal 变量作为线程内的局部变量, 在多线程下可以保持独立, 它存在于
线程的生命周期内, 可以在线程运行阶段多个模块间共享数据. 那么, ThreadLocal 变量
又如何与 node.js 扯上关系呢?
node 模型
node 的运行模型无需再赘言: "事件循环 + 异步执行", 可是 node 开发工程师比较感兴趣的点
大多集中在 "编码模式" 上, 即异步代码同步编写, 由此提出了多种解决回调地狱的解决方案:
- yield
- thunk
- promise
- await
可是如果从代码执行流程的微观视角中跳出来, 宏观上看待 node 服务器处理每个 HTTP 请求, 就会
发现这其实是多线程 web 服务器的另一种体现, 虽然设计上并不像多线程模型那么直观. 在单核 cpu 中
每一时刻 node 服务器只能处理一个请求, 可是 node 在当前请求中执行异步调用时, 就会 "中断" 进入下一个
事件循环处理另一个请求, 直到上一个请求的异步任务事件触发执行对应回调, 继续执行该请求的后续逻辑.
这在某种程度上类似于 CPU 的时间片抢占机制, 微观上的顺序执行, 宏观上却是同步执行.
node 在单进程单线程 (js 执行线程) 中 "模拟" 了常见的多线程处理逻辑, 虽然在单个 node 进程中无法
充分利用 CPU 的多核及超线程特性, 可是却避免了多线程模型下的临界资源同步和线程上下文
切换的问题, 同时内存资源开销相对较小, 因此在 I/O 密集型的业务下使用 node 开发 web 服务
往往有着意想不到的好处.
可是在 node 开发中需要追踪每个请求的调用链路, 通过获取请求头的 traceId 字段在每一级
的调用链路中传递该字段, 包括 "http 请求, dubbo 调用, dao 操作, redis 和日志打点" 等操作.
这样通过追踪 traceId, 就可以分析请求所经过的所有中间链路, 评估每个环节的时延与瓶颈,
更容易进行性能优化和错误排查.
那么, 如何在业务代码中无侵入性的获取到相关的 traceId 呢? 这就引出了本文的 ThreadLocal 变量.
传统的日志追踪模式
需手动传递 traceId 给日志中间件:
- var koa = require('koa');
- var app = new koa();
- var Logger = {
- info(msg,traceId){
- console.log(msg,traceId);
- }
- };
- let business = async function(ctx){
- let v = await new Promise((res)=>{
- setTimeout(()=>{
- Logger.info('service 执行结束',ctx.request.headers['traceId'])
- res(123);
- },1000);
- });
- ctx.body = 'hello world';
- Logger.info('请求返回',ctx.request.headers['traceId'])
- };
- app.use(async(ctx,next)=>{
- ctx.request.headers['traceId'] = Date.now() + Math.random();
- await next();
- });
- app.use(async(ctx,next)=>{
- await business(ctx);
- });
- app.listen(8080);
在 business 业务处理函数中, 在 service 执行结束和 body 返回后都进行日志打点, 同时手动
传递请求头 traceId 给日志模块, 方便相关系统追踪链路.
目前这样编码无法规范化日志接口, 同时也对开发人员造成了很大的困扰. 对于业务开发人员他们
理应不关心如何进行链路追踪, 而目前的编码则直接侵入了业务代码中, 这块功能应该由日志模块
Logger 来实现, 可是在与请求上下文没有任何联系的 Logger 模块如何获取每个请求的 traceId 呢?
这就需要依靠 node.js 中的 ThreadLocal 变量. 文章开头提到, 多线程下 ThreadLocal 变量是与
每个线程的生命周期对应的, 那么如果在 node.js 的 "单线程 + 异步调用 + 事件循环" 的特性下实现
类似的 ThreadLocal 变量, 不就可以在每个请求的异步回调执行时获取到对应的 ThreadLocal 变量,
拿到相关的上下文信息吗?
ThreadLocal 的 node 实现
单纯实现 web 服务器的中间链路请求追踪其实并不复杂, 使用全局变量 Map 并通过每个请求的唯一标识
存储上下文信息, 当执行到该请求的下一个异步调用时便通过在全局 Map 中获取到与该请求绑定的 ThreadLocal
变量, 不过这是在应用层面的一种投机行为, 是与请求紧耦合的简易实现.
最彻底的方案则是在 node 应用层实现一种栈帧, 在该栈帧内重写所有的异步函数, 并添加各个
hook 在异步函数的各个生命周期执行, 实现异步函数执行上下文与栈帧的映射, 这便是最为
彻底的 ThreadLocal 实现, 而不是仅仅停留在与 HTTP 请求的映射过程中.
目前已经有 zone.js 库实现了 node 应用层栈帧的可控编码, 同时可以在该栈帧存活阶段绑定
相关数据, 我们便可以利用这种特性实现类似多线程下的 ThreadLocal 变量.
我们的目标是实现无侵入的编写包含链路追踪的业务代码, 如下所示:
- app.use(async(ctx,next)=>{
- let v = await new Promise((res)=>{
- setTimeout(()=>{
- Logger.info('service 执行结束')
- res(123);
- },1000);
- });
- ctx.body = 'hello world';
- Logger.info('请求返回')
- });
相比较, Logger.info 中不需要手动传递 traceId 变量, 由日志模块通过访问 ThreadLocal 变量获取.
通过 zone.js 提供的创建 Zone(对应于栈帧)功能, 我们不仅可以获取当前请求 (类似于多线程下的单个线程) 的
ThreadLocal 变量, 还可以获取上一个请求的相关信息.
- require('zone.js');
- var koa = require('koa');
- var app = new koa();
- var Logger = {
- info(msg){
- console.log(msg,Zone.current.get('traceId'));
- }
- };
- var koaZoneProperties = {
- requestContext: null
- };
- var koaZone = Zone.current.fork({
- name: 'koa',
- properties: koaZoneProperties
- });
- let business = async function(ctx){
- let v = await new Promise((res)=>{
- setTimeout(()=>{
- Logger.info('service 执行结束')
- res(123);
- },1000);
- });
- ctx.body = 'hello world';
- Logger.info('请求返回')
- };
- koaZone.run(()=>{
- app.use(async(ctx,next)=>{
- console.log(koaZone.get('requestContext'))
- ctx.request.headers['traceId'] = Date.now();
- await next();
- });
- app.use(async(ctx,next)=>{
- await new Promise((resolve)=>{
- let koaMidZone = koaZone.fork({
- name: 'koaMidware',
- properties: {
- traceId: ctx.request.headers['traceId']
- }
- }).run(async()=>{
- // 保存请求上下文至 parent zone
- koaZoneProperties.requestContext = ctx;
- await business(ctx);
- resolve();
- });
- });
- });
- app.listen(8080);
- });
创建了两个有继承关系的 zone(栈帧),koaZone 的 requestContext 属性存储上一个请求的上下文信息;
koaMidZone 的 traceId 属性存储 traceId 变量, 这是一个 ThreadLocal 变量.
Logger.info 中通过 Zone.current.get('traceId') 获取当前 "线程" 的
ThreadLocal 变量, 无需开发人员手动传递 traceId 变量.
关于 zone.js 的其他用法, 读者有兴趣可以自行研究. 本文主要利用 zone.js 保存一个执行栈帧
内的多个异步函数的执行上下文与特定数据 (即 ThreadLocal 变量) 的映射.
说明
目前, 这套模型已在线上业务中用来追踪各级链路, 各级中间件包括 dubbo client,dubbo provider,
配置中心等都依赖 ThreadLocal 变量实现数据透传和调用传递, 因此可以放心使用.
来源: https://www.cnblogs.com/accordion/p/9098708.html