1. 技术背景与价值

1.1 为什么需要链路追踪?

在微服务架构下,一个用户请求可能需要经过多个服务的协作才能完成。当系统出现性能瓶颈或异常时,传统单体应用的调试方式已经失效:

  • 问题定位困难:无法快速定位是哪个服务导致的响应延迟
  • 依赖关系复杂:服务间调用链路错综复杂,难以全貌掌握
  • 性能优化无据:缺乏量化数据支撑优化决策

链路追踪(Distributed Tracing)通过在请求的生命周期中打上唯一的标识,记录服务间的调用关系和耗时,为系统可观测性提供了核心能力。

1.2 Sleuth+Zipkin组合优势

对比维度Spring Cloud SleuthZipkin
核心职责链路数据采集与传递链路数据收集、存储与可视化
侵入性低,基于AOP实现无需侵入业务代码
集成难度极低,自动集成中等,需独立部署
优势与Spring Cloud生态无缝集成成熟的UI展示,多存储后端支持

这套组合拳的最大价值在于:Sleuth负责"采集"的轻量化,Zipkin负责"展示"的专业化,开发者只需引入依赖,即可获得完整的链路追踪能力。


2. 环境准备

2.1 开发环境要求

组件版本要求说明
JDK8+推荐JDK 8u201+(生产环境推荐8u311+)
Spring Boot2.3.x - 2.7.xJDK 8建议使用Spring Boot 2.x系列
Spring CloudHoxton / 2020.x需要与Spring Boot 2.x版本匹配
Maven3.6+依赖管理工具
Docker(可选)19.03+用于Zipkin Server容器化部署

2.2 版本兼容性速查

JDK 8 推荐组合:

Spring Boot版本对应Spring Cloud版本推荐组合
2.3.xHoxton.SR12稳定生产组合
2.4.x2020.0.x新特性支持
2.6.x2021.0.x最新LTS支持

选择建议:

  • 生产环境推荐:Spring Boot 2.6.15 + Spring Cloud 2021.0.8
  • 该组合在JDK 8上经过充分验证,稳定性高
核心组件:
├── Spring Cloud Sleuth          # 链路追踪客户端
├── Zipkin Server                # 链路数据收集服务器
├── Micrometer Tracing           # 现代化的追踪API(Spring Boot 3.x)
└── Brave                        # 底层追踪实现库

3. 核心组件解析

3.1 Spring Cloud Sleuth工作原理

核心概念模型:

Trace(追踪):一次完整的请求调用链路
    └── Span(跨度):链路中的单个工作单元
        ├── Trace ID:全局唯一标识,整个链路共享
        ├── Span ID:当前跨度标识
        └── Parent ID:父跨度标识,形成调用树结构

数据传递机制:

Sleuth通过拦截器机制自动织入链路数据:

// HTTP请求拦截示例
GET /api/order/123 → [TraceId=abc123, SpanId=def456]
    ↓ 调用用户服务
GET /api/user/789 → [TraceId=abc123, SpanId=ghi789, ParentId=def456]
    ↓ 调用库存服务
GET /api/stock/item/123 → [TraceId=abc123, SpanId=jkl012, ParentId=ghi789]

3.2 Zipkin架构组成

┌─────────────┐
│   Browser   │  用户访问
└──────┬──────┘
       │ HTTP请求
┌──────▼──────┐
│  Application│  Sleuth采集链路数据
└──────┬──────┘
       │ 上报(HTTP/MQ)
┌──────▼──────────────────────┐
│         Zipkin Server       │
│  ┌──────────────────────┐  │
│  │    Collector         │  ← 接收链路数据
│  ├──────────────────────┤  │
│  │    Storage           │  ← 数据持久化(内存/MySQL/ES)
│  ├──────────────────────┤  │
│  │    API & UI          │  ← 查询接口与可视化界面
│  └──────────────────────┘  │
└────────────────────────────┘

核心模块说明:

  • Collector:负责接收Sleuth上报的span数据,支持HTTP和Kafka两种方式
  • Storage:存储层抽象,支持内存、MySQL、Elasticsearch、Cassandra等多种后端
  • API:提供RESTful接口供外部查询链路数据
  • UI:Web界面,展示链路依赖图、时间线、性能统计等

项目集成实施指南

本章节将按照从零到一的顺序,分5个步骤完成Sleuth+Zipkin的完整集成。


步骤一:添加依赖

在项目的 pom.xml 中添加以下依赖:

<!-- 父工程版本管理 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.15</version>
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- 核心依赖:Sleuth -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    <!-- 核心依赖:Zipkin集成 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-zipkin</artifactId>
    </dependency>
    <!-- Web模块(如果你的项目是Web应用) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

步骤二:配置文件

在 src/main/resources/application.yml 中添加配置:

spring:
  application:
    name: order-service
  sleuth:
    zipkin:
      base-url: http://localhost:9411  # Zipkin服务器地址
      sender:
        type: web                      # 使用HTTP方式上报
    sampling:
      probability: 1.0                  # 采样率100%(开发环境)

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId},%X{spanId}] [%thread] %-5level %logger{36} - %msg%n"

关键配置说明:

  • base-url:填写Zipkin服务器的实际地址
  • probability:1.0表示全量采样,生产环境建议改为0.1(10%)
  • logging.pattern:在日志中显示链路ID,方便排查问题

步骤三:编写示例代码

创建一个简单的Controller来测试链路追踪:

