Spring Cloud Alibaba 作为国内最主流的微服务解决方案之一,集成了 Nacos、Sentinel、Seata 等强大的中间件。然而,这些组件在带来便利的同时,也增加了调用链路的复杂性:
本文将抛弃概念堆砌,直接钻入源码,以 spring-cloud-starter-alibaba-nacos-discovery 和 spring-cloud-starter-alibaba-sentinel 为切入点,结合 opentelemetry-java-instrumentation,手把手还原一个完整的、端到端的调用链路是如何被构建和传递的。
SCA 本身并不直接实现追踪功能,而是通过与 OpenTelemetry 或 Brave (Sleuth) 集成来提供自动化追踪。我们以更现代的 OpenTelemetry 为例。
OpenTelemetryAutoConfiguration当你的项目引入了 opentelemetry-spring-starter 后,Spring Boot 会自动加载 OpenTelemetryAutoConfiguration。这个配置类是整个追踪体系的起点。
// opentelemetry-spring-starter: io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration
@Bean
@ConditionalOnMissingBean
public Tracer tracer(OpenTelemetry openTelemetry) {
// 1. 从全局 OpenTelemetry 实例中获取 Tracer
return openTelemetry.getTracer("io.opentelemetry.spring");
}
@Bean
@ConditionalOnMissingBean
public OpenTelemetry openTelemetry(
ConfigProperties config,
@Autowired(required = false) Resource resource,
@Autowired(required = false) List<SpanExporter> spanExporters,
@Autowired(required = false) List<MetricExporter> metricExporters) {
// 2. 构建 OpenTelemetry SDK 实例
SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder();
// 3. 注册 Span Exporter (如 OTLP, Jaeger)
if (!spanExporters.isEmpty()) {
tracerProviderBuilder.addSpanProcessor(
BatchSpanProcessor.builder(spanExporters.get(0)).build()
);
}
// 4. 构建全局 OpenTelemetry 实例
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProviderBuilder.build())
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.buildAndRegisterGlobal();
return sdk;
}关键点:
OpenTelemetrySdk 被注册为全局单例 (buildAndRegisterGlobal()),确保整个应用使用同一套追踪上下文。W3CTraceContextPropagator,这是跨服务传播 traceparent 的标准。BatchSpanProcessor,它会批量、异步地将 Span 数据发送到后端(如 Jaeger)。至此,应用的基础追踪能力已经就绪。但如何让它与 SCA 的组件联动呢?
Nacos 是 SCA 的服务注册与发现中心。一次典型的 Feign 调用流程是:Feign Client -> LoadBalancer (Ribbon) -> Nacos Server。我们的目标是让这个过程中的每一个环节都产生 Span。
OpenTelemetryFeignClientOpenTelemetry 提供了对 Feign 的自动插桩。核心在于 FeignClientBeanPostProcessor。
// opentelemetry-java-instrumentation: FeignClientBeanPostProcessor
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof FeignClientFactoryBean) {
FeignClientFactoryBean factory = (FeignClientFactoryBean) bean;
// 1. 获取原始的 Builder
feign.Builder builder = factory.getBuilder();
if (builder == null) {
builder = Feign.builder();
}
// 2. 用 OpenTelemetry 的拦截器包装 Builder
factory.setBuilder(
builder
.requestInterceptor(new OpenTelemetryFeignRequestInterceptor(tracer))
.client(new OpenTelemetryFeignClient(tracer, factory.getClient()))
);
}
return bean;
}OpenTelemetryFeignRequestInterceptor 的核心逻辑:
public void apply(RequestTemplate template) {
// 1. 获取当前活跃的 Span (即上游服务传来的 Context)
Context current = Context.current();
// 2. 创建一个新的 CLIENT Span,代表这次 Feign 调用
Span span = tracer.spanBuilder(template.method() + " " + template.url())
.setParent(current) // 设置父 Span
.setAttribute(SemanticAttributes.HTTP_METHOD, template.method())
.startSpan();
// 3. 将新的 Span 放入 Context,并激活
try (Scope scope = current.with(span).makeCurrent()) {
// 4. 【关键】注入追踪信息到 HTTP Header
TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
propagator.inject(Context.current(), template, RequestTemplate::header);
}
// 注意:Span 的结束会在 Response 处理时完成
}这里发生了什么?
propagator.inject(),将 trace-id, span-id 等信息写入 template 的 Header 中。这是跨服务传播的关键一步。有趣的是,在 SCA 的默认配置下,Ribbon 和 Nacos 客户端本身通常不会产生独立的 Span。原因如下:
namingService.selectInstances())通常发生在应用启动时或缓存过期时,是一个后台任务,与具体的业务请求 Trace 关联性不强。因此,它不会主动去 Context 中查找父 Span。结论:在标准的 SCA + OTel 链路中,你会看到 Feign CLIENT Span 直接指向下游服务的 SERVER Span,中间没有关于 Nacos/Ribbon 的额外节点。这是一种合理的简化。
Sentinel 是 SCA 的流量治理组件,负责限流、熔断、系统自适应保护等。它必须能感知到当前请求的调用链路,才能做出精准的决策,并将决策结果反馈给追踪系统。
Entry 与 OpenTelemetry 的 SpanSentinel 的核心是 SphU.entry(resourceName)。每次进入一个受保护的资源,都会创建一个 Entry 对象。我们需要将这个 Entry 与 OTel 的 Span 关联起来。
SCA 通过 SentinelWebAutoConfiguration 提供了与 Web 场景的集成。
// spring-cloud-starter-alibaba-sentinel: SentinelWebAutoConfiguration
@Bean
@ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true)
public FilterRegistrationBean<SentinelWebMvcFilter> sentinelWebMvcFilter(
SentinelProperties properties) {
// 注册一个 Servlet Filter
FilterRegistrationBean<SentinelWebMvcFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SentinelWebMvcFilter());
// ...
return registration;
}SentinelWebMvcFilter 的核心逻辑:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String resourceName = getResourceName(httpRequest); // 通常是 URL path
Entry entry = null;
try {
// 1. 【Sentinel 核心】尝试进入资源
entry = SphU.entry(resourceName, EntryType.INBOUND, 1, request);
// 2. 【关键】获取当前 OTel Span
Span currentSpan = Span.fromContextOrNull(Context.current());
if (currentSpan != null) {
// 3. 将 Sentinel 的 block 事件与 Span 关联
entry.whenBlock(BlockException.class, e -> {
// 当发生限流/熔断时,更新 Span 状态
currentSpan.setStatus(StatusCode.ERROR, e.getClass().getSimpleName());
currentSpan.setAttribute("sentinel.blocked", true);
currentSpan.setAttribute("sentinel.rule", e.getRule().toString());
});
}
chain.doFilter(request, response);
} catch (BlockException ex) {
// 4. 处理被 Sentinel Block 的情况
handleBlockException(httpRequest, (HttpServletResponse) response, ex);
} finally {
if (entry != null) {
entry.exit(); // 退出资源
}
}
}深度解析:
whenBlock 回调:这是 Sentinel 与 OTel 深度集成的精髓。它允许我们在 Span 还未结束时,就为其添加特定的属性和状态。当 Sentinel 触发限流规则时,对应的 Span 会被标记为 ERROR,并附上详细的规则信息。Span.fromContextOrNull(Context.current()) 确保了我们操作的是当前请求的 Span,而不是其他线程的。Sentinel 本身也会收集大量的实时指标(QPS、RT、线程数等)。虽然这些指标通常由 Sentinel Dashboard 展示,但它们与调用链是相辅相成的关系:
/api/order 接口被限流了)。在高级的可观测性平台(如 Grafana Tempo + Prometheus),你可以将两者关联起来,形成一个闭环的故障诊断视图。
现在,让我们切换到被调用方(Provider)的视角,看看它是如何从 HTTP Header 中提取追踪信息,并重建调用链的。
OpenTelemetryHandlerMappingInteceptorOpenTelemetry 为 Spring MVC 提供了一个拦截器。
// opentelemetry-java-instrumentation: OpenTelemetryHandlerMappingInterceptor
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从 HTTP Header 中提取 Context
Context extractedContext = propagator.extract(
Context.current(),
request,
HttpServletRequestGetter.INSTANCE
);
// 2. 在提取出的 Context 中创建 SERVER Span
Span serverSpan = tracer.spanBuilder(getSpanName(request))
.setParent(extractedContext) // 父 Span 就是上游传来的 Span
.setSpanKind(SpanKind.SERVER)
.startSpan();
// 3. 将新的 SERVER Span 绑定到当前线程
Scope scope = extractedContext.with(serverSpan).makeCurrent();
request.setAttribute(OTEL_SCOPE_ATTRIBUTE, scope);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 4. 请求结束时,关闭 Span 和 Scope
Scope scope = (Scope) request.getAttribute(OTEL_SCOPE_ATTRIBUTE);
if (scope != null) {
scope.close();
}
Span span = Span.fromContextOrNull(Context.current());
if (span != null) {
span.end();
}
}关键流程:
extract:从 request.getHeader("traceparent") 中解析出 trace-id 和 parent-span-id。setParent:新创建的 SERVER Span 的父 Span 被设置为从 Header 中提取出的 Span。这样,Consumer 的 CLIENT Span 和 Provider 的 SERVER Span 就通过 parent-span-id 完美地链接在了一起。preHandle 开始 Span,afterCompletion 结束 Span,确保了 Span 的生命周期与 HTTP 请求完全一致。在 Provider 端,如果它也集成了 Sentinel,那么 SentinelWebMvcFilter 会在 OpenTelemetryHandlerMappingInterceptor 之后执行。这意味着:
SERVER Span 并将其放入 Context。Entry 可以通过 Span.fromContextOrNull() 找到这个 Span。SERVER Span 的状态。这种拦截器的执行顺序是由 Spring 的 Ordered 接口决定的,确保了正确的依赖关系。
现在,让我们把所有碎片拼在一起,看一个完整的 Trace 是如何形成的。
场景:用户访问 Order-Service 的 /create 接口,该接口通过 Feign 调用 Inventory-Service 的 /deduct 接口。
Jaeger UI 中的调用树:
[Order-Service] POST /create (SERVER Span)
├── [Order-Service] GET inventory-service/deduct (CLIENT Span)
│ └── [Inventory-Service] GET /deduct (SERVER Span)
│ ├── [Inventory-Service] DB Query (Span from JDBC instrumentation)
│ └── [Inventory-Service] Sentinel Check (隐式体现在 SERVER Span 的 attributes 中)
└── [Order-Service] DB Save Order (Span from JDBC instrumentation)源码层面的数据流:
POST /create 的 SERVER Span-A。GET /deduct 的 CLIENT Span-B,其 parent-id = Span-A.id。propagator.inject() 将 trace-id=123, parent-id=B.id 写入 HTTP Header。traceparent: 00-123-B.id-01 到达 Inventory-Service。extract() 出 trace-id=123, parent-id=B.id。GET /deduct 的 SERVER Span-C,其 parent-id = B.id。parent-id = C.id。trace-id=123 将它们归为一组。parent-id 关系重建调用树。在 SCA 中使用 @Async 或 CompletableFuture 时,必须确保 Context 能正确传递。
解决方案:使用 ContextTaskDecorator。
@Configuration
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 关键:设置 TaskDecorator
executor.setTaskDecorator(new ContextTaskDecorator());
executor.initialize();
return executor;
}
}ContextTaskDecorator 会捕获提交任务时的 Context,并在任务执行前将其 attach 到新线程。
对于核心业务逻辑,手动埋点能提供更丰富的上下文。
@Service
public class OrderService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("business");
public void createOrder(Order order) {
Span span = tracer.spanBuilder("validate-order").startSpan();
try (Scope ignored = span.makeCurrent()) {
span.setAttribute("order.id", order.getId());
// ... validation logic
} finally {
span.end();
}
}
}全量采集对性能和存储都是巨大挑战。建议采用动态采样策略:
可以在 OpenTelemetry Collector 中配置复杂的采样规则,实现精细化的成本控制。
通过对 Spring Cloud Alibaba、OpenTelemetry、Sentinel 等组件源码的层层剖析,我们可以清晰地看到微服务调用链路体系的构建并非黑盒,而是一系列精密协作的结果:
Context、Span、Propagator 等核心抽象。BeanPostProcessor、Filter、Interceptor 等机制,实现了对 Feign、Spring MVC、Sentinel 等框架的无侵入式插桩。W3CTraceContextPropagator 通过 traceparent Header 完成了 Context 的接力。whenBlock 回调,将流量治理决策无缝融入追踪数据。理解了这套机制,你不仅能熟练使用调用链路排查问题,更能根据业务需求对其进行定制和扩展,真正掌握微服务可观测性的核心命脉。