一個現網bug讓我徹底弄懂微服務日誌鏈路追蹤

                               一個現網bug讓我徹底弄懂微服務日誌鏈路追蹤    

 

           需求:

           異步幫批量用戶訂購話費套餐,成功發送短信,失敗記錄信息。

           問題描述:

           根據運營人員及測試反饋最近有一批500個賬戶的套餐開通中,失敗數+成功數=499,還有一筆訂單憑空消失了,因爲採用的是異步線程處理訂單,因此日誌很難定位。

           問題解決:

          嘗試1: 

          公司程序採用的是springcloud微服務框架採用sleuth日誌鏈路跟蹤,由於是異步線程,因此根據traceid查找只能跟蹤到主線程日誌,因此我決定顯示的在線程執行訂購操作的前後打印日誌,將traceid作爲參數傳入:

 

	log.debug("tracid批量白名單訂購={},調用單個訂購號碼={}",traceid,l.getUseraccount());
	JsonResult res = whiteListProductOrder(useraccount, l.getProductid(), batchno,
									task.getPartnerId());
    log.debug("tracid批量白名單訂購={},調用單個訂購號碼={},返回結果={}",traceid,l.getUseraccount(),res.getMessage().toString());

    可是事與願違,打印的僅僅只有這兩句,中間的過程日誌還是無法追蹤。

     嘗試2:

     有問題找度娘,度娘說sleuth有子線程日誌追蹤功能,需要顯示開啓如下配置:

     

 static {
        System.setProperty("log4j2.isThreadContextMapInheritable", "true");
    }

   

 結果可想而知,一樣無功而返。

     嘗試3:

    日誌追蹤MDC:

   MDC ( Mapped Diagnostic Contexts ) 有了日誌之後,我們就可以追蹤各種線上問題。但是,在分佈式系統中,各種無關日誌   穿 行其中,導致我們可能無法直接定位整個操作流程。因此,我們可能需要對一個用戶的操作流程進行歸類標記,比如使用線程+時間戳,或者用戶身份標識等;如此,我們可以從大量日誌信息中grep出某個用戶的操作流程,或者某個時間的流轉記錄。其目的是爲了便於我們診斷線上問題而出現的方法工具類。雖然,Slf4j 是用來適配其他的日誌具體實現包的,但是針對 MDC功能,目前只有logback 以及 log4j 支持。

 

    MDC原理:

    簡而言之,MDC就是日誌框架提供的一個InheritableThreadLocal,項目代碼中可以將鍵值對放入其中,然後使用指定方式取出打印即可。

    InheritableThreadLocal:

 InheritableThreadLocal主要用於子線程創建時,需要自動繼承父線程的ThreadLocal變量,方便必要信息的進一步傳遞。

     看着上述概念,好像和我們的問題還蠻契合,膽的去失敗吧,反正你現在也沒成功。

    

    

初步實現

首先創建攔截器,加入攔截列表中,在請求到達時生成traceId。當然你還可以根據需求在此處後或後續流程中放入spanId、訂單流水號等需要打印的信息。

public class Constants {

    /**
     * 日誌跟蹤id名。
     */
    public static final String LOG_TRACE_ID = "traceid";

    /**
     * 請求頭跟蹤id名。
     */
    public static final String HTTP_HEADER_TRACE_ID = "app_trace_id";
}
import org.slf4j.MDC;

public class TraceInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // "traceId"
        MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
        return true;
    }
}


然後在日誌配置xml文件中添加traceId打印:

<property name="normal-pattern" value="[%p][%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ,Asia/Shanghai}][%X{traceid}][%15.15t][%c:%L] %msg%n"/>
1
初步改造完成!是不是感覺還挺簡單的?且慢,僅僅這樣的改造在實際使用過程中會遇到以下問題:

線程池中的線程會打印錯誤的traceId
調用下游服務後會生成新的traceId,無法繼續跟蹤
下面來一一解決這些問題。

支持線程池跟蹤

MDC使用的InheritableThreadLocal只是在線程被創建時繼承,但是線程池中的線程是複用的,後續請求使用已有的線程將打印出之前請求的traceId。這時候就需要對線程池進行一定的包裝,在線程在執行時讀取之前保存的MDC內容。不僅自身業務會用到線程池,spring項目也使用到了很多線程池,比如@Async異步調用,zookeeper線程池、kafka線程池等。不管是哪種線程池都大都支持傳入指定的線程池實現,拿@Async舉例:

