線上 TraceId 集體失蹤,如何破局?
近期線上環(huán)境出現(xiàn)詭異問(wèn)題,異步任務(wù)里鏈路 ID(TraceId)莫名丟失,致使核心業(yè)務(wù)日志斷鏈,嚴(yán)重影響問(wèn)題排查。今天給大家分享三種有效解決辦法 。
1. 事件回顧
3.8 大促期間,我司交易系統(tǒng)流量劇增。在排查問(wèn)題過(guò)程中,我們發(fā)現(xiàn)下單主流程的日志出現(xiàn)異常,部分 TraceId 丟失,致使調(diào)用鏈路中斷,排查難度急劇上升 。
[2025-03-08 02:15:33] [TID:4a3b...8c2d] INFO 支付校驗(yàn)通過(guò) → 庫(kù)存扣減成功
// 異常日志片段(TraceId丟失!)
[2025-03-08 02:15:34] [TID:N/A] ERROR 優(yōu)惠券核銷失敗
2. 問(wèn)題定位
通過(guò)代碼逐層排查,最終鎖定“真兇”——一段使用 CompletableFuture 的異步處理代碼:
public void processOrder(Order order) {
// 主線程(攜帶TraceId)
log.info("[主線程] 開始處理訂單 {}", order.getId());
CompletableFuture.runAsync(() -> {
// 子線程(TraceId丟失!)
log.info("優(yōu)惠券核銷");
couponService.useCoupon(order.getCouponId());
}, executor);
}
3. 原因分析
根本原因:MDC 依賴 ThreadLocal 實(shí)現(xiàn)線程本地存儲(chǔ),每個(gè)線程都有獨(dú)立的上下文存儲(chǔ)空間。而線程池復(fù)用機(jī)制下,子線程被創(chuàng)建時(shí),無(wú)法自動(dòng)繼承父線程 ThreadLocal 中的上下文數(shù)據(jù),從而引發(fā) TraceId 丟失沖突 。
MDC 實(shí)現(xiàn)原理:
- MDC 底層基于 ThreadLocal 實(shí)現(xiàn),為每個(gè)線程創(chuàng)建獨(dú)立的鍵值存儲(chǔ)空間;
- 日志框架通過(guò)
%X{traceId}
模式從當(dāng)前線程的ThreadLocal
中提取鏈路ID。
線程池運(yùn)行機(jī)制:
- 線程復(fù)用:池化線程完成任務(wù)后不會(huì)銷毀,而是返回池中等待新任務(wù);
- 線程隔離:不同線程持有完全獨(dú)立的 ThreadLocal 存儲(chǔ)空間。
典型問(wèn)題場(chǎng)景:
public static void main(String[] args) {
// 主線程設(shè)置鏈路ID
ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
traceIdHolder.set("main-tid");
// 子線程無(wú)法訪問(wèn)主線程的ThreadLocal
CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get()); // 輸出null
});
System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get());
}
在這里插入圖片描述
4. 解決方案
方案一:手動(dòng)傳遞上下文
在提交異步任務(wù)時(shí),手動(dòng)捕獲并傳遞 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("[異步任務(wù)] 核銷優(yōu)惠券");
couponService.useCoupon(order.getCouponId());
}, executor);
}
這種方式簡(jiǎn)單直接,不過(guò)需要在每個(gè)異步任務(wù)中手動(dòng)添加代碼,代碼侵入性較強(qiáng),且容易遺漏。
方案二:自定義線程池包裝任務(wù)
自定義線程池,在提交任務(wù)時(shí)自動(dòng)保存當(dāng)前線程的 MDC 上下文,并在任務(wù)執(zhí)行時(shí)恢復(fù),避免手動(dòng)操作的繁瑣。
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("異步任務(wù)執(zhí)行,trace_id: {}", MDC.get("trace_id")));
executor.shutdown();
}
}
此方案將上下文傳遞的邏輯封裝在線程池中,對(duì)業(yè)務(wù)代碼的侵入性較小,但實(shí)現(xiàn)起來(lái)相對(duì)復(fù)雜。
方案三:使用分布式追蹤框架
借助分布式追蹤框架,如 Skywalking、Zipkin、Pinpoint等,它們能自動(dòng)為應(yīng)用程序生成鏈路 ID,并在多線程、異步調(diào)用等場(chǎng)景下正確傳遞鏈路 ID,大大簡(jiǎn)化開發(fā)人員在鏈路追蹤方面的操作。
這些框架通過(guò)內(nèi)置的機(jī)制,在不同的服務(wù)和線程之間自動(dòng)傳遞 TraceId,無(wú)需手動(dòng)干預(yù),降低了出錯(cuò)的概率,同時(shí)提供了可視化的界面和工具,方便開發(fā)人員監(jiān)控和分析調(diào)用鏈路。
5. 總結(jié)
并發(fā)工具極大提升了并發(fā)代碼編寫的效率,也預(yù)先為潛在問(wèn)題備好高效解法,是開發(fā)過(guò)程中的得力助手。
但開發(fā)人員不能僅滿足于表面應(yīng)用,務(wù)必深入剖析其實(shí)現(xiàn)邏輯,明晰不同場(chǎng)景下的適用規(guī)則。
若對(duì)并發(fā)工具一知半解、盲目套用,不僅難以發(fā)揮其最大效能,面對(duì)復(fù)雜問(wèn)題時(shí)會(huì)陷入被動(dòng),更可能在生產(chǎn)環(huán)境中引發(fā)嚴(yán)重線上故障。
所以 J.U.C 雖好,可不要貪杯哦!