主頁 > 後端開發 > 微服務呼叫鏈日志追蹤分析

微服務呼叫鏈日志追蹤分析

2021-02-05 06:21:31 後端開發

一、技術原理

1.1 背景

微服務架構是一個分布式架構,它按業務劃分服務單元,一個分布式系統往往有很多個服務單元,由于服務單元數量眾多,業務的復雜性,如果出現了錯誤和例外,很難去定位,主要體現在,一個請求可能需要呼叫很多個服務,而內部服務的呼叫復雜性,決定了問題難以定位,所以微服務架構中,必須實作分布式鏈路追蹤,去跟進一個請求到底有哪些服務參與,參與的順序又是怎樣的,從而達到每個請求的步驟清晰可見,出了問題,很快定位, 舉個例子,在微服務系統中,一個來自用戶的請求,請求先達到前端A(如前端界面),然后通過遠程呼叫,達到系統的中間件B、C(如負載均衡、網關等),最后達到后端服務D、E,后端經過一系列的業務邏輯計算最后將資料回傳給用戶,對于這樣一個請求,經歷了這么多個服務,怎么樣將它的請求程序的資料記錄下來呢?這就需要用到服務鏈路追蹤, Google開源的 Dapper鏈路追蹤組件,并在2010年發表了論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,這篇文章是業內實作鏈路追蹤的標桿和理論基礎,具有非常大的參考價值, 中文翻譯參考:http://bigbully.github.io/Dapper-translation/ 目前,鏈路追蹤組件有Google的Dapper,Twitter 的Zipkin,以及阿里的Eagleeye (鷹眼)等,它們都是非常優秀的鏈路追蹤開源組件,

1.2 名詞術語

微服務鏈路追蹤系統實作時,需設定一些關鍵節點記錄資訊,鏈路追蹤相關名詞如下: Span:基本作業單元,發送一個遠程調度任務 就會產生一個Span,Span是一個64位ID唯一標識的,Trace是用另一個64位ID唯一標識的,Span還有其他資料資訊,比如摘要、時間戳事件、Span的ID、以及進度ID, Trace:一系列Span組成的一個樹狀結構,請求一個微服務系統的API介面,這個API介面,需要呼叫多個微服務,呼叫每個微服務都會產生一個新的Span,所有由這個請求產生的Span組成了這個Trace, Annotation:用來及時記錄一個事件的,一些核心注解用來定義一個請求的開始和結束 ,這些注解包括以下: cs - Client Sent -客戶端發送一個請求,這個注解描述了這個Span的開始 sr - Server Received -服務端獲得請求并準備開始處理它,如果將其sr減去cs時間戳便可得到網路傳輸的時間, ss - Server Sent (服務端發送回應)–該注解表明請求處理的完成(當請求回傳客戶端),如果ss的時間戳減去sr時間戳,就可以得到服務器請求的時間, cr - Client Received (客戶端接收回應)-此時Span的結束,如果cr的時間戳減去cs時間戳便可以得到整個請求所消耗的時間,

1.3 呼叫鏈分析

一個服務呼叫程序如下圖所示:  

二、技術實作

呼叫方每一次向系統服務發起請求時,會生成這一次呼叫產生的相關呼叫鏈日志,生成一個全域的traceId,生成不同節點的span資訊,其中當首個服務生成全域編碼后,放入到header中,基于http傳遞給下級服務(其他模式類似),下級服務可通過設定Filter過濾器(其他方案也可以),接收鏈路日志編碼,并記錄呼叫的日志資訊,在將全域編碼繼續傳遞給下級服務,最終本次業務呼叫完成后,記錄呼叫日志并清空本次呼叫鏈產生的全域編碼,簡易流程如下圖所示:

2.1 單服務流程說明

  1. 呼叫方請求服務A,進入服務A過濾器;
  2. 服務A過濾器判斷請求的header中是否攜帶了TraceId,ParentSpanId,有則使用攜帶的,沒有就自動生成,
  3. 過濾器前置部分記錄初始請求的一些資訊,如請求地址,引數,請求時間等;
  4. 過濾器轉發請求進入到Service方法;
  5. 過濾器后置部分再次記錄Service方法執行完成后的一些資訊,如回傳內容,結束時間;
  6. 過濾器前后分別記錄了資訊,組合生成呼叫鏈路日志;
  7. 請求完成后,清空本次產生的TraceId;
