文章目录
- [1. 案例概述](#1. 案例概述)
-
- [1.1 跨进程问题](#1.1 跨进程问题)
- [1.2 整体架构](#1.2 整体架构)
- [1.3 技术栈与版本对齐](#1.3 技术栈与版本对齐)
- [2. 环境搭建](#2. 环境搭建)
-
- [2.1 前置:Nacos 服务端 3.x](#2.1 前置:Nacos 服务端 3.x)
- [2.2 父 POM:多模块 + 版本对齐](#2.2 父 POM:多模块 + 版本对齐)
- [2.3 子模块依赖](#2.3 子模块依赖)
- [2.4 两个服务的 application.yaml](#2.4 两个服务的 application.yaml)
- [3. 代码实现](#3. 代码实现)
-
- [3.1 被调方 payment-service](#3.1 被调方 payment-service)
- [3.2 调用方 order-service:@HttpExchange 声明式接口](#3.2 调用方 order-service:@HttpExchange 声明式接口)
- [3.3 调用方 order-service:装配客户端](#3.3 调用方 order-service:装配客户端)
- [3.4 调用方 order-service:业务 + 观测](#3.4 调用方 order-service:业务 + 观测)
- [3.5 跨进程 trace 自动传播的原理](#3.5 跨进程 trace 自动传播的原理)
- [4. 运行与验证](#4. 运行与验证)
-
- [4.1 构建](#4.1 构建)
- [4.2 启动两个服务](#4.2 启动两个服务)
- [4.3 远程调用验证](#4.3 远程调用验证)
- [4.4 错误路径](#4.4 错误路径)
- [4.5 跨服务链路验证](#4.5 跨服务链路验证)
- [5. 总结和扩展](#5. 总结和扩展)
-
- [5.1 与单进程案例的对比](#5.1 与单进程案例的对比)
- [5.2 其他远程调用框架集成](#5.2 其他远程调用框架集成)
1. 案例概述
1.1 跨进程问题
单进程里,Micrometer 的 Observation 靠 openScope() 维护一张 ThreadLocal 栈,子观测自动成为父观测的 child,Span 自动串成树。
但分布式系统里,「下单」和「支付」通常不在同一个进程:
- 它们各自是一个独立的
Spring Boot应用,有各自的端口、各自的JVM、各自的ThreadLocal; order-service通过Http、Grpc远程调payment-service,垮进程场景线程栈已经断了 ,payment怎么知道自己是order这条链路上的子节点呢
1.2 整体架构
一个简单的微服务架构:
curl / 浏览器
│ POST /api/order/create?orderNo=NO-001&userId=1001
▼
┌──────────────────────┐ 注册/发现 ┌──────────────┐
│ order-service :8081 │ ─────────────────▶ │ Nacos :8848 │
└──────────────────────┘ └──────────────┘
│ 远程调用支付(服务名 payment-service)
│
│
▼
┌──────────────────────┐
│ payment-service :8082 │
└──────────────────────┘
模块拆分为:

1.3 技术栈与版本对齐
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 4.1.0 | 与 micrometer-boot-demo 一致,JDK 17 |
| Spring Cloud BOM | 2025.1.2 (Oakwood) | 官方矩阵:Boot 4.1 必须 ≥ 2025.1.2(2025.1.1 只支持 4.0.x) |
| Spring Cloud Alibaba BOM | 2025.1.0.0 | 追踪 Spring Cloud 2025.1.x;要求 Nacos 服务端 3.x |
| Nacos 服务端 | 3.x | 本机 localhost:8848,开启鉴权(nacos / nacos) |
| 链路导出 | OTLP → 192.168.1.1:4318 |
复用现有 Collector(Jaeger 后端) |
Boot 4对starter做了模块化拆分,本案例用到三个关键新命名:
spring-boot-starter-webmvc(原-web)spring-boot-restclient(RestClient自动配置 + 观测,独立模块)spring-boot-micrometer-tracing-opentelemetry(链路自动配置,独立模块)
2. 环境搭建
2.1 前置:Nacos 服务端 3.x
SCA 2025.1.0.0 要求 Nacos 服务端 3.x 。本机已启动在 localhost:8848,且开启了鉴权,默认账号 nacos / nacos :
bash
# 控制台
http://localhost:8080/nacos
服务端开启鉴权后,客户端必须显式传 username / password,否则注册时报 401 User not found!。
2.2 父 POM:多模块 + 版本对齐
micrometer-cloud-demo/pom.xml:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.yovis.example</groupId>
<artifactId>micrometer-cloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>order-service</module>
<module>payment-service</module>
</modules>
<properties>
<java.version>17</java.version>
<!-- Boot 4.1 必须配 Spring Cloud 2025.1.2(2025.1.1 只支持 4.0.x) -->
<spring-cloud.version>2025.1.2</spring-cloud.version>
<!-- 2026-02 发布,追踪 Spring Cloud 2025.1.x;要求 Nacos 服务端 3.x -->
<spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.3 子模块依赖
order-service (调用方)比 payment 多 3 个依赖:
xml
<!-- MVC 入口(Boot 4 模块化) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- RestClient 自动配置与观测,供 @HttpExchange 使用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-restclient</artifactId>
</dependency>
<!-- 服务名解析:@HttpExchange 里的 payment-service 经 LoadBalancer 解析到 Nacos 实例 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 链路:OpenTelemetry bridge + OTLP 导出(Boot 4 独立模块) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-micrometer-tracing-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
payment-service (被调方):只有 webmvc、actuator、nacos-discovery、tracing 三件套,不需要 RestClient 和 LoadBalancer。
2.4 两个服务的 application.yaml
order-service (src/main/resources/application.yaml):
yaml
server:
port: 8081
spring:
application:
name: order-service
cloud:
nacos:
username: nacos
password: 123456
discovery:
server-addr: localhost:8848
management:
tracing:
sampling:
probability: 1.0
opentelemetry:
tracing:
export:
otlp:
endpoint: http://192.168.1.1:4318/v1/traces
payment-service 结构完全一致,只有 port: 8082 和 name: payment-service 不同。
3. 代码实现
3.1 被调方 payment-service
业务服务 PaymentService.java:故意 sleep 200ms 模拟网关耗时,FAIL 渠道抛异常演示错误路径:
java
@Service
public class PaymentService {
public PaymentResult pay(String orderId, String payChannel) {
if ("FAIL".equalsIgnoreCase(payChannel)) {
throw new IllegalStateException("模拟支付失败: channel=" + payChannel);
}
try {
TimeUnit.MILLISECONDS.sleep(200);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
String payNo = "PAY-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
return new PaymentResult(payNo, orderId, new BigDecimal("99.90"), "PAID");
}
public record PaymentResult(String payNo, String orderId, BigDecimal amount, String status) {
}
}
控制器 PaymentController.java:
java
@RestController
@RequestMapping("/api")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/pay")
public PaymentService.PaymentResult pay(@RequestParam String orderId,
@RequestParam(defaultValue = "WECHAT") String payChannel) {
return this.paymentService.pay(orderId, payChannel);
}
}
3.2 调用方 order-service:@HttpExchange 声明式接口
@HttpExchange 是 spring-web 自带的 HTTP Interface(Feign 的替代品),无需额外依赖 。接口里的 url 用服务名 payment-service 而非 IP,解析交给 LoadBalancer。
api/PaymentApi.java:
java
/**
* 支付服务 HTTP Interface(@HttpExchange,Feign 的替代)。
* url 里的主机名 {@code payment-service} 是 Nacos 注册的服务名,
* 由 {@code @LoadBalanced RestClient} 经 LoadBalancer 解析到实际实例。
*/
@HttpExchange(url = "http://payment-service/api/pay")
public interface PaymentApi {
@PostExchange
PaymentResult pay(@RequestParam("orderId") String orderId,
@RequestParam("payChannel") String payChannel);
}
api/PaymentResult.java:
java
public record PaymentResult(String payNo, String orderId, BigDecimal amount, String status) {
}
3.3 调用方 order-service:装配客户端
config/PaymentClientConfig.java:两个 Bean 完成「服务名 → 可调用客户端」的组装:
java
@Configuration
public class PaymentClientConfig {
// ① @LoadBalanced:让 RestClient 在发送时把 payment-service 交给 LoadBalancer 解析
@Bean
@LoadBalanced
RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
// ② HttpServiceProxyFactory:把声明式接口 PaymentApi 生成成可调用的代理
@Bean
PaymentApi paymentApi(@LoadBalanced RestClient.Builder builder, ObservationRegistry observationRegistry) {
// ③ 关键:@LoadBalanced 的原型 builder 只被 Spring Cloud 挂上 LoadBalancer 拦截器,
// 不经过 Boot 的 RestClientBuilderCustomizer(观测),必须手动补挂观测,
// 否则出站请求不生成 http.client span、不发 traceparent,跨服务链路会断成两条。
RestClient restClient = builder
.observationRegistry(observationRegistry)
.observationConvention(new DefaultClientRequestObservationConvention())
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(PaymentApi.class);
}
}
关键点 :@LoadBalanced 注解是这一切的枢纽。它给 RestClient.Builder 装上一个 LoadBalancer 拦截器,http://payment-service/api/pay 里以服务名 形式出现的主机名,会在真正发请求前被解析成 Nacos 注册表里的实例地址(192.168.142.1:8082)。
3.4 调用方 order-service:业务 + 观测
service/OrderService.java:用 Observation 包住下单逻辑,paymentApi.pay(...) 的远程调用发生在观测作用域内:
java
@Service
public class OrderService {
private final PaymentApi paymentApi;
private final ObservationRegistry observationRegistry;
public OrderService(PaymentApi paymentApi, ObservationRegistry observationRegistry) {
this.paymentApi = paymentApi;
this.observationRegistry = observationRegistry;
}
public OrderResult createOrder(String orderNo, Long userId, String orderType) {
return Observation.createNotStarted("order.create", this.observationRegistry)
.lowCardinalityKeyValue("order.type", orderType)
.observe(() -> {
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
BigDecimal amount = new BigDecimal("99.90");
// 远程调用支付服务:内部经 @HttpExchange + LoadBalancer 打到 payment-service
PaymentResult payment = this.paymentApi.pay(orderId, "WECHAT");
return new OrderResult(orderId, orderNo, amount, "CREATED", payment);
});
}
public record OrderResult(String orderId, String orderNo, BigDecimal amount, String status,
PaymentResult payment) {
}
}
controller/OrderController.java:
java
@RestController
@RequestMapping("/api/order")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/create")
public OrderService.OrderResult createOrder(@RequestParam(defaultValue = "NO-001") String orderNo,
@RequestParam(defaultValue = "1001") Long userId,
@RequestParam(defaultValue = "NORMAL") String orderType) {
return this.orderService.createOrder(orderNo, userId, orderType);
}
}
3.5 跨进程 trace 自动传播的原理
本案例没有写任何 trace 相关代码 ,链路却能跨进程拼起来,靠的是 micrometer-otel 的自动埋点 + OTel 的 W3C TraceContext Propagator:
order-service 进程内 │ 网络 │ payment-service 进程内
│ │
Observation("order.create") 打开 scope │ │
└─ http.client span(发请求前) │ inject │
Span.current() 已存在 │ traceparent: │
→ 生成子 span C │ 00-T-A-01 │
→ 把 C 的信息注入 header │ ─────────────────▶│
└─ GET /api/pay HTTP/1.1 │ │ http.server span(收到请求)
traceparent: 00-T-A-01 │ │ extract 出 (T, A, sampled)
│ │ → 新 span B 的 parentId = A
│ │ → 自动成为 C 的兄弟、A 的子
关键点:
- inject(注入) :客户端在发请求前,把当前
Span的traceId/spanId/采样标志写进traceparent: 00-<traceId>-<spanId>-01。 - extract(提取) :服务端收到请求时,从
header还原出父上下文,新建的http.serverspan自动把parentId设为传来的spanId。
这就是之前「
Propagator跨进程上下文透传」的源码原理在本工程的开箱即用 落地,手动版要自己inject/extract,Boot自动配置版零成本。
4. 运行与验证
4.1 构建
bash
cd micrometer-cloud-demo
mvn clean package
4.2 启动两个服务
bash
java -jar payment-service/target/payment-service-0.0.1-SNAPSHOT.jar
java -jar order-service/target/order-service-0.0.1-SNAPSHOT.jar
启动日志里出现注册成功的标志:
text
payment-service: nacos registry, DEFAULT_GROUP payment-service 192.168.142.1:8082 register finished
order-service: nacos registry, DEFAULT_GROUP order-service 192.168.142.1:8081 register finished
到 Nacos 控制台(http://localhost:8848/nacos,服务管理 → 服务列表)确认两个实例都在。
4.3 远程调用验证
bash
curl -X POST "http://localhost:8081/api/order/create?orderNo=NO-001&userId=1001"
返回:
json
{
"orderId": "ORD-F80D970C",
"orderNo": "NO-001",
"amount": 99.90,
"status": "CREATED",
"payment": {
"payNo": "PAY-695ECB31",
"orderId": "ORD-F80D970C",
"amount": 99.90,
"status": "PAID"
}
}
4.4 错误路径
bash
curl -X POST "http://localhost:8082/api/pay?orderId=ORD-F80D970C&payChannel=FAIL"
返回 500 Internal Server Error,对应 PaymentService 抛出的 IllegalStateException("模拟支付失败: channel=FAIL")。
4.5 跨服务链路验证
在 Jaeger 控制台按 order-service 的 traceId 查询,应看到同一条 trace 下的父子结构。

四个 span 共享同一个 traceId :
text
http post /api/order/create order-service 根(无父)
└─ order.create order-service 父 = 根
└─ http post(http.client) order-service 父 = order.create
└─ http post /api/pay payment-service 父 = order 的 http.client,同 traceId
5. 总结和扩展
5.1 与单进程案例的对比
| 维度 | 单进程(micrometer-boot-demo) | 跨进程(micrometer-cloud-demo) |
|---|---|---|
| 部署 | 一个应用、一个 JVM | order-service + payment-service 两个应用 |
| 调用方式 | 方法调用 orderService.createOrder() |
HTTP 远程调用 paymentApi.pay() |
| 服务定位 | 不需要 | Nacos 注册发现 + LoadBalancer 服务名解析 |
| 上下文传播 | ThreadLocal + openScope() 栈 |
W3C traceparent header(inject/extract) |
| 父子 Span 形成 | 同线程自动嵌套 | 跨进程靠 header 提取 parentId |
| 观测代码 | 五件套(Context/Documentation/Convention/Handler/Config) | Observation.createNotStarted 极简 API |
| 追踪配置 | /actuator/prometheus 看指标 |
OTLP 导出到 Collector,Tempo/Jaeger 看链路 |
两篇合起来正好是「统一观测」的完整拼图:单进程 靠
Observation栈把指标、日志、嵌套Span对齐到同一份上下文;跨进程 靠Propagator把这份上下文沿HTTP传到下游,同一个traceId、同一棵调用树,贯穿整条业务链路。
5.2 其他远程调用框架集成
官方文档已经提供了详细的集成说明,这里就不赘述了
Java 生态的组件或者框架,一般都提供了可观测性内置集成,比如常用的 Dubbo ,在官方文档中可以看到全套支持:

Dubbo 目前借助 Micrometer Observation 完成 Tracing 的所有埋点工作,依赖 Micrometer 提供的各种 Bridge 适配,我们可以实现将 Tracing 导入各种后端系统,具体工作原理如下:

对于 SpringBoot 用户,Dubbo 提供了 Tracing 相关的 starters,自动装配 Micrometer 相关的配置代码,且用户可自由选择 Tracer 和 Exporter。
OpenTelemetry 作为 Tracer,将 Trace 信息 export 到 OTlp Collector :
xml
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-tracing-otel-otlp-starter</artifactId>
<version>${version}</version>
</dependency>