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 one
if (StringUtils.isBlank(traceId)) {
traceId = RandomStrUtils.generateRandomString(15);
}
//replace current thread name with a trace id
Thread.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 end
Thread.currentThread().setName(currentThreadName);
return ApiResponse.success("Sandwich login success", traceId);
}
@Data
public 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; then
openssl rand -base64 20 | tr -dc 'a-zA-Z0-9' | fold -w 15 | head -n 1
else
# 使用系统方法生成
local chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
local result=""
result=$(printf "%s" "${chars:$((RANDOM % ${#chars})):1}"{1..15} | tr -d '\n')
echo "$result"
fi
}
function normalLogin() {
# 生成15位随机字符串作为traceId
traceId=$(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 Current
Dload 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 默认的日志系统。

无疑它是非常不完善的,请关注我,下期我再逐步优化它。
888

被折叠的 条评论
为什么被折叠?