服務A呼叫鏈日志資訊參考:
// trace日志
{
"message":"trace log",
"context":{
"trace_id":"e0d5c5ba-f497-4407-b8ca-f657a88452fc517513",
"request_uri":"/customize-trace-A/trace/jdk/async",
"request_method":"GET",
"refer_service_name":null,
"service_name":"customize-trace-A",
"refer_service_host":"127.0.0.1",
"request_time":1608896030.689531,
"response_time":1608896030.692276,
"time_used":3.479,
"service_addr":"192.168.45.42",
"service_port":8095,
"request_id":"9adfcf3c-d606-418f-abc7-6600bff6adf0533098"
},
"datetime":"2020-12-25 19:33:50.690014"
}
 
// span節點
{
"trace_id":"e0d5c5ba-f497-4407-b8ca-f657a88452fc517513",
"request_id":"9adfcf3c-d606-418f-abc7-6600bff6adf0533098",
"span":{
"span_id":"eb12eaf8-df3d-4dd2-923a-685360a4fd79588942",
"parent_id":null,
"duration":3426,
"annotations":[
{
"timestamp":1608896030686322,
"action":"sr"
},
{
"timestamp":1608896030689748,
"action":"ss"
}
]
} ,
"datetime":"2020-12-25 19:34:50.690014"
}
View Code

2.2 多服務流程說明

多個服務與單個服務對比,是在不同的微服務里面分別記錄對應的Trace資訊,Span資訊,同一個呼叫請求,所有微服務記錄的TraceId一致,父服務的SpanId為子服務的ParentSpanId, 舉例兩個服務間的呼叫流程如下:
  1. 呼叫方發起呼叫,請求服務A,進入服務A過濾器;
  2. 服務A過濾器判斷請求的header中是否攜帶了TraceId,ParentSpanId,有則使用攜帶的,沒有就自動生成;
  3. 服務A過濾器前置部分記錄初始請求的一些資訊,如請求地址,引數,請求時間等;
  4. 服務A過濾器轉發請求進入到Service方法;
  5. 服務A的Service方法內部執行部分邏輯后,開始通過中間件呼叫服務B;
  6. 將服務A中已生成的TraceId,ParentSpanId資訊,通過header設定引數(其他類似)的模式傳遞給服務B;
  7. 進入服務B過濾器,服務B過濾器獲取header中傳遞過來的TraceId,ParentSpanId;
  8. 服務B過濾器前置部分記錄初始請求的一些資訊,如請求地址,引數,請求時間等
  9. 服務B過濾器轉發請求進入到Service方法;
  10. 服務B過濾器后置部分再次記錄Service方法執行完成后的一些資訊,如回傳內容,結束時間;
  11. 服務B過濾器前后分別記錄了資訊,組合生成呼叫鏈路日志;
  12. 服務B基于中間件回傳呼叫的請求資訊處理結果給服務A;
  13. 服務A清空本次接收到的TraceId等編碼資訊,
  14. 服務A過濾器后置部分再次記錄Service方法執行完成后的一些資訊,如回傳內容,結束時間;
  15. 服務A過濾器前后分別記錄了資訊,組合生成呼叫鏈路日志;
  16. 服務A清空本次請求產生的TraceId,

2.3 中間件記錄Span資訊

中間件是否需要記錄Span資訊 上述舉例并未記錄服務的Service方法執行一段時間后,何時通過中間件發起呼叫其他服務的Span資訊,現實業務中,服務呼叫經常存在這種情況,服務A中某一個方法,先呼叫了服務B,獲取到服務B的回傳結果后,后續還又呼叫了服務C,服務D,此刻若不記錄中間件的Span資訊,在分析部分呼叫鏈超時情況時,會難以定位分析,只能獲取到接受方的接收時間,不知道某一個服務呼叫時具體的發起時間(如服務D最終接收請求時的時間與最初進入服務A記錄的請求時間相差一分鐘,但這并不能說服務A呼叫服務D的介面就耗時一分鐘), 因此,中間件模塊記錄Span資訊也至關重要,比如一個http請求的中間件,可重寫他的Client實作類,記錄開始發起請求和請求完成(類似于Filter)這一段時間的Span資訊,

2.4 TraceId的管理

  1. 為什么每次服務呼叫完成后,需要清空traceId?
  2. 多個請求同時發起時,如何保證呼叫鏈日志在不同執行緒中隔離,互不影響?
