微服务架构下,一次用户请求可能经过网关、认证服务、订单服务、库存服务和支付服务等多个节点。当请求耗时突增或返回错误时,没有链路追踪几乎无法定位瓶颈环节。OpenTelemetry作为CNCF的统一可观测性标准,提供了Trace、Span、Context Propagation的完整规范。本文记录在Spring Boot微服务集群中集成OpenTelemetry的完整实践过程。
OpenTelemetry核心概念与Trace数据模型
OpenTelemetry的数据模型围绕Trace和Span展开。一个Trace代表一次完整的请求链路,由一个全局唯一的TraceID标识。Span是链路中的一个操作单元,每个Span包含操作名称、起始时间、持续时间、属性(Attributes)、事件(Events)和状态(Status)。Span之间通过ParentSpanID构成调用树。
Context Propagation(上下文传播)是分布式追踪的技术基础。请求从服务A调用服务B时,服务A在HTTP Header中注入当前Span的上下文信息(TraceID、SpanID、采样标志),服务B从Header中提取这些信息作为自身Span的Parent。OpenTelemetry默认使用W3C Trace Context标准,Header名称为traceparent。
traceparent的格式:version-traceid-parentid-traceflags,例如00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01。01表示采样标记为true。
Spring Boot集成OpenTelemetry自动埋点
使用OpenTelemetry Java Agent实现无侵入式自动埋点。Agent通过字节码增强技术在类加载时拦截HTTP客户端、数据库驱动、消息队列等组件的方法调用,自动创建和关联Span。
<!-- pom.xml 依赖 -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.40.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Dockerfile中集成OTel Agent:
FROM eclipse-temurin:17-jre
WORKDIR /app
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /app/opentelemetry-javaagent.jar
COPY target/order-service.jar /app/app.jar
ENV JAVA_TOOL_OPTIONS="-javaagent:/app/opentelemetry-javaagent.jar"
ENV OTEL_SERVICE_NAME="order-service"
ENV OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4317"
ENV OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
ENV OTEL_TRACES_EXPORTER="otlp"
ENV OTEL_TRACES_SAMPLER="parentbased_traceidratio"
ENV OTEL_TRACES_SAMPLER_ARG="0.1"
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
OTEL_TRACES_SAMPLER="parentbased_traceidratio"设置采样策略为基于父Span的按比例采样,0.1表示10%的请求被采样。parentbased策略确保同一Trace链路上的所有Span要么全采、要么全不采,避免链路断裂。
手动埋点与自定义Span属性注入
自动埋点覆盖了HTTP、JDBC、Redis等标准组件,但业务逻辑层面的关键操作需要手动埋点。通过OpenTelemetry API创建自定义Span,记录业务语义信息。
package com.yunthe.order.service;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");
public OrderResult createOrder(OrderRequest request) {
Span span = tracer.spanBuilder("order.create")
.setAttribute("order.user_id", request.getUserId())
.setAttribute("order.product_id", request.getProductId())
.setAttribute("order.quantity", request.getQuantity())
.startSpan();
try (Scope scope = span.makeCurrent()) {
span.addEvent("checking_inventory");
InventoryResponse inventory = inventoryClient.checkInventory(
request.getProductId(), request.getQuantity());
if (!inventory.isAvailable()) {
span.setAttribute("order.result", "insufficient_inventory");
span.setStatus(StatusCode.ERROR, "库存不足");
return OrderResult.fail("库存不足");
}
span.addEvent("persisting_order");
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus(OrderStatus.CREATED);
orderRepository.save(order);
span.addEvent("deducting_inventory");
inventoryClient.deduct(request.getProductId(), request.getQuantity());
span.setAttribute("order.order_id", order.getId());
span.setAttribute("order.result", "success");
span.setStatus(StatusCode.OK);
return OrderResult.success(order.getId());
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
关键字段说明:span.setAttribute录入业务维度数据,可在Jaeger/Tempo的查询界面作为过滤条件。span.addEvent记录时间点事件,在链路时间线上显示为标记点。span.recordException将异常堆栈作为Span Event记录,包含exception.type、exception.message和exception.stacktrace三个标准属性。
OTel Collector部署与数据管道配置
OpenTelemetry Collector作为数据中转站,从各服务接收Span数据,经过处理后转发到Jaeger或Grafana Tempo等存储后端。
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1000
memory_limiter:
check_interval: 2s
limit_mib: 512
spike_limit_mib: 128
attributes:
actions:
- key: http.request.header.authorization
action: delete
- key: db.statement
action: hash
- key: user.password
action: delete
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, attributes, resource, batch]
exporters: [otlp/jaeger, otlp/tempo]
batch处理器将小批量Span合并为大批量再发送,降低网络开销。memory_limiter设置内存使用上限,防止Collector OOM。attributes处理器删除Authorization Header和密码等敏感字段。Pipeline定义了数据流向:otlp接收,内存保护,属性过滤,资源标签注入,批量处理,双写Jaeger和Tempo。
分布式事务问题诊断实战
用户反馈下单接口偶发性超时,P99延迟从200ms飙升至5秒。通过Jaeger按TraceID查询完整链路:
order-service: order.create (4.8s) [ERROR]
|- inventory-service: GET /inventory/check (1.2s)
| |- mysql: SELECT inventory (0.8s) -- 全表扫描
|- mysql: INSERT orders (0.1s)
|- inventory-service: POST /inventory/deduct (3.4s) [ERROR]
| |- redis: GET lock:product_123 (0.1s)
| |- mysql: SELECT ... FOR UPDATE (2.8s) -- 行锁等待
| |- mysql: UPDATE inventory (0.1s)
|- redis: DEL lock:product_123 (0.1s)
链路分析暴露两个问题:第一,GET /inventory/check中的库存查询耗时0.8秒,查看Span的db.statement属性发现缺少索引的查询在百万级数据表上做了全表扫描。第二,POST /inventory/deduct在SELECT ... FOR UPDATE上等待了2.8秒的行锁,说明高并发下多个请求同时锁定同一商品行。
解决方案:添加索引,库存扣减改为乐观锁模式:
-- 添加索引
CREATE INDEX idx_inventory_product_code ON inventory(product_code);
-- 乐观锁扣减:CAS机制
UPDATE inventory
SET stock = stock - #{quantity},
version = version + 1
WHERE product_id = #{productId}
AND stock >= #{quantity}
AND version = #{currentVersion};
修改后重新发布,P99延迟回落到180ms。链路追踪的价值在于将为什么慢从猜测变为可见的数据链路——每一步的耗时、SQL语句、调用关系一目了然。
采样策略与存储成本控制
全量采样在生产环境中成本过高。合理设置采样率在可观测性和成本间取得平衡:
public class SmartSampler implements Sampler {
private final Sampler defaultSampler = Sampler.traceIdRatioBased(0.01);
@Override
public SamplingResult shouldSample(Context parentContext, String traceId,
String name, SpanKind spanKind, Attributes attributes,
List<LinkData> parentLinks) {
SamplingResult defaultResult = defaultSampler.shouldSample(
parentContext, traceId, name, spanKind, attributes, parentLinks);
if (defaultResult.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) {
return defaultResult;
}
// 对关键操作强制采样
if (name.startsWith("order.create") || name.startsWith("payment")) {
return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
}
return defaultResult;
}
}
parentbased采样策略保证了链路完整性——入口服务的采样决策会沿调用链传播到所有下游服务。关键业务操作强制采样,保证核心链路的可观测性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-wei-fu-wu-lian-lu-zhui-zong-shi-zhan/