@RestController
@RequestMapping("/api/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    // 需要配置RestTemplate的Bean(见步骤四)
    
    @GetMapping("/create/{userId}")
    public String createOrder(@PathVariable Long userId) {
        // 业务逻辑:创建订单
        String result = "Order created for user: " + userId;
        
        // 调用下游服务(模拟)
        String userInfo = restTemplate.getForObject(
            "http://localhost:8081/api/user/" + userId, 
            String.class
        );
        
        return result + ", " + userInfo;
    }
}

创建RestTemplate配置(用于服务间调用):

@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced  // 如果使用服务发现,需要这个注解
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

步骤四:启动Zipkin服务器

方式一:使用Docker启动(推荐)

docker run -d \
  --name zipkin \
  -p 9411:9411 \
  openzipkin/zipkin:2.24

方式二:使用JAR包启动

# 下载Zipkin Server
wget https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.24.3/zipkin-server-2.24.3-exec.jar

# 启动服务
java -jar zipkin-server-2.24.3-exec.jar

启动成功后,访问:http://localhost:9411


步骤五:启动应用并验证

  1. 启动你的Spring Boot应用

  2. 访问接口生成链路数据

curl http://localhost:8080/api/order/create/123
  1. 在Zipkin UI中查看

    • 打开 http://localhost:9411
    • 点击 “Run Query” 查询最近的链路
    • 点击某个Trace查看详细的时间线
  2. 验证日志中的链路ID

查看应用日志,你会看到类似输出:

2026-02-16 18:47:04 [abc123def456,789ghi012jkl] [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - GET "/api/order/create/123"

其中 [abc123def456,789ghi012jkl] 就是 TraceId 和 SpanId。


集成检查清单

  • 依赖已正确添加到pom.xml
  • application.yml配置正确填写
  • Zipkin Server已启动并可访问
  • Spring Boot应用启动成功
  • 调用了至少一个接口生成链路数据
  • 在Zipkin UI中能够查询到Trace
  • 日志中能看到TraceId和SpanId


5. 可视化平台搭建与数据持久化

本章节详细介绍Zipkin Server的部署方式,以及两种主流数据持久化方案(MySQL、Elasticsearch)的完整配置。


5.1 存储方案选择

存储方案对比:

存储类型适用场景优点缺点
内存开发、测试、短期演示部署简单、无依赖重启丢失、容量有限
MySQL中小规模、已有MySQL环境成本低、运维熟悉性能瓶颈、写入压力大
Elasticsearch大规模生产环境高性能查询、水平扩展运维复杂、资源占用高
Cassandra超大规模写入场景写入性能极高运维复杂度高

选择建议:

  • 开发/测试:内存存储
  • 中小规模(QPS < 1000):MySQL
  • 大规模(QPS > 1000):Elasticsearch

5.2 MySQL存储方案(推荐中小规模)

5.2.1 环境准备

1. 创建数据库和用户

-- 创建数据库
CREATE DATABASE zipkin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 创建专用用户(可选)
CREATE USER 'zipkin'@'%' IDENTIFIED BY 'zipkin_password';
GRANT ALL PRIVILEGES ON zipkin.* TO 'zipkin'@'%';
FLUSH PRIVILEGES;

2. 初始化数据库表结构

Zipkin首次启动时会自动创建表结构,无需手动执行SQL。

5.2.2 方式一:JAR包启动(MySQL)

# 下载Zipkin Server(包含MySQL驱动)
wget https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.25.2/zipkin-server-2.25.2-exec.jar

# 启动Zipkin并连接MySQL
java -jar zipkin-server-2.25.2-exec.jar \
  --STORAGE_TYPE=mysql \
  --MYSQL_HOST=localhost \
  --MYSQL_PORT=3306 \
  --MYSQL_DB=zipkin \
  --MYSQL_USER=root \
  --MYSQL_PASS=your_password \
  --MYSQL_USE_SSL=false \
  --MYSQL_MAX_CONNECTIONS=10

参数说明:

参数说明默认值
STORAGE_TYPE存储类型mem
MYSQL_HOSTMySQL地址localhost
MYSQL_PORTMySQL端口3306
MYSQL_DB数据库名称zipkin
MYSQL_USER用户名root
MYSQL_PASS密码-
MYSQL_MAX_CONNECTIONS最大连接数10
MYSQL_USE_SSL是否使用SSLfalse

5.2.3 方式二:Docker Compose部署(MySQL)

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: zipkin-mysql
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=zipkin
      - TZ=Asia/Shanghai
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - zipkin-net
    command: 
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci

  zipkin:
    image: openzipkin/zipkin:2.25
    container_name: zipkin
    environment:
      - STORAGE_TYPE=mysql
      - MYSQL_HOST=mysql
      - MYSQL_PORT=3306
      - MYSQL_DB=zipkin
      - MYSQL_USER=root
      - MYSQL_PASS=root_password
      - MYSQL_MAX_CONNECTIONS=20
    ports:
      - "9411:9411"
    depends_on:
      - mysql
    networks:
      - zipkin-net

volumes:
  mysql-data:

networks:
  zipkin-net:
    driver: bridge

启动命令:

docker-compose up -d

# 查看日志
docker-compose logs -f zipkin

5.2.4 MySQL存储表结构

Zipkin会在MySQL中创建以下表:

-- 查看表结构
USE zipkin;
SHOW TABLES;

主要表说明:

表名说明
zipkin_spans存储链路跨度信息
zipkin_annotations存储注解信息(时间戳)
zipkin_dependencies存储服务依赖关系

5.2.5 数据验证

1. 启动应用并调用接口

curl http://localhost:8080/api/order/create/123

2. 在MySQL中查询链路数据

-- 查询最近的链路
SELECT trace_id, name, start_ts, duration 
FROM zipkin_spans 
ORDER BY start_ts DESC 
LIMIT 10;

-- 查询特定TraceId的所有Span
SELECT trace_id, id, parent_id, name, start_ts, duration 
FROM zipkin_spans 
WHERE trace_id = 'abc123def456' 
ORDER BY start_ts;

-- 查询带有错误信息的Span
SELECT s.trace_id, s.name, a.value as error_message
FROM zipkin_spans s
JOIN zipkin_annotations a ON s.trace_id = a.trace_id AND s.id = a.span_id
WHERE a.key = 'error'
ORDER BY s.start_ts DESC
LIMIT 10;

5.2.6 MySQL性能优化建议

-- 为常用查询字段添加索引
CREATE INDEX idx_trace_id ON zipkin_spans(trace_id);
CREATE INDEX idx_start_ts ON zipkin_spans(start_ts);
CREATE INDEX idx_name ON zipkin_spans(name);

数据定期清理策略:

-- 创建存储过程:清理30天前的数据
DELIMITER $$
CREATE PROCEDURE cleanup_old_traces()
BEGIN
    DELETE FROM zipkin_annotations 
    WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));
    
    DELETE FROM zipkin_spans 
    WHERE start_ts < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));
    
    DELETE FROM zipkin_dependencies 
    WHERE call_count > 0 AND id IN (
        SELECT DISTINCT id FROM zipkin_dependencies 
        WHERE call_count > 0
    );
