Open Telemetry概述

2025-11-11T14:11:02+08:00 | 30分钟阅读 | 更新于 2025-11-11T14:11:02+08:00

@

学习目标

学完本章你应该能够:

  1. 讲清可观测性的起源:它最早是 1960 年代控制论(卡尔曼博士)提出的概念,在工业制造里靠传感器输出判断系统状态,落到软件工程就是"日志 + 指标"两大类输出。
  2. 说清单体应用迁移到微服务后,在完整链路追踪、分布式日志收集、服务依赖与网络故障定位上分别带来什么挑战,以及为什么需要"指标、日志、分布式追踪"这三大基础。
  3. 区分监控、APM、可观测性三者的关系:监控是可观测性的子集、APM 偏单服务性能分析、可观测性提供面向整个微服务架构的系统级视图。
  4. 讲透分布式追踪(tracing)的核心原理:trace context 沿调用链传播,并根据协议(HTTP header / gRPC metadata / RPC 自定义字段)结合 Jaeger 的 Tracestate 头(Uber-Trace-ID)理解其结构。
  5. 读懂一个 Span 的五个关键字段(context / attributes / events / links / status)各自的用途,以及它们和"父 span、异步关联"之间的关系。
  6. 讲清 OTel Metric 的五种类型(counter / up-down counter / gauge / histogram 等)、三层数据模型(事件模型→数据流模型→时间序列模型),以及 Sum 的 Delta 与累计两种计算方式。
  7. 掌握 Metrics、Logs 分别怎么与 Trace 关联(metrics 用 exemplar 携带 trace/span id,logs 把 trace id 打出来并配置日志系统关联)。
  8. 说清 OTel 两大组件 Collector(pipeline = receiver + processor + exporter,可 fan-out 到多后端)与 SDK(三支柱支持成熟度矩阵)。
  9. 理解 OTel 的数据流、混合部署方式,并对比集成 OTel 的三种方式(零侵入插桩 / 接入 SDK / 使用 API 库)。
  10. 讲清为什么需要采样、何时采何时不采,并对比头部采样与尾部采样的原理与取舍。

前置知识:

  • 一点 Prometheus / Grafana 使用经验(理解指标与监控大盘)
  • 分布式系统基础:一次请求会跨多个服务、经过多种协议(HTTP / gRPC / RPC)、调用中间件和数据库
  • 至少熟悉一门编程语言(Node / Python / Go)的埋点概念,无需精通

本章你会动手做的事:

  • 对照文中的 Span 五字段,给一条自己的模拟请求画出包含 root span、子 span、父子关系的调用链。
  • 抄一遍 Collector 的 pipeline 结构,标出每个 processor 的作用,并设计一个"过滤敏感数据 + 扇出到多个后端"的最小配置。
  • 对比头部采样与尾部采样的场景,为自己一个高 QPS 的服务写出一份采样策略并说明理由。
OpenTelemetry 概述可观测性起源Trace·传播三大支柱数据Collector·数据流集成·采样控制论·工业制造微服务三大挑战·三大支柱trace context 沿链传播HTTP/gRPC/RPC·JaegerSpan 五字段·Metric 类型Metrics/Logs 关联 TraceReceiver+Processor+ExporterSDK·语言支持矩阵零侵入·SDK·API 库头部·尾部采样

一、可观测性:从控制论到微服务

这一讲先回答一个底层问题:可观测性到底从哪来、它解决了什么问题、又为什么在微服务时代变得如此重要。 理解这段历史,才能明白 OpenTelemetry 为什么会存在。

可观测性起源于控制论

可观测性(observability)最早来源于**系统控制理论(control theory)**这门科学,由控制论专家 卡尔曼博士(Rudolf Kalman) 在 1960 年代提出——远在云计算兴起之前。

它的核心思想非常朴素:通过系统的"输出"来判断系统当前的"内部状态"。我不用打开机器看它内部怎么转,只要看到它的输出(转速、压力、温度),就能推断它现在健不健康。

复习提示:类比:可观测性就像你没法拆开一台发动机看内部活塞,但通过仪表盘上的转速表、油温表、压力表,就能判断这台机器是否正常。控制论要回答的就是"只给我外部输出,我能反推出内部状态到什么程度"。这个"从输出反推状态"的能力,就是可观测性。

工业制造里的"第一个监控大盘"

可观测性在工业制造领域用得最多很早。比如下图那个工业制造过程的监控大盘——它很像我们用 Prometheus 或者 Grafana 看的那种监控仪表盘:每个系统运行的组件、运行的环节是否良好,都能实时看到,例如电机的转速、阀门的控制、压力、温度等等,全部显示在仪表盘上。

它们靠什么判断状态?靠传感器的输出。而在软件工程领域,这个"输出"主要有两大部分:

  • 日志(Logs):应用运行过程中产生的离散事件记录。
  • 指标(Metrics):请求成功率、延迟等数值型数据。

比如我们会用请求成功率指标、延迟指标等信息,综合判断当前系统的整体运行情况。

flowchart LR
    OB[可观测性
通过系统输出推断内部状态] --> IND[工业制造
传感器输出
转速/压力/温度] OB --> SW[软件工程
日志 + 指标
成功率/延迟] IND --> DL[监控仪表盘
像 Prometheus/Grafana] SW --> OBS[分布式可观测性]

单体应用时代:系统级监控 + APM

