分佈式環境下,跨服務之間的調用錯綜複雜,若是忽然爆出一個錯誤,雖然有日誌記錄,但究竟是哪一個服務出了問題呢?是移動端傳的參數有錯誤,仍是系統X或者系統Y提供的接口致使?在這種狀況下,錯誤排查起來就很是費勁。前端
爲了追蹤一個請求完整的流轉過程,我能夠給請求分配一個惟一的traceId
,當請求調用其餘服務時,咱們傳遞這個traceId
。在輸出日誌時,將這個traceId
打印到日誌文件中,這樣,從日誌文件中,根據traceId
就能夠分析一個請求完整的調用過程,若更進一步,還能夠作性能分析。segmentfault
在一個服務的內部,咱們不但願在調用每一個方法時,都帶上traceId
這個參數(這樣實在太蠢了- . -)。app
在Java中,咱們通常將traceId
放到ThreadLocal
中,這樣在打印日誌時,日誌框架從ThreadLocal
取出traceId
,和其餘須要打印的信息一塊兒打印出來。這樣對框架的使用者來講,traceId
就是透明的,並不須要去關注它。框架
咱們來看代碼實現:dom
/** * 創建日誌MDC上下文屬性的攔截器 */ public class WebLogMdcHandlerInterceptor extends HandlerInterceptorAdapter { /** * traceId通常由前端的負載生成,好比Nignx */ private boolean generateTraceId = false; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ctxTraceId = null; String ctxOpId = null; // 判斷Http header中是否有traceId字段,若是沒有,則經過隨機數生成 if (StringUtils.isNotBlank(request.getHeader(Conventions.TRACE_ID_HEADER))) { ctxTraceId = request.getHeader(Conventions.TRACE_ID_HEADER); } else if (generateTraceId) { ctxTraceId = getTraceId(); } ctxOpId = UUID.randomUUID().toString(); MDC.put(Conventions.CTX_TRACE_ID_MDC, ctxTraceId + "," + ctxOpId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { MDC.clear(); } // 經過隨機數生成traceId,也能夠經過其餘方式實現,只要保證惟一便可 private static String getTraceId() { Random random = new Random(); String rs1 = String.valueOf(random.nextInt(10000)); String rs2 = String.valueOf(random.nextInt(10000)); return rs1 + rs2; } public void setGenerateTraceId(boolean generateTraceId) { this.generateTraceId = generateTraceId; } }
實現其實比較簡單,使用MDC
(Mapped Diagnostic Contexts)來實現,logback
和log4j
支持MDC
,MDC
的底層實現其實很容易理解,就是經過ThreadLocal
來維護key-value
,源碼以下:分佈式
public final class LogbackMDCAdapter implements MDCAdapter { final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>(); ... ... }
WebLogMdcHandlerInterceptor
繼承了HandlerInterceptorAdapter
,HandlerInterceptorAdapter
是一個攔截器適配器,咱們實現了它其中的2個方法:ide
咱們在afterCompletion
方法中對MDC
進行了clear
操做,底層調用了ThreadLocal
的remove
方法,清除當前線程中的線程局部變量。其做用有兩個,一是防止ThreadLocal
致使的內存溢出,二是Tomcat
容器線程複用時,新請求會依舊使用原來的MDC
中的traceId
,會致使traceId
的"串碼"現象。性能
咱們再來說一下preHandle
方法中的ctxOpId
,即咱們向MDC
中不單單寫入http header
中的traceId
,還經過UUID生成了一個ctxOpId
。this
如上圖,A服務的某個方法連續調用了B服務的某個接口3次(多是重試機制致使,也有可能確實是業務邏輯),如何區分這3次調用呢?只經過traceId
沒法區分,由於這三次的traceId
都相同,因此每次調用時UUID生成ctxOpId
,來區分這三次調用。spa
而後在logback.xml文件中配置pattern
,以下:
<pattern>%d %-5level [%X{ctxTraceId}][%thread] %logger{5} - %msg%n</pattern>
具體打印日誌時,會根據pattern
格式打印,各字段的含義可自行百度。
最後,當咱們在調用其餘Http服務時,先獲取當前線程的ThreadLocal
上下文,將traceId
寫入http client
的header
中,從而達到跨服務傳遞traceId
。
這是一個簡單的實現分佈式調用追蹤的實踐,以上。