END$$
DELIMITER ;

-- 创建事件:每天凌晨3点执行
CREATE EVENT IF NOT EXISTS auto_cleanup_traces
ON SCHEDULE EVERY 1 DAY
STARTS CONCAT(CURRENT_DATE, ' 03:00:00')
DO
BEGIN
    CALL cleanup_old_traces();
END;

5.3 Elasticsearch存储方案(推荐大规模)

5.3.1 环境准备

Elasticsearch推荐使用Docker Compose一键部署,确保版本兼容性。

5.3.2 方式一:Docker Compose一键部署(ES)

version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.15
    container_name: zipkin-elasticsearch
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      - xpack.security.enabled=false
      - cluster.name=zipkin-cluster
      - action.auto_create_index=true
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - zipkin-net
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  zipkin:
    image: openzipkin/zipkin:2.25
    container_name: zipkin
    environment:
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=elasticsearch:9200
      - ES_INDEX=zipkin
      - ES_INDEX_SHARDS=3
      - ES_INDEX_REPLICAS=1
      - ES_HTTP_LOGGING=BODY
      - ES_DATE_SEPARATOR=T
    ports:
      - "9411:9411"
    depends_on:
      elasticsearch:
        condition: service_healthy
    networks:
      - zipkin-net

volumes:
  es-data:

networks:
  zipkin-net:
    driver: bridge

启动命令:

docker-compose up -d

# 验证Elasticsearch启动成功
curl http://localhost:9200/_cluster/health?pretty

# 验证Zipkin连接ES
docker logs zipkin | grep -i elasticsearch

5.3.3 方式二:JAR包启动(ES)

# 下载Zipkin Server(包含ES客户端)
wget https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.25.2/zipkin-server-2.25.2-exec.jar

# 启动Zipkin并连接Elasticsearch
java -jar zipkin-server-2.25.2-exec.jar \
  --STORAGE_TYPE=elasticsearch \
  --ES_HOSTS=http://localhost:9200 \
  --ES_INDEX=zipkin \
  --ES_INDEX_SHARDS=3 \
  --ES_INDEX_REPLICAS=1 \
  --ES_TIMEOUT=10s \
  --ES_MAX_REQUESTS=64

参数说明:

参数说明默认值
STORAGE_TYPE存储类型mem
ES_HOSTSES地址(可多个,逗号分隔)http://localhost:9200
ES_INDEX索引名称前缀zipkin
ES_INDEX_SHARDS分片数5
ES_INDEX_REPLICAS副本数1
ES_TIMEOUT请求超时时间10s
ES_MAX_REQUESTS最大并发请求数64

5.3.4 Elasticsearch索引结构

Zipkin会在ES中创建按时间滚动的索引:

zipkin-span-2026-02-16      # 2026年2月16日的Span数据
zipkin-span-2026-02-17      # 2026年2月17日的Span数据
zipkin-dependency-2026-02-16 # 依赖关系索引

查看索引:

# 查看所有Zipkin相关索引
curl -X GET "localhost:9200/_cat/indices/zipkin*?v"

# 查看索引mapping
curl -X GET "localhost:9200/zipkin-span-*/_mapping?pretty"

5.3.5 数据验证

1. 生成链路数据

# 调用应用接口
curl http://localhost:8080/api/order/create/123

2. 在Elasticsearch中查询链路数据

# 查询最近的链路
curl -X GET "localhost:9200/zipkin-span-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "size": 5,
  "sort": [
    {"timestamp_millis": {"order": "desc"}}
  ]
}'

# 根据TraceId查询完整链路
curl -X GET "localhost:9200/zipkin-span-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "term": {
      "traceId": "abc123def456"
    }
  },
  "sort": [
    {"timestamp_millis": {"order": "asc"}}
  ]
}'

# 查询错误链路
curl -X GET "localhost:9200/zipkin-span-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "must": [
        {"match": {"tags.error": "*"}}
      ]
    }
  },
  "size": 10
}'

