第2章:OpenTelemetry 入门

2.1 OpenTelemetry 项目介绍

说到可观测性,就绕不开 OpenTelemetry。我刚开始接触这个项目时,它还没合并,那时候叫 OpenTracing 和 OpenCensus。后来 CNCF 把它们整合到一起,才有了现在的 OpenTelemetry。说白了,它就是一套标准化的数据采集框架。

你想想看,以前每个监控系统都有自己的 SDK。Prometheus 用一套,Jaeger 用另一套。业务代码里塞满了各种厂商的客户端。迁移成本高得吓人。OpenTelemetry 要解决的就是这个问题——统一数据格式,一次接入,到处可用。

核心定位:OpenTelemetry 不是监控平台,也不是 APM 系统。它只负责采集和导出数据。至于数据怎么存、怎么展示,那是后端的事。

我在项目中遇到过最头疼的事:微服务架构下,不同团队用不同的链路追踪方案。A 团队用 SkyWalking,B 团队用 Zipkin。排查跨服务问题时,数据根本对不上。后来统一接入 OpenTelemetry,才算真正打通了。

2.2 核心概念:Signal

OpenTelemetry 把可观测性数据抽象成三种 Signal(信号)。嗯,这里要注意,不是所有系统都这么分,但这是业界共识。

Signal 类型 描述 典型场景
Traces(链路) 记录请求在分布式系统中的完整路径 排查慢请求、定位故障点
Metrics(指标) 聚合后的数值数据,如 QPS、延迟、错误率 监控告警、容量规划
Logs(日志) 带时间戳的文本记录 详细排查、审计追踪

我个人习惯把这三者比作「侦探工具」:Traces 是行动路线图,Metrics 是统计报表,Logs 是现场笔录。单独看哪个都不够,结合起来才能还原真相。

2.3 核心概念:Span

Span 是链路追踪的基本单元。每个 Span 代表一次操作,比如一次数据库查询、一次 RPC 调用。多个 Span 通过父子关系组成 Trace。

一个 Span 包含这些关键字段:

  • SpanId:唯一标识
  • TraceId:所属 Trace 的 ID
  • ParentSpanId:父 Span 的 ID
  • Name:操作名称,比如 "SELECT users"
  • StartTime / EndTime:起止时间
  • Attributes:自定义属性,比如 SQL 语句、HTTP 状态码
  • Events:时间点事件,比如异常发生
  • Status:成功或失败

小技巧:Span 的 Name 要遵循统一命名规范。我见过有人把 SQL 全文塞进去,结果存储成本暴涨。建议用「操作类型 + 资源名」的格式,比如 "HTTP GET /api/users"。

我曾经踩过一个坑:忘记设置 Span 的 Status。结果排查问题时,明明接口返回了 500,链路里却显示成功。嗯,从那以后我强制要求所有 Span 必须显式设置状态。

2.4 核心概念:Metric

Metric 是聚合后的数值数据。OpenTelemetry 支持三种基本类型:

  • Counter:只增不减的计数器,比如请求总数
  • Histogram:分布统计,比如请求延迟的 P50、P99
  • Gauge:可增可减的瞬时值,比如当前连接数

为什么要有这些类型?说白了,不同场景需要不同的聚合方式。Counter 适合统计总量,Histogram 适合分析分布,Gauge 适合监控水位。

我记得有一次线上告警:某个接口的 P99 延迟突然飙升到 5 秒。用 Counter 只能看到请求量没变,用 Histogram 才发现是少数大请求拖慢了整体。这就是 Metric 类型选择的重要性。

注意:Metric 的维度(Attributes)不要太多。每个唯一组合都会产生一个时间序列。我曾经见过一个团队给 Metric 加了 10 个维度,结果 Prometheus 直接 OOM。建议控制在 5 个以内。

2.5 架构组件

OpenTelemetry 的架构分为三层:

  1. API:定义数据模型和接口,不依赖具体实现
  2. SDK:实现 API,提供采样、导出等能力
  3. Exporter:将数据发送到后端,比如 Jaeger、Prometheus

你想想看,这种分层设计的好处是什么?API 层保证标准统一,SDK 层提供灵活配置,Exporter 层实现解耦。换后端只需要改 Exporter,业务代码完全不用动。

下面是一个简单的 Go 代码示例,展示如何创建 Span:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

func handleRequest(ctx context.Context) {
    tracer := otel.Tracer("my-service")
    ctx, span := tracer.Start(ctx, "handle-request")
    defer span.End()

    // 添加属性
    span.SetAttributes(
        attribute.String("http.method", "GET"),
        attribute.Int("http.status_code", 200),
    )

    // 记录事件
    span.AddEvent("processing started")
    // ... 业务逻辑
    span.AddEvent("processing completed")
}

我个人习惯在 SDK 初始化时配置采样率。生产环境一般设 0.1% 到 1%,开发环境可以设 100%。采样率太低会丢失数据,太高又浪费存储。这个平衡点需要根据业务量来调。

2.6 避坑指南

最后分享几个我踩过的坑:

  • Span 泄漏:忘记调用 End(),导致 Span 一直不关闭。内存会慢慢涨上去。建议用 defer 确保释放。
  • TraceId 传播:跨服务时,必须手动传递 TraceId。我见过有人忘了在 HTTP Header 里传,结果链路断成两截。
  • Exporter 阻塞:默认的 Exporter 是同步的,如果后端挂了,业务也会卡住。建议配置异步导出或设置超时。

我曾经在生产环境遇到过 Exporter 阻塞导致接口超时。排查了半天才发现是 Jaeger 挂了,Exporter 一直在重试。从那以后,我所有项目都强制开启异步导出。

好了,这一章就到这里。下一章我们会深入 SDK 的配置细节,包括采样策略、资源属性、批处理等。到时候再聊。