医院里曾经有三个互不相通的科室:体检科负责量血压、拍片子,护士站负责写护理记录,导诊台负责追踪病人在各楼层间的流转路径。病人明明只有一位,病历却是三份各写各的。后来院长拍板:把三份单子统一成一份电子病历,所有科室按同一种格式采集、同一种编号关联、同一套系统下发——这套“标准”,就是可观测性世界里的 OpenTelemetry。 在软件系统里,Metrics 是“生命体征”(每秒请求数、CPU 使用率、错误率),Logs 是“护理记录”(某时刻发生了什么具体事件),Traces 是“就医路径”(一个请求从入口到数据库经历了哪些服务)。过去我们要搭三套独立系统,监控告警相互割裂;OpenTelemetry(简称 OTel)把它们统一成一套标准:一套 SDK 完成埋点,一个 Collector 完成采集转发,后端想接 Prometheus、Jaeger 还是云厂商,随时切换。本篇文章将带你用零基础的方式,把这三根柱子一次性认全。
对 AI 从业者而言,掌握 OTel 意味着你能用一套标准接入 AI 模型服务的监控、日志与链路追踪,降低可观测性建设成本,也让自己在 SRE/DevOps 面试中手握“统一标准”的加分项。
可视化演示
展示Metrics/Logs/Traces三支柱统一采集、Collector处理管线及分布式Trace传播链路,揭示OTel标准化可观测性核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
可观测性三支柱 Metrics:时序指标数据,监控计数器与仪表盘 Logs:结构化日志记录,诊断系统运行状态 Traces:分布式追踪链路,定位请求性能瓶颈 OTel = Metrics + Logs + Traces 三支柱统一采集消除数据孤岛
统一标准背景
想象一家医院,病人多、科室多、仪器多。一开始每个科室自己买系统:体检科有 A 厂的体检表,护士站有 B 厂的护理单,导诊台有 C 厂的流转单。表面的问题是“要多学三套软件”,暗面的问题更严重——三份单子上的“病人编号”互不相同,护士想查体检报告时,只能靠病人姓名人工拼接。
软件世界也如此。Metrics(指标)通常由 Prometheus 生态负责,Logs(日志)由 ELK 生态负责,Traces(链路追踪)由 Jaeger/Zipkin 生态负责。它们各自的 SDK、协议、语义完全独立,联调一个跨服务问题,要在三套系统间来回切换。这就是可观测性世界的“三份病历”困境。
OpenTelemetry(OTel)正是为了终结这种局面而生的开源标准。它由 CNCF 孵化,是 OpenTracing 与 OpenCensus 两个项目合并的产物。OTel 提供统一的 SDK(埋点库)、统一的协议 OTLP 和统一的采集器 Collector。业务代码只需要接入一套 SDK,数据就能以同一种语言进入后端。它的核心诉求用一句话概括:采集一次,到处使用。
回到医院比喻:统一标准之后,护士在护理单上填的“病人编号”,和体检科拍的片子、导诊台的流转记录自动共享同一个 ID。软件世界里,OTel 也有类似的“统一 ID 规范”,后文会看到它如何打通三根柱子。
- SDK(埋点采集)嵌入到业务代码里的库,负责生成指标、日志和链路数据。它好比医院发给每个科室的“标准空白单”,任何人都用同一版式记录。
- Collector(集中处理)一个独立进程,接收来自各个服务的遥测数据,做过滤、批处理、重试后导出。它好比医院的“病历归档中心”,所有科室的单子先汇聚到这里,再分发给需要的地方。
- OTLP(统一协议)OTel 定义的传输格式,规定了数据怎么编码、怎么压缩、怎么传输。它好比医院统一的“电子病历信封”,不管内页是心脏科还是骨科,信封规格全球一致。
Metrics 指标体系
继续用医院做类比。体检科有一台“生命体征监护仪”,屏幕上滚动着心率、血压、血氧三条曲线——这就是 Metrics。它回答的问题是“系统现在健康吗”。运维人员盯着的 QPS、错误率、CPU 使用率、内存水位,都属于这一支柱。
OTel 将指标建模为带名称、时间戳、数值和一组标签(attribute)的采样点。指标数据的核心特征在于“可聚合”:单机内存不足不要紧,只要所有实例的指标汇总起来,就能看出集群整体健康状况。OTel 将常见指标类型归纳为三种:
- Counter(计数器):只增不减,适合统计总请求数、总错误数。
- Gauge(瞬时值):可增可减,适合描述内存使用量、当前连接数。
- Histogram(直方图):记录数值分布,适合描述请求延迟的分位数。
回到体检科:监护仪每 15 秒刷新一次曲线,这个刷新间隔在 OTel 里叫采集间隔。SDK 按固定节奏将聚合后的指标推给 Collector,再由 Collector 转给后端。正是指标数据的“定期采样”特性,让它的数据量可控、查询最快——但也让它无法回答“当时到底发生了什么”这类细节问题,这就要靠下一根柱子:日志。
- name(指标名)指标的唯一标识,如
http.server.request.duration。语义约定保证同一个指标名在不同语言 SDK 里含义一致。 - value(数值)指标的测量结果。Counter 累加、Gauge 取当前值、Histogram 累积到桶内。
- attributes(标签)描述指标观察对象的维度,如
method="GET"、status="500"。标签越多,切分查询越灵活。
Logs 日志体系
护士站有一本电子护理记录,值班护士每次巡视都往里追加一条:19:02 病人体温 38.5℃,已物理降温;19:30 复测体温 38.8℃,通知医生。这些记录按时间排列,是“护理日志”。软件系统里的 Logs 正是这样——由开发者在代码关键路径上打印的一段段事件描述。
日志记录的是“发生了什么”这一具体事实,因此它信息量最大,但也最“散”。在 OTel 的标准里,一条日志被建模为 LogRecord,包含时间戳、日志级别、日志正文和一组属性。为了支持自动检索,日志还需要结构化:把 "time=19:02 level=ERROR msg=rds timeout service=auth" 这样的文本,解析成可查询的键值对。
回到医院场景:护理日志写得再好看,如果无法和体检数据关联,医生仍然要手工比对。OTel 的方案是让日志自动携带 traceId——一旦日志带上这个“与链路共享的编号”,就能在调大故障时循线找到前端和后端。这就是日志与链路的联动,也是三支柱统一的价值所在。
- timestamp(时间戳)事件发生的精确时间。同一条请求的日志,时间戳是最重要的排序依据。
- level(级别)INFO/WARN/ERROR 等严重程度分级,用于过滤噪音。
- body(正文)日志主体内容,通常是人类可读的自然语言。
- attributes(属性)Otel 的日志属性常包含 traceId、spanId 等关联字段,让日志能挂到具体链路节点上。
Traces 链路追踪
导诊台有一张“就医路径表”:病人 10:00 挂号 → 10:15 抽血 → 10:40 心电 → 11:20 复诊。这张表记录病人“去过哪、何时去、花了多久”,对应的正是软件世界里 Traces 做的:追踪一个请求自进入网关到调用数据库的完整路径。
一个 Trace 由多个 Span 组成。Span 是追踪的基本单元,代表一次具名操作,比如 HTTP 请求、数据库查询、消息队列发送。每个 Span 记录操作名、开始时间、结束时间和标签;多个 Span 通过父子关系组成一棵调用树——就像就医路径表里的每步检查都记录着“上一个环节是谁”。
OTel 沿用了 Dapper/OpenTracing 的模型。为了把追踪信息跨服务传递,OTel 遵循 W3C TraceContext 规范,在一个名为 traceparent 的标准 HTTP 头里写入 TraceId 与 SpanId:
回到导诊台视角:如果每次跨科室信息传递都按同一张“路径表”记录,院长想复盘病人就医全流程,只需按 trace-id 把各科室的记录串起来。同理,在大规模微服务架构中,OTel 能让一个请求从网关到数据库的每一跳都有据可查,故障定位从“猜”变成“看树”。
- trace-id(全局追踪号)32 位十六进制随机数,唯一标识一整条请求链路。所有服务共享同一 trace-id,日志与链路靠它关联。
- parent-id(父 Span 标识)上一跳服务生成的 spanId。通过 parent-id 就能在分析端画出一棵 Span 树。
- flags(标志位)表示采样标记,例如
01代表“这条链路已被采样记录”。
Collector 架构
医院改革进入第二步:每个科室还保留自己的仪器,但所有化验单、护理单、流转单先统一交到新设立的“病历归档中心”。这个中心不关心单子最后由哪家保险公司结算,只负责三件事:收单、标准化、转交。OTel 世界中,承担“归档中心”职责的就是 Collector。
Collector 是一个独立部署的进程,它向上接收来自 SDK 的数据,向下连接一个或多个后端。它内部定义了清晰的三段式处理管道:
- 接收器(Receiver):决定数据怎么进来,支持 OTLP、Prometheus 等格式;
- 处理器(Processor):对数据做裁剪、采样、批处理、加标签;
- 导出器(Exporter):决定数据往哪儿去,支持 OTLP、Jaeger、Prometheus 等后端协议。
回到医院归档中心:它对外展示统一的收单窗口,对内提供各种转交格式,于是“病历归档中心”部署在靠近病人的科室(Agent 模式),还是集中分布于院区(Gateway 模式),完全取决于人流与网络情况。OTel 的 Collector 同样支持两种部署模式:Agent 模式随业务实例部署在节点上,低延迟、本地缓冲;Gateway 模式独立部署在集群内,适合统一出口策略。正是这种“采集与后端解耦”的设计,让换监控后端成为改配置而非改代码。
- receiver(接单窗口)从各服务 SDK 拿到 OTLP 数据,解包为内部数据模型。
- processor(分拣与加工)执行过滤掉 debug 日志、按 traceId 采样、攒一批再发等策略。
- exporter(寄送出口)把数据编码为后端期望的格式(如 Prometheus Remote Write、Jaeger Thrift)。
后端对接方式
电子病历统一之后,医院面临最后一公里:保险报销平台只认 PDF,卫健委平台只认 XML,科研中心只认 CSV。归档中心不会要求所有科室改单子,而是自己准备不同的转交模板,把统一病历翻译成各平台要的格式。OTel 的最后一公里,正是“后端对接”。
OTel 将数据从 SDK 到后端的传输统一为 OTLP 协议。OTLP 是 gRPC/HTTP 之上的二进制编码,支持指标、日志、链路三种数据的传输。由于后端种类繁多,OTel 采取“双管齐下”策略:
- 后端支持 OTLP 则直接用;
- 后端只认自家协议,则由 Collector 的导出器完成格式转换。
回到医院比喻,这相当于“病历只填一次,分发到处都能用”。对开发者最直接的收益是:公司从自建 Prometheus 迁到云厂商时,不需要改动业务代码,只需调整 Collector 配置文件。OpenTelemetry 统一标准让“一份数据、多种用途”成为现实——这正是它作为可观测性标准的价值所在。
- OTLP-native(原生支持)Jaeger、Grafana Tempo、云厂商后端可以直接接收 OTLP。
- vendor formats(厂商格式)Prometheus 使用 Remote Write,Zipkin 使用 Thrift,由 Collector 导出器适配。
- 多后端同时导出一份数据可同时发给监控告警、链路分析和日志检索三套系统,互不冲突。
AI 应用场景
AIOps 统一数据底座
阶段:数据采集与实时监控
解决的问题:传统监控三套系统数据割裂,AIOps 平台做异常检测时,无法将指标突刺、错误日志、调用链放大三个维度的信息关联到同一次故障。OTel 统一采集后,AIOps 算法可以拿到带 traceId 对齐的三类数据,训练根因分析模型时有了一致的时间轴与关联键。
具体案例:某电商公司搭建 AIOps 平台,用 OTel SDK 埋点所有微服务,Collector 同时导出到 Prometheus 与 Elasticsearch。当支付成功率指标下降 20% 时,算法自动关联同时间窗的 ERROR 日志与对应 trace 调用链,定位到“库存服务缓存雪崩”,把平均故障恢复时间从 45 分钟降到 8 分钟。
MLOps 模型链路追踪
阶段:模型训练与推理监控
解决的问题:ML 推理管线涉及特征服务、模型服务、后处理服务,传统日志彼此割裂,难以定位“特征漂移导致精度下降”发生在哪个环节。OTel Trace 可将一次推理请求串成完整调用链,Metrics 记录推理延迟,Logs 记录输入特征摘要。
具体案例:某推荐系统团队在推理服务中接入 OTel:每个推理请求生成一个 trace,从特征拉取到模型打分到结果排序整条链路可见;同时用 Gauge 记录特征分布均值的漂移量。当线上 AUC 下跌时,运维能立刻在链路图上发现是特征服务超时,而非模型本身退化。
云原生 AI 基础设施监控
阶段:基础设施可观测性建设
解决的问题:GPU 训练任务显著依赖节点的网络、存储、调度状态,传统监控无法把“GPU 利用率下跌”与“PvP 调度重试”事件关联起来。OTel 统一采集节点指标和 Kubernetes 事件日志后,异常分析不再靠人工翻三套系统。
具体案例:一家大模型训练团队用 OTel Collector 采集所有 GPU 节点的 DCGM 指标(服务器级别的 GPU 管理指标),并通过结构化日志记录调度器事件;当出现 NCCL 超时(分布式通信超时)时,通过 traceId 把训练通信链路与节点网络指标对齐,快速判定为某交换机拥塞,替换网络路径后训练恢复稳定。
Python 演示
# -*- coding: utf-8 -*-"""简化版 OpenTelemetry 三支柱数据统一采集演示"""import loggingimport randomimport time class MetricsRecorder: """模拟 OTel Metrics SDK:计数器 + 直方图""" def __init__(self): self.request_count = 0 self.error_count = 0 self.latency_samples = [] # 所有延迟样本 def observe_request(self, latency_ms, is_error): self.request_count += 1 self.latency_samples.append(latency_ms) if is_error: self.error_count += 1 def snappy_export(self): """导出聚合结果""" avg = ( sum(self.latency_samples) / len(self.latency_samples) if self.latency_samples else 0 ) return { "http.server.request.count": self.request_count, "http.server.request.error": self.error_count, "http.server.request.latency.avg": round(avg, 2), } class LogRecord: def __init__(self, level, message, trace_id, span_id): self.timestamp = time.strftime("%H:%M:%S") self.level = level self.message = message self.trace_id = trace_id self.span_id = span_id def to_dict(self): return { "ts": self.timestamp, "level": self.level, "msg": self.message, "traceId": self.trace_id, "spanId": self.span_id, } class Span: def __init__(self, name, trace_id, span_id, parent_id): self.name = name self.trace_id = trace_id self.span_id = span_id self.parent_id = parent_id def to_dict(self): return { "name": self.name, "traceId": self.trace_id, "spanId": self.span_id, "parentId": self.parent_id, } class TraceRecorder: """模拟 OTel Trace SDK""" def __init__(self): self.trace_list = {} def start(self, trace_id): self.trace_list[trace_id] = [] def add_span(self, trace_id, name, span_id, parent_id): self.trace_list[trace_id].append(Span(name, trace_id, span_id, parent_id)) def export(self): return [s.to_dict() for spans in self.trace_list.values() for s in spans] class Collector: """模拟 OTel Collector:收单 -> 加工 -> 导出""" def __init__(self): self.metrics = None self.logs_list = [] self.traces = None def receive(self, metrics, logs_list, traces): self.metrics = metrics self.logs_list = logs_list self.traces = traces def export_to_console(self): print("=== Metrics 指标 ===") for key, value in self.metrics.items(): print(f" {key}: {value}") print("=== Logs 日志 ===") for log in self.logs_list: print(f" {log}") print("=== Traces 链路 ===") for span in self.traces: print(f" {span}") def simulate_user_request(trace_rec, mrec, logs_list, trace_id): """模拟一次用户请求穿过两个服务""" span_server = "server-1" span_db = "db-2" trace_rec.add_span(trace_id, "GET /api/order", span_server, None) trace_rec.add_span(trace_id, "SELECT order", span_db, span_server) latency = random.uniform(5, 200) is_error = latency > 150 mrec.observe_request(latency, is_error) logs_list.append( LogRecord("WARN" if is_error else "INFO", "query order", trace_id, span_db) ) def main(): mrec = MetricsRecorder() trec = TraceRecorder() logs_list = [] logging.basicConfig(level=logging.INFO) for i in range(5): tid = f"trace-{i+1}" trec.start(tid) simulate_user_request(trec, mrec, logs_list, tid) collector = Collector() metrics_snappy = mrec.snappy_export() logs_ready = [log.to_dict() for log in logs_list] traces_ready = trec.export() collector.receive(metrics_snappy, logs_ready, traces_ready) collector.export_to_console() if __name__ == "__main__": main()
class MetricsRecorder:对应 OTel Metrics SDK,维护请求计数、错误计数和延迟样本列表。def observe_request(self, latency_ms, is_error):模拟每次 HTTP 请求产生一条指标观测。def snappy_export(self):对应 Collector 内的指标聚合/处理器,将原始样本缩成平均延迟等汇总值。class Span:模型化 Trace 的最小单元,持有 traceId、spanId、parentId 三个关键字段。def add_span(...)模拟一次内部调用如何往链路树上追加一个节点。class Collector:对应 OTel Collector,先接收来自三个 SDK 的数据,再统一导出。def export_to_console(self):模拟“后端对接”:把三类数据用统一格式交给下游系统。simulate_user_request(...)模拟一个请求穿过 API 网关与服务端,同时产生三类数据。TraceRecorder.start(...)用同一个 traceId 为一次用户请求建立追踪上下文。
常见误区
误区Metrics、Logs、Traces 都差不多,选一种就够了吧?反正都是监控。
正解如果把可观测性比作体检报告,Metrics 是体温单(告诉我发烧了),Logs 是护理记录(告诉我 19:02 喝了退烧药),Traces 是就诊路径(告诉我先去内科又转外科)。三者各回答不同问题:指标负责“是否异常”,日志负责“当时发生了什么”,链路负责“路径是怎样”。少了任何一根,故障定位都像医生只拿体温单不给病历——能说不舒服,但说不出原因。
为何常见:因为大多数初学者只接触过 Prometheus 或 ELK 其中一种,以为“一个工具打天下”,没有经历过真正的跨服务分布式排障。
误区Collector 就是个中转管道,数据进来什么样,出去就是什么样。
正解Collector 有真实的数据加工能力。它内部有 processors,能做批处理、过滤采样、属性增删、重试发送。比如可以把 INFO 级别日志全部丢弃,可以按百分比对链路上采样,可以把两个后端协议同时输出。它更接近医院“病历归档中心”:接收后先核对、再分拣、再转译,而非原封不动的邮筒。
为何常见:多数教程只演示了 receiver -> exporter 的最小配置,容易让读者误以为没有处理器。
误区学了 OpenTelemetry 就必须把数据存到它的官方存储、用它的官方 UI。
正解OTel 明确不提供商业化后端,它定位是“数据采集与传输标准”。数据到了 Collector 之后,可以导给 Prometheus、Jaeger、Zipkin,也可以导给云厂商托管服务。这个解耦正是它最大的价值:业务代码与后端厂商脱钩,切换后端的成本接近于改配置。
为何常见:名字带 "Telemetry",又有统一协议 OTLP,容易让人误以为它自带监控平台。
误区只要装了 OTel SDK,所有性能指标、所有慢查询、所有业务错误都会自动采集。
正解SDK 只提供自动埋点框架(如 HTTP 请求钩子、数据库驱动钩子),但业务级指标(订单转化率、队列积压深度)仍需手动 instrumentation。类比:医院能自动测体温,但“病人有没有头晕”必须由护士手动记录。完备可观测性 = 自动埋点 + 有策略的业务自定义埋点。
为何常见:官方 demo 展示开箱即用的 HTTP 指标,让新手误以为“装了就全自动”。
面试题
Q1. OpenTelemetry 统一的三支柱分别是指什么?
- A.Metrics、Logs、Traces← 正确答案
- B.CPU、内存、磁盘
- C.Docker、K8s、Istio
- D.Grafana、Prometheus、Jaeger
答案:A。OTel 的三大支柱是 Metrics(指标)、Logs(日志)、Traces(链路追踪),类比医院分别为体温仪、护理记录、就诊路径。CPU/内存/磁盘是基础设施资源,容器/调度是云原生技术栈,而 Grafana/Prometheus/Jaeger 是具体实现工具,与“三支柱”不是一个层面。
Q2. 关于 OpenTelemetry 的特点,下列说法准确的是?
- A.它是一键安装的监控产品
- B.它是采集数据统一标准← 正确答案
- C.它只能搭配云厂商后端
- D.它取代了 Kubernetes 调度
答案:B。OTel 是一套数据采集与导出标准,类比医院的“电子病历统一格式”。它不自带商业 UI,不强绑后端,也与 K8s 调度无关。初学者容易把“标准”误解为“全家桶产品”。
Q3. 若想统计某接口的请求延迟分布,最适合的 OTel 指标类型是?
- A.Counter 计数器
- B.Gauge 瞬时值
- C.Histogram 直方图← 正确答案
- D.Sum 求总值
答案:C。延迟分布需要分位数(P50/P99),Histogram 将样本划分到多个桶中记录分布,类似医院体检记录“身高区间人数”。Counter 适合只增不减的总量,Gauge 只拍当前快照,无法还原分布。
Q4. OTel 中日志与链路进行关联,通常依赖什么字段?
- A.log message 内容关键字
- B.traceId 和 spanId 属性← 正确答案
- C.服务名和 IP 地址
- D.时间戳和日志级别
答案:B。日志记录里带上 traceId/spanId,就能在故障分析时把日志挂到链路树上,实现“日志与调用链联动”。按内容关键字或服务名只能做到粗粒度过滤,时间戳与级别也无法唯一定位到某次请求。
Q5. 下面符合 OTel Collector 管道处理顺序的是?
- A.Exporter 到 Processor 再到 Receiver
- B.Receiver 到 Exporter 再到 Processor
- C.Receiver 到 Processor 再到 Exporter← 正确答案
- D.Processor 到 Receiver 再到 Exporter
答案:C。Collector 管道是“接单→加工→寄出”,对应 receiver 解码、processor 过滤与批处理、exporter 编码导出。顺序若反,数据还没解析就加工,显然不成立。
Q6. Collector 的 Agent 模式与 Gateway 模式的区别是?
- A.Agent 贵,Gateway 便宜
- B.Agent 随应用节点部署,Gateway 独立部署← 正确答案
- C.Agent 只处理日志,Gateway 只处理指标
- D.Agent 不支持 OTLP,Gateway 才支持
答案:B。Agent 模式以 sidecar 方式跑在业务节点上,采集距离近、延迟低;Gateway 模式独立部署为集群级服务,适合统一出口策略。两者承载的数据类型完全相同。
Q7. 关于 OTel 与后端采集平台的关系,理解正确的是?
- A.OTel 必须搭配 Prometheus 才能用
- B.OTel 无法导出到商业 SaaS 平台
- C.OTel 是标准,后端可自由替换← 正确答案
- D.OTel 已内置 Grafana 可视化界面
答案:C。OTel 定位是标准而非产品,Collector 的 exporter 可将数据转给开源或商业后端,服务端只是换配置。选项 C 准确,其他描述与 OTel 的“采集与后端解耦”设计相悖。
Q8. 以下哪些属于 OpenTelemetry 三支柱的数据?(多选)
- A.磁盘每秒读写字节数← 正确答案
- B.某次请求的 ERROR 日志← 正确答案
- C.跨服务调用的调用链信息← 正确答案
- D.请求接口的延迟分布直方图← 正确答案
答案:ABCD。四类都落在三支柱内:磁盘字节数是 Metrics,ERROR 日志是 Logs,跨服务调用链是 Traces,延迟分布直方图是 Metrics 的 Histogram。可观测性追求多维度统一,四选项分别覆盖不同支柱及组合。
Q9. 关于 OTel 语义约定(Semantic Conventions),下面做法恰当的是?
- A.指标名使用点分命名空间← 正确答案
- B.标准属性字段内填自定义值
- C.自定义动作使用自定义命名空间← 正确答案
- D.资源标签直接写入业务自定义键
答案:AC。OTel 约定通用指标名使用点分命名空间(如 http.server.request.duration),自定义业务指标应进入自定义命名空间以保证不冲突。标准属性必须严格按语义约定取值,资源标签也不能被业务自定义键污染。
Q10. 公司想让 Java 与 Python 微服务共用一套采集方案,最合适的设计是哪些?(多选)
- A.两边各接一套独立监控栈
- B.两边统一接入 OTel SDK← 正确答案
- C.中间加一层 OTel Collector← 正确答案
- D.Java 用 Jaeger,Python 用 Zipkin
答案:BC。统一 SDK 产生同一数据格式,Collector 承担汇聚转发的角色,两者搭配才能实现“一处埋点、处处可用”。A 与 D 会重新制造数据孤岛,与统一标准的目标背道而驰。