5.3.6 Elasticsearch索引生命周期管理

创建ILM策略(自动滚动和清理):

# 创建ILM策略
curl -X PUT "localhost:9200/_ilm/policy/zipkin_policy" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          },
          "shrink": {
            "number_of_shards": 1
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}'

# 创建索引模板(应用ILM策略)
curl -X PUT "localhost:9200/_index_template/zipkin_template" -H 'Content-Type: application/json' -d'
{
  "index_patterns": ["zipkin-span-*", "zipkin-dependency-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "lifecycle": {
        "name": "zipkin_policy",
        "rollover_alias": "zipkin-span"
      }
    }
  }
}'

5.3.7 ES性能调优建议

1. JVM内存配置

elasticsearch:
  environment:
    - "ES_JAVA_OPTS=-Xms2g -Xmx2g"  # 根据服务器内存调整

2. 段合并优化

# 手动触发段合并(减少文件数量)
curl -X POST "localhost:9200/zipkin-span-*/_forcemerge?max_num_segments=1"

3. 查询优化(避免深度分页)

# 使用search_after代替from+size
curl -X GET "localhost:9200/zipkin-span-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "size": 100,
  "sort": [{"timestamp_millis": {"order": "desc"}}]
}'

5.4 链路数据查询接口

Zipkin Server提供了RESTful API,支持直接查询链路数据。

5.4.1 基础查询接口

1. 根据服务名查询

# 查询某个服务的最近链路
curl "http://localhost:9411/api/v2/spans?serviceName=order-service&limit=10"

# 响应示例
[
  {
    "traceId": "abc123def456",
    "id": "789ghi012",
    "name": "get /api/order/create",
    "timestamp": 1708073200000000,
    "duration": 450000,
    "localEndpoint": {
      "serviceName": "order-service",
      "ipv4": "192.168.1.100",
      "port": 8080
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/api/order/create"
    }
  }
]

2. 根据TraceId查询完整链路

# 查询完整Trace
curl "http://localhost:9411/api/v2/trace/abc123def456"

3. 根据时间范围查询

# 查询指定时间范围内的链路
curl "http://localhost:9411/api/v2/spans?serviceName=order-service&startTs=1708000000000000&endTs=1708100000000000&limit=50"

5.4.2 高级查询接口

1. 根据标签查询

# 查询特定用户ID的链路
curl "http://localhost:9411/api/v2/spans?serviceName=order-service&tag=user.id:123&limit=10"

# 查询错误链路
curl "http://localhost:9411/api/v2/spans?serviceName=order-service&tag=error&limit=10"

2. 查询服务依赖关系

# 获取服务调用依赖图
curl "http://localhost:9411/api/v2/dependencies?endTs=1708100000000000&lookback=86400000"

3. 服务列表查询

# 获取所有服务列表
curl "http://localhost:9411/api/v2/services"

4. Span名称查询

# 获取某个服务的所有Span名称
curl "http://localhost:9411/api/v2/spans?serviceName=order-service"

5.4.3 API响应格式详解

完整Trace响应示例:

[
  [
    {
      "traceId": "abc123def456",
      "id": "123",
      "name": "http:/get /api/order/create",
      "timestamp": 1708073200000000,
      "duration": 450000,
      "localEndpoint": {
        "serviceName": "order-service",
        "ipv4": "192.168.1.100",
        "port": 8080
      },
      "remoteEndpoint": {
        "serviceName": "user-service",
        "ipv4": "192.168.1.101"
      },
      "annotations": [
        {
          "timestamp": 1708073200100000,
          "value": "cs"
        },
        {
          "timestamp": 1708073200600000,
          "value": "cr"
        }
      ],
      "tags": {
        "http.method": "GET",
        "http.path": "/api/order/create",
        "http.status_code": "200",
        "user.id": "123",
        "error": "false"
      },
      "kind": "CLIENT"
    }
  ]
]

字段说明:

字段说明示例
traceId链路ID(64位十六进制)abc123def456
idSpan ID789
parentId父Span ID123
nameSpan名称http:/get /api/order/create
timestamp时间戳(微秒)1708073200000000
duration持续时间(微秒)450000
localEndpoint本地端点信息服务名、IP、端口
remoteEndpoint远程端点信息被调用服务信息
annotations时间点注解cs=client send, cr=client receive
tags键值对标签自定义业务信息
kindSpan类型CLIENT/SERVER/PRODUCER/CONSUMER

5.4.4 与业务系统集成示例

Java代码:通过HTTP查询Zipkin API

import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class ZipkinQueryService {

    @Autowired
    private RestTemplate restTemplate;
    
    private static final String ZIPKIN_API = "http://localhost:9411/api/v2";

    /**
     * 根据服务名查询最近链路
     */
    public List<String> getRecentTraces(String serviceName, int limit) {
        String url = String.format("%s/spans?serviceName=%s&limit=%d", 
                                  ZIPKIN_API, serviceName, limit);
        
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        
        // 解析响应,提取TraceId
        ObjectMapper mapper = new ObjectMapper();
        JsonNode root = mapper.readTree(response.getBody());
        List<String> traceIds = new ArrayList<>();
        
        for (JsonNode span : root) {
            String traceId = span.get("traceId").asText();
            if (!traceIds.contains(traceId)) {
                traceIds.add(traceId);
            }
        }
        
        return traceIds;
    }

    /**
     * 根据TraceId查询完整链路详情
     */
    public String getTraceDetail(String traceId) {
        String url = String.format("%s/trace/%s", ZIPKIN_API, traceId);
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        return response.getBody();
    }

    /**
     * 查询错误链路
     */
    public List<String> getErrorTraces(String serviceName) {
        String url = String.format("%s/spans?serviceName=%s&tag=error", 
                                  ZIPKIN_API, serviceName);
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        // 解析逻辑同上...
        return parseTraceIds(response.getBody());
    }
}