每一個請求過來時,產生一個獨立的子執行緒,在這個子執行緒內部設定對應的traceId,可基于ThreadLocal存盤呼叫鏈相關資訊,達到子執行緒資訊隔離的目的, 了解呼叫鏈資訊基本原理后,自定義編碼實作一套基于traceId的呼叫鏈追蹤技術方案,需解決如下問題:
  1. 全域traceId的生成和清空;
  2. traceId呼叫鏈路傳遞與追蹤;
  3. traceId基于Filter接收;
  4. Span生成與管理;
  5. 呼叫鏈路日志存盤;

三、技術細節分析

3.1 生成呼叫鏈相關編碼

traceId:全域呼叫鏈日志id編碼,在多個服務呼叫的一條呼叫鏈日志中,為同一個日志編碼 spanId:spanId節點的唯一編碼 requestId:本次請求生成的唯一id編碼,在多個服務呼叫的一條呼叫鏈日志中,為不同的日志編碼 每一次發起業務呼叫完成后,需清空本次產生的編碼,同時,不同執行緒的呼叫鏈日志應互不影響,故呼叫鏈資訊可基于MDC技術實作,查看MDC的實作原理,本質還是基于ThreadLocal實作,本例直接基于ThreadLocal實作,部分偽代碼如下:
public class LoggerUtil{
/**
* 生成traceId ,requestId,spanId 類似,設定不同的方法名即可
*/
static String traceId() {
return UUID.randomUUID().toString() + new Random().nextInt(1000000);
}
}
 
public final class ThreadHolderUtil {
/**
* 任意型別資料集合
*/
private static final ThreadLocal<Map<Object, Object>> VALUE_MAP = ThreadLocal.withInitial(HashMap::new);
/**
* 設定key值
*
* @param key key
* @param value 值
*/
public static void setValue(Object key, Object value) {
Optional.ofNullable(VALUE_MAP.get()).ifPresent(valueMap -> valueMap.put(key, value));
}
 
/**
* 清除指定Key
*
* @param key 指定key
*/
public static void clearValue(Object key) {
Optional.ofNullable(VALUE_MAP.get()).ifPresent(valueMap -> valueMap.remove(key));
}
 
/**
* 清除整個map
*/
public static void clearValueMap() {
VALUE_MAP.remove();
}
}
View Code
  1. 獲取traceId:String traceId = LoggerUtil.traceId();
  2. 單次呼叫程序中存盤traceId:ThreadHolderUtil.setValue(TRACD_ID, traceId );
  3. 整個呼叫完成后,清空整個變數:ThreadHolderUtil.clearValueMap();

3.2 呼叫鏈編碼傳遞

呼叫鏈編碼傳遞主要是一個請求涉及到多個微服務時,一般是從網關(或首個請求的微服務)生成呼叫鏈編碼后,該編碼在不同微服務中的流轉程序,本文主要介紹Feign和執行緒池中traceId的鏈路傳遞 參考檔案:基于TraceId鏈路追蹤 Feign傳遞編碼-重寫RequestInterceptor 網上介紹方案大多是通過重寫實作RequestInterceptor介面實作的,參考代碼如下:
/**
* 呼叫服務追蹤資訊feign攔截器
*
*/
public class FeignTraceInterceptor implements RequestInterceptor {
private static final Logger LOGGER = LoggerUtil.getTraceLogger();
 
@Override
public void apply(RequestTemplate template) {
String projectName = LoggerUtil.PROJECT_NAME;
if (!StringUtils.isEmpty(projectName)) {
template.header(REFER_SERVICE_NAME, projectName);
}
if (!StringUtils.isEmpty(HOST_IP)) {
template.header(REFER_REQUEST_HOST, HOST_IP);
}
String traceId = TraceUtil.getTraceId();
if (StringUtils.isEmpty(traceId)) {
traceId = LoggerUtil.traceId();
}
template.header(GATEWAY_TRACE, traceId);
String spanId = TraceContext.parentSpanId();
template.header(PARENT_ID_HEADER, spanId);
}
}
 
