SpringBoot?MDC全局鏈路最新完美解決方案
需求
在訪問量較大的分布式系統(tǒng)中,時時刻刻在打印著巨量的日志,當我們需要排查問題時,需要從巨量的日志信息中找到本次排查內容的日志是相對復雜的,那么,如何才能使日志看起來邏輯清晰呢?如果每一次請求都有一個全局唯一的id,當我們需要排查時,根據其他日志打印關鍵字定位到對應請求的全局唯一id,再根據id去搜索、篩選即可找到對應請求全流程的日志信息。接下來就是需要找一種方案,可以生成全局唯一id和在不同的線程中存儲這個id。
解決方案
LogBack這個日志框架提供了MDC( Mapped Diagnostic Context,映射調試上下文 ) 這個功能,MDC可以理解為與線程綁定的數據存儲器。數據可以被當前線程訪問,當前線程的子線程會繼承其父線程中MDC的內容。MDC 在 Spring Boot 中的作用是為日志事件提供上下文信息,并將其與特定的請求、線程或操作關聯起來。通過使用 MDC,可以更好地理解和分析日志,并在多線程環(huán)境中確保日志的準確性和一致性。此外,MDC 還可以用于日志審計、故障排查和跟蹤特定操作的執(zhí)行路徑。
代碼
實現日志打印全局鏈路唯一id的功能,需要三個信息:
- 全局唯一ID生成器
- 請求攔截器
- 自定義線程池(可選)
- 日志配置
全局唯一ID生成器
生成器可選方案有:
UUID,快速隨機生成、極小概率重復Snowflake,有序遞增時間戳
雪花算法(Snowflake)更適用于需要自增的業(yè)務場景,如數據庫主鍵、訂單號、消息隊列的消息ID等, 時間戳一般是微秒級別,極限情況下,一微秒內可能同時多個請求進來導致重復。系統(tǒng)時鐘回撥時,UUID可能會重復,但是一般不會出現該情況,因此UUID這種方案的缺點可以接受,本案例使用UUID方案。
/** * 全局鏈路id生成工具類 * * @author Ltx * @version 1.0 */ public class RequestIdUtil { public RequestIdUtil() { } public static void setRequestId() { //往MDC中存入UUID唯一標識 MDC.put(Constant.TRACE_ID, UUID.randomUUID().toString()); } public static void setRequestId(String requestId) { MDC.put(Constant.TRACE_ID, requestId); } public static String getRequestId() { return MDC.get(Constant.TRACE_ID); } public static void clear() { //需要釋放,避免OOM MDC.clear(); } }
/** * Author: liu_pc * Date: 2023/8/8 * Description: 常量定義類 * Version: 1.0 */ public class Constant { /** * 全局唯一鏈路id */ public final static String TRACE_ID = "traceId"; }
自定義全局唯一攔截器
Filter是Java Servlet 規(guī)范定義的一種過濾器接口,它的主要作用是在 Servlet 容器中對請求和響應進行攔截和處理,實現對請求和響應的預處理、后處理和轉換等功能。通過實現 Filter 接口,開發(fā)人員可以自定義一些過濾器來實現各種功能,如身份驗證、日志記錄、字符編碼轉換、防止 XSS 攻擊、防止 CSRF 攻擊等。那么這里我們使用它對請求做MDC賦值處理。
@Component public class RequestIdFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException{ try { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String requestId = httpServletRequest.getHeader("requestId"); if (StringUtils.isBlank(requestId)) { RequestIdUtil.setRequestId(); } else { RequestIdUtil.setRequestId(requestId); } // 繼續(xù)將請求傳遞給下一個過濾器或目標資源(比如Controller) filterChain.doFilter(servletRequest, servletResponse); } finally { RequestIdUtil.clear(); } } @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void destroy() { Filter.super.destroy(); } }
/** * 測試MDC異步任務全局鏈路 * * @param param 請求參數 * @return new String Info */ public String test(String param) { logger.info("測試MDC test 接口開始,請求參數:{}", param); String requestId = RequestIdUtil.getRequestId(); logger.info("MDC RequestId :{}", requestId); return "hello"; }
日志配置
輸出到控制臺:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 配置輸出到控制臺(可選輸出到文件) --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 配置日志格式 --> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mdc %msg%n</pattern> </encoder> </appender> <!-- 配置根日志記錄器 --> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> <!-- 配置MDC --> <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"> <resetJUL>true</resetJUL> </contextListener> <!-- 配置MDC插件 --> <conversionRule conversionWord="%mdc" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> </configuration>
輸出到文件:
<configuration> <!-- 配置輸出到文件 --> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <!-- 指定日志文件路徑和文件名 --> <file>/Users/liu_pc/Documents/code/mdc_logback/logs/app.log</file> <encoder> <!-- 配置日志格式 --> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mdc %msg%n</pattern> </encoder> </appender> <!-- 配置根日志記錄器 --> <root level="INFO"> <appender-ref ref="FILE"/> </root> <!-- 其他配置... --> </configuration>
功能實現。
子線程獲取traceId問題
使用多線程時,子線程打印日志拿不到traceId。如果在子線程中獲取traceId,那么就相當于往各自線程中的MDC賦值了traceId,會導致子線程traceId不一致的問題。
public void wrongHelloAsync(String param) { logger.info("helloAsync 開始執(zhí)行異步操作,請求參數:{}", param); List<Integer> simulateThreadList = new ArrayList<>(5); for (int i = 0; i <= 5; i++) { simulateThreadList.add(i); } for (Integer thread : simulateThreadList) { CompletableFuture.runAsync(() -> { //在子線程中賦值 String requestId = RequestIdUtil.getRequestId(); logger.info("子線程信息:{},traceId:{} ", thread, requestId); }, executor); } } }
子線程獲取traceId方案
使用子線程時,可以使用自定義線程池重寫部分方法,在重寫的方法中獲取當前MDC數據副本,再將副本信息賦值給子線程的方案。
/** * Author: liu_pc * Date: 2023/8/7 * Description: 自定義異步線程池配置 * Version: 1.0 */ @Configuration @EnableAsync public class AsyncConfiguration implements AsyncConfigurer { private final Logger logger = LoggerFactory.getLogger(AsyncConfiguration.class); private final TaskExecutionProperties taskExecutionProperties; public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) { this.taskExecutionProperties = taskExecutionProperties; } @Override @Bean(name = "taskExecutor") public Executor initAsyncExecutor() { logger.debug("Creating Async Task Executor"); ThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor(); executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize()); executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize()); executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity()); executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix()); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new SimpleAsyncUncaughtExceptionHandler(); } }
/** * Author: liu_pc * Date: 2023/8/7 * Description: 自定義攜帶MDC信息線程池 * Version: 1.0 */ public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { @Override public void execute(@Nonnull Runnable task) { Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap(); super.execute( () -> { if (Objects.nonNull(copyOfContextMap)) { String requestId = RequestIdUtil.getRequestId(); if (StringUtils.isBlank(requestId)) { copyOfContextMap.put("traceId", UUID.randomUUID().toString()); } //主線程MDC賦值子線程 MDC.setContextMap(copyOfContextMap); } else { RequestIdUtil.setRequestId(); } try { task.run(); } finally { RequestIdUtil.clear(); } } ); } }
測試代碼:
/** * 測試MDC異步任務全局鏈路 * * @param param 請求參數 * @return new String Info */ public String test(String param) { logger.info("測試MDC test 接口開始,請求參數:{}", param); String requestId = RequestIdUtil.getRequestId(); logger.info("MDC RequestId :{}", requestId); helloAsyncService.helloAsync(param, requestId); return "hello"; }
/** * 使用異步數據測試打印日志 * * @param param 請求參數 * @param requestId 全局唯一id */ @Async("taskExecutor") public void helloAsync(String param, String requestId) { logger.info("helloAsync 開始執(zhí)行異步操作,請求參數:{}", param); List<Integer> simulateThreadList = new ArrayList<>(5); for (int i = 0; i <= 5; i++) { simulateThreadList.add(i); } for (Integer thread : simulateThreadList) { CompletableFuture.runAsync(() -> { //在子線程中賦值 RequestIdUtil.setRequestId(requestId); logger.info("子線程信息:{},traceId:{} ", thread, requestId); }, executor); } }
MDC原理
到此這篇關于SpringBoot MDC全局鏈路解決方案的文章就介紹到這了,更多相關SpringBoot MDC全局鏈路內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot解決Class path contains multiple 
這篇文章主要介紹了springboot解決Class path contains multiple SLF4J bindings問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07Java8中l(wèi)ambda表達式的應用及一些泛型相關知識
這篇文章主要介紹了Java8中l(wèi)ambda表達式的應用及一些泛型相關知識的相關資料2017-01-01