5.5 链路追踪日志查询实战

5.5.1 通过TraceId关联日志

场景: 在Zipkin中发现某个链路耗时过长,需要查看该链路的详细日志。

步骤:

  1. 在Zipkin UI中获取TraceId

    TraceId: abc123def456789
    
  2. 在应用日志中搜索

# 搜索包含该TraceId的所有日志
grep "abc123def456789" /var/log/order-service/app.log

# 输出示例
2026-02-16 18:47:04 [abc123def456789,123abc456def] [http-nio-8080-exec-1] INFO  OrderController - 开始处理订单创建请求
2026-02-16 18:47:04 [abc123def456789,123abc456def] [http-nio-8080-exec-1] DEBUG OrderService - 校验订单信息
2026-02-16 18:47:05 [abc123def456789,789ghi012jkl] [http-nio-8080-exec-2] INFO  StockService - 开始扣减库存
2026-02-16 18:47:05 [abc123def456789,789ghi012jkl] [http-nio-8080-exec-2] ERROR StockService - 库存不足,扣减失败

日志格式配置(application.yml):

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId},%X{spanId}] [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/app.log

5.5.2 MySQL存储的日志关联查询

场景: 将Zipkin存储的链路数据与应用日志关联,进行深度分析。

1. 创建日志表(如果需要)

CREATE TABLE app_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    trace_id VARCHAR(64),
    span_id VARCHAR(16),
    timestamp BIGINT,
    level VARCHAR(10),
    logger VARCHAR(100),
    message TEXT,
    exception TEXT,
    INDEX idx_trace_id (trace_id),
    INDEX idx_timestamp (timestamp)
);

2. 联合查询示例

-- 查询某个Trace的完整链路+日志
SELECT 
    s.trace_id,
    s.name as span_name,
    s.duration,
    l.level,
    l.message,
    l.exception
FROM zipkin_spans s
LEFT JOIN app_logs l ON s.trace_id = l.trace_id
WHERE s.trace_id = 'abc123def456789'
ORDER BY s.start_ts;

-- 查询包含错误日志的链路
SELECT DISTINCT
    s.trace_id,
    s.name,
    s.duration
FROM zipkin_spans s
JOIN zipkin_annotations a ON s.trace_id = a.trace_id AND s.id = a.span_id
WHERE a.key = 'error'
ORDER BY s.start_ts DESC
LIMIT 20;

5.5.3 Elasticsearch存储的日志关联查询

场景: 将Zipkin数据与ELK日志系统集成,统一查询。

1. 在Elasticsearch中同时查询链路和日志

# 查询某个TraceId的所有数据(链路+日志)
curl -X GET "localhost:9200/zipkin-span-*,app-logs-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "term": {
      "traceId": "abc123def456789"
    }
  },
  "sort": [
    {"timestamp_millis": {"order": "asc"}}
  ],
  "size": 100
}'

# 响应示例(包含链路Span和日志记录)
{
  "hits": {
    "hits": [
      {
        "_index": "zipkin-span-2026-02-16",
        "_source": {
          "traceId": "abc123def456789",
          "name": "http:/get /api/order/create",
          "kind": "SERVER",
          "timestamp_millis": 1708073200000
        }
      },
      {
        "_index": "app-logs-2026-02-16",
        "_source": {
          "traceId": "abc123def456789",
          "level": "INFO",
          "message": "开始处理订单创建请求",
          "logger": "OrderController"
        }
      }
    ]
  }
}

2. Kibana可视化配置

步骤:

  1. 在Kibana中创建索引模式:zipkin-span-* 和 app-logs-*
  2. 创建Discover视图,添加 traceId 字段
  3. 使用 traceId 进行筛选,查看完整的调用链路和日志

KQL查询示例:

# 查询特定TraceId的所有数据
traceId: "abc123def456789"

# 查询错误链路
tags.error: *

# 查询耗时超过1秒的链路
duration: > 1000000

# 查询特定服务的链路
localEndpoint.serviceName: "order-service"

5.5.4 链路日志关联的最佳实践

1. 统一TraceId格式

# application.yml
spring:
  sleuth:
    trace-id128: true  # 使用128位TraceId,减少冲突

2. 异步日志处理

@Component
public class LogAppender {
    @Autowired
    private Tracer tracer;
    
    public void log(String message) {
        Span currentSpan = tracer.currentSpan();
        String traceId = currentSpan.context().traceId();
        String spanId = currentSpan.context().spanId();
        
        // 记录日志时携带链路信息
        log.info("[TraceId:{}, SpanId:{}] {}", traceId, spanId, message);
    }
}

3. 链路日志关联工具类

import brave.Tracer;

public class TraceContextUtil {
    
    private static Tracer tracer;
    
    @Autowired
    public void setTracer(Tracer tracer) {
        TraceContextUtil.tracer = tracer;
    }
    
    /**
     * 获取当前TraceId
     */
    public static String getTraceId() {
        if (tracer != null && tracer.currentSpan() != null) {
            return tracer.currentSpan().context().traceId();
        }
        return "unknown";
    }
    
