6.9 OpenTelemetry 与统一可观测性
预计阅读时间:11 分钟
📖 目录
学习目标
- 理解 Metrics / Logs / Traces 三大可观测性支柱及 OpenTelemetry 的定位
- 掌握 OTel Collector 的 Pipeline 架构与核心配置
- 学会使用 OTel SDK 对 Python / Node.js / Java 应用做自动插桩
- 了解 Jaeger 与 Grafana Tempo 两种链路追踪后端的部署与用法
- 能够在 Grafana 中配置 Metrics↔Logs↔Traces 的关联跳转
核心知识
| 概念 | 说明 |
|---|---|
| OpenTelemetry | CNCF 毕业项目(2025 年 3 月从孵化器毕业),提供统一的 API、SDK、协议(OTLP)采集遥测数据 |
| OTLP | OpenTelemetry Protocol,gRPC(4317)与 HTTP(4318)双通道传输 |
| Collector | 代理/网关模式,通过 Receivers → Processors → Exporters 三段式 Pipeline 处理数据 |
| Auto-instrumentation | 通过字节码注入或 monkey-patch 零代码改动即可采集 Traces 和 Metrics |
| Sampling | 按比例或按策略降采样以控制成本,推荐 tail-based sampling 保留错误/慢请求 |
| TraceQL | Tempo 的查询语言,支持基于 Span 属性和结构的高阶聚合检索 |
知识关联
- 前置知识:3.12:系统监控与告警 系统监控方案(Prometheus/Grafana 基础)、2.7:日志与故障排查 日志与故障排查(日志概念)
- 后续影响:4.8:集中式日志管理 集中式日志管理(Loki 集成)、5.12:eBPF 基础 eBPF 基础(内核级可观测性补充 OTel 覆盖不到的深度指标)
- 配套技术:OTel Collector + Prometheus + Jaeger/Tempo + Loki 构成完整可观测性栈,OTLP 协议实现数据面统一
原理讲解
传统运维依赖单一日志,"出了事再看"。现代微服务和分布式系统跨越成百上千个进程,一个用户请求可能穿越 10+ 服务,任何一个环节的异常都可能导致整体体验下降。可观测性(Observability) 要求系统能通过外部输出来回答"内部发生了什么"。
三大支柱职责清晰:
| 支柱 | 数据类型 | 回答的问题 |
|---|---|---|
| Metrics | 数值时间序列 | 系统健康吗?P99 延迟多少?错误率? |
| Logs | 结构化/非结构化文本 | 具体发生了什么?错误栈、SQL 慢查询? |
| Traces | 带上下文的 Span DAG | 哪个环节耗时最长?请求走了哪条路径? |
OpenTelemetry 的作用:提供一套不绑定厂商的 API/SDK 和协议,让应用一次插桩,数据就能发送到任意后端(Jaeger、Tempo、Prometheus、Loki 等)。Collector 位于应用和后端之间,负责协议转换、缓冲、过滤和采样。
┌─────────┐ OTLP ┌──────────────────────┐ OTLP/Prom/HTTP ┌──────────┐
│ 应用 A │──────────→│ OTel Collector │──────────────────→│ Jaeger │
│ (SDK) │ │ Receivers │ │ (Trace) │
└─────────┘ │ ↓ │ └──────────┘
┌─────────┐ OTLP │ Processors (batch, │ Prometheus HTTP ┌──────────┐
│ 应用 B │──────────→│ memory_limiter, │──────────────────→│Prometheus│
│ (SDK) │ │ filter, sampler) │ │ (Metric) │
└─────────┘ │ ↓ │ └──────────┘
┌─────────┐ OTLP │ Exporters │ Loki HTTP ┌──────────┐
│ Nginx │──────────→│ (otlp/prometheus/ │──────────────────→│ Loki │
└─────────┘ │ loki/otlp) │ │ (Log) │
└──────────────────────┘ └──────────┘
Metrics——健康仪表盘:指标是数值型时间序列,回答"系统现在健康吗"。指标体积小、可长期保存,适合告警与容量规划。核心注意点是基数(Cardinality):把用户 ID、请求 URL 等无限取值放进取样标签会造成存储爆炸,标签取值组合数必须可控。
Logs——事件真相:日志回答"具体发生了什么"。日志要集中收集(Loki/ELK)并尽可能结构化(JSON),且必须包含 trace_id / service.name 等关联字段,否则只是"能搜的文本",无法与链路对接。
Traces——调用链路地图:一个 Trace 由若干 Span 组成有向无环图,每个 Span 记录操作名称、起止时间、父引用与属性。跨进程传播依赖 W3C traceparent 请求头(格式 00-{trace_id}-{span_id}-{flags}),SDK 自动注入 HTTP/gRPC 请求,服务间才能拼出完整链路。
三大支柱如何联动:典型排障路径是——Metrics 告警(P99 延迟飙升)→ 跳转 Trace(发现支付服务耗时占 80%)→ 跳转该 Span 的日志(定位到 SQL 慢查询)。Grafana 的 Data Link 与 Loki derived fields 正是完成这三跳的胶水。
OpenTelemetry 架构四件套:整套体系由四个组件构成,职责边界清晰:
| 组件 | 部署位置 | 职责 |
|---|---|---|
| API | 应用内(库) | 定义 Trace/Meter/Logger 接口,零开销探针 |
| SDK | 应用内(库) | 实现采样、上下文传播、批处理与导出 |
| Instrumentation | 应用内(插件) | 自动插桩:HTTP 框架、数据库驱动、消息队列客户端 |
| Collector | 独立进程 | 接收 → 处理 → 导出三段式 Pipeline,协议转换与缓冲 |
Collector 有两种部署形态:Agent 模式(每台主机一个,随应用部署、就近接收)与 Gateway 模式(集中式集群,做采样、脱敏、多后端分发)。生产标准做法是 Agent 收集 → Gateway 汇流 → 后端存储,SDK 永远不直写存储后端。
示例代码
1. Collector 配置(otel-collector-config.yaml)
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
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp/tempo:
endpoint: tempo.monitoring:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
loki:
endpoint: http://loki.monitoring:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
2. Python 自动插桩
pip install opentelemetry-distro opentelemetry-exporter-otlp
# 输出: Successfully installed opentelemetry-api-1.28.0 opentelemetry-sdk-1.28.0 ...
opentelemetry-bootstrap -a install
# 输出: Instrumenting flask requests urllib ...
OTEL_SERVICE_NAME=my-python-app \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_TRACES_SAMPLER=parentbased_traceidratio \
OTEL_TRACES_SAMPLER_ARG=0.1 \
opentelemetry-instrument python app.py
3. Node.js 手动初始化
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-proto
# 输出: added 342 packages in 5s
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
4. Java Agent 注入
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=my-java-app \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.traces.sampler=traceidratio \
-Dotel.traces.sampler.arg=0.1 \
-jar app.jar
5. Grafana 联动配置
# Loki derived fields — 从日志行提取 trace_id 跳转到 Tempo
# 正则: trace_id=(\w+)
# URL: ${__value.raw} → http://grafana:3000/explore?orgId=1&left=["now-1h","now","tempo",{"query":"${value}"}]
# Metrics Panel Data Link — 从高延迟跳转到 Loki 日志
# 字段: ${__value.raw} → 打开 Loki 查询
# TraceQL 示例(Tempo 中查询所有 5xx POST 请求)
# { .http.method = "POST" && .http.status_code >= 500 } | count() > 10
6. Python 手动埋点(自定义 Span 与属性)
pip install opentelemetry-sdk opentelemetry-api
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
tracer = trace.get_tracer("order-service")
with tracer.start_as_current_span("checkout.process") as outer:
outer.set_attribute("order.id", "A-20260731-001")
with tracer.start_as_current_span("payment.deduct") as inner:
inner.set_attribute("payment.method", "alipay")
# 业务代码...
inner.record_exception(RuntimeError("balance insufficient"))
inner.set_status(trace.Status(trace.StatusCode.ERROR))
7. Go 自动埋点
# 依赖
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
# main.go:初始化 TracerProvider(OTLP 导出 + 服务名资源)
exporter, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure())
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL, semconv.ServiceName("go-order-api"))))
otel.SetTracerProvider(tp)
# HTTP 服务一行接入:用 otelhttp 包装 handler
mux.Handle("/api/order",
otelhttp.NewHandler(orderHandler, "order.api"))
Trace 采样策略
全量采集成本极高(单个 Span 约 1-2 KB,高流量服务每秒产生数万 Span),生产必须采样。两种主流策略对比:
| 维度 | Head-based(头部采样) | Tail-based(尾部采样) |
|---|---|---|
| 决策时机 | 请求入口(Span 创建时) | 完整 Trace 聚合后(Collector 端) |
| 保留对象 | 按比例随机保留 | 精确保留错误 / 慢请求 / 指定路径 |
| 数据一致性 | 依赖 parentbased_* 前缀保证父子一致 | 天然完整(先全量缓冲再筛选) |
| 资源开销 | 低(入口一次判断) | 高(需缓冲全部 Trace 等待决策) |
| 典型配置 | traceidratio + 1%-10% | Collector tail_sampling processor |
关键原则:head-based 采样必须用 parentbased_traceidratio 而非 traceidratio,否则同一 Trace 的父子 Span 可能分别被保留/丢弃,链路断裂。开发环境 100% 采样,生产 1%-10% 起步,成本允许再上调。
Tail-based 配置示例(Collector):
processors:
tail_sampling:
decision_wait: 10s # 等待完整 Trace 的最大时间
num_traces: 50000 # 内存中最多缓冲的 Trace 数
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 1000 }
- name: sample-ratio
type: probabilistic
probabilistic: { sampling_percentage: 10 }
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
实战案例:微服务链路追踪定位慢请求
背景:订单系统由 gateway → order → inventory → payment 四个服务组成(Python + Go 混合),用户反馈下单耗时从 200ms 涨到 3 秒。
第一步看 Metrics:Grafana 中 gateway 的 P99 延迟从 250ms 飙升至 3.2s,错误率平稳——排除宕机与 500 风暴,锁定为单请求变慢。
第二步看 Trace:Tempo 中过滤慢请求({ resource.service.name = "gateway" } | duration > 2s),对比正常与异常两个 Trace 的 Span 瀑布图:
Trace A(正常,200ms) Trace B(异常,3s)
gateway: 201ms gateway: 3012ms
├─ order.place: 180ms ├─ order.place: 2950ms
│ ├─ inventory.check: 120ms │ ├─ inventory.check: 120ms
│ └─ payment.deduct: 40ms │ └─ payment.deduct: 2750ms
└─ db.query: 15ms └─ db.query: 18ms
第三步看 Logs:从慢 Trace 的 payment.deduct Span 取出 trace_id,在 Loki 中过滤对应日志,发现大量 retry: connection timeout to payment-gw (3rd attempt)。
定位与修复:payment 服务依赖的外部支付网关连接池配置了 3 秒超时且重试 3 次,网关侧抖动导致每次请求空等。将连接超时降到 800ms 并加熔断后,P99 恢复 240ms。
复盘沉淀:为 payment 服务补充 payment.gateway.latency 指标与告警;为网关超时日志统一注入 span_id;此后同类问题可从告警一步跳到 Trace 再跳 Logs,全程不切换工具。
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| Collector 启动后日志无数据进入 | 端口被占用或防火墙未放行 4317/4318 | ss -tlnp | grep 4317 检查监听 |
| SDK 报错 "Failed to export span" | OTLP endpoint 配置错误或 Collector 未就绪 | 验证 OTEL_EXPORTER_OTLP_ENDPOINT 可达 |
| Trace 不完整,缺少中间服务 | 采样策略不一致,导致父子 Span 被分别丢弃 | 使用 parentbased_traceidratio 确保一致性 |
| 日志中有 trace_id 但无法跳转 | Loki derived fields 正则不匹配日志格式 | 用 grep -oP 'trace_id=(\w+)' 确认实际输出 |
| Grafana 中看不到任何 Trace | Tempo 数据源未配置或 Collector exporter 指向错误 | curl tempo:4317 测试 gRPC 连通性 |
| Metrics 或 Traces 存储膨胀过快 | 未开启采样或 batch processor 配置不当 | 设置 sampler=traceidratio 降至 1-10% |
最佳实践
- 始终在生产环境部署 OTel Collector,而不是让 SDK 直写后端
- Collector 配置三段:
receivers: [otlp]→processors: [memory_limiter, batch]→exporters: [...] - Trace 采样用
parentbased_traceidratio+10%;生产用 tail-based sampling(保留错误/慢请求) - 日志中包含
trace_id和service.name字段,方便 Grafana 关联跳转 - Grafana 每个 Panel 的 Data Link 至少配置 Log 和 Trace 两个跳转目标
- 使用官方
docker-otel-lgtm一键启动 Tempo + Loki + Prometheus + Collector 全套 - 定期监控 Collector 自身的 CPU/内存使用,设置
memory_limiter防止 OOM - 用
OTEL_RESOURCE_ATTRIBUTES统一注入环境信息(region、version、环境名),Span 属性是后续过滤与聚合的基础 - 给 Collector 配置
health_check/pprof扩展并导出自身指标到 Prometheus,避免"观测系统自身失明"
练习题
- 部署 OTel Collector 并配置接收 OTLP gRPC,将 Traces 转发至本地启动的 Jaeger all-in-one。
- 用 Python 编写一个简单的 Flask 应用,通过自动插桩在访问后能看到 Jaeger 中的 Trace。
- 在 Collector 配置中添加 Prometheus exporter,验证
curl localhost:8889/metrics输出指标。 - 使用 Grafana docker-otel-lgtm 套件启动全部组件,配置一个 Loki derived field 从日志提取 trace_id 跳转到 Tempo。
- TraceQL 查询:找出过去 1 小时内所有 HTTP 响应码 >= 400 的 Trace 并统计占比。
点击查看答案
- Collector 配置
receivers: otlp: protocols: grpc:+exporters: jaeger: endpoint: localhost:14250。Jaeger 用docker run -d --name jaeger -p 16686:16686 -p 14250:14250 jaegertracing/all-in-one。 - 安装
opentelemetry-distro opentelemetry-exporter-otlp。OTEL_SERVICE_NAME=flask-app OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 opentelemetry-instrument python app.py。访问后 Jaeger 显示 trace。 - Collector config 加
exporters: prometheus: endpoint: 0.0.0.0:8889。curl localhost:8889/metrics应输出otel_*指标。 - LGTM 套件启动后用 Loki 查询日志,配置 derived fields 正则提取 trace_id。从日志行直接链接到 Tempo 的 trace 视图。
{ .http.status_code >= 400 } | stats count() by (.http.status_code)。Tempo 返回匹配 Trace 列表和统计。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 OpenTelemetry 的三大信号和 SDK 架构 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 OTel Collector 部署和应用埋点 | 在终端实际执行 |
| 原理掌握 | 能说出 OpenTelemetry 的 Trace、Metrics、Logs 关联原理 | 画出流程图 |
| 故障排查 | 能独立排查遥测数据丢失或采样率不正确的问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么需要统一可观测性标准而非使用多种私有方案 | 对比不同方案 |
本章总结
速查表
| 组件 | 用途 |
|---|---|
| OTel SDK | 应用内自动/手动插桩生成遥测数据 |
| OTel Collector | 接收→处理→导出三段式 Pipeline |
| OTLP 协议 | 统一遥测传输格式(gRPC/HTTP) |
| Jaeger/Tempo | Trace 存储与查询后端 |
| Prometheus | Metrics 存储与告警 |
| Loki | Logs 聚合与查询 |
OpenTelemetry 是 CNCF 主推的统一遥测数据标准,核心生态包含:
- OTLP 协议——gRPC/HTTP 双通道,SDK→Collector→后端的标准通讯格式
- Collector——三段式 Pipeline(Receiver→Processor→Exporter),缓冲/采样/转发
- SDK 自动插桩——Python/Node.js/Java 等语言零代码或一行启动
- Jaeger / Tempo——链路追踪后端,配合 TraceQL 做聚合分析
- Grafana——统一可视化层,配置 Data Link 实现 Metrics↔Logs↔Traces 互跳
推荐实施路线:部署 LGTM 栈(Loki + Grafana + Tempo + Metrics/Prometheus + Collector)→ 关键服务开启自动插桩 → 设置 tail-based sampling → 配置 Grafana 关联跳转。