@ConditionalOnClass(Feign.class)
public static class FeignTraceAutoConfiguration {
@Bean
public FeignTraceInterceptor feignTraceInterceptor() {
return new FeignTraceInterceptor();
}
}
View Code 該方案是把呼叫鏈編碼通過header傳遞給下級服務了,但并沒有記錄Feign處的Span資訊,參考模型如下圖所示: Feign傳遞編碼-重新實作內部呼叫的 Http Client 擴展方案是需要記錄每一次呼叫Feign時,記錄Feign處的Span資訊,Feign最終可通過在http發起請求時,調整內部的Http Client擴展實作,達到記錄Span資訊的目的,(整體方案偏復雜,要考慮負載均衡時,池化請求等模式時,都可以記錄資訊) Feign添加自定義注解 目的是為了記錄Feign在執行方法前后的呼叫鏈資訊,可采用加入注解,在Feign類上面標記,記錄方法執行前后時的情況,呼叫鏈資訊還是通過重寫RequestInterceptor實作傳遞給下級服務, 采用Feign呼叫其他服務,記錄Fegin的Span資訊,可通過方案:(Feign傳遞編碼重寫RequestInterceptor, Feign請求添加注解,組合實作,) 撰寫一個注解,并記錄呼叫方法前后的時間資訊,參考偽代碼:
@Aspect
@Component
public class FeignSpanAspect {
@Pointcut("@annotation(com.trace.base.tool.annotation.FeignSpan)")
public void pointcut() {
}
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) {
try {
// 先生成spanId
String spanId = TraceContext.parentSpanId();
ThreadHolderUtil.setValue("feign-spanId", spanId);
// cs
Annotation cs = TraceContext.cs();
List<Annotation> annotations = new ArrayList<>(2);
annotations.add(cs);
// 避免執行超時,所以先設定span cs資訊
Span span = new Span.Builder()
.parentId(ThreadHolderUtil.getValue(PARENT_SPAN_ID_KEY, String.class))
.spanId(spanId)
.annotations(annotations)
.build();
List<Span> subSpanList = ThreadHolderUtil.getValue(SUB_SPAN_LIST_KEY, List.class);
if (subSpanList != null) {
subSpanList.add(span);
}
joinPoint.proceed();
// cr
Annotation cr = TraceContext.cr();
// 增加cr
annotations.add(cr);
span.setDuration(cr.getTimestamp() - cs.getTimestamp());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
View Code 執行緒池傳遞編碼 主執行緒中記錄的呼叫鏈資訊通過執行緒池執行時,子執行緒會獲取不到主執行緒的呼叫鏈資訊(子執行緒獲取traceId為null),因此,需要在子執行緒執行時,主執行緒向子執行緒傳遞呼叫鏈相關編碼資訊,參考檔案: 多執行緒相關知識:多執行緒-JUC執行緒池 Spring 回呼方法裝飾器:多執行緒呼叫如何傳遞背景關系 JDK原生擴展Callable,Runnable:traceId跟蹤請求全流程日志 其他方法:Transmittable ThreadLocal(TTL) 支持快取執行緒池的 ThreadLocal

3.3 微服務過濾器接收呼叫鏈編碼

上游服務向下游服務發起呼叫請求時,下游服務接收到請求時,加入一個基礎過濾器(設定過濾器order值小于其他業務的order值,保證優先執行),獲取上游服務請求資訊中的呼叫鏈資訊,獲取出來后,記錄請求Trace日志資訊,并通過ThreadLocal模式,記錄呼叫鏈資訊,參考實作部分偽代碼如下:  
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String uri = servletRequest.getRequestURI();
// 服務健康檢查日志不統計,根目錄和HEAD請求忽略
final String slash = "/";
if (Arrays.stream(ignorePath).anyMatch(uri::startsWith) || slash.equals(uri) || HttpMethod.HEAD.name().equalsIgnoreCase(servletRequest.getMethod())) {
chain.doFilter(request, response);
} else {
try {
int port = request.getLocalPort();
TraceLog traceLog = new TraceLog();
traceLog.setRequestTime(getNowUs());
//服務名稱
traceLog.setServiceName(LoggerUtil.PROJECT_NAME);
// 開始時間戳(微秒)
long start = LocalDateTimeUtil.getCurrentMicroSecond();
// traceId
String traceId = servletRequest.getHeader(GATEWAY_TRACE);
// 沒有就新生成一個
if (StringUtils.isEmpty(traceId)) {
traceId = LoggerUtil.traceId();
}
// 嘗試獲取上游傳遞的parent_id
String parentId = servletRequest.getHeader(TraceContext.PARENT_ID_HEADER);
// 首先設定span id,作為后續子span的父span id
String spanId = TraceContext.parentSpanId();
ThreadHolderUtil.setValue(PARENT_SPAN_ID_KEY, spanId);
// 需要提前初始化子span串列,否則父子執行緒無法持有一個資料參考
ThreadHolderUtil.setValue(SUB_SPAN_LIST_KEY, new ArrayList<>());
 
// sr
Annotation sr = TraceContext.sr();
String requestId = LoggerUtil.requestId();
// 設定trace,用于ResponseBody能夠獲取
Trace trace = new Trace(traceId, requestId);
ThreadHolderUtil.setValue(TRACE_KEY, trace);
 
traceLog.setTraceId(traceId);
// 遠程呼叫服務名稱
traceLog.setReferServiceName(servletRequest.getHeader(REFER_SERVICE_NAME));
traceLog.setRequestUri(servletRequest.getRequestURI());
String method = servletRequest.getMethod();
traceLog.setRequestMethod(method);
traceLog.setServicePort(port);
// 原始response物件
chain.doFilter(request, response);
// 結束時間戳(微秒)
long end = LocalDateTimeUtil.getCurrentMicroSecond();
// ss
Annotation ss = TraceContext.ss();
// duration
long duration = ss.getTimestamp() - sr.getTimestamp();
// span日志
SpanLog spanLog = new SpanLog();
// 父span
Span span = new Span.Builder()
.parentId(parentId)
.spanId(spanId)
.duration(duration)
.annotations(Arrays.asList(sr, ss))
.build();
spanLog.setTraceId(traceId);
spanLog.setRequestId(requestId);
spanLog.setSpan(span);
List<Span> subSpanList = ThreadHolderUtil.getValue(SUB_SPAN_LIST_KEY, List.class);
spanLog.setSubSpans(subSpanList);
// todo 存盤span資訊
// todo 存盤trace資訊
} finally {
// 最后清除VALUE_MAP
// 執行完成后,清空產生的日志資訊
ThreadHolderUtil.clearValueMap();
}
}
}
View Code

3.4 Span生成與管理

通過技術原理分析,生成Span的場景為每一個微服務請求開始至請求完成時,記錄一個Span節點資訊,若服務執行程序中,通過中間件呼叫了其他微服務時,每一次中間件呼叫時,再記錄一個Span節點資訊(呼叫多少次,記錄多少個),

3.5 呼叫鏈日志存盤

發起一次呼叫后,會生成Trace請求資訊,Span節點資訊,針對這些日志資訊,可以通過寫入到Log4g2日志中,或者寫入到其他資料庫等系統中做日志資訊存盤,便于后續分析問題, 舉例一個場景: 發起請求,先呼叫服務A,服務A通過Feign呼叫一次服務B,整體記錄日志參考如下: 服務A對應traceLog
  1. 生成全域traceId: 2bf002c7-c140-4304-9c42-98ec0e359e1a314225,
  2. 服務A呼叫起止時間:1612344583.027557~ 1612344589.716305,
{
"message":"trace log",
"context":{
"trace_id":"2bf002c7-c140-4304-9c42-98ec0e359e1a314225",
"request_uri":"/customize-trace-A/trace/feign/name",
"request_method":"GET",
"refer_service_name":null,
"service_name":"customize-trace-A",
"refer_service_host":"127.0.0.1",
"request_time":1612344583.027557,
"response_time":1612344589.716305,
"time_used":4774.917,
"service_addr":"192.168.45.42",
"service_port":8095,
"request_id":"01d91c6f-1745-414c-a556-06d2e2630995119672"
},
"level":200,
"level_name":"INFO",
"channel":"REQUEST",
"datetime":"2021-02-03 17:29:50.405499"
}
View Code 服務A對應spanLog
  1. 服務A本身具備一個span節點資訊,且服務A的spanId,為sub_spans的parentSpanId,因為服務A通過Feign呼叫了一次服務B,記錄中間件的Span資訊一次,(呼叫多少次,記錄多少個孩子span節點,)
  2. 孩子節點的span資訊,內部的開始請求時間,結束請求時間,小于上級節點的起止時間,
全域traceId: 2bf002c7-c140-4304-9c42-98ec0e359e1a314225,
  1. sub_spans 節點下面,所有相關的子節點,他的parentId為上級span節點的spanId,值為e495b1e3-72e3-4dfc-92ad-8526c1c05e68901528,
{
"message":"span log",
"context":{
"trace_id":"2bf002c7-c140-4304-9c42-98ec0e359e1a314225",
"request_id":"01d91c6f-1745-414c-a556-06d2e2630995119672",
"span":{
"span_id":"e495b1e3-72e3-4dfc-92ad-8526c1c05e68901528",
"parent_id":null,
"duration":4772900,
"annotations":[
{
"timestamp":1612344583030172,
"action":"sr"
},
{
"timestamp":1612344587803072,
"action":"ss"
}
]
},
"request_uri":null,
"request_method":null,
"sub_spans":[
{
"span_id":"6a112df7-762d-4467-aab5-8d4ea8d30e34265554",
"parent_id":"e495b1e3-72e3-4dfc-92ad-8526c1c05e68901528",
"duration":4064421,
"annotations":[
{
"timestamp":1612344583090733,
"action":"cs"
},
{
"timestamp":1612344587155154,
"action":"cr"
}
]
}
]
},
"level":200,
"level_name":"INFO",
"channel":"SPAN",
"datetime":"2021-02-03 17:29:49.705213"
}
View Code 服務B對應traceLog
  1. 服務B接收上級的傳入的TraceId,全域編碼:2bf002c7-c140-4304-9c42-98ec0e359e1a314225,
  2. 服務B呼叫起止時間:1612344586.914167~ 1612344587.162829.
  3. 服務A通過Feign發起的時間為: 1612344583090733,服務B接收到的請求時間1612344586914167,表明中間件到服務B中還是存在細微的時間差,
{
"message":"trace log",
"context":{
"trace_id":"2bf002c7-c140-4304-9c42-98ec0e359e1a314225",
"request_uri":"/customize-trace-B/trace/name",
"request_method":"GET",
"refer_service_name":"customize-trace-A",
"service_name":"customize-trace-B",
"refer_service_host":"127.0.0.1",
"request_time":1612344586.914167,
"response_time":1612344587.162829,
"time_used":218.196,
"service_addr":"192.168.45.42",
"service_port":8096,
"request_id":"c3141791-b5c4-49e3-ad4a-08c40782f687651638"
},
"level":200,
"level_name":"INFO",
"channel":"REQUEST",
"datetime":"2021-02-03 17:29:47.161630"
}
View Code 服務B對應spanLog
  1. 服務B接收上級的傳入的TraceId,全域編碼:2bf002c7-c140-4304-9c42-98ec0e359e1a314225.
  2. 服務B沒有再次呼叫其他的服務了,故不存在下級sub_spans節點,
  3. 服務B節點資訊中的parent_id,為服務A中的孩子節點spanId,值為:6a112df7-762d-4467-aab5-8d4ea8d30e34265554,
{
"message":"span log",
"context":{
"trace_id":"2bf002c7-c140-4304-9c42-98ec0e359e1a314225",
"request_id":"c3141791-b5c4-49e3-ad4a-08c40782f687651638",
"span":{
"span_id":"d4a7f2d5-d49d-4f88-95ee-4f73c18ff9d5967084",
"parent_id":"6a112df7-762d-4467-aab5-8d4ea8d30e34265554",
"duration":207818,
"annotations":[
{
"timestamp":1612344586929937,
"action":"sr"
},
{
"timestamp":1612344587137755,
"action":"ss"
}
]
},
"request_uri":null,
"request_method":null,
"sub_spans":[
 
]
},
"level":200,
"level_name":"INFO",
"channel":"SPAN",
"datetime":"2021-02-03 17:29:47.139560"
}
View Code

四、自實作方案優缺點

  1. 自定義一個呼叫鏈插件,便于根據專案需求,充分的定制化開發,
  2. 結合公司專案的需求,調整呼叫鏈方案,在呼叫鏈模塊成熟后,可做為中間件模塊,應用于公司的其他專案;
  3. 實作一個呼叫鏈插件,有利于了解整個呼叫鏈技術體系的技術關鍵點,技術細節,后續就算切換為其他的成熟的呼叫鏈產品,當使用中出現問題時,也能從原理層面分析問題,
  4. 自定義呼叫鏈插件在日志管理方面更靈活,便于后期業務日志分析,日志存盤切換方案等可以做出快速調整,
  5. 隨著Spring體系的升級,中間件的升級,自定義的呼叫鏈插件受到影響時,也需要升級,存在一定的維護成本,
  6. 在更加多元化的日志分析中,如權重管理,比例攔截日志等方面,自定義的插件都需要開發才能支持,
  7. 自定義插件的性能,技術實作方案與開發者掌握的技術密切相關,同開源的優秀呼叫鏈工具對比,肯定還是存在差異,需要開發者更新和替換, 

五、案例原始碼

參考完整實作代碼:https://github.com/wuya11/TraceDemo

運行截圖參考:

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/256606.html

標籤:Java

上一篇:(二)MyBatis從入門到入土——開發一個Mybatis專案

下一篇:mysql進階學習二之搭建主從

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more