分散式追蹤:OpenTracing 整合實戰

商業價值:分散式追蹤讓「問題定位從小時變分鐘」,直接支援 導讀篇提到的「系統穩定性」——當訂單卡在某個環節時,能快速找到是哪個服務出問題。

為什麼不用其他方案?

方案 優點 缺點 適用場景
B3 + Jaeger(本文) 業界標準、視覺化強 需要額外部署 微服務架構
Log 關聯查詢 簡單 跨服務追蹤困難 單體應用
Zipkin 輕量 功能較少 小型微服務
商業 APM 功能完整 費用高 預算充足團隊

前言:微服務的可觀測性挑戰

在多通路系統中,一個訂單請求的流程可能是:

使用者 → API Gateway → Order Service → Kafka → Consumer Job → 蝦皮 API

資料庫更新

回呼通知
問題:當某個環節出問題時,如何追蹤請求在各服務之間的流向?

這就是分散式追蹤(Distributed Tracing)要解決的問題。


追蹤標準:B3 Propagation

我們採用 B3 標準,定義一組 HTTP Header 來傳遞追蹤資訊:

Header 說明 範例
x-request-id 請求唯一識別碼 uuid-xxxx
x-b3-traceid 追蹤 ID(整個請求鏈共用) abc123
x-b3-spanid 當前操作 ID def456
x-b3-parentspanid 父操作 ID abc123
x-b3-sampled 是否取樣 1

實作:傳遞追蹤 Header

定義追蹤 Header 清單

public class TracingHeaders {

public static final List<String> HEADERS = List.of(
“x-request-id”,
“x-b3-traceid”,
“x-b3-spanid”,
“x-b3-parentspanid”,
“x-b3-sampled”,
“x-b3-flags”
);
}

從 Request 提取追蹤 Header

@Component
public class TracingExtractor {

/**
* 從 HTTP Request 提取追蹤 Header
*/

public Map<String, String> extract(HttpServletRequest request) {
Map<String, String> tracingHeaders = new HashMap<>();

for (String headerName : TracingHeaders.HEADERS) {
String value = request.getHeader(headerName);
if (value != null && !value.isEmpty()) {
tracingHeaders.put(headerName, value);
}
}

return tracingHeaders;
}
}


使用情境

情境 1:API 呼叫外部服務

@RestController
public class OrderController {

@Autowired private TracingExtractor tracingExtractor;
@Autowired private HttpClientService httpClient;

@GetMapping(“/api/orders/{orderId}”)
public Order getOrder(
@PathVariable String orderId,
HttpServletRequest request) {

// 1. 提取追蹤 Header
Map<String, String> tracingHeaders = tracingExtractor.extract(request);

// 2. 呼叫外部 API,傳遞追蹤 Header
HttpResult result = httpClient.get(
“https://api.platform.com/orders/” + orderId,
tracingHeaders
);

return parseOrder(result);
}
}

情境 2:Kafka 訊息傳遞

// Producer:將追蹤資訊放入訊息
public String buildMessage(Object body, Map<String, String> tracingHeaders) {
Map<String, Object> message = new HashMap<>();

// Header 包含追蹤資訊
Map<String, Object> header = new HashMap<>();
header.put(“version”, 1);
header.put(“tracing”, tracingHeaders);

message.put(“header”, header);
message.put(“body”, body);

return JsonUtil.toJson(message);
}

// Consumer:從訊息取出追蹤資訊
public void processMessage(String message) {
Map<String, Object> data = JsonUtil.toMap(message);
Map<String, Object> header = (Map) data.get(“header”);

// 取出追蹤 Header
Map<String, String> tracingHeaders = (Map) header.get(“tracing”);

// 呼叫外部 API 時繼續傳遞
HttpResult result = httpClient.get(platformApiUrl, tracingHeaders);
}


自動傳遞:Filter + ThreadLocal