@Bean("SpExecutor")
public Executor getAsyncExecutor() {
    // 對線程池進行包裝,使之支持traceId透傳
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor() {
        @Override
        public <T> Future<T> submit(Callable<T> task) {
	        // 傳入線程池之前先複製當前線程的MDC
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    };
    executor.setCorePoolSize(config.getPoolCoreSize());
    ... // 其他配置
    executor.initialize();
    return executor;
}

public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
    return new Callable<T>() {
        @Override
        public T call() throws Exception {
	        // 實際執行前導入對應請求的MDC副本
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
	        if (MDC.get(Constants.LOG_TRACE_ID) == null) {
	            MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
	        }
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        }
    };
}

ThreadPoolExecutor的包裝也類似,注意爲了嚴謹考慮,需要對連接池中的所有調用方法進行封裝。

在ThreadPoolExecutor中有:

public void execute(Runnable command)
public <T> Future<T> submit(Callable<T> task)
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Runnable task, T result)
在ThreadPoolTaskExecutor中有:

public void execute(Runnable command)
public void execute(Runnable task, long startTimeout)
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Runnable task, T result)
public <T> ListenableFuture<T> submitListenable(Callable<T> task)
public ListenableFuture<?> submitListenable(Runnable task)
方式與上述的實現類似,不做贅述。
提供一下我的工具類:

 

public class ThreadMdcUtil {

    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.LOG_TRACE_ID) == null) {
            MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
        }
    }

    public static void setTraceId() {
        MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
    }

    public static void setTraceId(String traceId) {
        MDC.put(Constants.LOG_TRACE_ID, traceId);
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }

    public static class ThreadPoolTaskExecutorMdcWrapper extends ThreadPoolTaskExecutor {
        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public void execute(Runnable task, long startTimeout) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);
        }

        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public ListenableFuture<?> submitListenable(Runnable task) {
            return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
            return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }

    public static class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }

        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        }

        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
        }

        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                            RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        }

        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public <T> Future<T> submit(Runnable task, T result) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
        }

        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }

    public static class ForkJoinPoolMdcWrapper extends ForkJoinPool {
        public ForkJoinPoolMdcWrapper() {
            super();
        }

        public ForkJoinPoolMdcWrapper(int parallelism) {
            super(parallelism);
        }

        public ForkJoinPoolMdcWrapper(int parallelism, ForkJoinWorkerThreadFactory factory,
                                      Thread.UncaughtExceptionHandler handler, boolean asyncMode) {
            super(parallelism, factory, handler, asyncMode);
        }

        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }

        @Override
        public <T> ForkJoinTask<T> submit(Runnable task, T result) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
        }

        @Override
        public <T> ForkJoinTask<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }
}


 


下游服務使用相同traceId

以上方式在多級服務調用中每個服務都會生成新的traceId,導致無法銜接跟蹤。這時就需要對http調用工具進行相應的改造了,在發送http請求時自動將traceId添加到header中,以RestTemplate爲例,註冊攔截器:

// 以下省略其他相關配置
RestTemplate restTemplate = new RestTemplate();
// 使用攔截器包裝http header
restTemplate.setInterceptors(new ArrayList<ClientHttpRequestInterceptor>() {
    {
        add((request, body, execution) -> {
            String traceId = MDC.get(Constants.LOG_TRACE_ID);
            if (StringUtils.isNotEmpty(traceId)) {
                request.getHeaders().add(Constants.HTTP_HEADER_TRACE_ID, traceId);
            }
            return execution.execute(request, body);
        });
    }
});

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
// 注意此處需開啓緩存,否則會報getBodyInternal方法“getBody not supported”錯誤
factory.setBufferRequestBody(true);
restTemplate.setRequestFactory(factory);


下游服務的攔截器改爲:

public class TraceInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = request.getHeader(Constants.HTTP_HEADER_TRACE_ID);
        if (StringUtils.isEmpty(traceId)) {
            traceId = TraceLogUtils.getTraceId();
        }
        MDC.put(Constants.LOG_TRACE_ID, traceId);
        return true;
    }
}

若使用自定義的http客戶端,則直接修改其工具類即可。

針對其他協議的調用暫無實踐經驗,可以借鑑上面的思路,通過攔截器插入特定字段,再在下游讀取指定字段加入MDC中。

總結

實現日誌跟蹤的基本方案沒有太大難度,重在實踐中發現問題並一層一層解決問題的思路。

 

     

     

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章