1. 概述
在分布式系统中,服务请求时偶尔出现错误是常态。一个集中的可观测性平台通过捕获应用追踪/日志,并提供查询特定请求的接口来解决这个问题。OpenTelemetry 则帮助标准化遥测数据的捕获和导出过程。
本教程将学习如何使用 Micrometer 门面模式将 Spring Boot 应用与 OpenTelemetry 集成。同时,我们将运行 OpenTelemetry 服务捕获应用追踪数据,并将其发送到中央系统监控请求。
首先,理解几个基础概念。
2. OpenTelemetry 简介
OpenTelemetry (Otel) 是一套标准化的、厂商无关的工具、API 和 SDK 集合。它是 CNCF 孵化项目,由 OpenTracing 和 OpenCensus 项目合并而成。
OpenTracing 是厂商中立的 API,用于将遥测数据发送到可观测性后端。OpenCensus 项目提供一组语言特定库,开发者可用其检测代码并发送到任何支持的后端。Otel 使用与前身项目相同的 trace 和 span 概念表示微服务间的请求流。
OpenTelemetry 允许我们检测、生成和收集遥测数据,帮助分析应用行为或性能。遥测数据包括日志、指标和追踪。我们可以自动或手动检测 HTTP、数据库调用等代码。
Spring Boot 3 通过 Micrometer Tracing 支持 OpenTelemetry——这是一个一致、可插拔、厂商中立的 API。
⚠️ 注意:早期的 Spring Cloud Sleuth 框架在 Spring Boot 3 中已弃用,其追踪功能已迁移到 Micrometer Tracing。
下面通过示例深入探讨。
3. 示例应用
假设需要构建两个微服务,其中一个服务与另一个交互。
为检测应用的遥测数据,我们将应用与 Micrometer tracing 和 OpenTelemetry 导出器库集成。
3.1. Maven 依赖
micrometer-tracing、micrometer-tracing-bridge-otel 和 opentelemetry-exporter-otlp 依赖可自动捕获追踪数据并导出到任何支持的收集器。
首先创建 Spring Boot 3 Web 项目,在两个应用中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.39.0</version>
</dependency>
接下来实现下游服务。
3.2. 实现下游应用
下游应用将提供一个返回 Price 数据的接口。
首先定义 Price 类:
public class Price {
private long productId;
private double priceAmount;
private double discount;
}
然后实现 PriceController 的获取价格接口:
@RestController(value = "/price")
public class PriceController {
private static final Logger LOGGER = LoggerFactory.getLogger(PriceController.class);
@Autowired
private PriceRepository priceRepository;
@GetMapping(path = "/{id}")
public Price getPrice(@PathVariable("id") long productId) {
LOGGER.info("Getting Price details for Product Id {}", productId);
return priceRepository.getPrice(productId);
}
}
在 PriceRepository 中实现 getPrice() 方法:
public Price getPrice(Long productId){
LOGGER.info("Getting Price from Price Repo With Product Id {}", productId);
if (!priceMap.containsKey(productId)){
LOGGER.error("Price Not Found for Product Id {}", productId);
throw new PriceNotFoundException("Price Not Found");
}
return priceMap.get(productId);
}
上述代码在产品未找到时返回价格或抛出异常。
3.3. 实现上游应用
上游应用将提供获取 Product 详情的接口,并调用上述价格接口。
首先定义 Product 类:
public class Product {
private long id;
private String name;
private Price price;
}
然后实现 ProductController 的获取产品接口:
@RestController
public class ProductController {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);
@Autowired
private PriceClient priceClient;
@Autowired
private ProductRepository productRepository;
@GetMapping(path = "/product/{id}")
public Product getProductDetails(@PathVariable("id") long productId){
LOGGER.info("Getting Product and Price Details with Product Id {}", productId);
Product product = productRepository.getProduct(productId);
product.setPrice(priceClient.getPrice(productId));
return product;
}
}
在 ProductRepository 类中实现 getProduct() 方法:
public Product getProduct(Long productId){
LOGGER.info("Getting Product from Product Repo With Product Id {}", productId);
if (!productMap.containsKey(productId)){
LOGGER.error("Product Not Found for Product Id {}", productId);
throw new ProductNotFoundException("Product Not Found");
}
return productMap.get(productId);
}
✅ 关键点:需使用 RestTemplateBuilder 显式定义 RestTemplate Bean(Spring Boot 3 要求):
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
最后在 PriceClient 类中实现 getPrice() 方法:
public Price getPrice(@PathVariable("id") long productId){
LOGGER.info("Fetching Price Details With Product Id {}", productId);
String url = String.format("%s/price/%d", baseUrl, productId);
ResponseEntity<Price> price = restTemplate.getForEntity(url, Price.class);
return price.getBody();
}
上述代码调用下游服务获取价格。
4. 配置 Spring Boot 与 OpenTelemetry
OpenTelemetry 提供名为 Otel 收集器的组件,用于处理遥测数据并导出到 Jaeger、Prometheus 等可观测性后端。
可通过 Spring 管理配置将追踪数据导出到任何 OpenTelemetry 收集器。
4.1. 配置 Spring
需配置 management.tracing.sampling.probability 和 management.otlp.tracing.endpoint 属性导出追踪数据。
在 application.yml 中添加管理配置:
management:
tracing:
sampling:
probability: '1.0'
otlp:
tracing:
endpoint: http://collector:4318/v1/traces
基于概率的追踪采样属性定义了 span 的采样比例。值 1.0 表示导出所有 span。
5. 运行应用
现在配置并运行整个环境:应用和 Otel 收集器服务(如 Jaeger)。
5.1. 配置应用 Dockerfile
为 Product 服务实现 Dockerfile:
FROM openjdk:17-alpine
COPY target/spring-cloud-open-telemetry1-1.0.0-SNAPSHOT.jar spring-cloud-open-telemetry.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/spring-cloud-open-telemetry.jar"]
Price 服务的 Dockerfile 基本相同。
5.2. 使用 Docker Compose 配置服务
services:
product-service:
build: spring-boot-open-telemetry1/
ports:
- "8080:8080"
price-service:
build: spring-boot-open-telemetry2/
ports:
- "8081"
collector:
image: jaegertracing/jaeger:2.5.0
ports:
- "4318:4318"
- "16686:16686"
上述 collector 服务使用 jaegertracing/jaeger v2 一体化镜像。它内置支持 OpenTelemetry,无需单独运行 OpenTelemetry 服务。
整体架构如下:
架构图中,API 服务将 span 导出到 Jaeger v2 服务。Jaeger v2 内部包含三个主要组件:收集器/接收器、存储和 UI 服务。
⚠️ 生产建议:生产环境应独立运行 OpenTelemetry 收集器、存储和查询/UI 服务,确保职责分离。
通过 docker-compose 启动服务:
$ docker-compose up
验证运行中的 Docker 服务。
5.3. 验证运行中的 Docker 服务
除 product-service 和 price-service 外,我们添加了 Jaeger 服务。
上述服务使用 HTTP 端口 4318 向 Jaeger 收集器发送追踪数据。
使用 docker container 命令验证服务状态:
$ docker container ls --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
输出类似:
125c47300f69 spring-boot-open-telemetry-product-service-1 Up 19 seconds 0.0.0.0:8080->8080/tcp
5e8477630211 spring-boot-open-telemetry-price-service-1 Up 19 seconds 0.0.0.0:49775->8081/tcp
6ace8520779a spring-boot-open-telemetry-collector-1 Up 19 seconds 4317-4318/tcp, 5778-5779/tcp, 9411/tcp, 13132-13133/tcp, 14250/tcp, 14268/tcp, 0.0.0.0:16686->16686/tcp
确认所有服务正在运行。
6. 在收集器中监控追踪
OpenTelemetry 收集器工具(如 Jaeger)提供前端应用监控请求。可实时或后续查看请求追踪。
监控成功和失败请求的追踪。
6.1. 监控成功请求的追踪
调用 Product 接口 http://localhost:8080/product/100001,日志输出:
product-service-1 | 2025-04-08T04:21:08.372Z INFO 1 --- [product-service] [nio-8080-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] c.b.o.repository.ProductRepository : Getting Product from Product Repo With Product Id 100001
product-service-1 | 2025-04-08T04:21:08.373Z INFO 1 --- [product-service] [nio-8080-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] c.b.o.api.client.PriceClient : Fetching Price Details With Product Id 100001
price-service-1 | 2025-04-08T04:21:08.731Z INFO 1 --- [price-service] [nio-8081-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-60bf6b4856b145f6] c.b.o.controller.PriceController : Getting Price details for Product Id 100001
Micrometer 自动配置 ProductService 将 trace 和 span id 附加到当前线程上下文,并作为 HTTP 头传递给下游 API 调用。PriceService 也会在线程上下文和日志中包含相同的 trace id。Jaeger 收集器服务使用此 trace id 确定跨服务的请求流。
如预期,trace id ....f61874a 在 PriceService 和 ProductService 日志中一致。
在端口 16686 的 Jaeger UI 中查看请求时间线:
图中显示请求流时间线及元数据。
6.2. 监控失败请求的追踪
模拟下游服务抛出异常导致请求失败的场景。
使用同一 UI 分析根本原因。
调用产品接口 /product/100005(下游应用中不存在该产品)。
查看失败请求的 span:
可追溯错误发生的最终 API 调用位置。
7. 结论
本文学习了 OpenTelemetry 如何标准化微服务可观测性模式。
通过示例展示了如何使用 Micrometer tracing 门面将 Spring Boot 3 应用与 OpenTelemetry 集成。最后在 Jaeger UI 服务中追踪了 API 请求流。