最早期应用是单体方式部署的。那时候怎么监控一个单体应用呢?通常会部署一个 agent,用来采集单体应用的可观测性数据,比如 CPU、内存等指标。以早期的 PHP 应用为例,我们往往用开源工具去监控它的日志输出,或者 PHP-FPM 的进程状态,同时结合日志文件做告警。

单体应用时代的监控指标通常是系统级的,例如:

  • PHP-FPM 进程数、进程性能、响应时间
  • 进程占用的 CPU、内存

除了开源工具,还有一类 APM(应用性能监控) 服务可选,比如早期的 New Relic、Datadog(转录中写作 data dog)。这两个工具现在其实都能用在分布式追踪里,只不过最早他们以单体应用监控为主。

云原生 + K8s:单体迁往微服务

随着云原生技术发展、Kubernetes(K8s)大规模落地,大家逐渐把单体应用迁移到微服务架构上。一次用户请求往往要经过多个服务、多次跨服务调用才能完成:

  • 服务之间通过各种各样的协议通信:HTTP、gRPC(J RPC 之误)、各种 RPC 等。
  • 服务还会调用中间件(Redis、MQ 等)、数据库。

于是一次简单请求会流经非常非常多的服务。这给可观测性带来了三个典型的挑战:

挑战具体表现
难以追踪完整链路出现系统瓶颈或错误时,很难定位到具体是哪个服务、哪一步出了问题、过程是怎样的
日志收集与分析变复杂每个微服的日志分布在多个节点上,不像单体只看一两个日志文件就行,聚合分析更复杂
服务依赖与网络故障难发现微服调用会产生网络延迟、服务依赖、通信故障。比如一次调用返回 500,你得查整个调用链,才知道是某个服务之间请求超时,还是下游服务故障

这些问题正好用分布式可观测性方案来系统性解决:分布式追踪(trace)、指标(metrics)、日志(logs)——这就是可观测性的三大基础。

数据能发现什么适合回答
日志(Logs)应用启动异常、报错信息具体发生了什么
指标(Metrics)系统深入细节:请求延迟、当前 QPS系统是否健康
分布式追踪(Traces)微服间依赖关系、请求拓扑图、每个服务的延迟和性能请求慢在哪、谁调了谁

监控、APM 与可观测性:既有区别又递进

对于传统单体应用,我们更常谈"监控";但对于可观测性和 APM,很多人还比较陌生。三者关系如下:

flowchart TB
    OBS["可观测性
涵盖三大基础,提供系统级视图
面向微服务架构"] --> SUB[监控
可观测性的子集
收集并展示指标 + 告警策略] OBS --> APM[APM
面向单服务的性能分析
代码性能/事务跟踪/资源使用] SUB --> UP[单体应用时代足够用] APM --> UP UP --> MIG[微服务后 APM 不够用] MIG --> OBS
  • 监控:可观测性的一个子集,主要收集并展示指标,通常还会一并配置告警策略。
  • APM:性能分析工具,面向单个服务的代码性能分析、事务跟踪、资源使用情况。单体开发早期常用 New Relic、Datadog 做性能分析。
  • 可观测性:从单体发展到微服后,APM 已经"不太够用",于是把监控 / APM 提升到下一个阶段。可观测性涵盖所有我们可以了解的系统状态和工具,包含三大基础,目的是为微服架构提供整体的、系统级视图,尤其适合微服务。

怎么构建可观测性:收集 → 监控 → 分析

以跨语言(Node / Python / Go)+ 数据库的分布式微服应用为例,传统监控和 APM 在分布式架构下已经不适用。抽象地看,构建可观测性分三步:

[收集] --统一汇总--> [监控大盘] --分析--> [分布式调用链]
 日志 / 指标 / 分布式追踪   用 Grafana 设计仪表盘   把调用链串起来做跨服务分析
  1. 收集:收集日志、指标、分布式追踪三方面数据。有了 K8s 底座之后,很方便地收集 Pod 的应用日志、CPU、内存、网络等,因为 K8s 已经做了标准化。
  2. 监控:把采集到的指标设计成仪表盘,用 Grafana 做数据汇总,更直观地分类——可以按集群、命名空间、业务线归纳。
  3. 分析:把调用链串起来,形成分布式追踪,就能跨微服查看它们的调用链关系,分析延迟和错误情况。

分布式追踪的原理:trace context 沿链路传播

在三大基础里,分布式追踪尤其重要。tracing 提供了一个请求从接收到处理的生命周期跟踪路径。一次请求往往经过 N 个系统,所以我们叫它分布式链路追踪。

分布式链路追踪的实现原理是:将 trace context(追踪上下文)在整个调用链里传播。每个请求链路中都有这个携带追踪信息的元数据,它标识了:

  • 当前请求是从哪里来的(上游是谁)
  • 我要请求的下游是谁

于是把 context 一层层带下去,就组成了完整的"父请求 → 子请求 → 父请求 → 子请求"链路。

sequenceDiagram
    participant F as Frontend 前端
    participant A as 服务 A
    participant B as 服务 B
    participant DB as 数据库
    F->>A: 请求 (trace context)
    A->>B: 请求 (同一 trace context)
    B->>DB: 请求 (同一 trace context)
    note over F,DB: 同一 trace_id 贯穿整条链路
