Sleuth+Zipkin实现链路追踪与可视化的项目实践
1. 技术背景与价值
1.1 为什么需要链路追踪?
在微服务架构下,一个用户请求可能需要经过多个服务的协作才能完成。当系统出现性能瓶颈或异常时,传统单体应用的调试方式已经失效:
- 问题定位困难:无法快速定位是哪个服务导致的响应延迟
- 依赖关系复杂:服务间调用链路错综复杂,难以全貌掌握
- 性能优化无据:缺乏量化数据支撑优化决策
链路追踪(Distributed Tracing)通过在请求的生命周期中打上唯一的标识,记录服务间的调用关系和耗时,为系统可观测性提供了核心能力。
1.2 Sleuth+Zipkin组合优势
| 对比维度 | Spring Cloud Sleuth | Zipkin |
|---|---|---|
| 核心职责 | 链路数据采集与传递 | 链路数据收集、存储与可视化 |
| 侵入性 | 低,基于AOP实现 | 无需侵入业务代码 |
| 集成难度 | 极低,自动集成 | 中等,需独立部署 |
| 优势 | 与Spring Cloud生态无缝集成 | 成熟的UI展示,多存储后端支持 |
这套组合拳的最大价值在于:Sleuth负责"采集"的轻量化,Zipkin负责"展示"的专业化,开发者只需引入依赖,即可获得完整的链路追踪能力。
2. 环境准备
2.1 开发环境要求
| 组件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 8+ | 推荐JDK 8u201+(生产环境推荐8u311+) |
| Spring Boot | 2.3.x - 2.7.x | JDK 8建议使用Spring Boot 2.x系列 |
| Spring Cloud | Hoxton / 2020.x | 需要与Spring Boot 2.x版本匹配 |
| Maven | 3.6+ | 依赖管理工具 |
| Docker(可选) | 19.03+ | 用于Zipkin Server容器化部署 |
2.2 版本兼容性速查
JDK 8 推荐组合:
| Spring Boot版本 | 对应Spring Cloud版本 | 推荐组合 |
|---|---|---|
| 2.3.x | Hoxton.SR12 | 稳定生产组合 |
| 2.4.x | 2020.0.x | 新特性支持 |
| 2.6.x | 2021.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
步骤五:启动应用并验证
-
启动你的Spring Boot应用
-
访问接口生成链路数据
curl http://localhost:8080/api/order/create/123
-
在Zipkin UI中查看
- 打开 http://localhost:9411
- 点击 “Run Query” 查询最近的链路
- 点击某个Trace查看详细的时间线
-
验证日志中的链路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_HOST | MySQL地址 | localhost |
| MYSQL_PORT | MySQL端口 | 3306 |
| MYSQL_DB | 数据库名称 | zipkin |
| MYSQL_USER | 用户名 | root |
| MYSQL_PASS | 密码 | - |
| MYSQL_MAX_CONNECTIONS | 最大连接数 | 10 |
| MYSQL_USE_SSL | 是否使用SSL | false |
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_HOSTS | ES地址(可多个,逗号分隔) | 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 |
| id | Span ID | 789 |
| parentId | 父Span ID | 123 |
| name | Span名称 | http:/get /api/order/create |
| timestamp | 时间戳(微秒) | 1708073200000000 |
| duration | 持续时间(微秒) | 450000 |
| localEndpoint | 本地端点信息 | 服务名、IP、端口 |
| remoteEndpoint | 远程端点信息 | 被调用服务信息 |
| annotations | 时间点注解 | cs=client send, cr=client receive |
| tags | 键值对标签 | 自定义业务信息 |
| kind | Span类型 | 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中发现某个链路耗时过长,需要查看该链路的详细日志。
步骤:
-
在Zipkin UI中获取TraceId
TraceId: abc123def456789 -
在应用日志中搜索
# 搜索包含该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可视化配置
步骤:
- 在Kibana中创建索引模式:
zipkin-span-*和app-logs-* - 创建Discover视图,添加
traceId字段 - 使用
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 代码示例总结
| 场景 | 关键方法 | 注意事项 |
|---|---|---|
| 创建自定义Span | tracer.newTrace().name().start() | 必须调用 finish() |
| 创建子Span | tracer.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分析性能瓶颈
步骤说明:
- 查找链路:在Zipkin首页通过服务名、时间范围、TraceId筛选
- 查看时间线:点击具体的Trace,查看各Span的时间轴
- 定位耗时操作:鼠标悬停在Span上查看详细信息
- 依赖分析:切换到"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 其他优化建议
-
异步上报:使用Kafka/RabbitMQ作为传输层,降低对业务性能的影响
spring: sleuth: zipkin: sender: type: kafka kafka: bootstrap-servers: localhost:9092 topic: zipkin -
敏感信息脱敏:配置自动过滤HTTP头中的敏感信息
spring: sleuth: propagation-keys: authorization,cookie # 不传播这些敏感头 http: enabled: true pattern: /api/** ignore-patterns: /actuator/** -
服务名称规范化:使用统一的服务命名规则
spring: application: name: ${SERVICE_NAME:order-service}-${ENV:prod}
8. 总结与展望
8.1 项目落地经验总结
成功要素:
- 渐进式集成:先在单服务验证,再逐步推广到全链路
- 采样策略动态调整:根据实际负载和需求灵活调整
- 与监控告警结合:将链路数据与Prometheus等监控工具联动
- 团队培训:确保团队成员理解链路追踪的价值和使用方法
常见陷阱:
- ❌ 生产环境100%采样导致性能问题
- ❌ 忽视数据清理,存储空间耗尽
- ❌ 仅部署不分析,未形成问题追踪闭环
8.2 链路追踪技术发展趋势
-
OpenTelemetry统一标准:逐步取代Sleuth等厂商特定实现
<!-- OpenTelemetry依赖示例 --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-spring-boot-starter</artifactId> <version>1.30.0</version> </dependency> -
eBPF技术兴起:实现零侵入的链路追踪
- 无需修改代码
- 系统级观测能力
- 适合云原生环境
-
AI辅助分析:自动识别异常模式和性能瓶颈
- 智能根因分析
- 预测性告警
- 自动优化建议
-
可观测性平台化:Metrics、Tracing、Logging三大支柱融合
参考资料链接
更多推荐



所有评论(0)