当前位置: 首页 > 后端技术 > Node.js

node.js的术语和ThreadLocal

时间:2023-04-03 14:55:01 Node.js

ThreadLocal变量来自Java,是多线程模型下的并发问题的解决方案。ThreadLocal变量作为一个线程中的局部变量,在多线程下可以保持独立。它存在于线程的生命周期中,可以在线程运行阶段在多个模块之间共享数据。那么,ThreadLocal变量与node.js有什么关系呢?Node模型Node的运行模型不用多说:“事件循环+异步执行”,但是Node开发工程师比较感兴趣的点大多都集中在“编码模式”上,即写异步代码同步,并提出了多种解决方案回调地狱的解决方案:yieldthunkpromiseawait但是如果你跳出代码执行过程的微观视角,从宏观的角度来看节点服务器处理每一个HTTP请求,你会发现这个实际上是多线程网络服务器的另一种体现。不如多线程模型直观。在单核cpu中,node服务器一次只能处理一个请求,但是当node在当前请求中执行异步调用时,会“中断”,进入下一个事件循环去处理另一个请求,直到异步任务完成上一个请求的事件触发相应回调的执行,继续执行请求的后续逻辑。这有点类似于CPU的时间片抢占机制,微观上是顺序执行,而宏观上是同步执行。Node在单进程单线程(js执行线程)中“模拟”了常见的多线程处理逻辑。虽然在单节点进程中无法充分发挥CPU的多核和超线程特性,但避免了多线程模型下的临界点。资源同步和线程上下文切换的问题,内存资源开销比较小,所以在I/O密集型业务中使用node开发web服务往往会有意想不到的好处。但是在节点开发中,需要跟踪每个请求的调用链路。通过获取请求头的traceId字段,在各个层级的调用链接中传递该字段,包括“http请求、dubbo调用、dao操作、redis和日志管理”等。这样,通过跟踪traceId,就可以分析出请求经过的所有中间环节,评估各个环节的延迟和瓶颈,更容易进行性能优化和错误排查。那么,如何在业务代码中非侵入式的获取相关的traceId呢?这就引出了本文的ThreadLocal变量。传统的日志追踪方式需要手动将traceId传递给日志中间件:varkoa=require('koa');varapp=newkoa();varLogger={info(msg,traceId){console.log(msg,traceId));}};letbusiness=asyncfunction(ctx){letv=awaitnewPromise((res)=>{setTimeout(()=>{Logger.info('服务执行结束',ctx.request.headers['traceId'])res(123);},1000);});ctx.body='你好世界';Logger.info('requestreturn',ctx.request.headers['traceId'])};app.use(async(ctx,next)=>{ctx.request.headers['traceId']=Date.now()+Math.random();awaitnext();});app.use(async(ctx,next)=>{awaitbusiness(ctx);});app.listen(8080);在业务业务处理函数,服务执行结束,body返回后,处理日志,同时手动将请求头traceId传递给日志模块,方便相关系统跟踪链接。目前这样的编码无法规范日志接口,同时也给开发者带来很大的困扰。对于业务开发人员来说,他们应该不关心如何跟踪链接,现在的编码直接侵入了业务代码。这个功能应该是日志模块Logger实现的,但是如何获取与请求上下文无关的Logger模块呢?每个请求的traceId怎么样?这需要依赖node.js中的ThreadLocal变量。文章开头提到,多线程下的ThreadLocal变量对应的是每个线程的生命周期,所以如果在node.js“单线程+异步调用+事件循环”的特点下实现一个类似的ThreadLocal变量。每次请求的异步回调执行时,获取对应的ThreadLocal变量,获取相关的上下文信息?ThreadLocal的节点实现只是简单的实现了web服务器的中间链接请求跟踪。其实并不复杂。它使用全局变量Map,通过每个请求的唯一标识符存储上下文信息。在执行请求的下一次异步调用时,通过在全局Map中获取请求绑定的ThreadLocal变量,但这是应用程序层面的推测行为,是一种简单的实现,紧加上请求。最彻底的方案是在node应用层实现一个栈帧,在栈帧中重写所有的异步函数,在异步函数的每个生命周期中添加每个钩子去执行,从而实现执行上下文之间的连接异步函数和栈帧Mapping,这是最彻底的ThreadLocal实现,而不是仅仅停留在与HTTP请求的映射过程中。目前zone.js库已经实现了node应用层栈帧的可控编码,同时可以在栈帧存活阶段绑定相关数据。我们可以利用这个特性来实现类似多线程的ThreadLocal变量。我们的目标是实现包含链接跟踪的业务代码的非侵入式编写,如下:app.use(async(ctx,next)=>{letv=awaitnewPromise((res)=>{setTimeout(()=>{Logger.info('服务执行结束')res(123);},1000);});ctx.body='helloworld';Logger.info('请求返回')});相比之下,Logger.info中的traceId变量不需要手动传递,它是日志模块通过访问ThreadLocal变量获取的。通过zone.js提供的创建Zone(对应一个栈帧)的函数,我们不仅可以获取到当前请求的ThreadLocal变量(类似于多线程下的单线程),还可以获取到请求的相关信息先前的请求。require('zone.js');varkoa=require('koa');varapp=newkoa();varLogger={info(msg){console.log(msg,Zone.current.get('traceId'));}};varkoaZoneProperties={requestContext:null};varkoaZone=Zone.current.fork({name:'koa',properties:koaZoneProperties});letbusiness=asyncfunction(ctx){letv=awaitnewPromise((res)=>{setTimeout(()=>{Logger.info('服务执行绑定')res(123);},1000);});ctx.body='你好世界';Logger.info('请请求返回')};koaZone.run(()=>{app.use(async(ctx,next)=>{console.log(koaZone.get('requestContext'))ctx.request.headers['traceId']=Date.now();awaitnext();});app.use(async(ctx,next)=>{awaitnewPromise((resolve)=>{让koaMidZone=koaZone.fork({名称:'koaMidware',属性:{traceId:ctx.request.headers['traceId']}}).run(async()=>{//将请求上下文保存到父区域koaZoneProperties.requestContext=ctx;awaitbusiness(ctx);resolve();});});});app.listen(8080);});创建两个具有继承关系的区域(栈帧)。koaZone的requestContext属性存储了上一次请求的上下文信息;koaMidZone的traceId属性存放的是traceId变量,它是Zone中传入的ThreadLocal变量Logger.info.current.get('traceId')获取当前“线程”的ThreadLocal变量,不需要开发者手动传递traceId多变的。对zone.js的其他用途感兴趣的读者可以自行研究。本文主要使用zone.js来保存一个执行栈帧中多个异步函数的执行上下文与具体数据(即ThreadLocal变量)的映射关系。需要注意的是,目前这个模型已经被用于跟踪在线业务中各个层级的链接。各级中间件,包括dubboclient、dubboprovider、配置中心,都依赖ThreadLocal变量实现数据透传和调用传输,可以放心使用。