商業價值:分散式追蹤讓「問題定位從小時變分鐘」,直接支援
導讀篇提到的「系統穩定性」——當訂單卡在某個環節時,能快速找到是哪個服務出問題。
為什麼不用其他方案?
| 方案 |
優點 |
缺點 |
適用場景 |
| 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 整合 |
視覺化追蹤鏈 |
這是「多通路電商 OMS 系統實戰」系列的第九篇。下一篇會介紹 Kubernetes 部署。