@Component
public class TracingFilter implements Filter {

@Autowired
private TracingExtractor extractor;

@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;

// 提取追蹤 Header 放入 ThreadLocal
Map<String, String> tracingHeaders = extractor.extract(httpRequest);
TracingContext.set(tracingHeaders);

try {
chain.doFilter(request, response);
} finally {
TracingContext.clear();
}
}
}

// ThreadLocal 儲存
public class TracingContext {

private static final ThreadLocal<Map<String, String>> CONTEXT =
new ThreadLocal<>();

public static void set(Map<String, String> headers) {
CONTEXT.set(headers);
}

public static Map<String, String> get() {
Map<String, String> headers = CONTEXT.get();
return headers != null ? headers : Collections.emptyMap();
}

public static void clear() {
CONTEXT.remove();
}
}

效果:HTTP 客戶端自動帶入追蹤 Header,開發者不用手動處理。

整合 Jaeger

Jaeger 是常用的分散式追蹤系統,可以視覺化追蹤鏈。

Spring Boot 設定

# application.yml
opentracing:
jaeger:
enabled: true
udp-sender:
host: jaeger-agent
port: 6831
log-spans: true
probabilistic-sampler:
sampling-rate: 1.0 # 100% 取樣(生產環境調低)

追蹤視覺化

┌─────────────────────────────────────────────────────────────────┐
│ Trace: abc123 │
├─────────────────────────────────────────────────────────────────┤
│ ▼ api-gateway [45ms] │
│ ├─ order-service [30ms] │
│ │ ├─ kafka-produce [5ms] │
│ │ └─ db-query [10ms] │
│ └─ consumer-job [25ms] │
│ └─ platform-api [20ms] │
└─────────────────────────────────────────────────────────────────┘

錯誤追蹤

@Aspect
@Component
public class TracingAspect {

@Autowired
private Tracer tracer;

@Around(“execution(* com.example..*Service.*(..))”)
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
Span span = tracer.buildSpan(joinPoint.getSignature().getName()).start();

try {
return joinPoint.proceed();

} catch (Exception e) {
// 記錄錯誤到追蹤系統
span.setTag(“error”, true);
span.log(Map.of(
“event”, “error”,
“error.message”, e.getMessage(),
“error.class”, e.getClass().getName()
));
throw e;

} finally {
span.finish();
}
}
}


實戰踩坑

踩坑 1:ThreadLocal 洩漏到其他請求
情境:偶發看到 traceId 對不上,追蹤鏈混亂
原因:Filter 的 finally 沒有正確清理 ThreadLocal,執行緒池重用導致污染
解法:確保 TracingContext.clear() 一定會執行,可用 try-finally 或 Spring 的 RequestContextHolder
踩坑 2:Kafka Consumer 追蹤斷裂
情境:Producer 到 Consumer 的追蹤連不起來
原因:只在 HTTP 層傳遞 Header,忘了在 Kafka 訊息中嵌入追蹤資訊
解法:Producer 把 tracingHeaders 放進訊息 header,Consumer 取出後設回 ThreadLocal
踩坑 3:取樣率設 100% 撐爆 Jaeger
情境:Jaeger 儲存空間暴增,查詢變慢
原因:生產環境忘了調低 sampling-rate,每個請求都記錄
解法:生產環境設 0.1(10%)或更低,搭配動態取樣根據錯誤率調整

總結

設計 效果
B3 標準 Header 相容主流追蹤系統
Filter 自動提取 開發者不需手動處理
ThreadLocal 儲存 任何地方都能取得追蹤資訊
Kafka 嵌入 異步訊息也能追蹤
Jaeger 整合 視覺化追蹤鏈

上一篇 系列目錄 下一篇
JSON處理與時區管理 系列導讀 Kubernetes Helm Charts 部署

這是「多通路電商 OMS 系統實戰」系列的第九篇。下一篇會介紹 Kubernetes 部署。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *