Spring Cloud链路追踪与可观测性
微服务拆分以后,一次请求可能经过很多服务。没有链路追踪时,每个服务都有自己的日志,排查问题只能靠时间、接口和猜测。
本页用于零基础建立概念。继续学习时进入Spring Boot 3可观测性内部原理与生产治理,面试复习进入独立面试题。深度页会从Boot自动配置源码一直推演到Observation生命周期、HTTP与线程传播、OpenTelemetry SDK、OTLP、Collector、采样、失败窗口和生产Runbook。
先区分新旧技术线
| 技术线 | Java与Spring基线 | 常见实现 | 应该怎样学习 |
|---|---|---|---|
| 极老存量线 | JDK 7、Boot 1.x或非Boot应用 | 项目锁定的旧追踪库或手工日志关联 | 先隔离维护并制定升级计划,不能使用Boot 2/3 API |
| 存量线 | JDK 8、Spring Boot 2.7.x、Spring Cloud 2021.x | Spring Cloud Sleuth 3.x、Brave、Zipkin | 理解维护方式、B3传播和迁移风险,不把老配置直接复制到Boot 3 |
| 现代线 | Java 17+、Spring Boot 3.x | Micrometer Observation、Micrometer Tracing、OpenTelemetry或Brave Bridge | 新项目主线,重点掌握自动配置、标准传播、异步上下文、OTLP和Collector |
JDK 7/8仍然是存量系统的重要基础,但Boot 2.7要求Java 8,Spring Boot 3要求Java 17起步。知识库不会让读者永远停留在旧技术,也不会用Java 17 API假装可以直接运行在JDK 7/8上:原理共用,依赖、配置、代码语法和迁移步骤分别讲解。
链路追踪解决的是:
一次请求经过哪些服务、每段耗时多少、在哪个服务失败、日志如何串起来。
阅读入口:先看上下文,再看三类信号
按“Trace Context 创建 → Header 传播 → Span 生命周期 → Logs/Metrics 关联 → Collector 查询”阅读。先保证链路串得起来,再讨论采样率、指标基数和存储成本。
核心原理:上下文传播驱动三类信号关联
入口创建或接收 Trace Context,Observation 在网关、HTTP 客户端、Controller 和数据库等边界创建 Span。上下文通过 W3C Trace Context 或 B3 Header 传播,日志写入 MDC,指标从同一 Observation 记录时延和错误,后端据此还原拓扑。
flowchart TD
A["Gateway接收请求"] --> B["创建trace/span上下文"]
B --> C["Header传播到Feign/WebClient"]
C --> D["下游服务创建子Span"]
D --> E["Observation记录耗时与异常"]
E --> F["Logs写入traceId"]
E --> G["Metrics聚合P95/错误率"]
F --> H["Collector/后端关联Trace"]
G --> HTrace 是请求级采样数据,Metrics 是聚合数据,Logs 是事件细节;不能用高基数用户 ID 充当指标标签,也不能只看单一信号判断根因。
为什么需要链路追踪
假设一次下单请求经过:
Gateway -> order-service -> user-service -> coupon-service -> payment-service用户只看到“下单失败”,但研发要回答:
- 请求有没有进入网关?
- 网关有没有路由到订单服务?
- 订单服务调用用户服务是否超时?
- 优惠券服务是否失败?
- 支付服务返回了什么错误?
- 哪个服务耗时最长?
没有 traceId,排查会非常困难。
核心概念
| 概念 | 说明 |
|---|---|
| Trace | 一次完整请求链路 |
| TraceId | 一次请求的唯一 ID |
| Span | 链路中的一个操作,例如一次 HTTP 调用、一次 SQL |
| SpanId | 某个操作的唯一 ID |
| ParentSpanId | 父操作 ID,用来组成调用树 |
| Baggage | 跨服务传播的业务上下文 |
| MDC | 日志上下文,让日志自动打印 traceId |
flowchart TD
A["TraceId: T100"] --> B["Span 1 Gateway"]
B --> C["Span 2 order-service"]
C --> D["Span 3 user-service"]
D --> E["Span 4 payment-service"]这里用纵向顺序调用保证小屏可读。Trace是整条链,Span是链上的一段;如果订单服务并行调用用户和支付服务,两个下游Span也可以拥有同一个父Span,但应在复杂原理页拆成更小的图说明,避免一张宽图在移动端被截断。
TraceId 如何跨服务传播
链路追踪的关键是把 traceId 放到请求头里,跨服务透传。
flowchart TD
A["Gateway 生成 TraceId"] --> B["请求头写入 TraceId"]
B --> C["order-service 读取并记录"]
C --> D["Feign 调用时继续放入请求头"]
D --> E["user-service 读取同一个 TraceId"]常见请求头标准:
traceparent:W3C Trace Context 标准。b3或X-B3-TraceId:Zipkin B3 传播格式。- 自定义
X-Trace-Id:很多老项目使用。
新项目更推荐使用标准传播格式,方便接入 OpenTelemetry、Zipkin、Tempo 等系统。
日志为什么要打印 traceId
如果日志里没有 traceId,你只能这样查:
2026-07-02 10:00:01 order-service 下单失败
2026-07-02 10:00:01 user-service 查询用户
2026-07-02 10:00:02 payment-service 支付失败你无法确定这些日志是否属于同一次请求。
有 traceId 后:
[traceId=T100] order-service 下单失败
[traceId=T100] user-service 查询用户
[traceId=T100] payment-service 支付失败排查范围立刻缩小。
Sleuth、Micrometer Tracing、OpenTelemetry、SkyWalking 对比
| 技术 | 时代/定位 | 说明 |
|---|---|---|
| Spring Cloud Sleuth | 老 Spring Cloud 常见 | 自动给日志和请求加 traceId/spanId,常配 Zipkin |
| Micrometer Tracing | 新 Spring Boot/Spring Cloud 常见 | Spring Boot 3 以后更主流,统一观测门面 |
| OpenTelemetry | 开放标准 | 跨语言、跨平台采集 traces、metrics、logs |
| Zipkin | 链路追踪后端 | 存储和展示调用链 |
| SkyWalking | APM 平台 | 通过探针采集链路、指标、拓扑和告警 |
| Jaeger / Tempo | 链路追踪后端 | 云原生场景常见 |
为什么从 Sleuth 走向 Micrometer Tracing
Sleuth 时代重点是“让 Spring Cloud 应用自动带 traceId”。后来的观测体系更强调统一:
- Metrics:指标。
- Traces:链路。
- Logs:日志。
Micrometer 本来就是 Spring 生态常用指标门面,Micrometer Tracing 让链路追踪也融入同一套观测体系。OpenTelemetry 则是更大的跨语言标准。
一次链路采集流程
flowchart TD
A["应用收到请求"] --> B["创建或读取 Trace Context"]
B --> C["创建当前 Span"]
C --> D["执行业务逻辑"]
D --> E["调用下游时注入 Trace Header"]
E --> F["记录耗时、状态、异常"]
F --> G["导出 Span 到追踪后端"]
G --> H["Zipkin / SkyWalking / Tempo 展示调用链"]链路追踪不是只加一个 ID。它还会记录:
- 服务名。
- 接口名。
- 开始时间。
- 结束时间。
- 耗时。
- 状态码。
- 异常信息。
- 调用关系。
RT、P95和P99怎么看
RT是Response Time,表示一次请求的响应耗时。P95/P99是百分位延迟,表示把一段时间内的请求耗时从小到大排序后,95%或99%位置上的耗时。
例如某接口P99=800ms,表示约99%的请求耗时不超过800ms,剩下约1%的请求比800ms更慢。它不是平均值,也不是最大值。
排查微服务接口慢时,不能只看平均RT。平均值会掩盖少量长尾请求,而真实用户体验经常被P95/P99影响。更完整的解释、计算示例和排查场景见:RT、P95和P99到底是什么。
存量系统手工透传traceId
下面代码只用于“暂时无法接入完整追踪框架”的存量兜底,用来说明Header与MDC的基本关系。它不是Boot 3生产推荐方案,也不具备Span父子关系、标准采样、Scope、Exporter和Collector数据链。Boot 3项目应使用框架管理的Micrometer Tracing与标准Propagator,完整实现见Receiver与Sender调用链。
@Configuration
public class FeignTraceConfig {
@Bean
public RequestInterceptor traceRequestInterceptor() {
return template -> {
String traceId = TraceContext.getTraceId();
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
};
}
}下游服务收到请求后,把 traceId 放入日志 MDC:
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null || traceId.trim().isEmpty()) {
traceId = UUID.randomUUID().toString();
}
MDC.put("traceId", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}日志格式中加入:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger - %msg%n存量Gateway手工生成traceId
Gateway是外部流量入口,下面仍是未接完整Tracing时的兜底示意。接入Boot 3观测框架后,不要再用手写UUID覆盖框架创建或提取的Trace Context,否则日志ID与真实Span树可能不一致。
@Component
public class GatewayTraceFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = exchange.getRequest().getHeaders().getFirst("X-Trace-Id");
if (traceId == null || traceId.trim().isEmpty()) {
traceId = UUID.randomUUID().toString();
}
final String finalTraceId = traceId;
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.header("X-Trace-Id", finalTraceId))
.build();
return chain.filter(newExchange);
}
@Override
public int getOrder() {
return -100;
}
}生产中如果接入 Micrometer Tracing 或 OpenTelemetry,很多传播动作会由框架自动完成,但你仍然要理解底层就是“上下文创建、请求头传播、日志关联、后端展示”。
可观测性三件套
| 类型 | 解决什么问题 | 例子 |
|---|---|---|
| Metrics 指标 | 系统整体是否健康 | QPS、RT、CPU、错误率、线程池 |
| Traces 链路 | 一次请求经过哪里 | Gateway 到订单服务再到支付服务 |
| Logs 日志 | 某个点发生了什么 | 异常堆栈、业务参数、错误码 |
三者要结合使用:
- 指标告诉你“系统变慢了”。
- 链路告诉你“慢在哪个服务”。
- 日志告诉你“为什么慢或为什么失败”。
常见问题
traceId 断了
常见原因:
- Feign 没有透传请求头。
- 异步线程没有复制上下文。
- MQ 消息没有携带 traceId。
- Gateway 生成了新 traceId 但没有向下游传。
- 日志 MDC 没有正确清理或设置。
链路平台没有数据
排查:
- 应用是否引入 tracing 依赖。
- 采样率是否过低。
- exporter 地址是否正确。
- 网络是否能访问 Zipkin/SkyWalking/Collector。
- 服务名是否配置正确。
日志里 traceId 错乱
通常是线程复用时 MDC 没清理。线程池中的线程会复用,如果请求结束不 remove,下一个请求可能打印上一个请求的 traceId。
生产建议
| 建议 | 原因 |
|---|---|
| 所有入口生成或接收 traceId | 保证请求从入口可追踪 |
| Feign、Gateway、MQ 都要传播上下文 | 防止链路断裂 |
| 日志格式必须包含 traceId | 方便按链路检索日志 |
| 异步线程要传递上下文 | 防止线程切换后丢失 traceId |
| 控制采样率 | 高流量系统全量链路成本高 |
| 关键错误链路可强制采样 | 方便排查异常请求 |
和服务调用链路的关系
服务调用链路 解决“请求怎么走”;链路追踪解决“请求走过哪里、哪里慢、哪里失败”。
没有链路追踪,微服务调用链路一旦变长,系统就会变成黑盒。
小结
链路追踪的核心不是某个具体框架,而是 trace、span、上下文传播和日志关联。老项目常见 Sleuth + Zipkin,新项目更常见 Micrometer Tracing、OpenTelemetry 或 SkyWalking。学习时要掌握原理:入口创建 trace,上下游传播 trace,日志和链路平台按 trace 聚合。
