4、OpenTelemetry入门:架构、SDK与API、自动与手动埋点
说到可观测性,OpenTelemetry 现在几乎是事实标准了。我刚开始接触这个领域的时候,市面上还各自为战——Jaeger 用 OpenTracing,Prometheus 用 OpenMetrics,Google 那边推 OpenCensus。后来大家发现这样下去不行,干脆合并成了 OpenTelemetry。嗯,这就像当年手机充电口终于统一成 Type-C 一样,省心多了。
这一章,咱们就聊聊 OpenTelemetry 的核心概念。说白了,它就是一个标准化的数据采集框架。你不需要关心底层是哪个后端存储,只要按照它的规范埋点,数据就能被各种平台消费。
4.1 OpenTelemetry 的整体架构
先看一张图,这是我个人习惯——学任何新技术,先看架构,再看代码。
从上图可以看到,整个链路非常清晰:
- API 层:定义接口规范,比如怎么创建 Span、怎么设置属性。这部分是抽象的,不依赖具体实现。
- SDK 层:API 的具体实现。包括采样逻辑、上下文传播、数据加工等。
- 处理器:数据在导出前经过的管道。你可以在这里做过滤、批处理、添加自定义属性。
- 导出器:把数据发送到后端。OTLP 是官方协议,但也支持 Jaeger、Prometheus 等。
4.2 API 与 SDK:到底有什么区别?
这个问题我经常被问到。其实很简单:
API 是合同,SDK 是干活的人。
你写代码的时候,只依赖 API。比如:
// 你只依赖这个 API 接口
Span span = tracer.spanBuilder("process-order")
.setAttribute("order.id", orderId)
.startSpan();
// 具体怎么实现,由 SDK 决定
// SDK 可能把数据写到 Jaeger,也可能写到 Zipkin
这样做的好处是什么?你想想看——如果有一天你想换后端,比如从 Jaeger 换成 Datadog,只需要换 SDK 的依赖,代码一行都不用改。我在项目中就经历过这种迁移,当时庆幸自己当初只依赖了 API。
4.3 自动埋点 vs 手动埋点
这是实际开发中最常纠结的问题。我个人的建议是:能自动就别手动,但手动是兜底方案。
4.3.1 自动埋点
自动埋点通过字节码增强或 AOP 实现。你几乎不用改代码,框架会自动拦截 HTTP 请求、数据库调用、消息队列等。
以 Java 为例,引入 OpenTelemetry Agent 就行:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=my-service \
-Dotel.traces.exporter=otlp \
-jar my-app.jar
启动后,所有 HTTP 请求、JDBC 查询、gRPC 调用都会被自动追踪。我记得第一次用的时候,看到控制台自动打印出 Span 信息,心里直呼「真香」。
- HTTP 框架:Spring Web、JAX-RS、Netty
- 数据库:JDBC、Redis、MongoDB
- 消息队列:Kafka、RabbitMQ、JMS
- RPC:gRPC、Dubbo
4.3.2 手动埋点
自动埋点虽然方便,但有些业务逻辑它不知道。比如你有一个复杂的订单处理流程,自动埋点只能看到「HTTP 请求进来」和「数据库查询出去」,中间的业务步骤是黑盒。
这时候就需要手动埋点了:
// 手动创建一个 Span
Span span = tracer.spanBuilder("validate-inventory")
.setAttribute("product.id", productId)
.setAttribute("quantity", quantity)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 你的业务逻辑
boolean available = inventoryService.check(productId, quantity);
span.setAttribute("available", available);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
} finally {
span.end();
}
这段代码看起来有点啰嗦,但它的好处是精确。你可以把任何一段代码变成可观测的。我曾经在一个支付系统中,手动埋点追踪「风控校验」这个步骤,结果发现 90% 的耗时都在这里——优化后系统吞吐量翻了一倍。
4.4 三种信号:Trace、Metric、Log
OpenTelemetry 支持三种信号,我习惯把它们比作:
| 信号 | 类比 | 典型问题 |
|---|---|---|
| Trace(链路) | 一次旅行的完整路线 | 这个请求经过了哪些服务?哪一步最慢? |
| Metric(指标) | 车速表、油量表 | QPS 是多少?错误率有没有飙升? |
| Log(日志) | 旅行日记 | 具体报了什么错?参数是什么? |
在实际项目中,这三种信号需要联动。比如:
- Metric 告诉你「错误率突然升高了」
- Trace 告诉你「出错的请求都经过了服务 B」
- Log 告诉你「服务 B 连接数据库超时了」
嗯,这才是完整的可观测性。缺一个都不行。
4.5 上下文传播:让链路串起来
这是 OpenTelemetry 最核心的机制之一。为什么一个请求经过多个服务,还能被识别为同一条链路?靠的就是上下文传播。
说白了,就是每个请求都带一个「身份证」——Trace ID 和 Span ID。服务 A 调用服务 B 时,会把这两个 ID 通过 HTTP 头或消息队列的属性传过去。
// HTTP 传播示例
// 服务 A 发送请求时注入上下文
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
OpenTelemetry.getPropagators()
.getTextMapPropagator()
.inject(Context.current(), connection, (carrier, key, value) -> {
carrier.setRequestProperty(key, value);
});
// 服务 B 接收请求时提取上下文
TextMapPropagator propagator = OpenTelemetry.getPropagators().getTextMapPropagator();
Context context = propagator.extract(
Context.current(),
httpServletRequest,
(carrier, key) -> carrier.getHeader(key)
);
这里要注意:如果你用了异步处理或者消息队列,上下文传播很容易断掉。我踩过这个坑——用线程池处理任务时,子线程拿不到父线程的上下文。解决方案是用 Context.taskWrapping() 包装一下。
00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01。
4.6 采样:省钱又省心的艺术
全量采集听起来很美好,但现实是——流量大的时候,存储根本扛不住。我见过一个团队,每天产生 10TB 的 Trace 数据,查询一次要等 5 分钟。后来他们做了采样,数据量降到 500GB,查询秒级返回。
常见的采样策略:
- 头部采样:在请求入口处决定是否采样。简单,但可能漏掉慢请求。
- 尾部采样:等请求结束后再决定。可以精准采样慢请求和错误请求。
- 概率采样:随机采样 10% 或 1%。适合流量均匀的场景。
我个人推荐混合策略:正常请求用 1% 概率采样,错误请求和慢请求(比如超过 1 秒)全量采样。这样既控制了成本,又不会漏掉关键数据。
// 自定义采样器示例
public class CustomSampler implements Sampler {
@Override
public SamplingResult shouldSample(SamplerParameters params) {
// 如果是错误请求,全量采样
if (params.getAttributes().get("http.status_code") >= 500) {
return SamplingResult.recordAndSample();
}
// 否则 10% 概率采样
return Math.random() < 0.1
? SamplingResult.recordAndSample()
: SamplingResult.drop();
}
}
4.7 实战:从零搭建一个可观测的 Spring Boot 应用
说了这么多,咱们动手试试。下面是一个最小化的集成示例:
- 引入依赖(Gradle):
implementation 'io.opentelemetry:opentelemetry-api:1.31.0'
implementation 'io.opentelemetry:opentelemetry-sdk:1.31.0'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.31.0'
- 配置 SDK:
@Configuration
public class OpenTelemetryConfig {
@Bean
public OpenTelemetry openTelemetry() {
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:4317")
.build()
).build())
.build();
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
}
}
- 手动埋点一个业务方法:
@Service
public class OrderService {
@Autowired
private OpenTelemetry openTelemetry;
public void processOrder(String orderId) {
Tracer tracer = openTelemetry.getTracer("order-service");
Span span = tracer.spanBuilder("process-order")
.setAttribute("order.id", orderId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
validateOrder(orderId);
chargePayment(orderId);
sendNotification(orderId);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}
启动应用后,打开 Jaeger 或 Grafana,你就能看到完整的调用链路了。第一次看到自己埋的点出现在可视化界面上,那种成就感还是很爽的。
- OpenTelemetry 架构分四层:API → SDK → Processor → Exporter
- API 是接口,SDK 是实现,两者分离便于切换后端
- 自动埋点覆盖通用框架,手动埋点覆盖业务逻辑
- 三种信号(Trace、Metric、Log)需要联动使用
- 上下文传播是链路追踪的基石,注意异步场景
- 采样是控制成本的关键,推荐混合策略