溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

日志排查問題困難?分布式日志鏈路跟蹤來幫你

發(fā)布時間:2020-07-31 16:50:52 來源:網絡 閱讀:226 作者:wx5c62e4b4e4ab9 欄目:編程語言

一、背景

開發(fā)排查系統(tǒng)問題用得最多的手段就是查看系統(tǒng)日志,在分布式環(huán)境中一般使用ELK來統(tǒng)一收集日志,但是在并發(fā)大時使用日志定位問題還是比較麻煩,由于大量的其他用戶/其他線程的日志也一起輸出穿行其中導致很難篩選出指定請求的全部相關日志,以及下游線程/服務對應的日志。

?

二、解決思路

  • 每個請求都使用一個唯一標識來追蹤全部的鏈路顯示在日志中,并且不修改原有的打印方式(代碼無***)
  • 使用Logback的MDC機制日志模板中加入traceId標識,取值方式為%X{traceId}

    MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當前線程綁定的Map,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執(zhí)行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對于一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。

?

三、方案實現(xiàn)

由于MDC內部使用的是ThreadLocal所以只有本線程才有效,子線程和下游的服務MDC里的值會丟失;所以方案主要的難點是解決值的傳遞問題。

3.1. 修改日志模板

logback配置文件模板格式添加標識%X{traceId}
日志排查問題困難?分布式日志鏈路跟蹤來幫你

?

3.2. 網關添加過濾器

生成traceId并通過header傳遞給下游服務

@Component
public class TraceFilter extends ZuulFilter {
    @Autowired
    private TraceProperties traceProperties;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        //根據配置控制是否開啟過濾器
        return traceProperties.getEnable();
    }

    @Override
    public Object run() {
        //鏈路追蹤id
        String traceId = IdUtil.fastSimpleUUID();
        MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(CommonConstant.TRACE_ID_HEADER, traceId);
        return null;
    }
}

?

3.3. 下游服務增加spring攔截器

接收并保存traceId的值
攔截器

public class TraceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = request.getHeader(CommonConstant.TRACE_ID_HEADER);
        if (StrUtil.isNotEmpty(traceId)) {
            MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
        }
        return true;
    }
}

注冊攔截器

public class DefaultWebMvcConfig extends WebMvcConfigurationSupport {
  @Override
  protected void addInterceptors(InterceptorRegistry registry) {
    //日志鏈路追蹤攔截器
    registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**");

    super.addInterceptors(registry);
  }
}

?

3.4. 下游服務增加feign攔截器

繼續(xù)把當前服務的traceId值傳遞給下游服務

public class FeignInterceptorConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        RequestInterceptor requestInterceptor = template -> {
            //傳遞日志traceId
            String traceId = MDC.get(CommonConstant.LOG_TRACE_ID);
            if (StrUtil.isNotEmpty(traceId)) {
                template.header(CommonConstant.TRACE_ID_HEADER, traceId);
            }
        };
        return requestInterceptor;
    }
}

?

3.5. 擴展線程池

主要針對業(yè)務會使用線程池(異步、并行處理),并且spring自己也有@Async注解來使用線程池,所以需要擴展ThreadPoolTaskExecutor線程池實現(xiàn)將父線程的MDC內容復制給子線程

public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
    /**
     * 把父線程的MDC內容賦值給子線程
     * @param runnable
     */
    @Override
    public void execute(Runnable runnable) {
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        super.execute(() -> run(runnable, mdcContext));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        return super.submit(() -> call(task, mdcContext));
    }

    /**
     * 子線程委托的執(zhí)行方法
     * @param runnable {@link Runnable}
     * @param mdcContext 父線程MDC內容
     */
    private void run(Runnable runnable, String tenantId, Map<String, String> mdcContext) {
        // 將父線程的MDC內容傳給子線程
        if (mdcContext != null) {
            MDC.setContextMap(mdcContext);
        }
        try {
            // 執(zhí)行異步操作
            runnable.run();
        } finally {
            // 清空MDC內容
            MDC.clear();
        }
    }

    /**
     * 子線程委托的執(zhí)行方法
     * @param task {@link Callable}
     * @param mdcContext 父線程MDC內容
     */
    private <T> T call(Callable<T> task, Map<String, String> mdcContext) throws Exception {
        // 將父線程的MDC內容傳給子線程
        if (mdcContext != null) {
            MDC.setContextMap(mdcContext);
        }
        try {
            // 執(zhí)行異步操作
            return task.call();
        } finally {
            // 清空MDC內容
            MDC.clear();
        }
    }
}

?

四、場景測試

4.1. 測試代碼如下

日志排查問題困難?分布式日志鏈路跟蹤來幫你
?

4.2. api網關打印的日志

網關生成traceId值為13d9800c8c7944c78a06ce28c36de670
日志排查問題困難?分布式日志鏈路跟蹤來幫你
?

4.3. 請求跳轉到文件服務時打印的日志

顯示的traceId與網關相同,這里特意模擬發(fā)生異常的場景
日志排查問題困難?分布式日志鏈路跟蹤來幫你
?

4.4. ELK聚合日志通過traceId查詢整條鏈路日志

當系統(tǒng)出現(xiàn)異常時,可直接通過該異常日志的traceId?的值,在日志中心中詢該請求的所有日志信息
日志排查問題困難?分布式日志鏈路跟蹤來幫你

?

五、源碼地址

附上我的開源微服務框架(包含本文中的代碼),歡迎 star 關注

https://gitee.com/zlt2000/microservices-platform

?
推薦閱讀

  • Spring Cloud開發(fā)人員如何解決服務沖突和實例亂竄?
  • zuul集成Sentinel最新的網關流控組件
  • 阿里注冊中心Nacos生產部署方案
  • Spring Boot自定義配置項在IDE里面實現(xiàn)自動提示
  • Spring Cloud Zuul的動態(tài)路由怎樣做?集成Nacos實現(xiàn)很簡單

?
日志排查問題困難?分布式日志鏈路跟蹤來幫你

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI