OpenTelemetry 实战——统一可观测性,不再被各家 vendor 绑架
OpenTelemetry 实战——统一可观测性,不再被各家 vendor 绑架
适读人群:关注可观测性、不想被 vendor lock-in 的工程师 | 阅读时长:约20分钟 | 核心价值:用 OpenTelemetry 建立厂商中立的可观测性基础设施,随时切换后端不改代码
我们团队经历过一次很痛苦的可观测性迁移。
当时用的是一家商业 APM 的 Java Agent,接入很方便,一行都不用改代码,jar 里加一个 -javaagent 就行了。用了两年,突然那家公司调整了定价策略,按照新的价格,我们的账单要涨三倍。
三倍。
老板说,能不能换一家?
然后我们才发现,那家 APM 的数据格式是私有的,所有数据都锁在他们的平台里,历史数据无法迁移。即使换了新平台,过去两年积累的追踪数据、告警规则、Dashboard 配置全部都要重建。
更麻烦的是,新的 APM agent 和老的冲突,有几个服务的 JVM 启动直接崩溃,折腾了好几周。
那次之后,我下定决心研究 OpenTelemetry。
OpenTelemetry 是什么
OpenTelemetry(简称 OTel)是 CNCF 的一个开源项目,目标是提供一套厂商中立的可观测性标准和实现。
核心思想:数据收集层(instrumentation)和数据存储层(backend)分离。
你用 OpenTelemetry 收集日志、指标、追踪数据,然后通过 OTLP(OpenTelemetry Protocol)协议发送到任何支持 OTLP 的后端:Jaeger、Zipkin、Prometheus、Grafana Tempo、Datadog、AWS X-Ray……随时可以换后端,不需要改代码。
你的 Java 应用
↓ OpenTelemetry SDK(代码里)
↓
OpenTelemetry Collector(中间层,可选)
↓
任意后端(Jaeger / Prometheus / Grafana / Datadog...)Java 应用接入 OTel:零代码侵入方式
OTel Java Agent
最简单的接入方式是使用 OpenTelemetry Java Agent,这是一个 javaagent,在 JVM 启动时通过字节码增强自动为常用框架添加追踪:
下载最新的 agent jar:
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar启动 Java 应用时加上:
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=payment-service \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar payment-service.jar这样,Spring MVC、JDBC、Kafka、Redis、gRPC 等几十种框架的 Span 都会自动生成,不需要改任何业务代码。
Docker Compose 部署示例
# docker-compose.yml
version: '3.8'
services:
app:
image: myapp:latest
environment:
JAVA_TOOL_OPTIONS: >-
-javaagent:/otel/opentelemetry-javaagent.jar
-Dotel.service.name=payment-service
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317
-Dotel.resource.attributes=env=production,team=payments
volumes:
- ./opentelemetry-javaagent.jar:/otel/opentelemetry-javaagent.jar
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-collector-config.yml:/etc/otel/config.yml
command: ["--config=/etc/otel/config.yml"]
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"OTel Collector 配置
OTel Collector 是可选但强烈推荐的中间层。它的作用是:接收来自应用的遥测数据,进行处理(过滤、采样、转换格式),然后发送到一个或多个后端。
# otel-collector-config.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# 批量处理,减少网络开销
batch:
timeout: 1s
send_batch_size: 1024
# 内存限制,防止 Collector OOM
memory_limiter:
limit_mib: 512
spike_limit_mib: 128
check_interval: 5s
# 采样:只保留 10% 的 Trace(或者所有包含 error 的 Trace)
tail_sampling:
decision_wait: 10s
policies:
- name: error-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 10}
# 添加公共属性
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
exporters:
# 追踪 → Jaeger
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# 指标 → Prometheus(通过 remote_write)
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
# 日志 → Loki
loki:
endpoint: http://loki:3100/loki/api/v1/push
# 同时发送到商业 APM(Datadog),用于特定团队
datadog:
api:
key: ${DD_API_KEY}
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, tail_sampling]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch, resource]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]OTel Collector 的强大之处在于 pipeline 配置——你可以把数据同时发给多个后端,也可以对数据做采样、过滤处理,而这些都对应用代码完全透明。
代码层面的 OTel 接入
有时候自动 instrumentation 不够,需要手动为业务逻辑创建 Span。
Maven 依赖
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.33.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>2.1.0</version>
</dependency>用注解方式(最简单)
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
@Service
public class PaymentService {
@WithSpan("payment.process") // 自动创建名为 payment.process 的 Span
public PaymentResult processPayment(
@SpanAttribute("payment.method") String paymentMethod, // 自动作为 Span attribute
@SpanAttribute("payment.amount") BigDecimal amount,
PaymentRequest request) {
// 业务逻辑
return doProcess(request);
}
}用 API 方式(更灵活)
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
@Service
public class RiskService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("risk-service", "1.0.0");
public RiskResult checkRisk(RiskRequest request) {
Span span = tracer.spanBuilder("risk.check")
.setAttribute("user.id", request.getUserId())
.setAttribute("order.amount", request.getAmount().toString())
.startSpan();
try (Scope scope = span.makeCurrent()) {
RiskResult result = externalRiskApi.evaluate(request);
span.setAttribute("risk.score", result.getScore());
span.setAttribute("risk.decision", result.getDecision().name());
return result;
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}踩坑实录
踩坑一:OTel Agent 和 Spring Boot Actuator 的 Micrometer 冲突
我们用 Spring Boot Actuator + Micrometer 暴露 Prometheus 指标,同时用 OTel Java Agent 收集追踪。
加上 OTel Agent 之后,发现 JVM 指标被上报了两遍——一遍来自 Micrometer,一遍来自 OTel Agent 的 JVM metrics instrumentation。
解决方法:在 OTel Agent 里禁用 JVM metrics(让 Micrometer 继续做这件事):
-Dotel.instrumentation.runtime-metrics.enabled=false或者反过来,完全用 OTel Agent 的指标,禁用 Micrometer 的:
management:
metrics:
export:
prometheus:
enabled: false踩坑二:Context Propagation 在异步调用里丢失
分布式追踪依赖 Context Propagation——TraceId 从 A 服务传递到 B 服务,B 服务的 Span 才能关联到同一个 Trace。
在 HTTP 调用里,OTel Agent 会自动在请求头里注入 traceparent 等传播头,B 服务也会自动提取。但在异步场景里会出问题:
// 问题代码:CompletableFuture 里的 Span 可能丢失 context
public void processAsync(Order order) {
CompletableFuture.runAsync(() -> {
// 这里的线程是线程池里的新线程
// OTel context 默认不会自动传播到新线程
processOrderInBackground(order);
});
}解决方法:使用 OTel 提供的 Context-aware 的 ExecutorService:
import io.opentelemetry.context.Context;
// 用 OTel 包装的 ExecutorService
ExecutorService executor = Context.taskWrapping(
Executors.newFixedThreadPool(10)
);
// 或者手动传播
Context currentContext = Context.current();
CompletableFuture.runAsync(() -> {
try (Scope scope = currentContext.makeCurrent()) {
processOrderInBackground(order);
}
});踩坑三:OTel Collector 成了单点故障
我们把 OTel Collector 部署成单实例,某天 Collector 的机器因为磁盘满挂了,所有应用的遥测数据直接丢失,也没有任何报警。
生产环境 OTel Collector 要做高可用:
# Kubernetes 部署,用 DaemonSet 每台节点部署一个
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector
spec:
selector:
matchLabels:
app: otel-collector
template:
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"每台节点上的应用只连接本节点的 Collector(localhost:4317),单台 Collector 挂了不影响其他节点的数据收集。
深度解析:OpenTelemetry Collector 的实用价值
很多人第一次看到 OTel Collector 这个组件,会觉得它是多余的中间层——应用直接发数据给后端(Jaeger、Prometheus)不是更简单吗?
但 OTel Collector 解决了几个非常实际的问题,理解这些问题之后,你就会觉得它是不可或缺的。
问题一:多后端扇出
假设你同时用 Jaeger 看追踪,用 Prometheus 看指标,用 Grafana Loki 看日志。没有 Collector,应用需要同时配置三个 exporter,代码里有三份后端配置,每次换后端都要改代码和重新部署应用。
有了 Collector,应用只需要连接一个地址(Collector),由 Collector 负责把数据分发到多个后端。应用代码与后端完全解耦。
问题二:数据预处理
从应用直接发给 Jaeger 的 Span,粒度很细,数据量很大。很多 Span 对于分析没有价值(比如定时任务的内部步骤)。
Collector 可以对数据做过滤、采样、属性添加,减少存储成本,同时保证有价值的数据被完整记录。这些处理逻辑集中在 Collector 里,不需要在每个应用里单独实现。
问题三:缓冲和背压
如果后端(Jaeger、ES)短暂不可用,直接连后端的 exporter 会开始丢数据(或者阻塞应用线程)。
Collector 有内置的缓冲和重试机制,后端短暂故障时,Collector 会把数据暂存在内存(或磁盘)里,等后端恢复后再发送,减少数据丢失。
深度解析:OTel 的 Semantic Conventions(语义约定)
OpenTelemetry 定义了一套 Semantic Conventions——各种操作应该用什么属性名、什么属性值来描述。
这套约定的价值在于:不同系统产生的遥测数据有统一的语义,可以互相关联和对比。
举个例子,HTTP 请求的 Span 应该有哪些属性:
http.method: GET/POST/PUT/DELETE
http.url: 请求的完整 URL
http.status_code: HTTP 响应码
http.flavor: HTTP 版本(1.1/2.0)
http.user_agent: 客户端 User-Agent
net.peer.name: 服务端主机名
net.peer.port: 服务端端口数据库操作的 Span:
db.system: postgresql/mysql/redis/mongodb
db.name: 数据库名
db.statement: 执行的 SQL(注意脱敏)
db.operation: SELECT/INSERT/UPDATE/DELETE遵循 Semantic Conventions 的好处:Grafana、Jaeger 等工具可以"理解"你的 Span,提供更好的可视化效果。不同语言、不同框架产生的遥测数据有一致的字段,可以在同一个查询里混合使用。
OTel 的 Java Agent 已经对所有主流框架的自动 instrumentation 遵循了 Semantic Conventions,手动创建 Span 时也建议参考这套约定。
OTel vs 商业 APM:我的选型建议
| 维度 | OpenTelemetry | 商业 APM |
|---|---|---|
| 成本 | 基础设施成本,无 license 费 | 按数据量或 host 收费,价格高 |
| Vendor Lock-in | 无,随时切换后端 | 高,数据格式私有 |
| 接入复杂度 | 中(需要部署 Collector、后端) | 低(一个 agent) |
| 自动 instrumentation 覆盖 | 覆盖广,持续增加 | 通常更全面 |
| 企业支持 | 社区支持 | 有商业支持 |
我的建议:新项目首选 OTel。老项目如果商业 APM 还在合同期,可以先并行接入 OTel,摸清楚之后再迁移。
绝对不要在没有退出机制的情况下深度绑定任何一家商业 APM。
深度解析:OpenTelemetry 的标准化价值
在 OpenTelemetry 出现之前,可观测性领域是一片碎片化的景象。每家 APM 厂商有自己的 SDK,Datadog 有 dd-trace,New Relic 有 newrelic-agent,Zipkin 有 brave,Jaeger 有 jaeger-client。团队用了哪家的工具,代码里就要引入哪家的依赖,业务代码和 APM 厂商深度耦合。
更痛苦的是:这些 SDK 的 API 互不兼容。如果某天要从 Zipkin 换到 Jaeger,除了修改配置,还要替换代码里的 API 调用,这是个不小的迁移工程。
OpenTelemetry 解决的核心问题是"API 层的标准化"。OTel API 是厂商无关的,你在代码里调用 tracer.startSpan(),无论底层用的是 Jaeger 还是 Zipkin 还是 Datadog,这行代码不需要改。切换后端只需要换一行配置。
这个设计和 JDBC 的思路是一样的。Java 的 JDBC API 是标准化的,你写 Connection connection = DriverManager.getConnection(url),不管底层是 MySQL 还是 PostgreSQL 还是 Oracle,这行代码不变,变的只是驱动和连接 URL。OTel 做的是可观测性领域的"JDBC 化"。
OTLP 协议的意义
OpenTelemetry Protocol(OTLP)是 OTel 定义的数据传输协议,基于 gRPC 或 HTTP,用于从应用把遥测数据发送到收集器。OTLP 的标准化让各种后端系统都可以直接支持接收 OTel 数据,不需要中间的格式转换。
这个协议的出现,实质上是可观测性生态进入了"成熟期"的标志——就像 HTTP 统一了 Web 传输,OTLP 正在统一可观测性数据传输。如果你现在从头建立可观测性体系,直接用 OTel SDK + OTLP 是最有生命力的选择。
Collector 的价值
很多人初次接触 OpenTelemetry 会跳过 OTel Collector 这个组件,直接从应用发数据到后端。这在小规模场景没问题,但在生产环境里,Collector 是值得部署的。
Collector 的价值在于它是一个可编程的数据管道。你可以在 Collector 里做:数据过滤(丢弃不需要的 span)、属性增减(统一添加 hostname、pod_name 等属性)、数据采样(减少数据量)、多路输出(同时发到 Jaeger 和 Datadog)。这些逻辑集中在 Collector 里,应用代码不需要知道这些细节,也不需要在每个服务里重复配置。
另一个实用价值:如果某个后端挂了,Collector 可以缓冲数据或切换到备用后端,应用层感知不到。这是"关注点分离"的体现,让应用只负责产生遥测数据,基础设施层负责数据的路由和处理。
深度解析:从埋点到告警的完整链路
很多团队接入了 OTel 之后,发现能看到 Trace 了,但还是不知道"系统什么时候出了问题"。这是因为 Trace 是被动的(需要你主动去查),而告警是主动的(问题发生时推送给你)。完整的可观测性体系需要两者结合。
从 OTel 追踪数据到告警,有两条路径:
第一条路径:追踪数据 → 指标 → 告警。OTel 支持从 Span 中生成指标(SpanMetrics),比如自动统计每个服务、每个操作的请求数和延迟分布。这些指标可以发到 Prometheus,然后基于 Prometheus 告警规则来告警。例如:"支付服务的 P99 延迟超过 2 秒,持续 5 分钟"这样的告警,就是从追踪数据自动衍生出来的指标来触发的。
第二条路径:日志 → 告警。结构化日志里的 ERROR 事件,可以在日志系统(Elasticsearch 或 Loki)里设置告警规则,当 ERROR 数量突然增多时触发通知。这条路径对于那些"不产生指标,但会产生错误日志"的异常特别有用。
两条路径互补:指标告警擅长检测"系统整体健康度"的变化(成功率下降、延迟升高),日志告警擅长检测"特定错误事件"的出现。同时运行,才能覆盖更广泛的异常场景。
建立这个完整链路之后,可观测性体系就从"帮你排查问题"升级到了"主动发现问题"。这才是可观测性的最高价值:让你在用户发现问题之前,系统就已经告诉了你。
深度解析:OTel 在团队中的落地策略
OpenTelemetry 在技术上的优越性不难理解,但在团队中落地才是真正的挑战。以下是一些让落地更顺畅的实践经验。
从一个服务开始,而不是全量迁移
尝试把整个微服务体系一次性迁移到 OTel,是一个常见的失败模式。这需要同时协调多个团队、多个服务,任何一个环节出问题都会影响整体进度,而且全量迁移期间的数据不完整会让追踪系统暂时失去价值(部分服务没接入,Trace 断掉了)。
更好的策略是:选一个最重要、问题最多的服务,完整接入 OTel,让整个团队感受到可观测性改善带来的效率提升。这个"灯塔"服务会成为最好的说服工具,让其他团队主动要求接入,而不是被要求接入。
Java 自动插桩的价值
对于 Java 服务,OTel 的 Java Agent(opentelemetry-javaagent.jar)是最省力的接入方式。只需要在 JVM 启动参数里加一个 -javaagent,不改一行业务代码,就能自动追踪所有的 HTTP 请求、数据库查询、Redis 操作、消息队列消费。
自动插桩的覆盖面已经非常广:Spring MVC、Spring Boot、MyBatis、JDBC、Redis(Lettuce/Jedis)、RabbitMQ、Kafka、gRPC……绝大多数 Java 服务的主要框架都在支持列表里。对于追求"快速接入"的团队,Java Agent 可以让一个服务在 10 分钟内有完整的追踪数据。
手动埋点(用 OTel SDK 自己创建 Span)适合以下场景:需要追踪业务层面的操作,以及需要在 Span 里附加自定义的业务 attribute(比如 order_id、user_id)。两者结合——自动插桩覆盖技术层面,手动埋点覆盖业务层面——是最完整的方案。
从更长的视角来看,OTel 的最大价值不只是"现在能追踪请求",而是"建立了一套可演进的可观测性基础设施"。今天你用 Jaeger 作为追踪后端,明年可以换成 Tempo,后年可以接入商业 APM,代码层面几乎不需要改动。这种灵活性在快速变化的技术环境里,是真正的资产。选择开放标准而不是封闭的专有方案,是技术决策中"长期主义"的体现,值得在团队中推广这种意识。标准化带来的是可替换性,可替换性带来的是议价能力,这在和商业 APM 厂商谈合同续约时会是看得见的筹码。
总结
OpenTelemetry 正在成为可观测性领域的事实标准。主流的商业 APM(Datadog、New Relic、Dynatrace)都已经支持接收 OTLP 数据,这说明这个标准已经被行业接受。
用 OTel 建立可观测性体系,你拥有的是自己的数据,后端可以随时替换。这是数据主权,也是商业谈判的筹码。
