前言
Prometheus 的核心是时间序列数据。理解它的数据模型------指标类型、标签、时间序列------是写好 PromQL 和做好监控的前提。本篇深入讲解数据模型的每个细节。
一、时间序列(Time Series)
什么是时间序列
一个时间序列 = 指标名 + 标签组合 + 一系列带时间戳的值
http_requests_total{method="GET", status="200", handler="/api"}
→ [(t1, 100), (t2, 120), (t3, 135), (t4, 140), ...]
其中:
- 指标名: http_requests_total
- 标签: {method="GET", status="200", handler="/api"}
- 数据点: (时间戳, 值)
时间序列的标识
时间序列唯一标识 = 指标名 + 所有标签的键值对
http_requests_total{method="GET", status="200"} ← 序列 A
http_requests_total{method="GET", status="500"} ← 序列 B
http_requests_total{method="POST", status="200"} ← 序列 C
每个唯一的标签组合就是一个独立的时间序列。标签值的组合数量称为基数(cardinality)。
基数问题
✅ 低基数(推荐):
http_requests_total{method="GET", status="200"} → ~10 个序列
http_requests_total{method="POST", status="500"} →
http_requests_total{method="DELETE", status="404"} →
❌ 高基数(禁止):
http_requests_total{user_id="user_12345", request_id="req_abc123"}
→ 每个用户×每个请求 = 新序列
→ 100万用户 = 100万序列 → TSDB 爆炸
⚠️ 踩坑提示:高基数是 Prometheus 性能的头号杀手。永远不要用 user_id、email、IP 地址等高维值作为标签。
二、四种指标类型
1. Counter(计数器)
特征 :只增不减(除非重启归零),适合用 rate() 计算速率。
http_requests_total
时间 t1: 100
时间 t2: 120 (+20)
时间 t3: 135 (+15)
时间 t4: 140 (+5)
典型场景:请求总数、错误总数、处理的字节数。
PromQL 用法:
promql
# 错误:直接用 Counter 值没意义
http_requests_total # 只是当前累计值
# 正确:用 rate() 计算每秒速率
rate(http_requests_total[5m]) # 过去 5 分钟平均每秒请求数
# 用 increase() 计算总量增长
increase(http_requests_total[1h]) # 过去 1 小时增加了多少请求
应用埋点示例:
python
from prometheus_client import Counter
http_requests_total = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
# 使用
http_requests_total.labels(
method='GET',
endpoint='/api/users',
status='200'
).inc()
2. Gauge(仪表盘)
特征:可增可减,反映当前状态。
memory_usage_bytes
时间 t1: 2147483648 (2GB)
时间 t2: 2254857830 (2.1GB)
时间 t3: 1879048192 (1.75GB) ← 可以下降
时间 t4: 2013265920 (1.875GB)
典型场景:内存使用量、CPU 使用率、队列长度、连接数、温度。
PromQL 用法:
promql
# 直接查询当前值
memory_usage_bytes
# 计算变化率
deriv(memory_usage_bytes[5m]) # 每秒变化量
# 预测
predict_linear(memory_usage_bytes[1h], 4*3600) # 4小时后预测值
# 超过阈值
memory_usage_bytes > 8e+09 # 大于 8GB
应用埋点示例:
python
from prometheus_client import Gauge
queue_size = Gauge(
'message_queue_size',
'Current message queue size',
['queue_name']
)
# 使用
queue_size.labels(queue_name='orders').set(42)
queue_size.labels(queue_name='orders').inc() # +1
queue_size.labels(queue_name='orders').dec() # -1
3. Histogram(直方图)
特征:将数据分桶统计,适合计算分位数。
http_request_duration_seconds
Bucket le="0.005": 10 # ≤5ms 的请求 10 个
Bucket le="0.01": 50 # ≤10ms 的请求 50 个(包含上面 10 个)
Bucket le="0.025": 200 # ≤25ms 的请求 200 个
Bucket le="0.05": 350
Bucket le="0.1": 450
Bucket le="0.25": 490
Bucket le="0.5": 498
Bucket le="1": 499
Bucket le="2.5": 499
Bucket le="+Inf": 500 # 所有请求
Sum: 45.2 # 总耗时
Count: 500 # 总请求数
典型场景:请求延迟、响应大小。
PromQL 用法:
promql
# 计算 P99 延迟
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 计算 P50(中位数)
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))
# 平均延迟
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])
应用埋点示例:
python
from prometheus_client import Histogram
request_duration = Histogram(
'http_request_duration_seconds',
'HTTP request duration',
['method', 'endpoint'],
buckets=(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0)
)
# 使用
with request_duration.labels(method='GET', endpoint='/api/users').time():
# 处理请求
handle_request()
4. Summary(摘要)
特征:客户端直接计算分位数,不需要聚合。
http_request_duration_seconds
Quantile 0.5: 0.012 # 中位数 12ms
Quantile 0.9: 0.045 # P90 45ms
Quantile 0.99: 0.150 # P99 150ms
Sum: 45.2
Count: 500
典型场景:单实例延迟监控(不跨实例聚合)。
Summary vs Histogram 对比:
| 特性 | Histogram | Summary |
|---|---|---|
| 分位数计算 | 服务端(PromQL) | 客户端 |
| 可聚合 | ✅ 支持跨实例聚合 | ❌ 不可聚合 |
| 灵活性 | 高(任意分位数) | 低(预定义分位数) |
| 客户端开销 | 低 | 高(需维护流式分位数) |
| 推荐场景 | 生产环境 | 特殊单实例场景 |
⚠️ 踩坑提示:Summary 的分位数不能跨实例聚合。如果你有 3 个实例,每个的 P99 都是 100ms,合并后的 P99 不是 100ms。要用 Histogram。
三、标签(Labels)深入
标签的作用
没有标签:
http_requests_total = 1000
→ 只知道"总共 1000 个请求"
有标签:
http_requests_total{method="GET", status="200", service="user-service"} = 800
http_requests_total{method="POST", status="201", service="user-service"} = 150
http_requests_total{method="GET", status="500", service="user-service"} = 50
→ 知道每个方法、每个状态码、每个服务各多少请求
标签命名规范
python
# ✅ 好的标签
labels={
'method': 'GET', # HTTP 方法
'status': '200', # HTTP 状态码
'service': 'user-service',# 服务名
'env': 'production' # 环境
}
# ❌ 差的标签
labels={
'user_id': '12345', # 高基数!
'email': 'a@b.com', # 高基数!
'request_id': 'abc123', # 高基数!
'timestamp': '1234567', # 不要在标签里放时间戳
}
命名约定
指标名:
- 小写下划线:http_requests_total(不是 HTTP_Requests_Total)
- 后缀表示类型:_total(counter)、_bytes(gauge)、_seconds(histogram)
- 单位后缀:_bytes、_seconds、_count
标签名:
- 小写下划线:method(不是 Method)
- 简洁明确:status(不是 http_status_code)
四、指标命名约定
官方建议格式
<application>_<subsystem>_<name>_<unit>_<type>
示例:
http_requests_total # HTTP 请求总数
node_memory_MemTotal_bytes # Node Exporter 内存总量
http_request_duration_seconds # HTTP 请求延迟(秒)
container_cpu_usage_seconds_total # 容器 CPU 使用
常见命名模式
| 类型 | 命名模式 | 示例 |
|---|---|---|
| Counter | *_total |
http_requests_total |
| Gauge | *_current, *_usage |
memory_usage_bytes |
| Histogram | *_duration_seconds, *_size_bytes |
request_duration_seconds |
| Summary | *_duration_seconds |
rpc_duration_seconds |
五、TSDB 存储原理
倒排索引
时间序列:
S1: http_requests_total{method="GET", status="200"}
S2: http_requests_total{method="GET", status="500"}
S3: http_requests_total{method="POST", status="200"}
倒排索引:
__name__="http_requests_total" → [S1, S2, S3]
method="GET" → [S1, S2]
method="POST" → [S3]
status="200" → [S1, S3]
status="500" → [S2]
查询 http_requests_total{method="GET"}:
交集(method="GET", __name__="...") → [S1, S2]
数据压缩
时间戳压缩(Delta-of-Delta):
t1=1000, t2=1005, t3=1010, t4=1015
delta: 5, 5, 5
delta-of-delta: 0, 0, 0 → 压缩为 3 bits
值压缩(Gorilla XOR):
v1=1.0, v2=1.1, v3=1.2
XOR: 0.1 → 压缩为 ~4 bits
存储容量估算
假设 100 万时间序列,15s 采集间隔,保存 15 天:
数据点数 = 1,000,000 × (15×24×3600/15) = 1,000,000 × 86400 = 86.4B
→ 每个 ~1.37 bytes → ~118 GB
但 Prometheus 压缩后实际约:
~1.5 bytes/sample → ~130 GB → 仍然偏大
优化方案:
- 减少时间序列数(控制基数)
- 降低采集频率(非关键指标 30s/60s)
- 缩短保存时间(7-15 天)
- 使用 Thanos 长期存储
六、指标设计最佳实践
1. 控制基数
python
# ❌ 每个用户一个序列
Counter('orders_total', ['user_id']) # 100万用户 = 100万序列
# ✅ 按状态码分组
Counter('orders_total', ['status', 'payment_method']) # ~20 序列
2. 使用有意义的标签
python
# ❌ 不清晰的标签
Histogram('latency', ['v1', 'v2'])
# ✅ 清晰的标签
Histogram('http_request_duration_seconds', ['method', 'route', 'status'])
3. 避免重复指标
python
# ❌ 重复
Counter('http_requests_total')
Counter('http_requests_count') # 同一概念两个指标
# ✅ 一个指标
Counter('http_requests_total')
4. 合理设置 Histogram 桶
python
# ❌ 默认桶不适合
Histogram('db_query_duration_seconds', buckets=(0.005, 0.01, ..., 10.0))
# 如果 DB 查询都在 1-50ms,大部分桶没用
# ✅ 定制桶
Histogram('db_query_duration_seconds', buckets=(0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0))
要点回顾
| 概念 | 要点 |
|---|---|
| 时间序列 | 指标名 + 标签组合 = 唯一序列 |
| Counter | 只增不减,用 rate/increase |
| Gauge | 可增可减,直接用值 |
| Histogram | 分桶统计,服务端算分位数,可聚合 |
| Summary | 客户端算分位数,不可聚合 |
| 标签基数 | 控制在合理范围,避免高维标签 |
| 命名规范 | 小写下划线,后缀表示类型和单位 |
| TSDB | 倒排索引 + Gorilla 压缩 |
下一篇预告
理解了数据模型后,下一篇 【Prometheus·入门篇】PromQL 查询:语法、聚合运算与常用模式 将深入 PromQL 查询语言,带你掌握监控查询的核心技能。