每个服务记录自己的 span_id + parent_span_id
  • trace context 从最初的请求方生成(如图里的 frontend 前端),通过整个链路传递给下游每个微服务节点,甚至数据库的请求。
  • 这个 context 记录每个服务的执行信息,帮我们识别并聚合整个微服务请求,也好在整条请求链里分析各环节的操作。

trace context 的三种传播方式

trace context 在不同微服务之间怎么传播?取决于通信协议,一般分三种:

调用协议trace context 放在哪说明
HTTP 请求请求头(header)微服务之间最常用
gRPC 调用metadata(元数据)类似 HTTP 的 header 机制
RPC 协议自定义字段RPC 没有元数据存 trace,需自定义字段存储并传播

微服务之间最常使用 HTTP 调用来相互调用。我们可以继续深入看 trace context 长啥样——以 Uber 开源的分布式追踪工具 Jaeger(转录中为 ubber)为例,它通过名为 uber-trace-id 的 header key 来传递上下文。

这个 uber-trace-id 通常包含四个部分,用短斜杠连接/分割:

uber-trace-id = <trace_id>-<span_id>-<parent_span_id>-<flags>
  • trace ID:整个追踪(trace)唯一的标识,所有属于同一个请求的链路都共享它。
  • span ID:当前这个 span 自己的 ID。
  • parent span ID:父 span 的 ID,靠它构建调用树的层级关系(根 span 该字段为空)。
  • flags:跟踪标志位(如表示采样与否)。

这就是"一次分布式追踪"数据模型的最小骨架。下一讲我们深入 Span 的内部字段。

二、OTel 的数据格式与组件

理解了 trace context 怎么传播,这一讲深入 OpenTelemetry(OTel)在数据采集时的数据格式,以及两大组件怎么分工。

Trace 数据格式:一个跨服务的请求例子

先看一个分布式请求的例子:假设有 hello 服务,它再请求上游的 greeting 服务,greeting 又请求另一个服务。每个服务在处理时都会生成一个 span,且这个 span 有自己独立的 span ID;但它的 parent span ID 会指向调用它的那个服务的 span ID。

例如:

  • hello 生成一个 span(span ID = 13),它被外部请求触发,是 root span,没有 parent span。
  • hello 请求 greeting,greeting 生成自己的 span(span ID 独立,比如 14),它的 parent span ID 指向 hello 那个 span 的 ID(13)。
  • greeting 再请求下游服务,下游服务的 span 的 parent span ID 又指向 greeting 的 span ID。

而这三者(甚至整条链路上所有 span)共享同一个 trace ID,所以它们属于同一次分布式追踪请求——父 span + 子 span + 孙 span,加上共享 trace ID,就形成了一棵完整的调用树。

flowchart TD
    H[hello
span_id=13
root, 无 parent] G[greeting
span_id=14
parent_span_id=13] D[下游服务
span_id=15
parent_span_id=14] H --> G --> D note[同一 trace_id=D2F2...
贯穿整条链路]

Span 数据结构:五个关键字段

对于 trace 的数据格式,一个 span 里会包含 context、attributes、events、links、status 等字段。看每一个字段的用途:

Span
├── context     不可变对象:trace ID + span ID + 跟踪标志 + 跟踪状态
├── attributes  业务元数据键值对(操作信息:url / port / host 等)
├── events      带时间戳的结构化日志点(span 内某个时刻发生的事件)
├── links       关联另一个 span(面向异步场景,不一定有层级关系)
└── status      状态:UNSET / ERROR / OK

context 字段

context 是每个 span 的一个不可变对象,包括:

  • trace ID 和 span ID
  • 跟踪标志(trace flags):包含相关的跟踪信息
  • 跟踪状态(trace state):可以携带特定供应商的跟踪信息键值对

attributes 属性字段

attributes 属性是包含元数据的键值对,用来携带追踪的操作信息或者额外信息。比如你请求的 url(root)是什么、请求的端口(port)、主机(host)、你是否被续期(t) 等,都可以放进 attributes。这些字段会在分布式追踪服务里被查询到。

attributes 可以在 span 创建期间或者创建之后添加,并遵循两个规则:

  1. 键是非空字符串。
  2. 值也是非空字符串;但值可以是布尔值、浮点、整数,以及这些值组成的数组。
"attributes": {
  "http.method": "GET",
  "url.path": "/api/orders",
  "server.port": 8080,
  "host.name": "order-pod-7d8b",
  "tag_ids": ["a", "b"]   // 值可以是数组
}

events 事件字段

events 可以认为是 span 结构化的日志消息,通常用来表示在 span 持续时间内单一的某个事件点。例如标识"页面在什么时候变成可交互的",你就可以用一个自定义 event step 来定义那一刻的时间。换句话说,从 span 的 start time 到 end time 之间,你在每一刻发生的事件,都能用 event 记录下来。

它强调"发生在某个时刻",比 attributes 多一个时间维度。

"events": [
  {
    "name": "interaction/step",
    "time_unix_nano": 1731319860050000000,
    "attributes": {"rate_limiting": 3}
  }
]

links 字段用来把一个 span 和一个 span 关联起来。这里要特别注意和 parent span ID 区别:

  • parent span ID:用来构建层级关系,有明确的父子/复制关系。
  • links:建立 span 之间的关联性,不一定有层级关系,但可能具有相关性。

典型场景是异步的消息队列:消息队列里有一条消息被不同的消费者消费,这些消费者产生的 span 之间是有相关性的,但它们不存在父子层级关系,所以不适合用 parent span ID 建立关系,而应该用 links 来给它们建立连接/关联。

"links": [
  {
    "trace_id": "...",
    "span_id": "消费者B的span_id",   // 与当前 span 相关联但无父子关系
    "attributes": {"message.id": "msg-001"}
  }
]

status 状态字段

每个 span 里都有一个状态(status),三个取值:

取值含义
UNSET追踪成功完成,且没有产生任何错误
ERROR跟踪过程产生错误。例如处理请求产生了 500,就用 ERROR 标识是哪个 span 产生了错误,这样一次 trace 里哪几个服务出错一目了然
OK被开发人员明确手动标识为"无错误",排查过程中有时会用

Metric 数据结构:名称 + 种类两个维度

OTel 里的 metrics(指标)有 名称 和 种类 两个维度。在种类方面,常见类型有:

  • Counter:只增加的(monotonic),每次导出时需要你通过聚合的方式来访问。
  • Up/Down Counter:累计值可以增加或减少,注意跟 Counter 不一样——Counter 只能增加。
  • Gauge(转录中为 age):度量当前值的一种指标,用来衡量某个时刻的状态,注意它是异步的。
  • Histogram(直方图):比如常用的请求延迟分布。

相比 Prometheus,OpenTelemetry 的 metric 类型会更丰富一些。

类型特点典型用途
Counter只增不减请求总数、错误总数
Up/Down Counter可增可减的累计值当前在线人数、队列深度
Gauge度量当前值,异步CPU 使用率、内存占用
Histogram分桶统计请求延迟分布、P99

Metric 的三层数据模型

OTel 的 metric 数据模型主要分三个部分:

事件模型 (Event Model)      数据的最初形态 / 指标的最初形态
   ↓(打包成流式数据)
数据流模型 (Data Stream Model)  OTel 的流式模型,传输到后端监控系统
   ↓(落库存储)
时间序列模型 (Time Series Model)  监控系统里的最终存储形态
  • 最上层:事件模型(Event Model)——数据最初的形态,是 metric 的最初形态。
  • 中间层:OTel 的数据流模型(Data Stream Model)——处理时把数据打包成流式数据模型,传输到后端的监控系统。
  • 监控系统内:时间序列模型(Time Series Model)——数据最终存储时的形态(按时间序列存)。

在三个数据模型里,我们主要看最上层的事件模型。在 OTel 中,事件模型主要由 ValueRecorder(值记录器)来捕获指标;捕获之后,会对指标做不同的统计和处理:

  • 比如采集到一个 request_size(请求大小)指标后,会去提取 max(最大值)、做分桶(bucket)操作、做求和(sum)操作。
  • 于是在 OTel 层便生成了不同的指标:max_request(最大值请求)、直方图、sum 等不同的指标。

这种方式赋予了 OTel 在处理和存储指标、并把指标发给后端时更大的灵活性。

flowchart LR
    VR["ValueRecorder
采集 request_size"] --> CAL[统计处理] CAL --> MX[提取最大值 → max_request] CAL --> BK[分桶操作 → Histogram] CAL --> SM[求和 → sum]

Sum 的两种计算方式

对于 sum 而言,它有两种计算方式:

  1. Delta 计算方式:计算一段时间窗口的总和。它会以 start time 作为一个时间窗口,计算这个窗口内 sum 的聚合值。每个时间窗口互不重叠——比如把一段时间分成 5 个时间窗口,各窗口独立,基于不同的时间窗口计算各自的聚合 sum。
  2. 累计聚合(Cumulative)计算方式:从开始算总和。这里的"开始"通常指进程 / 应用启动时作为起点。它不像 Delta 那样切分时间段,而是基于不同的时间点做累计——从某个时间点往前的全部(从开始到该点)都累计出来。

Gauge 与直方图的细节

  • Gauge:会对给定的时间窗口做最后的数据采样。它包含一组独立的属性名(key-value 键值对),以及采样值和时间戳。这里要特别注意的是:虽然 Gauge “做了采样”,但它最终不会展示——因为在采集 Gauge 时,它采集的其实是时间窗口内最后一个时间点的值。
  • 直方图(Histogram):跟 Prometheus 差不多,比较好理解。每个数据点包括属性名(key-value)、start/end 时间窗口、count、sum,以及 min/max 等相关独立属性。

两个问题:Metrics / Logs 怎么和 Trace 关联

数据都清楚了,还剩两个关键问题没解决——指标、日志如何和 trace 关联起来。这一步是 AIOps 做三支柱联动的基础。

Metrics 怎么关联 Trace

以 trace 为核心,我们可以把 trace ID 和 span ID 在指标输出的时候记录下来去实现关联。比如做指标输出时,通过注释(annotation)的形式(带 # 号的 exemplar / 关联注释)把 trace ID 输出并注入进去,来实现关联。前提条件是:整体微服务需要实现上下文的传播(baggage / context propagation)。

# 指标行里带上 trace 关联信息(示意)
http_request_duration_seconds_bucket{le="0.1", trace_id="4bf9...", span_id="0f06..."} 14300

Logs 怎么关联 Trace

这个更简单:从 trace context 里把 trace ID 和 span ID 取出来,在日志里跟着日志信息一起打印出来即可。不过有一个小细节:打印出 trace/span id 之后,你还需要在日志系统里配置,告诉日志系统怎么去关联、怎么取 trace id,这样才能真正把 log 和 trace 关联起来。这一部分我们在下一个模块带大家实战。

{
  "severity_text": "ERROR",
  "body": "order creation failed: redis timeout",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}

第四部分:OTel 的两大组件 —— Collector 与 SDK

OTel 的组件有两个核心:Collector 和客户端的 SDK。

Collector:供应商无关的代理

Collector 是一个与供应商无关的代理(agent),它可以接收、处理、导出遥测数据(遥测就是我们说的可观测性数据)。注意:Collector 不是集成 OTel 所必须的——做实战时你不用它也可以。

它的典型价值:

  1. 处理敏感数据:借助 Collector 的管道(pipeline),在处理 metrics / traces / logs 时把敏感数据过滤掉。
  2. 构建更复杂的可观测性系统:比如多层 Collector 网关——你的应用把可观测数据发送到各自的 Collector,再有一个 Collector 当作网关收集所有数据,最后统一发送到 backend(后端)。这种采集方式很灵活,扩展性强。
flowchart LR
    A[应用1] --> C1[Collector]
    B[应用2] --> C2[Collector]
    C1 --> GW[Collector 网关
汇总所有数据] C2 --> GW GW --> BE[后端监控系统]

Collector 的架构:Pipeline = Receiver + Processor + Exporter

Collector 整体架构由一个或多个 pipeline 组成。每个 pipeline 包含:

  • Receiver(接收器)
  • Processor(处理器 / processor)
  • Exporter(导出器)

pipeline 负责数据的接收、处理和导出,可以对三种数据(trace / metric / log)进行处理。

pipeline 的灵活性体现在:你可以定义 N 个接收器(receiver1…receiverN)、N 个处理器(processor1…processorN)、N 个导出方式。数据流如下:

  • 所有接收器接收到数据后,先进入第一个 processor 处理,然后依次进入第 2、3…N 个处理器。
  • 处理过程中,会根据 processor 的策略过滤或丢弃部分数据。
  • 到导出的时候,用 fan-out(扇出)机制把数据的副本导出到不同的后端——可以导出到 N 个后端。
flowchart LR
    A1[receiver1] --> P1[processor1]
    A2[receiver2] --> P1
    P1 --> P2[processor2]
    P2 --> P3[processor…N]
    P3 -->|fan-out 扇出| E1[exporter→后端1]
    P3 -->|fan-out| E2[exporter→后端2]
    P3 -->|fan-out| EN[exporter→N]

所以 Collector 里数据采集之后几乎任何处理数据的逻辑它都能实现,非常灵活。一个最小 Collector 配置长这样:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}                       # 攒一批再发,减少后端压力
  resource:
    attributes:
      - key: deployment.environment
        value: production
        action: upsert

exporters:
  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, resource]
      exporters: [otlp/jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch, resource]
      exporters: [prometheusremotewrite]

SDK:三支柱的语言支持矩阵

第二个组件是客户端 SDK。截止 2024 年 10 月,OpenTelemetry 的 SDK 已覆盖大部分语言,但在 trace、metrics、logs 三方面的支持度有差异:

  • Trace:OTel 的核心切入点,支持最广泛,基本都已 stable(仅个别语言处于 beta 阶段)。
  • Metrics:支持也不错,有一些处于开发中或 alpha、stable 阶段。
  • Logs(Logs):支持相对较差,许多还处于**开发中(development)**阶段。

所以在集成的时候,可以评估这张支持矩阵图,看你对应的业务语言当前处于什么阶段,再决定怎么接入。

三、OTel 数据流与如何集成

这一讲把前面串起来:OTel(尤其在 Collector 里)数据流怎么流转,以及我们到底有哪几种方式去集成 OTel、为什么需要采样。

OTel 的数据流:两个 pipeline 的例子

以一个例子看数据流。这里定义了两个 pipeline:traces 和 metrics(转录中简写)。

  • traces 和 metrics 都会通过各自的 receiver 接收 trace / metric 数据。
  • 接收到之后,分别进入对应的 processor 处理。
    • metrics 会用到 transform processor(转换处理器),做指标的聚合等操作。
    • traces 经过 processor 后(早期用某个"关联指标和追踪"的处理器,如今那个 processor 已废弃),现在改用**连接器(connector)**来处理 trace 与 metric 的关联。
  • 最后进入对应的 exporter 导出:
    • metric 导出到 Prometheus 等后端。
    • traces 导出到 Jaeger 等分布式追踪后端。

查询层面:我们可以通过 Jaeger 查询分布式追踪数据;同样可以用 Grafana 查询里面的数据并设计监控大盘。整体数据流基本如下:

flowchart LR
    subgraph Collector
      RT[traces receiver] --> TP[traces processor
+ connector 关联] TP --> TE[traces exporter] RM[metrics receiver] --> MP[transform processor
指标聚合] MP --> ME[metrics exporter] end TE --> J[(Jaeger)] ME --> P[(Prometheus)] J --> GR[Grafana 查询/大盘] P --> GR

混合部署方式

除了 K8s 集群,OTel 还支持混合部署方式:

  • 除了在 K8s 上部署,还支持 VM 应用(虚拟机应用),可以用 Agent 方式部署到你的虚拟机 / 主机上,把观测数据发送到你选定的分布式后端(如 Prometheus 等)。
  • 对于 Host 节点,可以用 DaemonSet 方式部署采集器,去采集集群里相关的(比如节点 / Pod)信息。
