线上 TraceId 集体失踪,如何破局?

向量数据库大数据微服务

近期线上环境出现诡异问题,异步任务里链路 ID(TraceId)莫名丢失,致使核心业务日志断链,严重影响问题排查。今天给大家分享三种有效解决办法 。

  1. 事件回顾 =======

3.8 大促期间,我司交易系统流量剧增。在排查问题过程中,我们发现下单主流程的日志出现异常,部分 TraceId 丢失,致使调用链路中断,排查难度急剧上升 。

  
[2025-03-08 02:15:33] [TID:4a3b...8c2d] INFO 支付校验通过 → 库存扣减成功    
  
// 异常日志片段(TraceId丢失!)  
[2025-03-08 02:15:34] [TID:N/A] ERROR 优惠券核销失败    

  1. 问题定位 =======

通过代码逐层排查,最终锁定“真凶”——一段使用 CompletableFuture 的异步处理代码:

  
public void processOrder(Order order) {  
    // 主线程(携带TraceId)  
    log.info("[主线程] 开始处理订单 {}", order.getId());   
      
    CompletableFuture.runAsync(() -> {  
        // 子线程(TraceId丢失!)  
        log.info("优惠券核销");   
        couponService.useCoupon(order.getCouponId());  
    }, executor);  
}  

  1. 原因分析 =======

根本原因:MDC 依赖 ThreadLocal 实现线程本地存储,每个线程都有独立的上下文存储空间。而线程池复用机制下,子线程被创建时,无法自动继承父线程 ThreadLocal 中的上下文数据,从而引发 TraceId 丢失冲突 。

MDC 实现原理:

  • MDC 底层基于 ThreadLocal 实现,为每个线程创建独立的键值存储空间;
  • 日志框架通过 %X{traceId} 模式从当前线程的 ThreadLocal 中提取链路ID。

线程池运行机制:

  • 线程复用:池化线程完成任务后不会销毁,而是返回池中等待新任务;
  • 线程隔离:不同线程持有完全独立的 ThreadLocal 存储空间。

典型问题场景:

  
public static void main(String[] args) {  
    // 主线程设置链路ID  
    ThreadLocal<String> traceIdHolder = new ThreadLocal<>();  
    traceIdHolder.set("main-tid");  
  
    // 子线程无法访问主线程的ThreadLocal  
    CompletableFuture.runAsync(() -> {  
        System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get()); // 输出null  
    });  
  
    System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get());  
}  

picture.image

  1. 解决方案 =======

方案一:手动传递上下文

在提交异步任务时,手动捕获并传递 TraceId,确保子线程能获取到主线程的 TraceId。

  
public void processOrder(Order order) {  
    // 主线程(携带TraceId)  
    log.info("[主线程] 开始处理订单 {}", order.getId());   
    String tid = MDC.get(TID);  
  
    CompletableFuture.runAsync(() -> {  
    MDC.put(TID,tid);  
        log.info("[异步任务] 核销优惠券");   
        couponService.useCoupon(order.getCouponId());  
    }, executor);  
}  

这种方式简单直接,不过需要在每个异步任务中手动添加代码,代码侵入性较强,且容易遗漏。

方案二:自定义线程池包装任务

自定义线程池,在提交任务时自动保存当前线程的 MDC 上下文,并在任务执行时恢复,避免手动操作的繁琐。

  
class MDCTaskDecorator implements Runnable {  
    privatefinal Runnable delegate;  
    privatefinal Map<String, String> context;  
  
    public MDCTaskDecorator(Runnable delegate, Map<String, String> context) {  
        this.delegate = delegate;  
        this.context = context;  
    }  
  
    @Override  
    public void run() {  
        Map<String, String> originalContext = MDC.getCopyOfContextMap();  
        try {  
            if (context != null) {  
                MDC.setContextMap(context);  
            }  
            delegate.run();  
        } finally {  
            if (originalContext != null) {  
                MDC.setContextMap(originalContext);  
            } else {  
                MDC.clear();  
            }  
        }  
    }  
}  
  
class MDCTaskExecutor extends ThreadPoolExecutor {  
    public MDCTaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {  
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);  
    }  
  
    @Override  
    public void execute(Runnable command) {  
        Map<String, String> context = MDC.getCopyOfContextMap();  
        super.execute(new MDCTaskDecorator(command, context));  
    }  
}  
  
class CustomThreadPoolSolution {  
    privatestaticfinal Logger logger = LoggerFactory.getLogger(CustomThreadPoolSolution.class);  
  
    public static void main(String[] args) {  
        MDC.put("trace\_id", "654321");  
        MDCTaskExecutor executor = new MDCTaskExecutor(  
                1, 1, 0L, TimeUnit.MILLISECONDS,  
                new LinkedBlockingQueue<>()  
        );  
        executor.execute(() -> logger.info("异步任务执行,trace\_id: {}", MDC.get("trace\_id")));  
        executor.shutdown();  
    }  
}  

此方案将上下文传递的逻辑封装在线程池中,对业务代码的侵入性较小,但实现起来相对复杂。

方案三:使用分布式追踪框架

借助分布式追踪框架,如 Skywalking、Zipkin、Pinpoint等,它们能自动为应用程序生成链路 ID,并在多线程、异步调用等场景下正确传递链路 ID,大大简化开发人员在链路追踪方面的操作。

这些框架通过内置的机制,在不同的服务和线程之间自动传递 TraceId,无需手动干预,降低了出错的概率,同时提供了可视化的界面和工具,方便开发人员监控和分析调用链路。

  1. 总结 =====

并发工具极大提升了并发代码编写的效率,也预先为潜在问题备好高效解法,是开发过程中的得力助手。

但开发人员不能仅满足于表面应用,务必深入剖析其实现逻辑,明晰不同场景下的适用规则。

若对并发工具一知半解、盲目套用,不仅难以发挥其最大效能,面对复杂问题时会陷入被动,更可能在生产环境中引发严重线上故障。

所以 J.U.C 虽好,可不要贪杯哦!

最后欢迎加入苏三的星球,你将获得:AI开发项目课程、苏三AI项目、商城微服务实战、秒杀系统实战、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

扫描下方二维码,即可加入星球:

picture.image

目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1700+小伙伴加入学习。

最后推荐一下我的技术专栏《性能优化35讲》,里面包含了:接口调用、Java、JVM、并发编程、MySQL、Redis、ElasticSearch、Spring、SpringBoot等多个性能优化技巧。无论在工作,还是在面试中,都会经常遇到,非常有参考价值。

picture.image

最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。

picture.image

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
云原生机器学习系统落地和实践
机器学习在字节跳动有着丰富业务场景:推广搜、CV/NLP/Speech 等。业务规模的不断增大对机器学习系统从用户体验、训练效率、编排调度、资源利用等方面也提出了新的挑战,而 Kubernetes 云原生理念的提出正是为了应对这些挑战。本次分享将主要介绍字节跳动机器学习系统云原生化的落地和实践。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论