Saikat Chakraborty 2023-02-08
1. 概述
在分布式系统中,服务请求过程中偶尔发生错误是不可避免的。一个集中式的可观测性平台可以通过捕获应用的追踪(traces)和日志(logs),并提供查询特定请求的界面,帮助我们快速定位问题。
OpenTelemetry 有助于标准化遥测数据(telemetry data)的采集与导出流程。
在本教程中,我们将学习如何通过 Micrometer 门面将 Spring Boot 应用与 OpenTelemetry 集成。同时,我们还将运行一个 OpenTelemetry 服务来捕获应用追踪数据,并将其发送到中央系统以监控请求。
首先,让我们了解一些基本概念。
2. OpenTelemetry 简介
OpenTelemetry(简称 Otel)是一套标准化、与厂商无关的工具、API 和 SDK 的集合。它是 CNCF(云原生计算基金会)的孵化项目,由 OpenTracing 和 OpenCensus 两个项目合并而成。
- OpenTracing 提供了一套与厂商无关的 API,用于将遥测数据发送到可观测性后端。
- OpenCensus 提供了一系列语言特定的库,开发者可以使用这些库对代码进行插桩(instrumentation),并将数据发送到任意支持的后端。
OpenTelemetry 延续了其前身项目中使用 trace(追踪) 和 span(跨度) 来表示请求在微服务之间流转的概念。
OpenTelemetry 允许我们对应用进行插桩、生成并收集遥测数据,从而分析应用的行为或性能。遥测数据包括日志(logs)、指标(metrics)和追踪(traces)。我们可以对 HTTP 请求、数据库调用等操作进行自动或手动插桩。
Spring Boot 3 通过 Micrometer Tracing 支持 OpenTelemetry —— 这是一个一致的、可插拔的、与厂商无关的 API。
值得注意的是,早期的 Spring Cloud Sleuth 框架已在 Spring Boot 3 中被弃用,其追踪功能已迁移到 Micrometer Tracing。
接下来,我们通过一个示例深入探讨。
3. 示例应用
假设我们需要构建两个微服务,其中一个服务会调用另一个服务。
为了采集遥测数据,我们将把应用与 Micrometer Tracing 和 OpenTelemetry 导出器(exporter)库集成。
3.1. Maven 依赖
micrometer-tracing、micrometer-tracing-bridge-otel 和 opentelemetry-exporter-otlp 依赖项可以自动捕获并导出追踪数据到任意支持的收集器(collector)。
首先,我们创建一个 Spring Boot 3 Web 项目,并在两个应用中添加以下 Spring Boot 3 Starter、Micrometer 和 OpenTelemetry 依赖:
<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 Service)
我们的下游应用将提供一个返回价格数据的端点。
首先,定义 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 Service)
上游应用将提供一个获取商品详情的端点,并调用上述价格服务。
首先,定义 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);
}
此外,由于 Spring Boot 3 的要求,我们需要显式地通过 RestTemplateBuilder 定义 RestTemplate Bean:
@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. 使用 OpenTelemetry 配置 Spring Boot
OpenTelemetry 提供了一个名为 Otel Collector(收集器) 的组件,用于处理遥测数据并将其导出到 Jaeger、Prometheus 等可观测性后端。
我们可以使用 Spring 的管理配置将追踪数据导出到任意 OpenTelemetry 收集器。
4.1. 配置 Spring
我们需要在 application.yml 中配置 management.tracing.sampling.probability 和 management.otlp.tracing.endpoint 属性,以导出追踪数据:
management:
tracing:
sampling:
probability: '1.0'
otlp:
tracing:
endpoint: http://collector:4318/v1/traces
sampling.probability: 1.0表示 100% 的 span 都会被导出(适用于开发环境;生产环境通常使用更低的采样率)。endpoint指向 OpenTelemetry 收集器的 OTLP(OpenTelemetry Protocol)接收端点。
5. 运行应用
现在,我们将配置并运行整个环境:两个 Spring Boot 应用 + 一个 OpenTelemetry 收集器(例如 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 配置服务
创建 docker-compose.yml 文件,整合所有服务:
services:
product-service:
build: spring-boot-open-telemetry1/
ports:
- "8080:8080"
price-service:
build: spring-boot-open-telemetry2/
ports:
- "8081:8081"
collector:
image: jaegertracing/jaeger:2.5.0
ports:
- "4318:4318"
- "16686:16686"
说明:
- 我们使用了
jaegertracing/jaeger:2.5.0的 all-in-one 镜像,它内置了 OpenTelemetry 支持,因此无需单独运行 OpenTelemetry Collector。 - 端口
4318用于接收 OTLP 格式的追踪数据。 - 端口
16686是 Jaeger UI 的访问端口。
架构图说明:
上游(Product)和下游(Price)服务将 span 数据导出到 Jaeger v2 服务。Jaeger 内部包含三个核心组件:Collector(收集器)、Storage(存储)和 Query/UI(查询与界面)。在生产环境中,建议将 Collector、Storage 和 UI 服务分离部署,以实现关注点分离。
启动服务:
$ docker-compose up
5.3. 验证运行中的 Docker 服务
使用以下命令查看容器状态:
$ 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:8081->8081/tcp
6ace8520779a spring-boot-open-telemetry-collector-1 Up 19 seconds 0.0.0.0:4318->4318/tcp, 0.0.0.0:16686->16686/tcp
确认所有服务均已正常运行。
6. 在收集器中监控追踪数据
像 Jaeger 这样的 OpenTelemetry 收集器提供了前端界面,可用于实时或事后查看请求追踪。
我们将分别观察成功请求和失败请求的追踪情况。
6.1. 监控成功请求的追踪
调用上游服务的端点:http://localhost:8080/product/100001
控制台日志示例:
product-service-1 | 2025-04-08T04:21:08.372Z INFO ... [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] ... Getting Product from Product Repo With Product Id 100001
product-service-1 | 2025-04-08T04:21:08.373Z INFO ... [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] ... Fetching Price Details With Product Id 100001
price-service-1 | 2025-04-08T04:21:08.731Z INFO ... [ca9845ffc9130c579d41f2f2ef61874a-60bf6b4856b145f6] ... Getting Price details for Product Id 100001
关键点:
- Micrometer 会自动将 trace ID 和 span ID 注入当前线程上下文,并作为 HTTP Header 传递给下游服务。
- 下游服务(Price)也会自动将相同的 trace ID 关联到其日志和 span 中。
- Jaeger 利用该 trace ID 将跨服务的请求串联起来。
打开 Jaeger UI(http://localhost:16686),可看到完整的请求链路。
图中展示了请求在各服务间的耗时、元数据及调用关系。
6.2. 监控失败请求的追踪
现在测试一个失败场景:调用 /product/100005,该商品在下游不存在。
Jaeger UI 中将显示带有错误标记的 span,并可追溯到抛出异常的具体位置。
通过该视图,我们可以快速定位故障根源(例如:PriceNotFoundException)。
7. 结论
本文介绍了 OpenTelemetry 如何帮助标准化微服务的可观测性模式。
我们演示了如何在 Spring Boot 3 应用中通过 Micrometer Tracing 门面集成 OpenTelemetry,并通过 Jaeger UI 可视化跨服务的请求追踪。
这种方案不仅提升了系统的可观测性,还能显著加快故障排查速度,是现代云原生应用不可或缺的组成部分。