K8s Pod 应用  --------->  Collector (Deployment)
VM 虚拟机应用  --------->  Agent 方式部署  --->  后端
Host 节点        ------>  DaemonSet 采集器  --->  后端

集成 OTel 的三种方式

搞清楚数据流之后,怎么把 OTel 集成进我们的应用?一共三种方式:零侵入方式、接入 SDK、以及通过 API 库。

方式一:零侵入(Zero-Code)插桩

零侵入以插桩(instrumentation)模式去集成,根据不同的语言实现可能用到字节码(bytecode)、monkey patch,甚至 eBPF 技术,把你的 OTel 程序 / SDK 注入到业务应用里。

优点:不需要做代码层面的开发;支持的语言比较广泛(.NET、Go、Java、Python 等都支持)。

它能自动插桩的东西包括:请求和响应、数据库调用、消息队列调用等等。

不能自动插桩的对象:业务指标——因为业务指标需要你自己定义,而你没手写代码,自然没法自动生成。另外,零侵入插桩在做分布式追踪时,有可能用到 eBPF 技术,但这取决于你的内核——有些内核可能不支持,所以要看整体系统架构来评估。

# 示例:Java 零侵入,挂 agent 即可自动埋点
java -javaagent:./opentelemetry-agent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
     -Dotel.traces.exporter=otlp \
     -Dotel.metrics.exporter=otlp \
     -jar order-service.jar

方式二:接入 SDK(最广泛、推荐)

接入 SDK 是最广泛且推荐的集成 OTel 的方式,前面提到它已覆盖大部分语言。对不同的应用,需要有一些开发工作,因此涉及开发过程的支持。

  • 如果你的团队属于基础设施 / 基础架构部门、有一定开发能力,就可以直接集成进业务。
  • 如果自己部门没有开发能力,那就需要让开发团队协作,涉及跨部门沟通,甚至要打通"部门墙"。

缺点:需要额外的开发工作、跨部门沟通成本。一个最小化 Go SDK 接入示例:

