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