1. 什么是鏈路追蹤
鏈路追蹤是指在分布式系統中,將一次請求的處理過程進行記錄并聚合展示的一種方法。目的是將一次分布式請求的調用情況集中在一處展示,如各個服務節點上的耗時、請求具體到達哪臺機器上、每個服務節點的請求狀態等。這樣就可以輕松了解一個請求在系統中的完整生命周期,包括經過的服務、調用的操作以及每個操作的延遲等。通過鏈路追蹤,可以更好地理解系統的性能瓶頸、找出問題的根源以及優化系統的性能。
如下圖就是一個簡單的微服務中的調用過程,如果我們沒有鏈路追蹤,且每個服務都是一個多節點集群,想要搞清楚一個請求是怎么走的就非常困難。
2. 鏈路追蹤的重要性
在分布式系統中,由于服務節點眾多且相互之間存在復雜的依賴關系,所以一旦出現故障,排查起來往往非常困難。而鏈路追蹤可以有效地幫助解決這個問題。具體是以下幾個方面:
快速定位問題:當應用程序出現故障時,開發人員可以通過鏈路追蹤來快速定位到故障的原因。通過查看元數據,可以確定故障發生的位置以及導致故障的請求數據,加速故障的排查過程。
優化程序性能:鏈路追蹤可以幫助開發人員分析應用程序的性能瓶頸。通過觀察數據在各個節點之間的流動情況,可以確定哪些節點的性能較差,并針對這些節點進行優化。
分析安全問題:通過觀察數據在系統中的流動情況,可以發現潛在的安全漏洞和攻擊路徑,例如DDoS攻擊、中間人攻擊、SQL注入攻擊等。有助于提高系統的安全性,并減少潛在的安全風險。
3. 鏈路追蹤的實現
鏈路追蹤的實現方式很多,你可以通過不同工具去實現。
如果你的微服務用的是Spring Cloud, 其實spring cloud已經有很完美的解決方案。
Spring Cloud 鏈路追蹤通常使用 Spring Cloud Sleuth 來實現。Spring Cloud Sleuth 集成了 Zipkin 和 Brave 來提供鏈路追蹤功能。
但是也有很多微服務沒有用Spring Cloud,如果我們只需要簡單的鏈路追蹤,也可以自己手寫一份實現。
手寫也不復雜,但是需要實現的人考慮周全,把鏈路追蹤寫好一點。本篇及后續篇章主要介紹手寫的不同實現方式。
鏈路追蹤的核心是日志追蹤,下面我們嘗試用代碼來實現日志追蹤。
3.1 實現一個API接口
模擬一個登錄接口API。 API包含如下實現
- API接口參數包括request body;
- 能接收可能包含trace id的header;
- 讀取當前線程名(用于線程結束前還原線程名);
- 如果請求頭中沒有包含trace id, 自動生成一個;
- 在請求開始的時候替換當前線程名,直到維持到請求結束前,用于修改日志文件的線程名,用它來做日志信息追蹤;
- 用一個for loop模擬登錄過程的日志
package com.sandwich.logtracing.controller;import com.sandwich.logtracing.entity.ApiResponse;
import com.sandwich.logtracing.util.RandomStrUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;/*** @Author 公眾號: IT三明治* @Date 2025/8/29* @Description: login demo controller*/
@Slf4j
@RestController
@RequestMapping("/test")
public class LoginController {@PostMapping("/login")public ApiResponse<String> login(@RequestBody LoginRequest loginRequest,@RequestHeader(value = "x-request-correlation-id", required = false) String traceId) {String currentThreadName = Thread.currentThread().getName();//if the request header don't have a trace id,then generate a random oneif (StringUtils.isBlank(traceId)) {traceId = RandomStrUtils.generateRandomString(15);}//replace current thread name with a trace idThread.currentThread().setName(traceId);log.info("previous thread name:{}", currentThreadName);for (int i=1; i<= 10; i++) {log.info("processing login for user {}, login step {} done", loginRequest.getUsername(), i);}log.info("user {} login success", loginRequest.getUsername());//restore thread name before api request endThread.currentThread().setName(currentThreadName);return ApiResponse.success("Sandwich login success", traceId);}@Datapublic static class LoginRequest {private String username;private String password;}
}
3.2 定義一個response entity
這個entity跟其他response不同之處在于它除了能支持一個泛型的data,還可以支持trace id返回
package com.sandwich.logtracing.entity;import lombok.Data;
import lombok.experimental.Accessors;/*** @Author 公眾號: IT三明治* @Date 2025/8/29* @Description: api response entity*/
@Data
@Accessors(chain = true)
public class ApiResponse<T> {private int responseCode;private String message;private T data;private String traceId;public static <T> ApiResponse<T> success(T data, String traceId) {return new ApiResponse<T>().setResponseCode(ResponseCode.SUCCESS.getCode()).setMessage(ResponseCode.SUCCESS.getMessage()).setData(data).setTraceId(traceId);}public static ApiResponse<String> success() {return new ApiResponse<String>().setResponseCode(ResponseCode.SUCCESS.getCode()).setMessage(ResponseCode.SUCCESS.getMessage());}public static <T> ApiResponse<T> success(T data) {return new ApiResponse<T>().setResponseCode(ResponseCode.SUCCESS.getCode()).setMessage(ResponseCode.SUCCESS.getMessage()).setData(data);}}
準備一個枚舉保存response code和對應的message, 這個demo我只用一個success的
package com.sandwich.logtracing.constant;import lombok.Getter;/*** @Author 公眾號: IT三明治* @Date 2025/8/29* @Description:*/
@Getter
public enum ResponseCode {SUCCESS(200, "success"),FAIL(500, "internal error"),NOT_FOUND(404, "not found"),UNAUTHORIZED(401, "unauthorized"),FORBIDDEN(403, "forbidden"),NOT_ACCEPTABLE(406, "not acceptable"),REQUEST_TIMEOUT(408, "request timeout"),CONFLICT(409, "conflict"),UNSUPPORTED_MEDIA_TYPE(415, "unsupported media type"),TOO_MANY_REQUESTS(429, "too many requests");private final int code;private final String message;ResponseCode(int code, String message) {this.code = code;this.message = message;}
}
3.3 用shell寫一個api請求(login.shell)
為了更好地展示api的所有信息,我選擇用shell完成api請求,shell需要完成的功能如下:
- 自動生成trace id
- 自動組裝api request,包括請求數據類型,header, payload
- 用python格式化返回的json結構體
#!/bin/bash# Define the API endpoint
API_URL="http://localhost:8080/test/login"function generate_random_string() {# 使用openssl生成隨機字符串(如果已安裝)if command -v openssl &> /dev/null; thenopenssl rand -base64 20 | tr -dc 'a-zA-Z0-9' | fold -w 15 | head -n 1else# 使用系統方法生成local chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"local result=""result=$(printf "%s" "${chars:$((RANDOM % ${#chars})):1}"{1..15} | tr -d '\n')echo "$result"fi
}function normalLogin() {# 生成15位隨機字符串作為traceIdtraceId=$(generate_random_string)response=$(curl -X POST $API_URL \-H "Content-Type: application/json" \-H "x-request-correlation-id: $traceId" \-d '{"username": "Sandwich", "password": "test"}')echo "Response from login API:"echo "$response" | python -m json.tool
}normalLogin
4. 測試驗證
- 啟動項目
- 執行請求
Administrator@USER-20230930SH MINGW64 /d/git/java/log-tracing/shell (master)
$ ./login.sh% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 144 0 100 100 44 493 217 --:--:-- --:--:-- --:--:-- 712
Response from login API:
{"responseCode": 200,"message": "success","data": "Sandwich login success","traceId": "wwDJbM12XdX562A"
}
- 用trace id追蹤日志信息
5. 總結
這就是一個最小的鏈路追蹤過程,非常簡單,我連日志管理文件都沒有配置,只用了springboot 默認的日志系統。
無疑它是非常不完善的,請關注我,下期我再逐步優化它。