為什麼不用其他方案?
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| B3 + Jaeger(本文) | 業界標準、視覺化強 | 需要額外部署 | 微服務架構 |
| Log 關聯查詢 | 簡單 | 跨服務追蹤困難 | 單體應用 |
| Zipkin | 輕量 | 功能較少 | 小型微服務 |
| 商業 APM | 功能完整 | 費用高 | 預算充足團隊 |
前言:微服務的可觀測性挑戰
在多通路系統中,一個訂單請求的流程可能是:
↓
資料庫更新
↓
回呼通知
這就是分散式追蹤(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 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
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 呼叫外部服務
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 訊息傳遞
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);
}
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
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();
}
}
}
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();
}
}
整合 Jaeger
Jaeger 是常用的分散式追蹤系統,可以視覺化追蹤鏈。
Spring Boot 設定
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] │
└─────────────────────────────────────────────────────────────────┘
錯誤追蹤
@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();
}
}
}
實戰踩坑
情境:偶發看到 traceId 對不上,追蹤鏈混亂
原因:Filter 的 finally 沒有正確清理 ThreadLocal,執行緒池重用導致污染
解法:確保
TracingContext.clear() 一定會執行,可用 try-finally 或 Spring 的 RequestContextHolder
情境:Producer 到 Consumer 的追蹤連不起來
原因:只在 HTTP 層傳遞 Header,忘了在 Kafka 訊息中嵌入追蹤資訊
解法:Producer 把 tracingHeaders 放進訊息 header,Consumer 取出後設回 ThreadLocal
情境:Jaeger 儲存空間暴增,查詢變慢
原因:生產環境忘了調低 sampling-rate,每個請求都記錄
解法:生產環境設 0.1(10%)或更低,搭配動態取樣根據錯誤率調整
總結
| 設計 | 效果 |
|---|---|
| B3 標準 Header | 相容主流追蹤系統 |
| Filter 自動提取 | 開發者不需手動處理 |
| ThreadLocal 儲存 | 任何地方都能取得追蹤資訊 |
| Kafka 嵌入 | 異步訊息也能追蹤 |
| Jaeger 整合 | 視覺化追蹤鏈 |
| 上一篇 | 系列目錄 | 下一篇 |
|---|---|---|
| JSON處理與時區管理 | 系列導讀 | Kubernetes Helm Charts 部署 |
這是「多通路電商 OMS 系統實戰」系列的第九篇。下一篇會介紹 Kubernetes 部署。
發佈留言