    /**
     * 获取当前SpanId
     */
    public static String getSpanId() {
        if (tracer != null && tracer.currentSpan() != null) {
            return tracer.currentSpan().context().spanId();
        }
        return "unknown";
    }
    
    /**
     * 创建链路上下文的日志Map
     */
    public static Map<String, String> getTraceContext() {
        Map<String, String> context = new HashMap<>();
        context.put("traceId", getTraceId());
        context.put("spanId", getSpanId());
        return context;
    }
}

// 使用示例
@RestController
public class OrderController {
    
    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    
    @GetMapping("/api/order/{id}")
    public String getOrder(@PathVariable Long id) {
        Map<String, String> traceCtx = TraceContextUtil.getTraceContext();
        
        // 记录日志时自动包含链路信息
        log.info("查询订单,订单ID:{}, TraceId:{}", 
                id, traceCtx.get("traceId"));
        
        // 业务逻辑...
        return "order detail";
    }
}

5.6 部署方案对比总结

对比维度MySQL方案Elasticsearch方案
适用规模中小规模(QPS < 1000)大规模(QPS > 1000)
部署复杂度⭐⭐ 简单⭐⭐⭐ 中等
资源占用低(仅需MySQL)高(ES + JVM)
查询性能一般优秀(全文搜索、聚合)
水平扩展困难(主从/分库分表)容易(ES集群)
维护成本低高
与ELK集成需要额外配置天然集成
数据保留手动清理自动ILM
推荐场景项目初期、预算有限生产环境、大数据量

6. 进阶:代码层面的定制化

java -jar zipkin-server.jar
–STORAGE_TYPE=mysql
–MYSQL_HOST=localhost
–MYSQL_PORT=3306
–MYSQL_DB=zipkin
–MYSQL_USER=root
–MYSQL_PASS=password


**Elasticsearch存储(大规模生产环境推荐):**

