> 原文首發:微信公眾號,悟空聊架構,https://mp.weixin.qq.com/s/SDxH9k96aP5-X12yFtus0w
你好,我是悟空。
最近在搭一個基礎版的專案框架,基於 SpringCloud 微服務架構。
如果把 SpringCloud 這個框架當做 1
,那麼現在已經有的基礎元件比如 swagger/logback 等等就是 0.5 ,然後我在這 1.5 基礎上進行組裝,完成一個微服務專案框架。
為什麼要造二代輪子呢?市面上現成的專案框架不香嗎?
因為專案組不允許用外部的現成框架,比如 Ruoyi。另外因為我們的專案需求具有自身的特色,技術選型也會選擇我們自己熟悉的框架,所以自己來造二代輪子也是一個不錯的選擇。
需要包含以下核心功能:
多個微服務模組拆分,抽取出一個 demo 微服務模組供擴充套件,已完成
提取核心框架模組,已完成
註冊中心 Eureka,已完成
遠端呼叫 OpenFeign,已完成
紀錄檔 logback,包含 traceId 跟蹤,已完成
Swagger API 檔案,已完成
組態檔共用,已完成
紀錄檔檢索,ELK Stack,已完成
自定義 Starter,待定
整合快取 Redis,Redis 哨兵高可用,已完成
整合資料庫 MySQL,MySQL 高可用,已完成
整合 MyBatis-Plus,已完成
鏈路追蹤元件,待定
監控,待定
工具類,待開發
閘道器,技術選型待定
審計紀錄檔進入 ES,待定
分散式檔案系統,待定
定時任務,待定
等等
本篇要介紹的內容是關於紀錄檔鏈路追蹤的。
一個請求呼叫,假設會呼叫後端十幾個方法,列印十幾次紀錄檔,無法將這些紀錄檔串聯起來。
如下圖所示:使用者端呼叫訂單服務,訂單服務中方法 A 呼叫方法 B,方法 B 呼叫方法 C。
方法 A 列印第一條紀錄檔和第五條紀錄檔,方法 B 列印第二條紀錄檔,方法 C 列印第三條紀錄檔和第四條紀錄檔,但是這 5 條紀錄檔並沒有任何聯絡,唯一的聯絡就是時間是按照時間循序列印的,但是如果有其他並行的請求呼叫,則會干擾紀錄檔的連續性。
每個微服務都會記錄自己這個程序的紀錄檔,跨程序的紀錄檔如何進行關聯?
如下圖所示:訂單服務和優惠券服務屬於兩個微服務,部署在兩臺機器上,訂單服務的 A 方法遠端呼叫優惠券服務的 D 方法。
方法 A 將紀錄檔列印到紀錄檔檔案 1 中,記錄了 5 條紀錄檔,方法 D 將紀錄檔列印到紀錄檔檔案 2 中,記錄了 5 條紀錄檔。但是這 10 條紀錄檔是無法關聯起來的。
主執行緒和子執行緒的紀錄檔如何關聯?
如下圖所示:主執行緒的方法 A 啟動了一個子執行緒,子執行緒執行方法 E。
方法 A 列印了第一條紀錄檔,子執行緒 E 列印了第二條紀錄檔和第三條紀錄檔。
本篇要解決的核心問題是第一個和第二個問題,多執行緒目前還未引入,目前也沒有第三方來呼叫,後期再來優化第三個和第四個問題。
① 使用 Skywalking traceId 進行鏈路追蹤,或者 sleuth + zipkin 方案。
② 使用 Elastic APM 的 traceId 進行鏈路追蹤
③ MDC 方案:自己生成 traceId 並 put 到 MDC 裡面。
專案初期,先不引入過多的中介軟體,用簡單可行的方案先嚐試,所以這裡用第三種方案 MDC。
MDC(Mapped Diagnostic Context)用於儲存執行上下文的特定執行緒的上下文資料。因此,如果使用 log4j 進行紀錄檔記錄,則每個執行緒都可以擁有自己的MDC,該 MDC 對整個執行緒是全域性的。屬於該執行緒的任何程式碼都可以輕鬆存取執行緒的 MDC 中存在的值。
我們先來看第一個痛點,如何在一個請求中,將多條紀錄檔串聯起來。
該方案的原理如下圖所示:
(1)在 logback 紀錄檔組態檔中的紀錄檔格式中新增 %X{traceId} 設定。
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{traceId} %-5level %logger - %msg%n</pattern>
(2)自定一個攔截器,從請求的 header
中獲取 traceId
,如果存在則放到 MDC 中,否則直接用 UUID 當做 traceId,然後放到 MDC 中。
(3)設定攔截器。
當我們列印紀錄檔的時候,會自動列印 traceId,如下所示,多條紀錄檔的 traceId 相同。
攔截器程式碼:
/**
* @author www.passjava.cn,公眾號:悟空聊架構
* @date 2022-07-05
*/
@Service
public class LogInterceptor extends HandlerInterceptorAdapter {
private static final String TRACE_ID = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader(TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
MDC.put("traceId", UUID.randomUUID().toString());
} else {
MDC.put(TRACE_ID, traceId);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//防止記憶體洩露
MDC.remove("traceId");
}
}
設定攔截器:
/**
* @author www.passjava.cn,公眾號:悟空聊架構
* @date 2022-07-05
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/**");
}
}
解決方案的原理圖如下所示:
訂單服務遠端呼叫優惠券服務,需要在訂單服務中新增 OpenFeign 的攔截器,攔截器裡面做的事就是往 請求的 header 中新增 traceId,這樣呼叫到優惠券服務時,就能從 header 中拿到這次請求的 traceId。
程式碼如下所示:
/**
* @author www.passjava.cn,公眾號:悟空聊架構
* @date 2022-07-05
*/
@Configuration
public class FeignInterceptor implements RequestInterceptor {
private static final String TRACE_ID = "traceId";
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(TRACE_ID, (String) MDC.get(TRACE_ID));
}
}
兩個微服務列印的紀錄檔中,兩條紀錄檔的 traceId 一致。
當然這些紀錄檔都會匯入到 Elasticsearch 中的,然後通過 kibana 視覺化介面搜尋 traceId,就可以將整個呼叫鏈路串起來了!
本篇通過攔截器、MDC 功能,全鏈路加入了 traceId,然後將 traceId 輸出到紀錄檔中,就可以通過紀錄檔來追蹤呼叫鏈路。不論是程序內的方法級呼叫,還是跨程序間的服務呼叫,都可以進行追蹤。
另外紀錄檔還需要通過 ELK Stack 技術將紀錄檔匯入到 Elasticsearch 中,然後就可以通過檢索 traceId,將整個呼叫鏈路檢索出來了。
- END -