尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Micrometer 系列【67】统一观测:基于 Spring Boot 的生产级演示案例 | 跨线程场景

Micrometer 系列【67】统一观测:基于 Spring Boot 的生产级演示案例 | 跨线程场景 文章目录1. 前言1.1 单线程回顾1.2 多线程的崩溃1.3 问题演示2. 解决方案2.1 场景一手动创建线程2.2 场景二自定义线程池2.2.1 方式 1 手动恢复2.2.2 方式 2 自定义 TaskDecorator2.3 场景三Async 注解2.4 场景四JDK 共享线程池2.5 场景五JDK 原生普通线程池3. 方案升级集成 context-propagation 传播库3.1 基础介绍3.2 场景一/二升级captureAll → wrap手动逻辑的标准化版3.3 场景五升级JDK 原生线程池 —— ContextExecutorService.wrap一行3.4 场景二升级TaskDecorator 不用自己写了3.5 场景三解密Async 的 propagate-context 底层就是传播库3.6 手动 vs 库什么时候选谁1. 前言1.1 单线程回顾前面所有案例都是单线程内嵌套调用底层依靠OTel的ThreadLocalContext自然继承父上下文实现链路父子关系、串联整个执行链路。埋点示例ObservationobservationObservation.start(style2.officialManual,registry).lowCardinalityKeyValue(style,2);try(Observation.Scopescopeobservation.openScope()){// 执行业务逻辑}catch(Exceptione){observation.error(e);throwe;}finally{observation.stop();}ThreadLocal是同线程内的上下文容器嵌套调用天然继承。1.2 多线程的崩溃一旦换线程ThreadLocal失效ExecutorServiceexecutorExecutors.newFixedThreadPool(4);ObservationobservationObservation.start(style2.officialManual,registry).lowCardinalityKeyValue(style,2);try(Observation.Scopescopeobservation.openScope()){// 主线程打开Scope提交异步任务executor.submit(()-{// 【问题】子线程无有效Trace上下文拿不到traceId// 链路上下文丢失businessLogic();});}catch(Exceptione){observation.error(e);throwe;}finally{observation.stop();}工作线程的ThreadLocal是一张白纸根本就没有父Span建出来的是根 SpantraceId都和主线程不一样。链路在这里彻底断裂。1.3 问题演示改造之前的OrderService在下单方法内部调用支付服务publicOrderResultcreateOrder(StringorderNo,LonguserId,StringorderType){OrderObservationContextcontextOrderObservationContext.builder().operationMetadata(BusinessOperationMetadata.builder().operationType(BusinessOperationType.ORDER.value()).provider(order-service).build()).orderNo(orderNo).userId(userId).orderType(orderType).build();returnOrderObservationDocumentation.ORDER_CREATE.observation(this.observationConvention.getIfAvailable(),newDefaultOrderObservationConvention(),()-context,this.observationRegistry).observe(()-{// —— 业务逻辑 ——// 下单StringorderIdORD-UUID.randomUUID().toString().substring(0,8).toUpperCase();BigDecimalamountnewBigDecimal(99.90);context.setOrderId(orderId);context.setAmount(amount);context.setStatus(CREATED);// 支付this.paymentService.pay(orderId,WECHAT);returnnewOrderResult(orderId,orderNo,amount,CREATED);});}链路层级原始链路层级排版spring-micrometer-service-aa http get/api/order/create66.9ms ← 父SpanRootSpan └── spring-micrometer-service-aa orderNORMAL16.9ms ← 子Span└── spring-micrometer-service-aa paymentWECHAT← 孙Span将支付使用异步执行任务// 支付newThread(()-{this.paymentService.pay(orderId,WECHAT);}).start();新线程不会继承父线程ThreadLocalOTel链路上下文会丢失导致异步任务中的支付Span变成了根Span2. 解决方案2.1 场景一手动创建线程上面的【问题演示】可以这么改异步线程外获取当前Scope中的观测对象再手动将其设置到异步现场的Scope中// 捕获当前观测跨线程恢复 scope只依赖 micrometer-observation 核心ObservationorderObservationobservationRegistry.getCurrentObservation();newThread(()-{assertorderObservation!null;try(Observation.ScopescopeorderObservation.openScope()){this.paymentService.pay(orderId,WECHAT);}}).start();基本原理observe(...)回调执行时order.create的scope已压在当前线程栈上所以getCurrentObservation能拿到它。new Thread里orderObservation.openScope()把该观测压到工作线程的scope栈 →paymentService.pay()里observe(...)读到的父级就是它 → 子span挂对。2.2 场景二自定义线程池定义一个支付线程池privateThreadPoolTaskExecutorbuildPaymentExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(100);executor.setThreadNamePrefix(order-pay-);executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(30);executor.initialize();returnexecutor;}2.2.1 方式 1 手动恢复和new Thread一样openScope()在目标线程恢复// 支付线程池异步执行跨线程恢复观测 scope 以保持链路ObservationorderObservationobservationRegistry.getCurrentObservation();paymentExecutor.execute(()-{assertorderObservation!null;try(Observation.ScopescopeorderObservation.openScope()){this.paymentService.pay(orderId,WECHAT);}});2.2.2 方式 2 自定义 TaskDecoratorTaskDecorator是Spring提供的任务边界装饰器接口用于在任务提交到线程池时做横切处理。FunctionalInterfacepublicinterfaceTaskDecorator{Runnabledecorate(Runnablerunnable);}decorate()在提交线程捕获当前观测包一层Runnable在工作线程observation.openScope()里执行任务/** * 提交任务时捕获当前观测工作线程执行时恢复其 scope * 使异步任务正确挂到父观测链路下面。只依赖 micrometer-observation 核心。 */publicclassObservationTaskDecoratorimplementsTaskDecorator{privatefinalObservationRegistryobservationRegistry;publicObservationTaskDecorator(ObservationRegistryobservationRegistry){this.observationRegistryobservationRegistry;}OverridepublicRunnabledecorate(Runnabletask){Observation.ScopecurrentScopethis.observationRegistry.getCurrentObservationScope();Observationobservation(currentScope!null)?currentScope.getCurrentObservation():null;if(observationnull){returntask;}return()-{try(Observation.Scopescopeobservation.openScope()){task.run();}};}}ThreadPoolTaskExecutor配置ObservationTaskDecoratorprivateThreadPoolTaskExecutorbuildPaymentExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(100);executor.setThreadNamePrefix(order-pay-);executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(30);executor.setTaskDecorator(newObservationTaskDecorator(this.observationRegistry));executor.initialize();returnexecutor;}调用线程池执行// 支付线程池异步执行TaskDecorator 负责跨线程恢复观测 scope 以保持链路paymentExecutor.execute(()-this.paymentService.pay(orderId,WECHAT));2.3 场景三Async 注解如果你使用Async方法且依赖自动配置的AsyncTaskExecutor需要通过配置spring.task.execution.propagate-contexttrue主动开启上下文传播。配置示例spring:task:execution:# 开启上下文传播适配 Async 线程池propagate-context:true2.4 场景四JDK 共享线程池ForkJoinPool.commonPool()JVM进程内唯一公共池不用自己创建全局所有代码共享。不指定线程池调用以下方法时触发// 使用 ForkJoinPool.commonPool()CompletableFuture.runAsync(()-{System.out.println(Thread.currentThread().getName());});CompletableFuture.supplyAsync(()-{returntest;});方式1手动传播// —— 提交线程抓当前观测此刻父 scope 仍打开工作线程 openScope() 恢复// 不依赖 TaskDecorator对 commonPool 同样生效但每处异步都要自己写一遍ObservationorderObservationobservationRegistry.getCurrentObservation();CompletableFuture.runAsync(()-{if(orderObservationnull){this.paymentService.pay(orderId,WECHAT);// 无观测时直接执行不传播return;}try(Observation.ScopescopeorderObservation.openScope()){this.paymentService.pay(orderId,WECHAT);}});方式2显式传带TaskDecorator的线程池// 显式传带 TaskDecorator 的线程池 —— scope 被恢复Trace 保持父子关系CompletableFuture.runAsync(()-this.paymentService.pay(orderId,WECHAT),this.paymentExecutor);2.5 场景五JDK 原生普通线程池常见创建方式Executors.newFixedThreadPool()Executors.newCachedThreadPool()Executors.newSingleThreadExecutor()JDK原生的ThreadPoolExecutor/ExecutorService没有setTaskDecorator那是Spring ThreadPoolTaskExecutor独有的能力。只能靠提交时手动getCurrentObservation() 工作线程openScope()方式// 三种常见创建方式JDK 原生 ThreadPoolExecutor均无 setTaskDecoratorExecutorServicefixedExecutors.newFixedThreadPool(2);// 固定 2 线程无界队列ExecutorServicecachedExecutors.newCachedThreadPool();// 按需创建空闲 60s 回收ExecutorServicesingleExecutors.newSingleThreadExecutor();// 单线程串行// 原生池不传播观测 scope只能手动恢复同方式 CObservationobsobservationRegistry.getCurrentObservation();RunnablepayTask()-{if(obsnull){this.paymentService.pay(orderId,WECHAT);return;}try(Observation.Scopescopeobs.openScope()){this.paymentService.pay(orderId,WECHAT);}};CompletableFuture.runAsync(payTask,fixed);CompletableFuture.runAsync(payTask,cached);CompletableFuture.runAsync(payTask,single);// 提交完立即 orderly shutdown已提交任务仍会执行池资源被回收避免演示端点反复调用泄漏线程fixed.shutdown();cached.shutdown();single.shutdown();3. 方案升级集成 context-propagation 传播库3.1 基础介绍上下文搬运是一个横切关注点。手写是点对点方案需要一种面上的统一机制这就是Micrometer生态的context-propagation库Spring、Micrometer、Reactor三团队共同设计Micrometer 1.10/Reactor 3.5起被框架内置支持。核心概念概念作用类比手写方案ContextRegistry注册中心单例登记有哪些上下文——ThreadLocalAccessor每种上下文的适配器读写 ThreadLocal 的契约——ContextSnapshotFactory.captureAll()拍快照一次带走全部已注册上下文≈getCurrentObservation()ContextSnapshot.wrap(runnable)包装任务执行前还原、执行后还原现场≈openScope()的标准化版Observation的适配器不用自己写micrometer-observation内置了ObservationThreadLocalAccessorkey micrometer.observation并经SPIjar内META-INF/services/io.micrometer.context.ThreadLocalAccessor自动注册进全局ContextRegistry。同理Slf4jThreadLocalAccessor负责MDC。就是说前文手写版的捕获逻辑库用captureAll()一行替代而且捕获的是全部已注册上下文不只是Observation一个。3.2 场景一/二升级captureAll → wrap手动逻辑的标准化版// 快照工厂每个工程一个即可ContextSnapshotFactoryfactoryContextSnapshotFactory.builder().build();// ① 捕获提交前调用此时父 scope 仍打开——等价于手写版的 getCurrentObservation()ContextSnapshotsnapshotfactory.captureAll();// ② 传播等价于手写版的 openScope()但判空、还原全部内置newThread(snapshot.wrap(()-this.paymentService.pay(orderId,WECHAT))).start();paymentExecutor.execute(snapshot.wrap(()-this.paymentService.pay(orderId,WECHAT)));和2.1/2.2.1手写8行对比差异在两点判空被消化掉了手写版getCurrentObservation()可能为null要自己分支captureAll()永远返回快照——线程上没有上下文时就是空快照wrap后原样执行无副作用还原是库保证的wrap内部是执行前把快照值set进ThreadLocal→try-with-resources→ 执行后还原工作线程原状态不会出现忘了finally关Scope的残留。3.3 场景五升级JDK 原生线程池 —— ContextExecutorService.wrap一行2.5的结论是JDK原生池没有setTaskDecorator只能手动恢复——有了库这条结论可以升级// 直接把 JDK 原生池包一层提交时自动捕获、执行时自动还原ExecutorServicewrappedContextExecutorService.wrap(Executors.newFixedThreadPool(2),factory);wrapped.execute(()-this.paymentService.pay(orderId,WECHAT));newFixedThreadPool/newCachedThreadPool/newSingleThreadExecutor全部适用业务代码零侵入——和TaskDecorator是同一个思路区别是它是ExecutorService包装器、不依赖 Spring纯JDK工程也能用。场景四的ForkJoinPool.commonPool()不是ExecutorService用wrap包任务即可CompletableFuture.runAsync(snapshot.wrap(()-this.paymentService.pay(orderId,WECHAT)));3.4 场景二升级TaskDecorator 不用自己写了2.2.2手写的ObservationTaskDecorator只传Observation一种上下文。Spring 6.1 内置了ContextPropagatingTaskDecorator它的核心逻辑就是一行——factory.captureAll().wrap(task)ObservationMDC 其他已注册上下文一次全带走ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();// 手写的 ObservationTaskDecorator 类直接删掉换 Spring 内置的executor.setTaskDecorator(newContextPropagatingTaskDecorator());2.2.2 手写 decoratorSpring 内置 decorator传播的上下文只有 Observation全部已注册Observation MDC 其他判空 / 还原自己写captureAll().wrap()内置代码量~20 行1 行配置3.5 场景三解密Async 的 propagate-context 底层就是传播库2.3的spring.task.execution.propagate-context: true是配置一行就全自动——它的底层正是3.5的ContextPropagatingTaskDecoratorBoot自动为自动配置的AsyncTaskExecutor挂上这个decorator本质就是captureAll().wrap()。也就是说2.3 已经在用传播库了只是 Boot 替你接线。两个版本注脚该配置由 Spring Boot 3.1引入早期默认false需要显式开启2.3的写法正确Spring Boot 3.2 起默认值改为true新工程即使什么都不配Async的上下文传播也是开着的。3.6 手动 vs 库什么时候选谁前文手写方案不是白写的——它是理解库的基础也是某些场景的合理选择选手写上下文只有一两种、异步调用点就三五个、或想避免多一个依赖——手写透明直白反而好维护选库上下文多种并存ObservationMDC 安全上下文 租户、异步边界多、或将来要碰Reactor/WebFlux——库的快照一次全量带走新加上下文只改注册表已有代码零改动。两者不是对立的库的wrap()内部执行的就是手写那套流程只是把最容易写错的判空、开闭、还原标准化了。判断规则一句话上下文种类 × 调用点规模乘积越大库的收益越明显。
返回列表