手写链路追踪

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 默认的日志系统。

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT三明治

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值