4、OpenTelemetry入门:架构、SDK与API、自动与手动埋点

说到可观测性,OpenTelemetry 现在几乎是事实标准了。我刚开始接触这个领域的时候,市面上还各自为战——Jaeger 用 OpenTracing,Prometheus 用 OpenMetrics,Google 那边推 OpenCensus。后来大家发现这样下去不行,干脆合并成了 OpenTelemetry。嗯,这就像当年手机充电口终于统一成 Type-C 一样,省心多了。

这一章,咱们就聊聊 OpenTelemetry 的核心概念。说白了,它就是一个标准化的数据采集框架。你不需要关心底层是哪个后端存储,只要按照它的规范埋点,数据就能被各种平台消费。

4.1 OpenTelemetry 的整体架构

先看一张图,这是我个人习惯——学任何新技术,先看架构,再看代码。

OpenTelemetry 核心架构 你的应用程序 OpenTelemetry API OpenTelemetry SDK 处理器(Processor) 采样、批处理、属性修改、过滤 导出器(Exporter) OTLP | Jaeger | Prometheus | Zipkin | 自定义 ↓ 数据流方向 后端存储 / 可视化平台 API: 接口定义 SDK: 具体实现

从上图可以看到,整个链路非常清晰:

  • API 层:定义接口规范,比如怎么创建 Span、怎么设置属性。这部分是抽象的,不依赖具体实现。
  • SDK 层:API 的具体实现。包括采样逻辑、上下文传播、数据加工等。
  • 处理器:数据在导出前经过的管道。你可以在这里做过滤、批处理、添加自定义属性。
  • 导出器:把数据发送到后端。OTLP 是官方协议,但也支持 Jaeger、Prometheus 等。
💡 我的经验:刚开始用 OpenTelemetry 时,我总想把所有数据都采集下来。结果存储成本飙升,查询也慢。后来我学会了在处理器层做采样——比如只采样 10% 的请求,或者只采样错误链路。效果立竿见影。

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% 的耗时都在这里——优化后系统吞吐量翻了一倍。

⚠️ 避坑指南:我曾经犯过一个错误——在循环里创建 Span。比如处理 1000 条消息,每条都创建一个 Span。结果导出器压力巨大,内存也爆了。正确的做法是:批量处理时只创建一个 Span,用属性记录批次信息。

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() 包装一下。

💡 小技巧:调试上下文传播问题时,可以先把所有传播的 Header 打印出来。看看 traceparent 头有没有正确传递。这个头长这样: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 应用

说了这么多,咱们动手试试。下面是一个最小化的集成示例:

  1. 引入依赖(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'
  1. 配置 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();
    }
}
  1. 手动埋点一个业务方法
@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)需要联动使用
  • 上下文传播是链路追踪的基石,注意异步场景
  • 采样是控制成本的关键,推荐混合策略

专注资料整理