OpenTelemetry:三个信号的统一语言
前面八篇,我们把可观测性的”能力”讲完了:四类信号让你看见一切,SLO 让你判断好坏。但有一个一直被绕过的、极其务实的问题:这些信号,到底是怎么从你的代码里被采集出来、送到那些后端去的?
这个问题听起来像管道工的活,不性感。但它藏着一个能让整个可观测性投资打水漂的陷阱——厂商锁定。你为追踪、为指标、为日志辛辛苦苦插满了一身的仪器化代码,结果发现这些代码全都绑死在某一家后端的 SDK 上;有一天你想换个更便宜或更好用的后端,才发现要把所有插桩推倒重写一遍。
OpenTelemetry(OTel)就是来拆这个陷阱的。它不增加任何新的观测能力,它做的是一件更基础、也更解放人的事:把”仪器化”和”后端”彻底解耦,让一次插桩成为永久可迁移的资产。 这是”标准化”子带的核心一篇。
一、不是看什么,是怎么采上来
信号都有了,数据怎么进来还没说
回看前八篇,我们一直在讨论”看什么”——指标看多少、追踪看因果、SLO 看好坏。但所有这些的前提,是数据已经在后端里了。数据是怎么进去的? 谁在你的代码里产生了那条 span、那个指标、那条日志?又是谁把它们送到了 Jaeger、Prometheus、Loki?这条从”代码”到”后端”的链路,前八篇默认它存在,这一篇专门拆开看。
仪器化锁定的痛
设想一个真实的处境。三年前,你的团队选了某商业可观测性平台(叫它 V 厂)。你们照着 V 厂的文档,在每个服务里引入了 V 厂的 SDK,手动加了几千处插桩——vendor.tracer.startSpan()、vendor.metrics.count(),撒满了整个代码库。
今天,V 厂涨价了,或者你们发现了一个更适合的方案。你想换。然后你绝望地发现:那几千处插桩,每一处都是 V 厂的 API。 换后端,等于把这几千处全部重写、重测、重新发布——成本高到让”换”在事实上变得不可能。你不是选了一个后端,你是被一个后端绑架了。 这就是仪器化锁定:你的代码和某家厂商,被插桩这件事焊死了。
OTel 的野心
OTel 的野心,是从根上消灭这种绑架。它的核心主张只有一句:
你的代码,不该知道数据最终去哪个后端。
代码只负责”产生遥测数据”这件纯粹的事,至于这些数据被送去 Jaeger 还是 Datadog 还是同时送给三家——那是部署配置的事,不是代码的事。把这两件事分开,仪器化就从”对某家厂商的承诺”,变成了”一笔中立的、可随时改变流向的资产”。怎么做到的?看它的架构。
二、解耦的机制:四层架构与 OTLP
OTel 实现解耦,靠的是一套清晰的分层。理解这四层各自的职责,就理解了”一次插桩、后端自由”是怎么成立的。
API:代码唯一依赖的中立接口
最上面一层是 API——一组厂商中立的接口。你的业务代码,只 import 这一层:
from opentelemetry import trace # 只依赖中立的 OTel API
tracer = trace.get_tracer("checkout")
with tracer.start_as_current_span("process_payment") as span:
span.set_attribute("user.tier", "premium")
# ... 业务逻辑注意这里没有任何厂商的名字——不是 jaeger.startSpan,也不是 datadog.trace,就是中立的 opentelemetry。你的几千处插桩,从此只认这一个中立接口。 这是解耦的第一块基石:代码依赖的是标准,不是实现。
SDK:可插拔的实现
API 只是接口,得有人实现它。这就是 SDK——API 背后的实际干活的那层:它真正地创建 span、采样、批处理、然后交给下一层导出。关键在于,SDK 是可配置、可替换的:用哪种采样策略、往哪导出,都在 SDK 的初始化配置里决定,而这部分不在你的业务代码里。业务代码调 API,SDK 在背后按配置执行——接口和实现的经典分离。
OTLP:三类信号的统一传输协议
数据要离开应用、流向后端,需要一个传输协议。OTel 定义了 OTLP(OpenTelemetry Protocol)——一个统一的协议,指标、日志、追踪全都用它传输。
这个”统一”是有分量的。在 OTel 之前,每类信号、每家厂商都有自己的协议:Prometheus 有自己的格式,Jaeger 有 Thrift,Zipkin 有自己的 JSON……OTLP 把它们收拢成一个。一种协议,承载三类信号——这是后面 Collector 能统一处理一切的前提。
Exporter:往各家后端吐
最后一层是 Exporter——把 OTLP 数据翻译成某个具体后端能接受的格式,发过去。要发 Jaeger 就用 Jaeger exporter,要发 Prometheus 就用 Prometheus exporter,要同时发三家就配三个 exporter。
全部的”后端相关性”,被压缩进了这一层。 你的代码(API)、你的处理逻辑(SDK),统统不知道后端是谁;只有最末端这薄薄的 Exporter 一层知道。于是换后端 = 换 Exporter 配置,业务代码一行不动。这就是前面那句”代码不该知道数据去哪”的技术兑现。
OTLP vs Prometheus scrape
把 OTLP 和 03 里的 Prometheus scrape 对比,能看清两种数据流哲学:
| Prometheus scrape | OTLP | |
|---|---|---|
| 方向 | pull——后端主动来拉 /metrics | push——应用/Collector 主动推 |
| 覆盖 | 只管指标 | 指标 + 日志 + 追踪统一 |
| 目标发现 | 靠服务发现找目标 | 应用主动上报,无需被发现 |
这不是谁取代谁——Prometheus 的 pull 模型在指标领域依然极其好用(03 讲过它配合服务发现的优势),OTel 也提供了和 Prometheus 兼容的桥。但对追踪、日志这类事件驱动、源头才知道全部上下文的信号,push 式的 OTLP 更自然。两者并存,是当下的常态。
战略意义:一次插桩,后端永久自由
把四层连起来看,OTel 的战略价值就清楚了。它把 仪器化代价这根标尺,做了一件前所未有的事——它没有降低插桩的一次性成本,但它消灭了这笔成本的”重复支付”。
过去,仪器化的成本是”每换一个后端,付一次”。OTel 之后,仪器化是”付一次,永久有效”。你的几千处中立插桩,是一笔一劳永逸的资产:后端可以随业务、随预算、随技术演进自由更换,那笔最贵的插桩投入再也不用重做。这就是把”仪器化”和”后端锁定”两个问题分离开来的全部意义。
三、Collector:可观测性的数据管道
四层架构解决了”代码侧”的解耦。但 OTel 还有一个独立的、同样重要的组件——Collector,它解决的是”数据在路上”的所有问题。
把”数据怎么处理、发去哪”从应用里拿出来
即便应用用 OTel API 产生了中立的数据,仍有一堆问题:要不要批量发以省网络?要不要把日志里的敏感字段脱敏?要不要做尾部采样?要不要同时发给生产后端和一个分析后端?
如果这些都写在应用里,应用又被这些”治理逻辑”污染了。OTel 的答案是:起一个独立的中间进程——Collector——把这一切从应用里拿出来。 应用只管用 OTLP 把原始数据吐给 Collector,剩下的处理和路由,全归 Collector。应用重新变回”只产生数据”的纯粹角色。
receive → process → export
Collector 内部是一条清晰的三段流水线,用配置描述:
receivers: # 1. 收:从哪接收数据
otlp:
protocols: { grpc: {}, http: {} }
processors: # 2. 处理:在路上做什么
batch: {} # 批量打包,省网络
attributes: # 脱敏:抹掉敏感字段
actions:
- key: user.email
action: delete
tail_sampling: # 尾部采样(见下)
policies:
- { name: errors, type: status_code, status_code: { status_codes: [ERROR] } }
exporters: # 3. 发:送去哪些后端(可多个)
otlp/tempo: { endpoint: tempo:4317 }
prometheus: { endpoint: "0.0.0.0:8889" }
service:
pipelines:
traces: # 把上面的组件串成一条管道
receivers: [otlp]
processors: [batch, attributes, tail_sampling]
exporters: [otlp/tempo]收、处理、发——一条数据从进到出,中间能被任意加工、复制、分流。
它能干的活:包括那个”有状态采样层”
Collector 这条管道能干的活,正是可观测性治理的全部动作:批处理、脱敏、过滤、降采样、格式转换、扇出到多个后端。
特别值得点出的是尾部采样(tail_sampling)。还记得 06 讲尾部采样时说,它需要”一个专门的、有状态的采样层,先缓存所有 span、等 trace 完成再决定留不留”——那个神秘的有状态采样层,真身就是 Collector。 上面配置里的 tail_sampling processor,就在做这件事:缓存 trace、判断它是否出错/超时、只留下有价值的。06 埋的伏笔,在这里落了地。
Collector 作为治理点
把 Collector 的角色拔高一层看:它是整个可观测性数据流的单一治理点。
成本要控制?在 Collector 上降采样、过滤掉没用的指标。合规要满足?在 Collector 上统一脱敏。要从一家后端迁到另一家?改 Collector 的 exporter,应用无感。所有关于”数据怎么流、留多少、去哪里”的策略,都收敛到这一个地方——而不是散落在几百个应用里。这让可观测性的成本治理有了一个可操作的抓手——这正是 MOC 第五节「运维的人与经济」里”遥测成本”那条线的工程落点。
四、统一语言:semantic conventions 与信号关联
到这里,OTel 已经统一了”管道”。但它还有一层更深的统一,常被忽略,却是它最有价值的部分——统一”语言”。
不只统一管道,还统一词汇
设想两个团队都记录 HTTP 请求。A 团队的字段叫 http.method,B 团队叫 httpMethod,C 团队叫 verb。数据都进了同一个后端,但你没法用一条统一的查询同时分析它们——同一个东西,三个名字。
OTel 的 semantic conventions(语义约定) 来终结这件事:它规定了一套标准字段名——HTTP 方法就叫 http.request.method,状态码就叫 http.response.status_code,数据库语句就叫 db.statement……所有遵循 OTel 的系统,对同一个概念用同一个名字。
为什么词汇统一这么重要
词汇统一的价值,是让遥测数据可互操作。因为大家都叫 http.response.status_code,所以:一条”按状态码统计错误率”的查询,能跨所有语言、所有团队、所有服务通用;一套 SLO 告警规则,能套用到任何一个遵循约定的服务上;一个后端的可视化,对任何来源的数据都认得。没有统一词汇,统一管道只是把一堆语言不通的数据汇到了一起;有了统一词汇,它们才真正能被当成一个整体来分析。
共享上下文:四类信号终于能彼此关联
更深一层:因为四类信号都经由 OTel、都携带同一套 trace context(06 那个 traceparent),它们之间第一次有了可靠的关联键。
回想 05 讲的"三大支柱的痛"——指标、日志、追踪是三个割裂的筒仓,排障时要在三个系统间手工拼线索。割裂的根因,正是它们没有共享的上下文。OTel 让四类信号都带上同一个 trace_id,于是:一条日志能关联到它所在的 span,一个 span 能关联到它影响的指标。筒仓之间,被一根共享的上下文打通了。
Exemplar:从指标一跳跳到追踪
这种关联,最漂亮的体现叫 Exemplar(范例)。它让一个聚合的指标数据点,携带一个指向具体追踪的 trace_id:
你在 Grafana 看 P99 延迟曲线,看到一个尖刺。在 OTel 之前,你只能知道”P99 升高了”,然后切去 Jaeger 大海捞针找一条慢 trace。有了 Exemplar,那个尖刺数据点上挂着一个小点,点一下——直接跳到造成这个尖刺的那一次具体的慢请求的完整追踪。
这是 "宏观异常发现"和 "微观原因定位"之间,第一次有了一键直达的桥。 指标告诉你”哪里不对”,一点,追踪告诉你”为什么不对”。前面几篇辛苦建立的、各自割裂的信号,被 OTel 缝成了一张能互相跳转的网。
★ 故障剧场:只改配置,不动代码
把 OTel 的全部价值,浓缩进一个最能说明问题的演示——不是一次故障,而是一次迁移。
你的系统跑了一年,几千处 OTel 插桩,追踪发往自建的 Jaeger。现在公司决定:追踪迁到成本更低的 Grafana Tempo,同时复制一份给商业平台做高级分析。
在 OTel 之前,这是一个要改几千处代码、跨多个团队、持续数月的大工程。
用 OTel,你做的全部事情是——打开 Collector 配置,把 exporter 从一个改成三个:
exporters:
otlp/tempo: { endpoint: tempo:4317 } # 新增
otlp/vendor: { endpoint: vendor-api:4317 } # 新增
# jaeger: 原来的,删掉或保留做灰度
service:
pipelines:
traces:
exporters: [otlp/tempo, otlp/vendor] # 一行,扇出到两家提交,重启 Collector。完成。几千处应用插桩,一行没动;几千次重新发布,一次没有。 后端从 Jaeger 变成了 Tempo + 商业平台,而产生数据的代码完全无感。这就是”一次插桩、永久可迁移”从口号变成现实的样子——也是前面那个战略意义最直白的证明。
五、实践与边界
落地形态:手动 SDK + 自动插桩
OTel 怎么进入你的系统,有两条路,对应 06 讲的传播负担的两个层次:
- 手动(manual instrumentation):用 SDK 在代码里显式打点,像前面那样。精确,能标注业务语义(“这个 span 是结账”),但要人写。
- 自动(auto-instrumentation):用一个 agent(Java 的 javaagent、Python 的 monkey-patching)在启动时挂上,自动拦截框架层的调用——HTTP 请求、数据库查询、gRPC——替你生成 span 并传播 context。常见框架的覆盖,几乎零代码。
实践中是两者结合:自动插桩铺好框架层的底,手动插桩补上业务语义的关键点。
信号成熟度:诚实标注
OTel 的三/四类信号,成熟度并不齐:
- Traces:最成熟、最稳定,OTel 起家于此,已是追踪仪器化的事实标准。
- Metrics:稳定,和 Prometheus 有成熟的兼容层。
- Logs:较新,设计上更倾向于”把已有日志桥接进 OTel”而非另起炉灶。
- Profiling:最年轻,持续分析作为第四类信号刚被正式纳入 OTel——这印证了 07 说的”四类信号正在被同一套标准收编”,但它还在早期。
用 OTel,traces/metrics 可以放心上生产;logs/profiling 要看你的具体栈成熟到什么程度。
边界一:OTel 是标准,不是后端
最重要的一个澄清:OTel 不存储、不查询、不画图。 它管的是”产生、处理、传输”——数据到达后端之前的一切。到了后端,存储和查询是 Prometheus、Jaeger、Tempo、Loki、Grafana 的活。
所以”我们上了 OTel”不等于”我们有了可观测性”。OTel 是把数据中立地、高效地、可迁移地送到后端的管道和语言;可观测性的”看见”,仍然由后端实现。把 OTel 当成后端,是常见的误解。
边界二:自动插桩也只到框架层
06 说过追踪的木桶效应,这里要诚实地接上:auto-instrumentation 覆盖的是框架层——它认得 HTTP 库、数据库驱动、消息队列客户端,但它认不得你的自定义业务逻辑。一段纯内存的复杂计算、一个自研的协议,自动插桩看不见,仍要你手写。
OTel 把仪器化的成本降到了很低,但”很低”不是”零”——总有一部分业务语义,得人去标注。那么,能不能连框架层的插桩都不要、让基础设施替你产出遥测? 这就把我们带到了最后一篇。
六、小结:坐标与去向
OTel 在五维标尺上的坐标
用 那把尺子给 OTel 定位。和 07 一样,它主要拨动第五根标尺,但角度不同:
| 维度 | OTel 的作用 |
|---|---|
| ① 故障预判权 | 不直接涉及 |
| ② 存储汇率 | 在 Collector 上做降采样/过滤,间接帮成本治理 |
| ③ 信号分工 | 不产生新信号,但用统一上下文把四类信号缝成网(Exemplar) |
| ④ 注意力阀门 | 不直接涉及 |
| ⑤ 仪器化负担 | 核心——把仪器化从”每换后端付一次”变成”付一次永久有效” |
07 用 eBPF 压低了仪器化的"成本",OTel 则消除了仪器化的”重复支付”——一个让它更便宜,一个让它可迁移,两篇从两个角度一起把第五维拨向了人最省力的方向。 那个贯穿全系列的问题——OTel 把天平推向了哪边?它把”仪器化”这笔投入,从一次性消耗品变成了永久资产,让人不必再为后端的更迭,反复支付同一笔插桩的力气。
去向
OTel 把仪器化降到了很低,但前面留了一个尾巴:它仍要你集成 SDK、挂 agent、补业务插桩。最后一篇要去回答那个被逼出来的问题:
- 能不能连应用代码、连框架插桩都不碰,让基础设施——服务网格、eBPF——在网络层、内核层替你产出遥测?这就是 10-云原生可观测性-零侵入是如何做到的:把仪器化负担,从应用彻底转移到平台。
- 而 Collector 作为成本治理点的那条线(前面讲过),完整的组织与经济视角,见 MOC 第五节「运维的人与经济」。
下一篇,也是云原生范式的收官篇:当 OTel 把仪器化降到”很低”还嫌不够,云原生给出了它的终极答案——零侵入。让平台替你看,应用一无所知。
返回 可观测性与运维工程 MOC | 上一篇 08-SLO-把可靠性变成数学 | 下一篇 10-云原生可观测性-零侵入是如何做到的