本节摘要:四家 TSDB 表面语法不同,底层都是 series identity + 时间戳 + 观测值。Prometheus 用 metric name + labels;Influx 用 measurement + tags + fields;TimescaleDB 用普通表行;QuestDB 用 designated timestamp 列。本节统一术语并给出 tag/field 选型原则。
阅读完本节,你应当能够:
同一条 HTTP 请求指标,四人四种写法——若概念不对齐,Remote Write 映射、OTel 转换、跨库联邦都会踩坑。Tag/label 是索引维度(低基数、字符串);field 是测值载体(可多、不建倒排索引)。InfluxData 案例:某金融客户把 instance_ip 细粒度 tag 改为 host 聚合 + service_group,时间线从 8.2 亿压到 4700 万,查询吞吐升 4.2 倍。
先看四家对同一条观测的写法,感受「语法不同、骨架相同」:
| 产品 | 同一观测的表示 | 标识部分 | 值部分 |
|---|---|---|---|
| Prometheus | cpu_usage{host="web-01"} 0.73 |
metric + labels | 单个 float |
| InfluxDB | cpu,host=web-01 usage=0.73 |
measurement + tags | field |
| TimescaleDB | (host, ts, usage) 行 |
表行键 | usage 列 |
| QuestDB | 表行 + designated timestamp | symbol 列 | 普通列 |
| 概念 | Prometheus | InfluxDB | TimescaleDB | QuestDB |
|---|---|---|---|---|
| 指标名 | metric name | measurement | 表名 | 表名 |
| 维度 | labels(全索引) | tags(索引) | 列(可索引) | symbol 列 |
| 值 | 单 float | fields(多值) | 任意列 | 列 |
| 时间 | scrape 时间 ms | 纳秒 | timestamptz | designated ts |
Prometheus 文本格式:
http_requests_total{method="GET",code="200"} 1027 1717023456
Influx line protocol:
http,method=GET,code=200 requests=1027i 1717023456789000000

| 设计选择 | 优势 | 风险 |
|---|---|---|
| labels 全索引 | 过滤极快 | 高基数 OOM |
| tags/fields 分离 | 多 field 一批写入 | field 类型冲突 |
| PG 行模型 | JOIN 维表、SQL | 纯时序压缩弱于 TSM |
| QuestDB symbol | 字符串 intern 省空间 | symbol Cardinality 仍要控 |
四家的差异本质上是「series key 怎么拼」:Prometheus 用 metric + 排序后的 labels 拼成 identity 哈希;Influx 用 measurement + tags(fields 不在 identity 里);Timescale 的表行本身是 identity;QuestDB 的 designated timestamp 加上 symbol 列定位序列。理解这点就能明白一个关键推论:同一个观测点,在四家库里的 series 数可能不同。例如一条带 3 个 field 的 Influx 记录,落到 Prometheus 会被拆成 3 个 metric——series 数随之 ×3。
series 数量直接决定内存与索引规模,因此「同一条观测在哪个库占几条 series」本身就是选型因素:
# 估算示例:Prometheus # metric: http_requests_total # labels: method(4) × status(6) × host(10) = 240 series # 加上 deployment(2) 后 = 480 series labels = {"method": 4, "status": 6, "host": 10, "deployment": 2} series = 1 for card in labels.values(): series *= card print(f"series 数 = {series}") # 480
Series 数量估算:series ≈ Π(每个 label 的取值数)。10 个 host × 20 个 pod × 5 个 status = 1000 series/ metric。
下面这张清单覆盖了实战中最常见的高基数事故源,评审 schema 时可以逐条对照:
| 反模式 | 例子 | 问题 | 正确做法 |
|---|---|---|---|
| 唯一 ID 入标签 | pod_uid、request_id |
series 爆炸 | 放入 log,不入 label |
| 时间相关值入标签 | ts 的秒级字符串 |
每个点一个新 series | 时间只放 timestamp |
| 连续数值入标签 | latency_bucket 原值 |
高基数且无意义 | 用 histogram bucket |
| 可派生属性 | env 与 namespace 冗余 |
series 翻倍 | 保留一个维度 |
_total 后缀表 counter,_bucket 表 histogram——与 Prometheus 生态一致。OTel 指标进入 Prometheus 系时,注意 _total 命名与类型映射,否则查询端会按错类型处理。
OTel metrics 经 collector 可 export 到 Prometheus Remote Write、Influx、Timescale;关键是 不要把 resource attributes 全变高基数 label。例如把 k8s.pod.uid 原样透传,会在所有后端同时引爆基数问题。常见策略是保留 k8s.pod.name、service.name 等中低基数属性,把 uid 类属性丢弃。
把同一份 OTLP metric 同时发往 Prometheus 系与 Influx 系时,最容易踩的坑是「标签与字段的边界」:OTel 的 resource attributes 会全部变成 Prometheus labels,而其中像 host.name 这类每台机器都不同但数量可控的维度可以保留;container.id、k8s.pod.uid 这类每个实例都不同的唯一值必须 drop。另一个坑是 field 类型:Influx 要求同一 series 的 field 类型稳定,而 OTel 的计数器经过转换后可能 int/float 混用,需要在 exporter 端统一 cast。用一个简单的规则记忆:凡是「会随每次请求变化」的属性都不该成为 label/tag,凡是「用来定位与分组」的属性才应该保留。
⚠️ 常见坑:Influx 同一 series 的 field 不能既 int 又 float——写入会拒或拆 series。
💡 关键直觉:Prometheus「一 metric 一值」换简单查询;Influx「多 field」换 ingest 效率——模型服务于 workload。
host(10) × method(4) × status(6) × deployment(2) 一共多少 series?下一节讲时间分区如何把上述 series 落盘并压缩。