
上周三下午,运维群里弹出一条告警:订单服务的P99延迟从80ms飙到了340ms。
第一反应是数据库。查了慢查询日志,空的。看JVM,堆正常,GC正常。看线程池,没有排队。看网络,延迟没波动。
所有人盯着Grafana面板发愣。直到有人把时间线拉回去,精确对齐——延迟飙升的时间点,和我们上线Micrometer Tracing全量采样,分秒不差。
采样率从0.1调到1.0,接口慢了4倍。不是数据库的锅,不是网络的问题,是我们自己对"全量追踪"的盲目崇拜,把系统拖进了性能泥潭。
这是最普遍的误解。大多数开发者的直觉模型是这样的:
请求进来 → 生成一个UUID → 塞进Header → 传给下游 → 完事
开销约等于一次Random生成 + 一个Map.put,忽略不计。
但Micrometer Tracing在Spring Boot 3里的实际行为完全不是这样。每次HTTP请求进来,框架在背后做的是:
这一整条链路,每一步都有真实开销。当采样率是10%时,这些开销被稀释到可以忽略。当采样率拉到100%时,开销叠加效应会彻底暴露。
我写了一个极简的Benchmark来验证。一个纯空接口,只返回"OK",不碰数据库、不调下游、不序列化任何对象。
@RestController
public class PingController {
@GetMapping("/ping")
public String ping() {
return "OK"; // 纯空接口,不依赖任何外部资源
}
}
然后在application.yml里切换采样率跑压测:
management:
tracing:
sampling:
probability: 0.0 # 第一轮:关闭Tracing
JMeter 50并发,持续30秒,结果:
采样率 | QPS | P99延迟 | CPU使用率 |
0.0(关闭) | 18,400 | 5ms | 12% |
0.1(10%) | 17,200 | 7ms | 14% |
0.5(50%) | 14,600 | 14ms | 19% |
1.0(100%) | 11,800 | 26ms | 27% |
一个什么都没干的空接口,QPS从18400跌到11800,降幅36%。P99延迟翻了5倍。
空接口都这样,真实的业务接口里夹着数据库查询、Redis调用、下游HTTP请求——每一个操作都是一个自动埋点,每一个埋点就是一个子Span。100%采样率下,一个请求生成十几个Span是家常便饭。
Span创建只是开销的一部分。更大的开销藏在 上下文传播(Context Propagation) 里。
Micrometer Tracing需要保证TraceId在整个调用链中不丢失。在Spring Boot 3里,传播机制依赖几个关键组件:
// Micrometer Tracing 内部传播机制简化示意
public class PropagationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从请求头提取父级TraceContext,不存在则新建
TraceContext parentContext = tracing.extract(request); // ← 涉及Header解析
// 创建当前Span并绑定到当前线程
Span currentSpan = tracer.nextSpan(parentContext).start(); // ← 时间戳记录
// 关键:将Span写入ThreadLocal,后续所有操作可感知当前Trace
try (SpanInScope scope = tracer.withSpan(currentSpan)) { // ← ThreadLocal写入
// 同时注入MDC,让日志框架也能输出TraceId
MDC.put("traceId", currentSpan.context().traceId()); // ← MDC写入
}
return true;
}
}
每个请求都要走一遍这个流程。当QPS上万的接口叠加100%采样时,ThreadLocal读写 + MDC操作的开销会被放大到不可忽略的程度。
更隐蔽的问题在线程切换时暴露。当你的代码里用了@Async、CompletableFuture或者自定义线程池,TraceContext必须从主线程传递到工作线程:
@Async
public CompletableFuture asyncQuery() {
// 框架在背后做了这件事:
// 1. 从主线程的ThreadLocal中取出TraceContext
// 2. 序列化为可传递对象
// 3. 放入工作线程的ThreadLocal
// 4. 工作线程创建子Span时从ThreadLocal恢复上下文
// 你的业务代码在这里...
return CompletableFuture.completedFuture("result");
}
每一次线程切换都意味着一次上下文快照+恢复。100%采样率下,这个动作在你察觉不到的地方反复执行,CPU时间片被蚕食。
把 probability 从1.0改成0.1,问题就解决了吗?只解决了一半。
真正的风险在于:当采样率降低后,你可能恰好丢掉那条出了问题的Trace。线上偶发的超时、间歇性的错误,因为没被采样到,你在Zipkin里永远查不到。
这就需要分层采样策略——不是对所有请求一视同仁,而是对不同的请求给出不同的采样权重:
@Configuration
public class TracingSamplerConfig {
@Bean
public SamplerFunction serverSampler() {
return request -> {
// 策略1:错误请求强制采样——宁可多采,不可漏掉
String path = request.getPath();
if (path.contains("/payment") || path.contains("/order/submit")) {
return 1.0f; // 核心交易链路100%采样
}
// 策略2:慢请求过采样——超过阈值的请求强制保留
// (需结合自定义Filter实现,此处展示逻辑)
// 策略3:普通查询接口降采样
if (path.startsWith("/api/query/")) {
return 0.02f; // 查询类接口2%采样
}
// 默认兜底
return 0.1f; // 其他接口10%采样
};
}
}
这个配置的核心思路:不是均匀地丢弃90%的请求,而是把采样预算花在最有价值的Trace上。支付链路一条不漏,查询接口大比例降采。10%的全局采样率下,你对关键链路的可见性仍然是100%。
还有一种更激进的方案:不依赖概率,而是基于结果反采。在Filter里先不创建Span,等请求处理完成后,根据状态码和耗时决定是否补采:
@Component
public class ConditionalTracingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 先正常执行业务逻辑,暂时不建Span
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long elapsed = System.currentTimeMillis() - start;
// 只对异常或慢请求做后补采样
if (response.getStatus() >= 500 || elapsed > 500) { // 500状态码或超500ms
// 创建Span并上报(此处为简化伪代码)
Span span = tracer.nextSpan().start();
span.tag("http.status_code", String.valueOf(response.getStatus()));
span.tag("http.duration_ms", String.valueOf(elapsed));
span.end();
}
// 正常快速请求:零开销,不建Span
}
}
正常请求完全零开销,慢请求和异常请求100%采集。99%的请求不产生任何Tracing开销,而那1%真正需要排查的问题一个不漏。
可观测性不是越全越好。加一个TraceId的开销你可能感觉不到,加一万个TraceId的开销就是一台服务器的算力被白白烧掉。
采样率这个参数背后,本质是一个取舍问题:你用多少系统资源,去换取多少排查问题的能力。
算清楚这笔账,比调对一个参数重要得多。
更新时间:2026-06-29
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 71396.com 闽ICP备11008920号
闽公网安备35020302034844号