主题
阶段 3:Metrics(指标)深入
一、Metrics 核心概念
1.1 什么是 Metrics?
Metrics 是对系统运行状况的数值化度量,它关注的是"系统现在怎么样?趋势如何?"
Traces 回答: "这个请求走了哪条路径?每步耗时多少?" → 单个请求的细节
Metrics 回答: "过去 5 分钟平均延迟是多少?错误率是多少?" → 全局的统计视角
Logs 回答: "10:23:45 发生了什么具体事件?" → 单个事件的详情Metrics 的核心价值
| 价值 | 说明 | 示例 |
|---|---|---|
| 告警 | 基于阈值触发通知 | 错误率 > 5% → 告警 |
| 趋势分析 | 观察指标随时间的变化 | 延迟 P99 从 100ms 逐渐升到 500ms |
| 容量规划 | 预测资源需求 | QPS 每月增长 20%,三个月后需扩容 |
| SLA/SLO 监控 | 衡量服务质量 | 99.9% 请求延迟 < 200ms |
| 业务洞察 | 用数据驱动决策 | 哪个 API 最热门?哪个地区流量最大? |
Metrics vs Traces 的定位差异
Traces(显微镜): Metrics(仪表盘):
┌────────────────────────┐ ┌────────────────────────┐
│ Trace ID: abc123 │ │ 请求总数: 125,430 │
│ ├─ Gateway 20ms │ │ 错误率: 0.3% │
│ ├─ UserSvc 5ms │ │ P50 延迟: 45ms │
│ └─ OrderSvc 15ms │ │ P99 延迟: 230ms │
│ ├─ DB 3ms │ │ 活跃连接: 342 │
│ └─ Redis 1ms │ │ CPU: 67% │
└────────────────────────┘ └────────────────────────┘
看单个请求的完整路径 看全局的整体健康状况
数据量大(每个请求一条 Trace) 数据量小(聚合后的数值)
排查具体问题 监控和告警日常类比
Metrics 就像汽车仪表盘:
- 速度表(Gauge) → 当前速度是多少?
- 里程表(Counter) → 总共跑了多少公里?
- 油量表(Gauge) → 还剩多少油?
- 转速分布(Histogram) → 引擎在各转速段运行了多少时间?
你不需要看每秒钟的行驶记录(Traces),
只需要看仪表盘(Metrics)就能知道车的状态。1.2 OpenTelemetry Metrics 架构
┌────────────────────────────────────┐
│ MeterProvider │
│ ├── Resource(服务标识信息) │
│ ├── Views(视图,自定义聚合规则) │
│ └── MetricReader(读取+导出) │
└──────────┬─────────────────────────┘
│ get_meter("name", "ver")
▼
┌────────────────────────────────────┐
│ Meter │
│ 创建各种 Instrument(仪器) │
└──────────┬─────────────────────────┘
│ create_counter() / create_histogram() / ...
▼
┌────────────────────────────────────┐
│ Instruments(仪器) │
│ ├── Counter │
│ ├── UpDownCounter │
│ ├── Histogram │
│ ├── Gauge │
│ ├── ObservableCounter │
│ ├── ObservableUpDownCounter │
│ └── ObservableGauge │
└──────────┬─────────────────────────┘
│ add() / record() / 回调
▼
┌────────────────────────────────────┐
│ Aggregation(聚合) │
│ ├── Sum(求和,默认用于 Counter) │
│ ├── LastValue(取最新值,用于 Gauge) │
│ └── ExplicitBucketHistogram │
│ (桶分布,用于 Histogram) │
└──────────┬─────────────────────────┘
│ MetricReader 定时读取
▼
┌────────────────────────────────────┐
│ MetricExporter(导出器) │
│ ├── ConsoleMetricExporter │
│ ├── OTLPMetricExporter │
│ └── PrometheusMetricReader │
└──────────┬─────────────────────────┘
│
▼
┌────────────┐
│ 后端系统 │
│ Prometheus │
│ Grafana │
└────────────┘与 Traces 架构的对比
| 维度 | Traces | Metrics |
|---|---|---|
| Provider | TracerProvider | MeterProvider |
| 创建者 | Tracer | Meter |
| 数据单元 | Span | Measurement(测量值) |
| 处理器 | SpanProcessor | MetricReader |
| 导出器 | SpanExporter | MetricExporter |
| 数据特点 | 高基数、单条记录 | 低基数、聚合后导出 |
| 导出方式 | 每条 Span 独立导出 | 聚合后按时间间隔批量导出 |
二、MeterProvider 与 Meter
2.1 MeterProvider 配置
MeterProvider 是 Metrics 子系统的入口,一个应用通常只需要一个 MeterProvider。
python
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
from opentelemetry.sdk.resources import Resource
# 1. 定义 Resource(标识你的服务)
resource = Resource.create({
"service.name": "order-service",
"service.version": "1.2.0",
"deployment.environment": "production",
})
# 2. 创建 MetricReader
# PeriodicExportingMetricReader 会定时从 MeterProvider 读取聚合数据并导出
reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(),
export_interval_millis=10000, # 每 10 秒导出一次(默认 60000ms)
export_timeout_millis=30000, # 导出超时 30 秒
)
# 3. 创建 MeterProvider
provider = MeterProvider(
resource=resource,
metric_readers=[reader],
)
# 4. 设置为全局 MeterProvider
metrics.set_meter_provider(provider)2.2 获取 Meter
Meter 是创建 Instrument 的工厂,类似 Tracer 对于 Span 的关系。
python
# 获取 Meter,传入模块名和版本
meter = metrics.get_meter(
name="order-service.metrics", # 标识产生指标的模块
version="1.0.0", # 模块版本(可选)
)
# 常见做法:按功能模块区分 Meter
http_meter = metrics.get_meter("myapp.http")
db_meter = metrics.get_meter("myapp.database")
biz_meter = metrics.get_meter("myapp.business")注意:多个 Meter 共享同一个 MeterProvider 的配置(Resource、Reader、Views),它们之间的区别仅在于 name 和 version,用于在后端标识指标来源。
2.3 MetricReader 的工作机制
MetricReader 的职责:定时从 MeterProvider 读取聚合好的指标数据,并交给 Exporter 导出。
时间轴示意:
t=0s t=10s t=20s t=30s
──┬────────┬────────┬────────┬──
│ │ │ │
│ 采集期 │ 采集期 │ 采集期 │
│ #1 │ #2 │ #3 │
│ │ │ │
└────────┘────────┘────────┘
↓ ↓ ↓
导出 #1 导出 #2 导出 #3
每个采集期内的所有 add() / record() 调用都会被聚合,
在导出时作为一个数据点输出。python
# PeriodicExportingMetricReader 参数详解
reader = PeriodicExportingMetricReader(
exporter=ConsoleMetricExporter(),
export_interval_millis=60000, # 导出间隔,默认 60 秒
export_timeout_millis=30000, # 单次导出的超时时间
)
# 也可以使用多个 Reader,将指标同时发送到多个后端
console_reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
otlp_reader = PeriodicExportingMetricReader(OTLPMetricExporter())
provider = MeterProvider(
metric_readers=[console_reader, otlp_reader],
)三、Instrument 类型详解
3.1 Instrument 分类总览
OpenTelemetry 定义了 7 种 Instrument,分为同步和异步两大类:
Instruments(仪器)
├── 同步(Synchronous)—— 在业务代码中主动调用
│ ├── Counter 只增不减的累计计数器
│ ├── UpDownCounter 可增可减的计数器
│ ├── Histogram 记录值的分布
│ └── Gauge 记录瞬时值(Python SDK 1.x 新增)
│
└── 异步(Asynchronous / Observable)—— 通过回调函数按需采集
├── ObservableCounter 异步只增计数器
├── ObservableUpDownCounter 异步可增减计数器
└── ObservableGauge 异步瞬时值同步 vs 异步的核心差异
| 维度 | 同步 Instrument | 异步 Instrument |
|---|---|---|
| 调用时机 | 业务代码中主动调用 | MetricReader 导出时回调 |
| 调用者 | 你的业务代码 | OpenTelemetry SDK |
| 适用场景 | 事件驱动(每次请求计数、每次操作记录) | 轮询采集(CPU 使用率、内存、队列深度) |
| 代码位置 | 分散在业务逻辑各处 | 集中在回调函数中 |
| 类比 | 每次投篮时记分 | 每隔 10 秒看一下比分板 |
3.2 Counter(计数器)
适用场景:累计值,只增不减。如请求总数、处理的消息数、错误总数。
python
# 创建 Counter
request_counter = meter.create_counter(
name="http.server.request.count",
description="Total number of HTTP requests received",
unit="1", # 无量纲单位用 "1"
)
# 使用:每次请求时调用 add()
def handle_request(method, route, status_code):
# add() 的第一个参数必须是非负数
request_counter.add(
1,
attributes={
"http.method": method,
"http.route": route,
"http.status_code": status_code,
},
)
# 示例调用
handle_request("GET", "/api/users", 200)
handle_request("POST", "/api/orders", 201)
handle_request("GET", "/api/users", 500)输出示例(ConsoleMetricExporter):
{
"resource_metrics": [{
"scope_metrics": [{
"metrics": [{
"name": "http.server.request.count",
"data": {
"data_points": [
{ "attributes": {"http.method": "GET", "http.route": "/api/users", "http.status_code": 200}, "value": 1 },
{ "attributes": {"http.method": "POST", "http.route": "/api/orders", "http.status_code": 201}, "value": 1 },
{ "attributes": {"http.method": "GET", "http.route": "/api/users", "http.status_code": 500}, "value": 1 }
]
}
}]
}]
}]
}关键理解:每组唯一的
attributes组合就是一个独立的时间序列(Time Series)。{"method": "GET", "route": "/api/users"}和{"method": "POST", "route": "/api/orders"}是两条不同的时间序列。
3.3 UpDownCounter(可增减计数器)
适用场景:值可以增加也可以减少的指标。如当前活跃连接数、队列中的消息数、线程池活跃线程数。
python
# 创建 UpDownCounter
active_connections = meter.create_up_down_counter(
name="http.server.active_connections",
description="Number of active HTTP connections",
unit="1",
)
# 连接建立时 +1
def on_connection_open(client_ip):
active_connections.add(1, {"net.peer.ip": client_ip})
# 连接关闭时 -1
def on_connection_close(client_ip):
active_connections.add(-1, {"net.peer.ip": client_ip})
# 模拟
on_connection_open("192.168.1.10")
on_connection_open("192.168.1.20")
on_connection_open("192.168.1.30")
on_connection_close("192.168.1.10")
# 此时 active_connections 净值为 2Counter vs UpDownCounter 的选择
问自己一个问题:这个值能减少吗?
总请求数 → Counter (只可能增加)
总错误数 → Counter (只可能增加)
总处理字节 → Counter (只可能增加)
活跃连接数 → UpDownCounter (连接打开+1,关闭-1)
队列深度 → UpDownCounter (入队+1,出队-1)
线程池大小 → UpDownCounter (创建+1,销毁-1)
CPU 使用率 → Gauge (这不是累计值,是瞬时测量值)3.4 Histogram(直方图)
适用场景:记录值的分布,不仅关心平均值,更关心分位数(P50、P90、P99)。如请求延迟分布、响应体大小分布、数据库查询耗时分布。
python
# 创建 Histogram
request_duration = meter.create_histogram(
name="http.server.request.duration",
description="HTTP request duration in milliseconds",
unit="ms",
)
# 每次请求结束时记录耗时
import time
def handle_request(method, route):
start = time.time()
# ... 处理请求 ...
process_request()
duration_ms = (time.time() - start) * 1000
request_duration.record(
duration_ms,
attributes={
"http.method": method,
"http.route": route,
},
)
# 或者用上下文管理器封装
import contextlib
@contextlib.contextmanager
def measure_duration(histogram, attributes=None):
start = time.time()
try:
yield
finally:
duration_ms = (time.time() - start) * 1000
histogram.record(duration_ms, attributes or {})
# 使用
with measure_duration(request_duration, {"http.method": "GET", "http.route": "/api/users"}):
process_request()Histogram 的默认桶边界
OpenTelemetry 的 Histogram 使用桶(Bucket) 来统计分布:
默认桶边界:[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]
假设记录了以下延迟值:[3, 8, 15, 42, 42, 78, 120, 350, 1200]
桶分布结果:
bucket ≤ 0: 0 个
bucket ≤ 5: 1 个 (3)
bucket ≤ 10: 2 个 (3, 8)
bucket ≤ 25: 3 个 (3, 8, 15)
bucket ≤ 50: 5 个 (3, 8, 15, 42, 42)
bucket ≤ 75: 5 个
bucket ≤ 100: 6 个 (+ 78)
bucket ≤ 250: 7 个 (+ 120)
bucket ≤ 500: 8 个 (+ 350)
bucket ≤ 1000: 8 个
bucket ≤ 2500: 9 个 (+ 1200)
...
bucket ≤ +Inf: 9 个
同时还会记录:
count = 9 (总记录数)
sum = 1858 (所有值的总和)
min = 3 (最小值)
max = 1200 (最大值)为什么用桶而不是精确值? 因为 Metrics 追求的是低存储开销的统计视图。存储每个请求的精确延迟代价太大(那是 Traces 的事),用桶就可以用固定的内存估算出 P50/P90/P99 等分位数。
3.5 Gauge(瞬时值)
适用场景:当前时刻的值,不需要累计。如温度、CPU 使用率、内存占用。
python
# 同步 Gauge(Python SDK 1.x 支持)
cpu_gauge = meter.create_gauge(
name="system.cpu.utilization",
description="CPU utilization percentage",
unit="%",
)
# 定期记录当前值
import psutil
def report_system_metrics():
cpu_gauge.set(psutil.cpu_percent())
# 注意:Gauge 使用 set(),直接设置当前值
# 与 Counter 的 add() 不同,Gauge 不是累加的注意:在 Python SDK 的早期版本中没有同步 Gauge,需要用 ObservableGauge(异步回调)来实现。从
opentelemetry-sdk >= 1.22.0开始,同步 Gauge 已可用。
3.6 Observable Instruments(异步仪器)
异步 Instrument 不在业务代码中主动调用,而是在 MetricReader 导出时,SDK 自动调用你注册的回调函数来采集数据。
python
import psutil
from opentelemetry import metrics
# ============ ObservableGauge ============
# 适合:轮询式获取瞬时值
def cpu_usage_callback(options):
"""每次 MetricReader 导出时,SDK 会调用此函数"""
cpu_percent = psutil.cpu_percent(percpu=True)
for i, usage in enumerate(cpu_percent):
yield metrics.Observation(
value=usage,
attributes={"cpu.core": str(i)},
)
meter.create_observable_gauge(
name="system.cpu.usage",
callbacks=[cpu_usage_callback],
description="Per-core CPU usage percentage",
unit="%",
)
# ============ ObservableCounter ============
# 适合:轮询式获取只增累计值(如系统的网络收发字节数)
def network_bytes_callback(options):
net_io = psutil.net_io_counters()
yield metrics.Observation(
value=net_io.bytes_sent,
attributes={"direction": "sent"},
)
yield metrics.Observation(
value=net_io.bytes_recv,
attributes={"direction": "received"},
)
meter.create_observable_counter(
name="system.network.bytes",
callbacks=[network_bytes_callback],
description="Total network bytes transferred",
unit="By",
)
# ============ ObservableUpDownCounter ============
# 适合:轮询式获取可增减值(如线程数、队列长度)
import threading
def thread_count_callback(options):
yield metrics.Observation(
value=threading.active_count(),
)
meter.create_observable_up_down_counter(
name="process.thread.count",
callbacks=[thread_count_callback],
description="Number of active threads",
)同步 vs 异步的选择决策树
你能在事件发生时立即记录吗?
│
├── 是 → 使用同步 Instrument
│ ├── 每次请求/事件时记录 → Counter / UpDownCounter / Histogram
│ └── 需要直接设置当前值 → Gauge
│
└── 否(需要定时采集)→ 使用异步 Instrument
├── 采集的是累计值 → ObservableCounter
├── 采集的值可增可减 → ObservableUpDownCounter
└── 采集的是瞬时值 → ObservableGauge
常见例子:
HTTP 请求计数 → Counter(每次请求时 add(1))
请求延迟 → Histogram(每次请求结束时 record())
CPU 使用率 → ObservableGauge(轮询 psutil)
网络发送字节总数 → ObservableCounter(轮询系统计数器)
线程池活跃线程 → ObservableUpDownCounter(轮询线程池状态)四、Attributes(属性)与基数控制
4.1 Attributes 的作用
Attributes 是附加在测量值上的键值对,用于维度拆分。同一个指标名 + 不同的 Attributes 组合 = 不同的时间序列。
python
# 同一个 counter,不同的 attributes 产生不同的时间序列
counter.add(1, {"http.method": "GET", "http.route": "/api/users"})
counter.add(1, {"http.method": "POST", "http.route": "/api/users"})
counter.add(1, {"http.method": "GET", "http.route": "/api/orders"})
# 在后端查询时可以灵活过滤和聚合:
# sum(http_request_count) where method="GET"
# sum(http_request_count) group by route4.2 基数爆炸(Cardinality Explosion)
这是 Metrics 领域最常见也最危险的问题。
什么是基数(Cardinality)?
= 一个指标的所有 attributes 组合产生的时间序列数量
低基数(安全):
http.method × http.route = 5 × 20 = 100 个时间序列 ✅
高基数(危险):
http.method × http.route × user.id = 5 × 20 × 100000 = 10,000,000 个时间序列 ❌基数爆炸的后果:
- 内存占用暴涨(每个时间序列都需要维护聚合状态)
- 导出数据量激增,网络带宽和后端存储压力增大
- 后端查询变慢(Prometheus 查询超时)
- 最终可能导致 OOM 或服务不可用
如何避免基数爆炸
python
# ❌ 错误做法:把高基数值放入 attributes
counter.add(1, {
"user.id": "user-12345", # 用户 ID 基数可能上百万
"request.id": "req-abc-123", # 每个请求都不同,基数无限
"http.url": "/api/users/12345", # URL 包含变量,基数随用户数增长
})
# ✅ 正确做法:使用低基数的分类值
counter.add(1, {
"http.method": "GET", # 基数 ~7(GET/POST/PUT/DELETE/PATCH...)
"http.route": "/api/users/{id}", # 路由模板,基数 = 路由数
"http.status_code": 200, # 基数 ~20
})
# 💡 原则:user_id 这样的高基数标识符应该放在 Traces 的 Span Attributes 中,
# 而不是 Metrics 的 Attributes 中。基数控制清单
| 检查项 | 说明 |
|---|---|
| 不要用 user_id / session_id | 高基数标识符放 Traces |
| 使用路由模板而非实际路径 | /api/users/{id} 而非 /api/users/12345 |
| 限制 attribute 值的种类 | 确保每个 attribute 的取值范围有限 |
| 监控时间序列数量 | 关注 Prometheus 的 scrape_series_added 指标 |
| 使用 Views 过滤不需要的 attributes | 参见下文 Views 章节 |
五、Views(视图)
5.1 Views 的作用
Views 是 MeterProvider 级别的配置,允许你自定义指标的聚合方式,而无需修改业务代码。
Views 能做什么?
- 重命名指标
- 过滤掉不需要的 attributes(降低基数)
- 修改 Histogram 的桶边界
- 更改聚合方式
- 丢弃(Drop)不需要的指标
5.2 Views 实战
python
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
# ============ 示例 1:自定义 Histogram 桶边界 ============
# 默认桶边界对于毫秒级延迟不够精细,自定义更合适的边界
latency_view = View(
instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(
boundaries=[1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000]
),
)
# ============ 示例 2:过滤 Attributes 降低基数 ============
# 只保留 method 和 status_code,丢弃 route 等高基数属性
filter_view = View(
instrument_name="http.server.request.count",
attribute_keys=["http.method", "http.status_code"], # 只保留这些 key
)
# ============ 示例 3:重命名指标 ============
rename_view = View(
instrument_name="http.server.request.count",
name="total_requests", # 导出时的指标名变为 total_requests
)
# ============ 示例 4:丢弃不需要的指标 ============
from opentelemetry.sdk.metrics.view import DropAggregation
drop_view = View(
instrument_name="debug.*", # 支持通配符匹配
aggregation=DropAggregation(),
)
# ============ 将 Views 配置到 MeterProvider ============
provider = MeterProvider(
metric_readers=[reader],
views=[latency_view, filter_view, drop_view],
)5.3 Views 的匹配规则
python
# View 可以按以下条件匹配 Instrument:
View(
instrument_name="http.*", # 按名称匹配(支持 * 通配符)
instrument_type=Counter, # 按类型匹配
meter_name="myapp.http", # 按 Meter 名称匹配
meter_version="1.0.0", # 按 Meter 版本匹配
meter_schema_url="https://...", # 按 schema URL 匹配
)
# 匹配优先级:精确匹配 > 通配符匹配 > 默认行为
# 如果没有 View 匹配到某个 Instrument,则使用默认聚合方式六、Temporality(时间性)
6.1 什么是 Temporality?
Temporality 定义了导出的数据点表示的是累计值还是增量值。
假设一个 Counter 每秒 add(1),导出间隔 10 秒:
Cumulative(累计)模式:
t=10s → 导出 value=10 (从程序启动以来的总和)
t=20s → 导出 value=20
t=30s → 导出 value=30
Delta(增量)模式:
t=10s → 导出 value=10 (最近 10 秒内的增量)
t=20s → 导出 value=10
t=30s → 导出 value=106.2 Cumulative vs Delta
| 维度 | Cumulative(累计) | Delta(增量) |
|---|---|---|
| 表示 | 从启动到现在的总值 | 本导出周期内的增量 |
| 优点 | 数据不会丢失,天然单调递增,易于告警 | 数据量小,不受重启影响 |
| 缺点 | 重启后从 0 开始,需要后端处理 | 丢失一个导出周期就丢数据 |
| 典型后端 | Prometheus(只支持 Cumulative) | OTLP / Datadog / Dynatrace |
| 默认 | Counter、Histogram 默认用 Cumulative | — |
python
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
# 使用 Delta Temporality
reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(
preferred_temporality={
Counter: AggregationTemporality.DELTA,
Histogram: AggregationTemporality.DELTA,
UpDownCounter: AggregationTemporality.CUMULATIVE,
}
),
)七、实践 Demo
Demo 1:完整的 Web 服务 Metrics
python
"""
demo_web_metrics.py
一个模拟 Web 服务的 Metrics 完整示例,展示所有常用 Instrument 类型。
运行:pip install opentelemetry-api opentelemetry-sdk psutil
python demo_web_metrics.py
"""
import time
import random
import threading
import psutil
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
from opentelemetry.sdk.resources import Resource
def setup_metrics():
resource = Resource.create({
"service.name": "demo-web-service",
"service.version": "1.0.0",
})
latency_view = View(
instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(
boundaries=[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
),
)
reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(),
export_interval_millis=5000,
)
provider = MeterProvider(
resource=resource,
metric_readers=[reader],
views=[latency_view],
)
metrics.set_meter_provider(provider)
return metrics.get_meter("demo-web-service", "1.0.0")
def create_instruments(meter):
"""创建所有 Instrument"""
instruments = {
"request_counter": meter.create_counter(
name="http.server.request.count",
description="Total HTTP requests",
unit="1",
),
"error_counter": meter.create_counter(
name="http.server.error.count",
description="Total HTTP errors",
unit="1",
),
"request_duration": meter.create_histogram(
name="http.server.request.duration",
description="HTTP request latency",
unit="ms",
),
"active_connections": meter.create_up_down_counter(
name="http.server.active_connections",
description="Current active connections",
unit="1",
),
"response_size": meter.create_histogram(
name="http.server.response.size",
description="HTTP response body size",
unit="By",
),
}
# 异步 Instrument:系统指标
def cpu_callback(options):
yield metrics.Observation(psutil.cpu_percent())
def memory_callback(options):
mem = psutil.virtual_memory()
yield metrics.Observation(mem.percent)
meter.create_observable_gauge(
name="system.cpu.usage",
callbacks=[cpu_callback],
description="CPU usage percentage",
unit="%",
)
meter.create_observable_gauge(
name="system.memory.usage",
callbacks=[memory_callback],
description="Memory usage percentage",
unit="%",
)
return instruments
ROUTES = ["/api/users", "/api/orders", "/api/products", "/api/health"]
METHODS = ["GET", "POST", "PUT", "DELETE"]
def simulate_request(instruments):
"""模拟一次 HTTP 请求"""
method = random.choice(METHODS)
route = random.choice(ROUTES)
attrs = {"http.method": method, "http.route": route}
instruments["active_connections"].add(1, attrs)
base_latency = {"GET": 30, "POST": 80, "PUT": 60, "DELETE": 40}
latency = base_latency.get(method, 50) + random.uniform(-20, 100)
time.sleep(latency / 1000)
is_error = random.random() < 0.05
status_code = random.choice([500, 502, 503]) if is_error else 200
attrs["http.status_code"] = status_code
instruments["request_counter"].add(1, attrs)
instruments["request_duration"].record(latency, attrs)
instruments["response_size"].record(random.randint(100, 10000), attrs)
if is_error:
instruments["error_counter"].add(1, attrs)
instruments["active_connections"].add(-1, {"http.method": method, "http.route": route})
def main():
meter = setup_metrics()
instruments = create_instruments(meter)
print("Starting metrics demo... (will export every 5 seconds)")
print("Press Ctrl+C to stop.\n")
try:
while True:
threads = []
for _ in range(random.randint(3, 10)):
t = threading.Thread(target=simulate_request, args=(instruments,))
t.start()
threads.append(t)
for t in threads:
t.join()
time.sleep(0.5)
except KeyboardInterrupt:
print("\nShutting down...")
metrics.get_meter_provider().shutdown()
if __name__ == "__main__":
main()Demo 2:与 Prometheus 集成
python
"""
demo_prometheus_metrics.py
将 Metrics 导出到 Prometheus 的示例。
安装:pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-prometheus
运行:python demo_prometheus_metrics.py
然后访问 http://localhost:8000 查看 Prometheus 格式的指标
"""
from prometheus_client import start_http_server
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.prometheus import PrometheusMetricReader
import time
import random
def setup_prometheus_metrics():
resource = Resource.create({"service.name": "prometheus-demo"})
# PrometheusMetricReader 会在 /metrics 端点暴露数据
prometheus_reader = PrometheusMetricReader()
provider = MeterProvider(
resource=resource,
metric_readers=[prometheus_reader],
)
metrics.set_meter_provider(provider)
return metrics.get_meter("prometheus-demo", "1.0.0")
def main():
# 启动 Prometheus HTTP server(端口 8000)
start_http_server(8000)
print("Prometheus metrics available at http://localhost:8000")
meter = setup_prometheus_metrics()
request_counter = meter.create_counter(
name="app_requests_total",
description="Total requests",
)
request_latency = meter.create_histogram(
name="app_request_duration_seconds",
description="Request duration in seconds",
unit="s",
)
while True:
method = random.choice(["GET", "POST"])
status = random.choice([200, 200, 200, 200, 500])
latency = random.uniform(0.01, 0.5)
request_counter.add(1, {"method": method, "status": str(status)})
request_latency.record(latency, {"method": method})
time.sleep(0.1)
if __name__ == "__main__":
main()Prometheus 端点输出示例(http://localhost:8000):
# HELP app_requests_total_total Total requests
# TYPE app_requests_total_total counter
app_requests_total_total{method="GET",status="200"} 142.0
app_requests_total_total{method="POST",status="200"} 98.0
app_requests_total_total{method="GET",status="500"} 12.0
# HELP app_request_duration_seconds Request duration in seconds
# TYPE app_request_duration_seconds histogram
app_request_duration_seconds_bucket{le="0.005",method="GET"} 0.0
app_request_duration_seconds_bucket{le="0.01",method="GET"} 3.0
app_request_duration_seconds_bucket{le="0.025",method="GET"} 15.0
app_request_duration_seconds_bucket{le="0.05",method="GET"} 32.0
...
app_request_duration_seconds_count{method="GET"} 154.0
app_request_duration_seconds_sum{method="GET"} 38.72Demo 3:Metrics 与 Traces 关联
python
"""
demo_metrics_with_traces.py
展示如何同时使用 Metrics 和 Traces,并通过 Exemplar 关联两者。
安装:pip install opentelemetry-api opentelemetry-sdk
运行:python demo_metrics_with_traces.py
"""
import time
import random
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
from opentelemetry.sdk.resources import Resource
resource = Resource.create({"service.name": "metrics-traces-demo"})
# 配置 Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)
# 配置 Metrics
metric_reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(),
export_interval_millis=5000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
tracer = trace.get_tracer("demo")
meter = metrics.get_meter("demo")
request_counter = meter.create_counter("demo.request.count")
request_duration = meter.create_histogram("demo.request.duration", unit="ms")
def handle_order(order_id):
with tracer.start_as_current_span("handle_order") as span:
span.set_attribute("order.id", order_id)
latency = random.uniform(10, 500)
time.sleep(latency / 1000)
attrs = {"operation": "create_order"}
request_counter.add(1, attrs)
request_duration.record(latency, attrs)
# 如果延迟过高,在 Span 中记录事件
if latency > 300:
span.add_event("high_latency_detected", {"latency_ms": latency})
return f"Order {order_id} processed in {latency:.1f}ms"
for i in range(20):
result = handle_order(f"ORD-{i:04d}")
print(result)
time.sleep(0.2)
meter_provider.shutdown()
trace_provider.shutdown()八、Semantic Conventions(语义约定)
OpenTelemetry 定义了一组标准的指标名和 attribute 名,确保不同服务、不同语言产生的指标可以统一查询。
8.1 常用 HTTP 指标语义约定
| 指标名 | 类型 | 单位 | 说明 |
|---|---|---|---|
http.server.request.duration | Histogram | s | HTTP 请求延迟 |
http.server.active_requests | UpDownCounter | 1 | 当前活跃请求数 |
http.server.request.body.size | Histogram | By | 请求体大小 |
http.server.response.body.size | Histogram | By | 响应体大小 |
8.2 常用 HTTP Attributes
| Attribute | 示例 | 说明 |
|---|---|---|
http.request.method | GET | HTTP 方法 |
url.scheme | https | URL scheme |
http.route | /api/users/{id} | 路由模板 |
http.response.status_code | 200 | 响应状态码 |
server.address | api.example.com | 服务器地址 |
network.protocol.version | 1.1 | HTTP 协议版本 |
8.3 常用系统指标语义约定
| 指标名 | 类型 | 单位 | 说明 |
|---|---|---|---|
system.cpu.utilization | Gauge | 1 | CPU 使用率 (0~1) |
system.memory.usage | UpDownCounter | By | 内存使用量 |
system.disk.io | Counter | By | 磁盘 IO 字节数 |
process.cpu.utilization | Gauge | 1 | 进程 CPU 使用率 |
参考:完整的语义约定见 OpenTelemetry Semantic Conventions
九、常见问题 QA
Q1:Counter 和 Gauge 有什么区别?什么时候用哪个?
A:核心区别在于是否累计:
| 维度 | Counter | Gauge |
|---|---|---|
| 值的变化 | 只增不减(单调递增) | 可以任意增减 |
| 语义 | 累计总量 | 当前瞬时值 |
| 重启后 | 从 0 开始重新累计 | 与历史无关,报告当前值 |
| 后端处理 | 通常用 rate() 计算速率 | 直接使用 |
决策指南:
"总共处理了多少请求?" → Counter (累计值,只增)
"当前 CPU 使用率是多少?" → Gauge (瞬时值,直接读取)
"当前有多少活跃连接?" → UpDownCounter(连接来了+1,走了-1,是累加逻辑)
"当前温度是多少?" → Gauge (直接报告当前值,不是累加的)Q2:为什么 Histogram 不直接记录精确值?桶分布有什么好处?
A:性能和存储的权衡。
精确值方案(不用 Histogram):
- 假设 QPS = 1000,每秒记录 1000 个精确值
- 1 分钟 = 60,000 个值
- 1 小时 = 3,600,000 个值
- 存储和查询成本巨大
桶分布方案(Histogram):
- 不管 QPS 多少,每个桶只存一个计数值
- 15 个桶 + count + sum + min + max = ~20 个数值
- 1 小时还是 ~20 个数值(按导出间隔聚合)
- 可以估算 P50/P90/P99
代价:
- 分位数是近似值(精度取决于桶边界设置)
- 桶边界需要预先配置如果你需要精确的分位数计算,可以考虑
ExponentialBucketHistogramAggregation(指数桶直方图),它能以更少的桶实现更高的精度。
Q3:Metrics 的 Attributes 和 Traces 的 Span Attributes 是一回事吗?
A:概念类似但有关键区别:
| 维度 | Metrics Attributes | Span Attributes |
|---|---|---|
| 影响 | 每组唯一组合 = 一个时间序列 | 只是 Span 的附加信息 |
| 基数敏感度 | 极度敏感,必须控制 | 相对宽松 |
| 能放 user_id 吗? | 不推荐(基数爆炸) | 可以 |
| 存储方式 | 聚合后的维度标签 | 单条记录的属性 |
python
# Traces:高基数 OK
span.set_attribute("user.id", "user-12345") # ✅
span.set_attribute("order.id", "ORD-67890") # ✅
# Metrics:高基数 NOT OK
counter.add(1, {"user.id": "user-12345"}) # ❌ 基数爆炸!
counter.add(1, {"http.method": "GET", "status": 200}) # ✅ 低基数Q4:PeriodicExportingMetricReader 的 export_interval_millis 设多大合适?
A:取决于场景:
| 场景 | 推荐间隔 | 理由 |
|---|---|---|
| 开发调试 | 1-5 秒 | 快速看到结果 |
| 生产环境(Prometheus) | 15-60 秒 | 与 Prometheus scrape 间隔匹配 |
| 生产环境(OTLP push) | 10-60 秒 | 平衡实时性和性能 |
| 高 QPS 生产环境 | 30-60 秒 | 减少导出开销 |
python
# 开发环境
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
# 生产环境
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=30000)Q5:Observable(异步)Instrument 的回调函数有什么限制?
A:
- 回调必须快速返回:SDK 在导出线程中调用回调,长时间阻塞会影响导出。
- 不要在回调中做 IO 密集操作:如数据库查询、HTTP 请求。
- 回调异常不会传播:SDK 会捕获异常并跳过该数据点。
- 每次回调应返回完整数据:不要依赖上一次回调的状态。
python
# ❌ 不好的做法
def bad_callback(options):
result = requests.get("http://monitoring-api/cpu") # HTTP 请求,太慢
yield metrics.Observation(result.json()["cpu"])
# ✅ 好的做法
def good_callback(options):
yield metrics.Observation(psutil.cpu_percent()) # 快速的本地调用Q6:如何给一个已有的 FastAPI/Flask 应用快速添加 Metrics?
A:推荐使用自动检测(auto-instrumentation),零代码修改:
bash
# 1. 安装
pip install opentelemetry-distro
pip install opentelemetry-instrumentation-fastapi # 或 flask
pip install opentelemetry-exporter-otlp
# 2. 自动安装所有可用的 instrumentation
opentelemetry-bootstrap -a install
# 3. 启动应用
OTEL_SERVICE_NAME=my-fastapi-app \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
opentelemetry-instrument uvicorn app:app --host 0.0.0.0 --port 8000自动检测会为你添加标准的 HTTP 指标(http.server.request.duration 等),无需修改代码。
如果需要添加自定义业务指标,则需要在代码中手动创建 Instrument:
python
from opentelemetry import metrics
meter = metrics.get_meter("myapp.business")
order_counter = meter.create_counter("business.orders.created")
@app.post("/orders")
async def create_order(order: OrderRequest):
result = await process_order(order)
order_counter.add(1, {"order.type": order.type, "region": order.region})
return resultQ7:如何在 Prometheus 中查询 OpenTelemetry 导出的 Histogram 指标?
A:OpenTelemetry Histogram 导出到 Prometheus 后会自动转换为 Prometheus 的 histogram 类型:
promql
# 计算 P50 延迟
histogram_quantile(0.5, rate(http_server_request_duration_bucket[5m]))
# 计算 P99 延迟
histogram_quantile(0.99, rate(http_server_request_duration_bucket[5m]))
# 计算平均延迟
rate(http_server_request_duration_sum[5m]) / rate(http_server_request_duration_count[5m])
# 计算 QPS
rate(http_server_request_duration_count[5m])
# 计算错误率
sum(rate(http_server_request_duration_count{http_response_status_code=~"5.."}[5m]))
/
sum(rate(http_server_request_duration_count[5m]))Q8:为什么我的 Metrics 在程序重启后从 0 开始了?这不会导致告警误报吗?
A:这是 Cumulative Temporality 的已知特征。
重启前 重启后
┌─────────────────────┐ ┌───────────────────
│ value: 1000 ────── │ → │ value: 0 ──────
└─────────────────────┘ │ value: 1
│ value: 2 ...
Counter 从 0 重新开始,曲线出现"断崖式下跌"。Prometheus 的处理:
rate()和increase()函数内置了重置检测。当检测到值变小(counter reset),Prometheus 会自动跳过这个断点,而不是把它算成负增长。- 所以生产中用
rate(counter[5m])而不是直接用counter的绝对值。
OTLP 的处理:
- 使用 Delta Temporality 可以完全避免这个问题,因为每次导出的都是增量值。
Q9:同一个指标名可以创建多次吗?会冲突吗?
A:可以创建多次,只要 name、unit、description 一致,SDK 会返回同一个 Instrument 实例:
python
# 在文件 A 中
counter_a = meter.create_counter(name="request.count", unit="1", description="Total requests")
# 在文件 B 中
counter_b = meter.create_counter(name="request.count", unit="1", description="Total requests")
# counter_a 和 counter_b 指向同一个底层 Instrument
# 在两个文件中 add() 的值会合并到同一个指标但如果 name 相同而 unit 或 description 不同,SDK 的行为取决于实现版本——有的会警告,有的会覆盖。最佳实践是统一在一处创建 Instrument,然后传递引用。
Q10:Metrics 的数据模型中,什么是 "数据点(Data Point)"?
A:
一个 Metric 的数据结构:
Metric
├── name: "http.server.request.count"
├── description: "Total HTTP requests"
├── unit: "1"
└── data: Sum
├── temporality: CUMULATIVE
├── is_monotonic: true
└── data_points:
├── DataPoint
│ ├── attributes: {method="GET", status=200}
│ ├── start_time: 2025-03-15T10:00:00Z
│ ├── time: 2025-03-15T10:01:00Z
│ └── value: 1523
├── DataPoint
│ ├── attributes: {method="POST", status=201}
│ ├── start_time: 2025-03-15T10:00:00Z
│ ├── time: 2025-03-15T10:01:00Z
│ └── value: 487
└── ...
每一组唯一的 attributes 组合对应一个 DataPoint。
DataPoint 是最终导出给后端的最小数据单元。十、小结与知识图谱
Metrics 子系统
│
┌─────────────────┼─────────────────┐
│ │ │
MeterProvider Meter Exporter
(全局配置) (创建 Instrument) (数据导出)
│ │ │
┌─────┼─────┐ ┌────┼────────┐ ┌───┼────┐
│ │ │ │ │ │ │ │ │ │
Resource Views Reader Cnt UpDn His Gau OTLP Prom Console
│ │
┌────┼────┐ Observable*
│ │ (异步版本)
Temporality Interval
(累计/增量) (导出间隔)本阶段核心收获:
- 理解 Metrics 在可观测性中的定位:全局统计视角,支撑告警和趋势分析
- 掌握 MeterProvider → Meter → Instrument 的层级关系
- 熟练使用 4 种同步 Instrument:Counter、UpDownCounter、Histogram、Gauge
- 理解 3 种异步 Instrument 的使用场景和回调机制
- 掌握 Views 的自定义聚合能力(桶边界、属性过滤、重命名、丢弃)
- 理解 Attributes 与基数控制的重要性,避免基数爆炸
- 了解 Temporality(累计 vs 增量)的区别和选择
- 能够将 Metrics 与 Prometheus 集成并使用 PromQL 查询
- 理解 Semantic Conventions 对指标命名的规范化价值
下一阶段预告:阶段 4 将深入 Logs,学习结构化日志、日志与 Traces 的关联、LogRecord 和 LoggerProvider 的使用。