```bash
# 环境变量配置
export STORAGE_TYPE=elasticsearch
export ES_HOSTS=http://elasticsearch:9200
export ES_INDEX=zipkin
export ES_INDEX_SHARDS=3
export ES_INDEX_REPLICAS=1

# 启动服务
java -jar zipkin-server.jar

存储方案对比:

存储类型适用场景优点缺点
内存开发、测试、短期演示部署简单、无依赖重启丢失、容量有限
MySQL中小规模、已有MySQL环境成本低、运维熟悉性能瓶颈、写入压力大
Elasticsearch大规模生产环境高性能查询、水平扩展运维复杂、资源占用高
Cassandra超大规模写入场景写入性能极高运维复杂度高

进阶:代码层面的定制化

本章节介绍如何对链路数据进行定制化处理,满足更复杂的业务需求。


6.1 自定义Span

场景: 需要追踪某个关键业务方法的执行情况

import brave.Span;
import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private Tracer tracer;  // Spring Cloud Sleuth提供的Tracer

    public void processOrder(Long orderId) {
        // 创建自定义Span
        Span customSpan = tracer.newTrace()
            .name("order-processing")
            .tag("order.id", String.valueOf(orderId))
            .tag("user.id", "user_" + System.currentTimeMillis())
            .start();
        
        try {
            // 业务逻辑
            validateOrder(orderId);
            processPayment(orderId);
            updateInventory(orderId);
            
            // 添加成功标记
            customSpan.tag("status", "success");
            
        } catch (Exception e) {
            // 记录异常信息
            customSpan.tag("error", e.getMessage());
            throw e;
        } finally {
            // 必须手动结束Span
            customSpan.finish();
        }
    }

    private void validateOrder(Long orderId) {
        // 这里的操作会自动成为子Span
        Span currentSpan = tracer.currentSpan();
        currentSpan.annotate("订单校验开始");
        // 校验逻辑...
        currentSpan.annotate("订单校验完成");
    }
}

关键方法说明:

  • newTrace():创建一个新的追踪链路
  • name():设置Span名称
  • tag():添加键值对标签,用于过滤和查询
  • annotate():添加时间戳注释
  • finish():结束Span(必须调用)

6.2 子Span使用

场景: 需要在一个Span下记录多个子操作

@Service
public class PaymentService {

    @Autowired
    private Tracer tracer;

    public void processPayment(Long orderId) {
        // 父Span
        Span parentSpan = tracer.currentSpan();
        
        // 创建子Span
        Span paymentSpan = tracer.newChild(parentSpan.context())
            .name("payment-process")
            .start();
        
        try {
            // 子操作1:检查余额
            Span checkSpan = tracer.newChild(paymentSpan.context())
                .name("check-balance")
                .start();
            try {
                checkBalance(orderId);
            } finally {
                checkSpan.finish();
            }
            
            // 子操作2:扣款
            Span deductSpan = tracer.newChild(paymentSpan.context())
                .name("deduct-money")
                .start();
            try {
                deductMoney(orderId);
            } finally {
                deductSpan.finish();
            }
            
        } finally {
            paymentSpan.finish();
        }
    }
}

Span层次结构:

payment-process (父Span)
    ├── check-balance (子Span1)
    └── deduct-money (子Span2)

6.3 异步线程的链路传递

场景: 在异步线程池中执行任务,需要保持链路上下文

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import brave.Tracer;
import brave.propagation.CurrentTraceContext;
import brave.propagation.TraceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AsyncOrderService {

    @Autowired
    private Tracer tracer;
    
    @Autowired
    private CurrentTraceContext currentTraceContext;
    
    // 自定义线程池
    private ExecutorService asyncExecutor = Executors.newFixedThreadPool(10);

    public void processAsyncOrder(Long orderId) {
        // 获取当前线程的链路上下文
        TraceContext parentContext = tracer.currentSpan().context();
        
        // 提交异步任务
        asyncExecutor.submit(() -> {
            // 在异步线程中创建新的Span,但继承父上下文
            Span asyncSpan = tracer.newChild(parentContext)
                .name("async-order-processing")
                .tag("order.id", String.valueOf(orderId))
                .start();
            
            try (CurrentTraceContext.Scope scope = currentTraceContext.newScope(asyncSpan.context())) {
                // 异步业务逻辑
                doAsyncWork(orderId);
            } finally {
                asyncSpan.finish();
            }
        });
    }
    
    private void doAsyncWork(Long orderId) {
        // 执行异步任务
        System.out.println("Processing order async: " + orderId);
    }
}

注意事项:

  • 使用 CurrentTraceContext.Scope 确保在异步线程中正确传递上下文
  • 必须在finally块中调用 finish(),避免内存泄漏
  • 自定义线程池需要手动处理上下文传递

6.4 在过滤器中添加业务信息

场景: 在HTTP请求处理时,自动添加用户、租户等信息

import brave.Span;
import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class TraceFilter extends OncePerRequestFilter {

    @Autowired
    private Tracer tracer;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request, 
            HttpServletResponse response, 
            FilterChain filterChain) throws ServletException, IOException {
        
        Span currentSpan = tracer.currentSpan();
        
        // 从请求头中提取用户信息
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");
        
        // 添加到Span标签中
        if (userId != null) {
            currentSpan.tag("user.id", userId);
        }
        if (tenantId != null) {
            currentSpan.tag("tenant.id", tenantId);
        }
        
        // 添加请求路径
        currentSpan.tag("http.path", request.getRequestURI());
        currentSpan.tag("http.method", request.getMethod());
        
        filterChain.doFilter(request, response);
    }
}

作用:

  • 所有经过过滤器的请求都会自动添加这些标签
  • 在Zipkin UI中可以根据这些标签进行筛选
  • 方便定位特定用户或租户的问题

6.5 代码示例总结

场景关键方法注意事项
创建自定义Spantracer.newTrace().name().start()必须调用 finish()
创建子Spantracer.newChild(parentContext)会自动形成调用树
异步线程传递currentTraceContext.newScope()需要手动管理上下文
添加标签span.tag("key", "value")避免添加敏感信息
记录异常span.tag("error", message)在catch块中处理
添加注解span.annotate("message")用于记录关键时间点

实际案例分析

本章节通过真实场景演示如何利用Zipkin定位和解决问题。


6.1 分布式调用链路图

业务场景: 用户下单流程,涉及订单服务、库存服务、支付服务、用户服务。

[图示:Zipkin UI中的链路依赖图]
┌──────────────┐
│ Gateway API  │  ← 入口服务,TraceId生成点
└──────┬───────┘
       │ 150ms
┌──────▼───────┐
│Order Service │  ← 创建订单,调用下游服务
└──┬───────┬───┘
   │       │
   │       │ 80ms
   │  ┌────▼──────┐
   │  │User Svc   │  ← 查询用户信息
   │  └────┬──────┘
   │       │
   │       │ 200ms
   │  ┌────▼──────┐
   │  │Stock Svc  │  ← 扣减库存(耗时较长)
   │  └────┬──────┘
   │       │
   │       │ 300ms
   │  ┌────▼──────┐
   │  │Payment Svc│  ← 处理支付
   │  └───────────┘
   │
   │ 50ms
┌──▼────────────┐
│ Response      │  ← 返回结果
└───────────────┘

关键指标分析:

  • 总耗时:780ms
  • 瓶颈识别:Stock Service(200ms)和 Payment Service(300ms)耗时最长
  • 并发度:User Service与Stock Service存在串行调用,可优化为并行

6.2 通过Zipkin UI分析性能瓶颈

步骤说明:

  1. 查找链路:在Zipkin首页通过服务名、时间范围、TraceId筛选
  2. 查看时间线:点击具体的Trace,查看各Span的时间轴
  3. 定位耗时操作:鼠标悬停在Span上查看详细信息
  4. 依赖分析:切换到"Dependencies"视图,查看服务间调用热力图

[图示:Zipkin UI时间线界面]

Timeline View:
┌────────────────────────────────────────────────────┐
│ order-service         ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       │ 450ms
│   └─ validate         ▓▓▓                           │ 30ms
│   └─ create-order     ▓▓▓▓▓                         │ 80ms
│   └─ check-stock      ▓▓▓▓▓▓▓▓▓▓                    │ 120ms ← 瓶颈
│   └─ process-payment  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   │ 280ms ← 严重瓶颈
│   └─ send-notification▓▓▓                          │ 40ms
└────────────────────────────────────────────────────┘
0ms    100ms    200ms    300ms    400ms    500ms

分析结论与优化建议:

  • check-stock 操作耗时120ms,建议增加缓存或优化数据库查询
  • process-payment 耗时280ms,考虑引入异步支付流程
  • validate 与 create-order 可并行执行,减少总耗时

6.3 常见问题排查案例

案例一:接口超时问题

问题现象:订单创建接口偶尔超时(超过3秒)

排查步骤:
1. 在Zipkin中搜索失败的Trace
2. 发现Stock Service的某次调用耗时2.5秒
3. 点击该Span查看详细信息:
   - tag "error": "true"
   - tag "error.message": "Connection timeout"
   - 显示为数据库连接池耗尽

解决措施:
- 检查Stock Service数据库连接池配置
- 增加连接数或优化慢SQL
- 添加熔断降级机制

案例二:异常追踪与根因定位

问题现象:用户反馈偶尔会收到"库存不足"的提示,但实际库存充足

排查步骤:
1. 在Zipkin中搜索包含"库存不足"错误信息的Trace
2. 追踪链路发现:
   Gateway → Order → Stock → Database
3. Stock Service的Span显示error标签
4. 进一步查看日志(结合TraceId):
   - 发现存在并发竞态条件

解决措施:
- 在库存扣减逻辑中添加分布式锁
- 增加乐观锁版本控制
- 引入消息队列实现最终一致性


7. 最佳实践与优化

7.1 采样率配置策略

配置原则: 在数据完整性与系统性能之间找到平衡

spring:
  sleuth:
    sampling:
      probability: 0.1  # 生产环境建议10%采样率

分级采样策略(高级配置):

@Configuration
public class SamplingConfig {

    @Bean
    public Sampler customSampler() {
        // 对核心接口100%采样
        // 对健康检查等接口0%采样
        return SamplerFunctions.andThen(
            (traceId, span) -> {
                String spanName = span.getName();
                if (spanName.contains("health")) {
                    return false;
                }
                if (spanName.contains("/api/order")) {
                    return true; // 核心业务全量采样
                }
                return Math.random() < 0.1; // 其他10%采样
            }
        );
    }
}

采样率参考:

环境类型推荐采样率说明
开发环境1.0(100%)便于完整调试
测试环境0.5-1.0保证问题复现
生产环境0.01-0.1降低性能影响
大促期间0.01-0.05应对高并发场景

7.2 链路数据存储与清理方案

数据生命周期管理:

# Elasticsearch自动清理策略(通过索引生命周期管理)
curl -X PUT "localhost:9200/_ilm/policy/zipkin_policy" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "7d"
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}'

MySQL数据清理脚本:

-- 创建事件清理30天前的数据
DELIMITER $$
CREATE EVENT IF NOT EXISTS clean_zipkin_spans
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_TIMESTAMP
DO
BEGIN
    DELETE FROM zipkin_spans 
    WHERE start_ts < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));
    
    DELETE FROM zipkin_annotations 
    WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));
END$$
DELIMITER ;

7.3 与ELK日志系统集成

日志格式配置:

# logback-spring.xml
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level 
                [%X{traceId},%X{spanId}] %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <!-- 输出到Elasticsearch -->
    <appender name="ELASTIC" class="logstash-logback-encoder-log4j2-logstash-appender">
        <destination>localhost:5044</destination>
        <customFields>{"service":"order-service","env":"prod"}</customFields>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="ELASTIC" />
    </root>
</configuration>

Kibana查询示例:

// 通过TraceId关联查询
GET /log-*/_search
{
  "query": {
    "match": {
      "traceId": "abc123def456789"
    }
  },
  "sort": [
    {
      "@timestamp": {
        "order": "asc"
      }
    }
  ]
}

集成价值:

  • 链路追踪提供"调用链"视角
  • 日志系统提供"细节"视角
  • 通过TraceId实现两者联动,快速定位问题

7.4 其他优化建议

  1. 异步上报:使用Kafka/RabbitMQ作为传输层,降低对业务性能的影响

    spring:
      sleuth:
        zipkin:
          sender:
            type: kafka
    kafka:
      bootstrap-servers: localhost:9092
      topic: zipkin
    
  2. 敏感信息脱敏:配置自动过滤HTTP头中的敏感信息

    spring:
      sleuth:
        propagation-keys: authorization,cookie  # 不传播这些敏感头
        http:
          enabled: true
          pattern: /api/**
          ignore-patterns: /actuator/**
    
  3. 服务名称规范化:使用统一的服务命名规则

    spring:
      application:
        name: ${SERVICE_NAME:order-service}-${ENV:prod}
    

8. 总结与展望

8.1 项目落地经验总结

成功要素:

  1. 渐进式集成:先在单服务验证,再逐步推广到全链路
  2. 采样策略动态调整:根据实际负载和需求灵活调整
  3. 与监控告警结合:将链路数据与Prometheus等监控工具联动
  4. 团队培训:确保团队成员理解链路追踪的价值和使用方法

常见陷阱:

  • ❌ 生产环境100%采样导致性能问题
  • ❌ 忽视数据清理,存储空间耗尽
  • ❌ 仅部署不分析,未形成问题追踪闭环

8.2 链路追踪技术发展趋势

  1. OpenTelemetry统一标准:逐步取代Sleuth等厂商特定实现

    <!-- OpenTelemetry依赖示例 -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-spring-boot-starter</artifactId>
        <version>1.30.0</version>
    </dependency>
    
  2. eBPF技术兴起:实现零侵入的链路追踪

    • 无需修改代码
    • 系统级观测能力
    • 适合云原生环境
  3. AI辅助分析:自动识别异常模式和性能瓶颈

    • 智能根因分析
    • 预测性告警
    • 自动优化建议
  4. 可观测性平台化:Metrics、Tracing、Logging三大支柱融合


参考资料链接

  1. Spring Cloud Sleuth官方文档
  2. Zipkin官方文档
  3. OpenTelemetry规范
  4. Micrometer Tracing文档
  5. Spring Cloud Alibaba集成实践
  6. Zipkin存储后端对比
  7. 分布式链路追踪最佳实践

更多推荐