import (
    "net/http"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

mux := http.NewServeMux()
mux.HandleFunc("/api/orders", handleOrders)
handler := otelhttp.NewHandler(mux, "http-server")
_ = http.ListenAndServe(":8080", handler)

方式三:借助 OTel 的 API 库

最后一种方式是可以借助 OpenTelemetry 的 API / 库——OTel 给我们提供了一个 library,你可以直接调用。但它是个封装库,一般不建议直接使用,只有在你要做二次开发时才考虑。

采样:为什么采、何时采、怎么采

知道怎么集成了,还要考虑**采样(sampling)**的问题。

为什么需要采样

从下图可以发现:对大部分分布式追踪链路数据而言,绝大多数 trace 都是没有问题的(没有错误、没有问题产生),只有一小部分是有延迟的、有错误的、需要额外采集属性。假如你把所有正常的 trace 都采集起来,存储成本和采集成本都会比较高,所以需要采样。

关于采样率,1% 的采样率比较常见——也就是 100 个请求里采样 1 条。

何时不采样、何时采样

场景采样与否
生成的数据很少(每秒几条、十几条、几十条)不采样,全部采集成本也不高
业务不允许丢弃任何追踪数据(金融 / 银行等)不采样,全部采集并存储
每秒生成几百上千条 trace、且大部分请求正常采样以降低成本

两种采样方式:头部采样 vs 尾部采样

维度头部采样(head-based)尾部采样(tail-based)
决策时机在请求入口、trace 生成时请求退出时(最后一个服务)决定
决策依据通常按百分比根据完整可观测信息(有错误 / 延迟就采样)
灵活性低,不能按追踪结果动态采样高,更智能地控制采样数据
优点实现简单,SDK / Collector 都支持不会漏掉关键的错误 / 慢请求数据
缺点可能丢失有价值的重要观测数据设计复杂度更高、采样延迟更高

头部采样(head-based):在请求入口(比如 frontend 前端、trace 被生成时)就决定是否需要采样,比如按百分比来决定,一般就是这种方式。采样则记录完整的可观测链路信息,不采样则这一条直接丢弃。

它的缺点很明显:因为是在头部入口采样的,所以不能根据追踪的结果动态采样。比如分布式调用过程中产生了错误,你当然希望把错误 trace 采样下来,但头部采样策略无法动态控制,很可能丢失有价值的重要观测数据。

尾部采样(tail-based):与头部采样不同,它是在最后面、请求退出时(最后一个服务)决定是否采样。这种方式很灵活——可以根据完整的可观测信息决定采样,比如有错误、有延迟就采样起来,能更智能地控制采样数据,所以在生产环境会更倾向于采用尾部采样。

它的缺点也很明显:设计复杂度更高,而且由于最终采样才决定是否丢弃,等价于每一条分布式追踪数据都先记录下来了,会导致采样时延迟更高。另外,Collector 支持尾部采样,但对于大部分以"直接发送 metrics 和 traces"方式的 SDK 而言,尾部采样支持并不是太好。

到这里,OpenTelemetry 模块的全部内容就介绍完了。接下来的模块我们会带大家做 OTel 的集成实战,并打造一个三合一的监控查询系统(统一看指标、追踪、日志)。

总结

OpenTelemetry(OTel)是"可观测性一网打尽"的统一标准,本章主线是:

  • 起源:可观测性出自 1960 年代控制论(卡尔曼),通过"系统输出判断内部状态";工业制造靠传感器,软件工程靠日志 + 指标。单体时代用系统级监控 + APM(New Relic / Datadog),微服务时代两者都不够用。
  • 微服务三大挑战:难追踪完整链路、日志分布式收集难、服务依赖与网络故障难定位 —— 由 指标 / 日志 / 分布式追踪三大基础系统性解决。监控是可观测性子集,APM 偏单服务,可观测性提供系统级微服务视图。
  • 分布式追踪原理:trace context 沿调用链传播,按协议分 HTTP header / gRPC metadata / RPC 自定义字段;Jaeger 的 uber-trace-id 由 trace id / span id / parent span id / flags 四部分组成。
  • Span 五字段:context(不可变 ID + flags + state)、attributes(操作元数据键值对)、events(带时间戳的事件点)、links(异步关联、无层级关系)、status(UNSET/ERROR/OK)。
  • Metric:类型更丰富(counter / up-down counter / gauge / histogram);三层模型(事件→数据流→时间序列);ValueRecorder 捕获后做 max / 分桶 / sum;Sum 分 Delta(时间窗口)与累计(自启动起)。
  • 关联:metrics 用 exemplar(注释形式)携带 trace/span id;logs 从 context 取 trace/span id 打印并配置日志系统。
  • 组件:Collector(供应商无关,pipeline = receiver + processor + exporter,可多 receiver / 多 processor / fan-out 到多后端);SDK 三支柱支持矩阵 trace 最成熟、logs 较差。
  • 数据流与集成:traces/metrics 各走独立 pipeline(metrics 用 transform 聚合、trace 用 connector 关联),后端 Prometheus / Jaeger / Grafana;支持 K8s、VM、Host(DaemonSet)混合部署。集成三方式:零侵入插桩 / 接入 SDK(推荐)/ 使用 API 库。
  • 采样:默认采样率 1%;低频或不允许丢弃(金融/银行)不采样;高频且大部分正常则采样。头部采样在入口按百分比决策、简单但可能丢关键数据;尾部采样按完整链路决策、更智能但复杂、延迟高,Collector 支持、多数 SDK 支持不佳。

自测题与动手练习

自测题(合上书能答出来,才算懂):

面试官
可观测性最早源于哪门学科、由谁在什么年代提出?它在工业制造里靠什么判断状态?落到软件工程领域,“输出"指哪两大类?
候选人
可观测性最早源于系统控制理论(控制论),由卡尔曼博士在 1960 年代提出。
工业制造里靠传感器输出判断状态(电机转速、压力、温度等);软件工程里输出主要指两大类:日志(Logs) 与指标(Metrics)(如请求成功率、延迟)。
面试官
单体迁移到微服务后,可观测性遇到哪三大典型挑战?为什么"监控 / APM"在微服务架构下不太够用了?
候选人
三大挑战:① 难追踪一次请求跨多个服务的完整链路、难定位瓶颈 / 错误步骤;② 日志分布在多个节点,收集、聚合、分析变复杂;③ 服务依赖、网络延迟、通信故障难发现。
监控只是可观测性的子集(收集展示指标 + 告警),APM 面向单个服务的性能分析;微服务是跨服务、跨协议的,所以要把监控 / APM 提升到可观测性,提供系统级视图。
面试官
分布式追踪的原理是什么?trace context 根据三种协议分别怎么传播?Jaeger 的 uber-trace-id 由哪四部分组成?
候选人
原理:把 trace context(追踪上下文) 沿调用链从入口一直传播到最后一个服务甚至数据库,每个服务都记录自己的 span 并把 parent span 指向上游。
传播方式:HTTP 放请求头 header;gRPC 放 metadata(类似 header 机制);RPC 没有元数据,自定义字段存储传播。
uber-trace-id = <trace_id>-<span_id>-<parent_span_id>-<flags>:trace 唯一标识、当前 span id、父 span id(构建层级)、跟踪标志位。
面试官
Span 的五个关键字段(context / attributes / events / links / status)各自起什么作用?link 和 parent span id 有什么区别?
候选人
context:不可变对象,含 trace id + span id + 跟踪标志 + 跟踪状态;
attributes:业务 / 操作元数据键值对(url / port / host 等),键和值都是非空字符串,值可为布尔 / 浮点 / 整数 / 数组;
events:带时间戳的结构化日志点,表示 span 内某个时刻发生的单一事件;
links:关联另一个 span;
status:UNSET(成功无错)/ ERROR(产生错误,标识哪个服务出错)/ OK(开发者手动标无错)。
区别:parent span id 构建层级父子关系;links 建立关联性、不一定有层级,适合异步场景(如消息被不同消费者消费)。
面试官
OTel 的 Metric 有哪些种类?它的三层数据模型是什么?Sum 的 Delta 和累计两种计算方式区别在哪?
候选人
种类:counter(只增)、up/down counter(可增可减)、gauge(异步度量当前值)、histogram(直方图)等,比 Prometheus 更丰富。
三层模型:事件模型(最初形态,用 ValueRecorder 捕获)→ 数据流模型(打包流式传给后端)→ 时间序列模型(后端存储最终形态)。
Sum 两种方式:Delta 按不重叠的时间窗口算总和(如 5 个窗口各算各的);累计(Cumulative) 从进程 / 应用启动算起,到某个时间点累计全部。
面试官
Metrics、Logs 分别怎么和 Trace 关联?为什么关联的前提与"上下文传播"有关?
候选人
Metrics 关联 Trace:以 trace 为核心,把 trace id / span id 在指标输出时通过注释 / exemplar(带 #)形式记录并注入;前提是整体微服务实现了上下文传播。
Logs 关联 Trace:从 trace context 里取出 trace id / span id,跟日志一起打印出来,再在日志系统里配置怎么取 trace id、怎么关联,就能实现 log 与 trace 联动。
面试官
Collector 的 pipeline 由哪三部分组成?为什么说它"很灵活"?它必须要用吗?
候选人
pipeline = Receiver(接收器)+ Processor(处理器)+ Exporter(导出器),负责数据接收 / 处理 / 导出。
灵活点:可以定义 N 个 receiver、N 个 processor(依次串行处理、可能过滤 / 丢弃)、N 个 exporter,导出时用 fan-out 扇出机制把数据副本发到不同后端。
它不是必须的——实战时可以不用,主要用于过滤敏感数据、构建多层 Collector 网关等复杂可观测系统。
面试官
集成 OTel 的三种方式分别是什么?为什么"接入 SDK"是最广泛推荐的方式,它的缺点是什么?
候选人
三种:零侵入插桩(字节码 / monkey patch / eBPF,不加代码,但业务指标无法自动插桩)、接入 SDK(覆盖大部分语言,推荐)、使用 API 库(封装库,一般用于二次开发)。
SDK 方式推荐是因为支持语言广、可精确控制;它的缺点是需要额外开发工作,若开发能力不在自己部门,还要跨部门协作、打通部门墙。
面试官
什么场景不用采样、什么场景该采样?头部采样和尾部采样各自的决策时机、优缺点是什么?
候选人
不采样:数据生成少(每秒几条到几十条),或业务不允许丢弃任何追踪数据(金融 / 银行),全部采集存储。
采样:每秒几百上千条且大部分请求正常时,用采样降低成本,1% 采样率较常见。
头部采样:在请求入口、trace 生成时按百分比决策,实现简单、SDK / Collector 都支持,但不能按追踪结果动态采样,可能丢失关键错误数据。
尾部采样:请求退出时按完整链路决定(有错误 / 延迟就采样),更智能、生产更倾向,但复杂度高、需先缓存整条链导致延迟更高;Collector 支持,多数直接发 metrics/traces 的 SDK 支持不佳。

动手练习(建议真做一遍):

  1. 模拟一条真实请求,画一篇最小调用链:请求进 frontend → hello → greeting → 数据库,每个环节写清楚 trace_id / span_id / parent_span_id,标出哪个是 root span,体会"共享 trace id、各自独立 span id + parent 指向上游"。
  2. 抄一遍本文的 Collector 最小配置,给它加上 filter(丢弃 /healthz 健康检查的 span)、attributes(删除 authorization 头)两个 processor,并配一个 tail_sampling,理清 pipeline 顺序为什么不能乱。
  3. 为你的一个高 QPS 服务写一份采样策略:说明选择头部还是尾部采样、采样率多少、金融场景为什么不能采样,并对比阅读给出理由。

本章小结

复习提示:
  • 可观测性源于 1960 年代控制论(卡尔曼),核心是"通过系统输出推断内部状态";工业制造靠传感器,软件工程靠日志 + 指标,微服务时代引出指标 / 日志 / 分布式追踪三大基础。
  • 监控是可观测性的子集、APM 偏单服务性能分析、可观测性面向整个微服务架构提供系统级视图;单体→微服务带来链路难追踪、日志难收集、依赖/网络故障难定位三大挑战。
  • 分布式追踪靠 trace context 沿调用链传播实现;按协议放 HTTP header / gRPC metadata / RPC 自定义字段;Jaeger 的 uber-trace-id = trace id + span id + parent span id + flags。
  • Span 五字段:context(不可变 ID/flags/state)、attributes(操作元数据)、events(带时间戳事件点)、links(异步关联、无层级)、status(UNSET/ERROR/OK);link 与 parent id 千万别混(层级 vs 关联)。
  • Metric:counter / up-down counter / gauge / histogram;三层模型(事件→数据流→时间序列);ValueRecorder 捕获后做 max/分桶/sum;Sum 有 Delta(时间窗口)与累计(自启动起)两种算法。
  • 三支柱关联是 AIOps 基础:Metrics 用 exemplar 带 trace/span id,Logs 从 context 取出并打印 trace/span id 再配置日志系统。
  • Collector 供应商无关、非必须,pipeline = receiver + processor + exporter,可多 receiver / 多 processor / fan-out 到多后端;SDK 三支柱支持 trace 最成熟、logs 较差,集成前对照语言支持矩阵。
  • 数据流上 traces/metrics 走独立 pipeline(transform 聚合 / connector 关联),后端可 Prometheus / Jaeger / Grafana,支持 K8s / VM / Host(DaemonSet) 混合部署。
  • 集成三方式:零侵入插桩 / 接入 SDK(推荐)/ API 库;采样上低频或金融场景不采样,高频且大部分正常时采样(常见 1%),头部采样简单但可能丢关键数据、尾部采样更智能但复杂延迟高。
  • 下一模块进入 OTel 集成实战,打造"指标 + 追踪 + 日志"三合一的监控查询系统。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!