数据模型:时间序列、日志、Span、Profile 的内部表达
你在 Grafana 上点开一张 Dashboard,看到的是一条平滑的 p99
延迟曲线、一个柱状的 QPS 图、一张按 method
分组的成功率面板。你鼠标悬停在曲线上的一个异常尖峰,点进去——Tempo
或 Jaeger 在几百毫秒内吐出一条完整的分布式调用链。你发现
redis_get_cart Span 花了 850ms,点击 Related
Logs——Loki 在几秒内把同 trace_id
的十几条日志全拉了出来。你切到
Pyroscope——一张火焰图在几百毫秒内渲染完成,红色方块精确指向
runtime.mallocgc 占用的 CPU 比例。
这整个过程行云流水——但背后是五种完全不同的数据模型、四种存储引擎、三种索引策略和两种压缩算法在同时工作。它们之所以能”行云流水”,是因为每一层的数据模型都被设计成了与它的查询模式精确匹配——而你之所以在每个月末收到一张可观测性账单,也是因为你对这些数据模型的理解和它们的设计假设之间存在着偏差。
本文拆解 Metrics、Logs、Traces、Profiles、Events 五大支柱在磁盘和内存中的内部表达。这不是”这些工具各有什么参数”的配置指南——那是官方文档覆盖的事。本文试图回答:当你把一条数据交给 Prometheus / Loki / Tempo / Pyroscope 之后,它经历了怎样的内部转换?为什么这些转换决定了存储成本和查询延迟?以及理解了这些之后,你在选型和设计埋点策略时应该做出什么不同的决策。
一、三种典型查询,三种数据模型
排障场景里最常见的三类查询,背后对应三种完全不同的存储假设:
| 查询 | 典型语句 | 数据模型 | 索引策略 |
|---|---|---|---|
| 看 p99 延迟 24 小时趋势 | PromQL histogram_quantile(0.99, ...) |
时间序列 (labels, timestamp) → value |
label 倒排 → series ID → chunk |
搜 order_id=xxx 的完整链路 |
LogQL
{service="order"} \| json \| order_id="xxx" |
日志流 (labels, timestamp) → line |
仅 label 索引;正文扫描 |
| 某 Pod 过去 5 分钟 CPU 火焰图 | Profile {service="checkout"} 时间范围 |
(labels, timestamp, stack[]) → value |
标签分块;栈符号去重 |
核心结论:存储结构和索引策略必须匹配查询模式。用
ES 全索引存 Trace、用 Loki 当高基数精确查找引擎、把
request_id 打进 Prometheus
label——都是在用错误的数据模型回答正确的问题。
二、时序数据模型
时序数据是可观测性栈中最成熟的一层。从 RRDtool(1999)到
Prometheus(2015),核心抽象不变:metric_name + labels → (timestamp, value)
序列。变化的是索引结构、压缩算法和分布式架构——这三个维度决定
series 上限、查询延迟和磁盘放大系数。
关于 Gorilla 压缩、WAL 重放、VictoriaMetrics mergeset 的源码级细节,见本系列 时序数据库内核。本节聚焦数据模型本身以及 Prometheus TSDB block 的字段级结构。
2.1 Prometheus 的数据模型
Prometheus 的一条样本在文本 exposition 格式下形如:
http_requests_total{method="GET", endpoint="/api/order", status="200"} 104723 @1718000000
整串 http_requests_total{...}
唯一标识一条时间序列(time series)。每条
series 是有序的 (timestamp_ms, float64)
列表。scrape interval(通常 15s 或 30s)决定采样密度。
内存与磁盘上的路径:
flowchart LR
A[Scrape 样本] --> B[WAL segment]
B --> C[Head Block memSeries]
C --> D[关闭的 chunk mmap]
D --> E[Compaction]
E --> F[Persistent Block]
WAL(Write-Ahead Log):所有新样本先追加到 128 MB 的 segment 文件,crash 后从 checkpoint 重放。WAL 不做压缩,职责是 durability。
Head Block:活跃 series 对应
memSeries,含 open head chunk(默认 120
样本后关闭)和 label set。每个 memSeries 约 3–5
KB——这是 Prometheus OOM 的首要来源。
Persistent Block:时间窗口内 head chunk 全部关闭后,compaction 合并为不可变 block 目录。
2.2 TSDB Block 字段级结构
一个 TSDB block 是 ULID 命名的目录。与 Loki chunk、Tempo block 对照时,以下字段是”同一抽象在不同后端上的投影”:
| 字段 / 文件 | 类型与含义 | 查询用途 | 压缩 |
|---|---|---|---|
meta.json |
JSON:minTime、maxTime、stats.numSeries、stats.numSamples、compaction.level |
块选择、compaction 层级 | 无 |
index |
倒排:label pair → posting list(series_ref) |
PromQL label matcher | 无(块级) |
chunks/NNNNNN |
变长 bit-stream:Gorilla 编码的 timestamp + value | 按 series_ref 读样本 | Gorilla 10:1–20:1 |
tombstones |
删除区间 (series_ref, mint, maxt) |
查询时跳过已删样本 | 极小 |
series ref |
uint64:块内 series 局部 ID |
连接 index 与 chunks | — |
series_ref 编码(Prometheus tsdb/index
约定):高 16 bit 为 chunk file 编号,低 48 bit 为文件内
offset。查询 {method="GET"} 时:index 取
posting list → 对每个 ref 定位 chunk → 解码 Gorilla stream →
时间范围过滤 → 聚合。
meta.json 示例(字段名与 Prometheus v2.45+
一致,数值为说明性示例):
{
"ulid": "01J2K3M4N5P6Q7R8S9T0",
"minTime": 1718000000000,
"maxTime": 1718007200000,
"stats": {
"numSamples": 12453021,
"numSeries": 89234,
"numChunks": 178468
},
"compaction": {
"level": 2,
"sources": ["01J2K...", "01J2L..."]
}
}2.3 Chunk 内部:Gorilla 编码要点
Gorilla(Pelkonen et al., VLDB 2015)利用两个假设:时间戳等间距(delta-of-delta 常为 0)、相邻值变化小(XOR 前导零多)。第一个时间戳 64 bit 完整存储;后续 timestamp 平均 ~1.5 bit;counter 类指标 value 平均 1–2 byte,剧烈波动指标 4–6 byte。
不重复推导——完整数学与 XOR 位布局见 07-tsdb-internals 第二节。此处只给工程口径:15s scrape、一天 5760 点、未压缩 \(5760 \times 16\) B ≈ 92 KB/series/day;Gorilla 后约 4–8 KB/series/day。
2.4 VictoriaMetrics 与 Prometheus 模型差异
VictoriaMetrics 兼容 Prometheus 文本模型,存储层差异:
| 维度 | Prometheus TSDB | VictoriaMetrics |
|---|---|---|
| Series 标识 | 文本 label set + 块内 ref | 整数 TSID |
| 索引 | 每 block 倒排 index 文件 | mergeset 全局索引 |
| 分区 | 2h head + compaction 层级 | 按月分区 + 后台 merge |
| 高基数 | label 字符串索引占内存 | TSID 映射,同等内存更多 series |
选型含义:label 治理良好时两者成本接近;label churn 严重时 VictoriaMetrics 缓冲期更长,但不替代治理。
2.5 Metrics 存储成本公式(假设模型)
以下公式用于数量级估算,参数需按你的环境代入。不引用任何云厂商单价。
变量定义:
- \(N_s\):活跃 time
series 数量(
prometheus_tsdb_head_series) - \(\Delta t\):scrape 间隔(秒)
- \(R\):保留天数
- \(b\):Gorilla 压缩后每样本字节数(counter/gauge 稳态取 1.5,高波动取 4)
- \(f_i\):index 放大系数(Prometheus 典型 1.2–1.8)
未压缩样本量(bytes):
\[ V_{\text{raw}} = N_s \times \frac{R \times 86400}{\Delta t} \times 16 \]
压缩后 chunk 体积:
\[ V_{\text{chunk}} = N_s \times \frac{R \times 86400}{\Delta t} \times b \]
含 index 的磁盘占用:
\[ V_{\text{disk}} \approx f_i \times V_{\text{chunk}} \]
内存 head 占用(经验):
\[ M_{\text{head}} \approx N_s \times 4\,\text{KB} \]
数值示例(假设模型):
- \(N_s = 800{,}000\),\(\Delta t = 15\) s,\(R = 15\) 天,\(b = 1.5\),\(f_i = 1.5\)
- 日样本数:\(800000 \times 86400 / 15 = 4.608 \times 10^9\)
- \(V_{\text{chunk}} \approx 4.608 \times 10^9 \times 1.5 \times 15 \approx 104\) GB(15 天)
- \(M_{\text{head}} \approx 800000 \times 4\text{KB} \approx 3.2\) GB(仅 memSeries,不含 query 缓存)
基数与成本的关系:成本对 \(N_s\) 线性,对 label
组合数线性。把 user_id 打进 label 使 \(N_s\) 从 \(10^5\) 升到 \(10^7\),磁盘与内存同比例恶化——这是
Metrics 数据模型对基数最敏感的原因。
三、日志数据模型
时序数据的挑战是压缩;日志数据的挑战是在未知查询模式与有限索引之间折中。
3.1 日志的通用模型
{"timestamp":"2024-06-11T03:14:22.423Z","level":"ERROR","service":"checkout",
"message":"redis: connection pool exhausted",
"trace_id":"0af7651916cd43dd8448eb211c80319c","duration_ms":843}特征:level/service
低基数、高过滤频率;message
高熵;trace_id 高基数、几乎只做精确匹配。
3.2 Elasticsearch:全字段倒排
Lucene
segment:词项 → (doc_id, tf, positions)。任意字段可搜,存储放大
1.5–3×。高基数 trace_id
若建索引,词典膨胀极快——mapping 中应
"index": false 仅 _source
精确查。
3.3 Loki:label 索引 + 正文扫描
Log stream:固定 label set
下的有序日志行。{service="checkout", level="ERROR"}
标识一流。
Chunk 是 Loki 的最小存储单元(与 TSDB
block、Tempo block 对照见第六节)。写入路径:Ingester 缓冲 →
切 chunk(目标压缩后 1–2 MB)→ 上传对象存储;index 存
(labels fingerprint, start, end) → chunk ref。
Loki 2.9+ Bloom Filter:chunk 级 n-gram
bloom,低频 token 查询可跳过大量 chunk。对
"error" 这类高频词加速有限。
3.4 ClickHouse:列存折中
MergeTree:列独立压缩;主键
(service, timestamp) 范围剪枝;跳数索引
bloom_filter/minmax。适合结构化聚合查询,不适合任意全文
ad-hoc。
3.5 日志存储成本公式(假设模型)
变量:
- \(Q\):日志行写入速率(行/秒)
- \(L\):平均行大小(字节,JSON 典型 300–800)
- \(\sigma\):ZSTD 压缩比(文本 JSON 典型 4–6)
- \(f_e\):ES 索引放大(2–3);Loki 无正文字段索引(1.0–1.2)
- \(R\):保留天数
原始日增量(GB/天):
\[ V_{\text{day,raw}} = \frac{Q \times 86400 \times L}{10^9} \]
压缩后对象存储:
\[ V_{\text{day,store}} = \frac{V_{\text{day,raw}}}{\sigma} \times f_e \]
总保留:
\[ V_{\text{retain}} = V_{\text{day,store}} \times R \]
示例(假设模型):\(Q=50000\) 行/s,\(L=500\) B,\(\sigma=5\),Loki \(f_e=1.1\),\(R=30\)
- \(V_{\text{day,raw}} = 50000 \times 86400 \times 500 / 10^9 \approx 2160\) GB/天
- \(V_{\text{day,store}} \approx 2160 / 5 \times 1.1 \approx 475\) GB/天
- 30 天 ≈ 14 TB(对象存储侧;不含 ingester 热缓存)
同一流量若 \(f_e=2.5\)(ES),30 天约 32 TB——数量级差异来自索引策略,不是 ZSTD 本身。
四、Trace 数据模型
Trace 查询双峰:trace_id 精确查找(高频)与 attribute 搜索(低频、排障关键)。Jaeger 与 Tempo 分别优化两端。
4.1 Span 内部结构(OTLP)
| 字段 | 类型 | 典型大小 | 索引价值 |
|---|---|---|---|
trace_id |
16 bytes | 固定 | 分区键 / 精确查 |
span_id |
8 bytes | 固定 | 树结构 |
parent_span_id |
8 bytes | 固定 | 树结构 |
name |
string | 20–80 B | 中基数 |
kind |
enum | 1 B | 低基数 |
start/end_time_unix_nano |
fixed64×2 | 16 B | 范围查 |
attributes[] |
KeyValue | 200–2000 B | 高基数风险 |
events[] |
可变 | 0–几 KB | 通常不索引 |
resource |
KeyValue | 100–500 B | 低基数 |
典型 HTTP SERVER Span protobuf 序列化约 500–800 B;含 8–10 个 attribute 约 1–2 KB。每个请求 10–20 个 Span 时,Trace 写入量远超 Metrics。
4.2 Jaeger:全索引路线
Span 写入 ES/Cassandra:字段映射为 ES document,tags
建倒排。可搜
http.status_code=500 AND service.name=checkout,代价是存储放大与分片压力。
4.3 Tempo:无 attribute 索引
Span 按 trace_id 分组 → Ingester WAL → flush
为 Parquet block → 对象存储。查询
trace_id=X:hash ring 定位 ingester 或 block →
读 parquet row group。不维护 attribute
倒排。
TraceQL 搜索(duration > 500ms)靠并行
block 扫描 + predicate
pushdown,秒级而非毫秒级——设计取舍,非实现缺陷。
4.4 Trace 存储成本公式(假设模型)
变量:
- \(Q\):入口 QPS
- \(S\):每请求 Span 数
- \(B\):每 Span 序列化大小(字节,取 1200)
- \(p_h\):头部采样率
- \(p_t\):尾部额外保留比例(相对未采样流量,错误+慢请求)
- \(\sigma_t\):Trace 块压缩比(ZSTD+Parquet 典型 2–4)
- \(f_j\):Jaeger+ES 索引放大(3–5);Tempo(1.0–1.3)
日 Span 数:
\[ N_{\text{span/day}} = Q \times 86400 \times S \times (p_h + p_t) \]
日存储(Tempo 类):
\[ V_{\text{tempo/day}} = \frac{N_{\text{span/day}} \times B}{\sigma_t \times 10^9} \times f_t \]
示例(假设模型):\(Q=5000\),\(S=12\),\(B=1200\),\(p_h=0.01\),\(p_t=0.005\)(错误+慢请求额外保留),\(\sigma_t=3\),\(f_t=1.1\)
- \(N_{\text{span/day}} = 5000 \times 86400 \times 12 \times 0.015 \approx 77.8 \times 10^6\)
- \(V_{\text{tempo/day}} \approx 77.8 \times 10^6 \times 1200 / 3 / 10^9 \times 1.1 \approx 34\) GB/天
若 \(f_j=4\)(Jaeger+ES 全索引),同参数约 136 GB/天——4× 差距来自索引,不是 Span 大小。
全量(\(p_h=1\)):\(N_{\text{span/day}} \approx 5.18 \times 10^9\),Tempo 约 2.3 TB/天——说明不采样不可规模化。
五、Profile 数据模型
Profile 单条体积大、频率低。核心是 Sample → Location → Function 映射与栈合并。
5.1 pprof 格式
- Function:符号表条目(name, file, line)
- Location:地址 + 行号 → Function
- Sample:
location_id[]栈 +value[]计数
符号去重:数万 sample 共享数千 Function,.gz
通常几十 KB–几百 KB。
5.2 Pyroscope / 火焰图
数据模型:(timestamp, labels, stacktrace[], value)。火焰图
= 按栈深度合并 sample 计数。查询 = 时间范围内扫描 block +
内存合并,不依赖复杂倒排。
5.3 Profile 成本公式(假设模型)
- \(F\):每服务 profile 频率(次/分钟,典型 1)
- \(P\):单次 pprof.gz 大小(KB,典型 100–500)
- \(N_svc\):被 profile 的服务数
\[ V_{\text{profile/day}} = \frac{N_{\text{svc}} \times F \times 1440 \times P}{10^6}\ \text{GB} \]
示例:200 服务,1 次/分钟,300 KB → 约 83 GB/天 未压缩语义;相对 Logs/Traces 通常 < 5% 账单。
六、TSDB Block vs Loki Chunk vs Tempo Block:字段级对照
这是本文的核心对照表。三者在工程上都叫 “block/chunk”,但字段语义不同。
| 概念 | Prometheus TSDB Block | Loki Chunk | Tempo Block (Parquet) |
|---|---|---|---|
| 唯一 ID | ULID 目录名 | chunk_id + fingerprint |
ULID + tenant |
| 时间边界 | meta.minTime/maxTime |
from/through ns |
start/end unix |
| 分区键 | 无(块内多 series) | label fingerprint | trace_id hash |
| 主索引 | index 倒排 label→ref |
boltdb/tsdb index: labels→ref | 仅 trace_id + 时间;可选 bloom |
| 数据文件 | chunks/* Gorilla |
.gz 日志行序列 |
.parquet 列存 Span |
| 单块典型大小 | 压缩后数百 MB–几 GB | 1–2 MB 压缩目标 | 100 MB–1 GB |
| 块内记录单位 | (series_ref, t, v) |
log line + ts | Span row |
| attribute 索引 | label 仅(metric 名+labels) | 无(正文) | 无(TraceQL 扫描) |
| 删除语义 | tombstones 区间 | retention 整 chunk 删 | compaction 合并丢弃 |
| 对象存储 | 可选(Thanos/Mimir) | 默认 S3/GCS | 默认 S3/GCS |
| 精确键查询 | series + 时间范围 | labels + 时间 + line filter | trace_id O(1) 定位 |
| ** ad-hoc 属性搜索** | PromQL label | LogQL \|= 扫描 |
TraceQL 块扫描 |
flowchart TB
subgraph prom [Prometheus TSDB Block]
PI[index: label→series_ref]
PC[chunks: Gorilla samples]
PI --> PC
end
subgraph loki [Loki Chunk]
LI[index: fingerprint→ref]
LC[chunk body: compressed lines]
LI --> LC
end
subgraph tempo [Tempo Block]
TB[footer: min/max id time]
TP[parquet: spans by trace_id]
TB --> TP
end
Loki chunk 字段级(Ingester 内存 → 持久化):
| 字段 | 含义 |
|---|---|
fingerprint |
label set 哈希 |
from / through |
chunk 时间边界 |
metric |
内部流标识 |
encoding |
压缩编码(gzip/snappy/zstd) |
head / tail |
块内首尾行(调试) |
synced |
是否已 flush 到对象存储 |
Tempo block 字段级(Parquet schema 简化):
| 列 / 元数据 | 含义 |
|---|---|
trace_id |
16 byte 十六进制 |
span_id |
8 byte |
parent_span_id |
8 byte |
name |
operation name |
start/end |
unix nano |
duration |
派生或存储 |
status_code |
OK/ERROR |
attributes |
map 或 JSON 列 |
resource |
service.name 等 |
footer |
bloom / 统计 / 版本 |
七、Events 数据模型
Events
查询模式:某时间点前后发生了什么。CloudEvents
最小模型:id, source, type, time, data。存储归宿通常是
Loki/Kafka,而非 Prometheus——Prometheus
无法保留离散事件原文。
K8s Event 经 kube-event-exporter 进 Loki
是常见模式;强行 event→metric 会丢失单次事件的上下文。
八、五大支柱访问模式与成本占比
| 维度 | Metrics | Logs | Traces | Profiles | Events |
|---|---|---|---|---|---|
| 写吞吐 | 低 | 极高 | 极高 | 低 | 中–高 |
| 查询模式 | 聚合 | 过滤+扫描 | trace_id + 属性搜索 | 火焰图 | 时间+type |
| 索引需求 | label 倒排 | 后端相关 | Jaeger 强 / Tempo 弱 | 几乎无 | 类日志 |
| 压缩率 | 极高 | 中–高 | 中 | 高 | 中 |
| 基数敏感度 | 极高 | 中–高 | 中/低 | 低 | 中 |
| 典型成本占比* | 10–20% | 40–60% | 20–35% | <5% | 并入 Logs |
*占比为假设模型下的工程经验区间,非实测账单;见 存储与成本。
80/20 法则:Logs + Traces 占 85–95% 存储;成本控制主战场是日志索引策略与 Trace 采样。
2.6 Head Block 内存结构与查询路径
Head Block 是 Prometheus 实时查询的数据源。理解
memSeries 结构有助于解释为什么”series
数量”比”样本数量”更决定内存。
memSeries 主要字段(概念模型,对应
tsdb/head.go):
| 字段 | 含义 | 内存影响 |
|---|---|---|
lset |
完整 label set | 与 label 数量、字符串长度正相关 |
chunks |
已关闭 chunk 列表 + head chunk | head chunk 满前持续增长 |
mmappedChunks |
mmap 到磁盘的 chunk 引用 | 不占堆内存 |
staleNaN |
标记 series 是否 stale | 极小 |
一次典型 PromQL 查询
{service="checkout", method="GET"} 的路径:
- PostingsForMatchers:在 index 上求
service=checkout∩method=GET的 posting list - 对每个 series_ref,加载 head chunk 或 mmap chunk
- Gorilla 解码时间范围内的样本
- 引擎聚合(rate、sum、histogram_quantile)
Churn 的非线性成本:若每秒创建 1000 个新
label 组合(例如把订单 ID 打进 label),即使总样本 QPS
不变,memSeries 分配速率也会拖垮
GC。这比”steady 800k series”更危险——因为除了 3–5 KB/series
的常驻内存,还有频繁的 map 插入、index 更新、WAL Series
record。
2.7 Exemplars 与 Native Histograms 的数据模型扩展
OpenMetrics Exemplars 在 TSDB 中作为
(series_ref, timestamp, value, trace_id, span_id)
附加存储,不进入主 Gorilla chunk——单独 WAL record 类型与
head exemplar storage。查询 histogram_quantile
时可选附带 exemplar 用于跳转 Trace。Exemplar 基数受 trace
采样率约束;若对每个 histogram bucket 都挂 exemplar,head
内存会显著上升。
Native Histograms(Prometheus 2.40+ 可选)改变 value
编码:从单 float64 变为 bucket span + counter 结构。chunk 内
encoding 类型字段区分 XOR float 与 native histogram
schema。选型含义:native histogram 减少 _bucket
系列爆炸,但 chunk 体积与查询路径更复杂——属于同一
TSDB block 模型上的 encoding 扩展,分区键仍是 label
set。
2.8 Mimir / Thanos 对 Block 模型的扩展
单机 Prometheus block 通过 sidecar
上传对象存储(Thanos)或由 Mimir ingester
写入共享块存储。逻辑模型不变:index + chunks + meta.json。差异在查询层
fan-out:Querier 并行读多个 block,Store Gateway
缓存 index header。成本公式中 \(f_i\) 需加上对象存储 GET 与跨
AZ 流量——见 存储与成本。
九、工程坑点
9.1 用 ES 存 Trace Span——全字段索引
Jaeger 默认 mapping 为大量 tag 建倒排,50 个 attribute 中
30 个从未被搜索却占 60% 磁盘。缓解:mapping
审查,"index": false 高基数 tag。
9.2 用 Loki 搜高基数字段
| json | user_id="xxx" 触发全 chunk 扫描。7
天 20 TB/天 保留时单次查询可扫数百 GB。缓解:Bloom
Filter、缩短 query
窗口、max_entries_limit_per_query、把
trace_id 放 label(低 Cardinality
流)而非正文。
9.3 Prometheus label 失控
动态 URL path 作为 endpoint label → 每次发布
series 暴涨 → OOM。缓解:路径模板化或
metric_relabel_configs:
metric_relabel_configs:
- source_labels: [endpoint]
regex: '/api/users/[^/]+/orders/[^/]+'
target_label: endpoint
replacement: '/api/users/:id/orders/:id'9.4 用 Metrics 存 Trace 级信息
把每个 trace_id 做成 label 或
high-cardinality gauge——数据模型错配。Trace 应用 Span 模型 +
采样,Metrics 保留 RED 聚合。
9.5 ClickHouse 日志不分区
全表扫描火焰图式查询数十秒。按
(service, toDate(timestamp)) 分区。
9.6 Tempo 当 ES 用
期望毫秒级 user_id 全历史搜索——TraceQL
扫描对象存储,延迟秒级。先 Metrics/Logs 缩小 trace_id,再
Tempo 精确拉取。
9.7 忽略 block/chunk 边界与查询计划
跨过多天 block 的 PromQL 会 merge 大量 index;Loki 跨 chunk 查询放大对象存储 GET。保留期与查询窗口应对齐块边界(天/小时)。
9.8 WAL 与对象存储双份成本
Tempo/Loki ingester WAL + S3 同时占空间;compaction 失败时 WAL 膨胀。监控 compactor lag。
十、落地清单
十一、关键概念回顾
- Prometheus TSDB:WAL → Head →
Block;
index+chunks+meta.json;Gorilla 压缩详见 07 篇。 - Loki chunk:label fingerprint 索引 + 正文 ZSTD;正文查询是扫描。
- Tempo block:Parquet 按 trace_id;attribute 搜索靠 TraceQL 扫描。
- 成本公式:对 Metrics 线性依赖 \(N_s\);Logs 依赖 \(Q,L,f_e\);Traces 依赖 \(Q,S,p_h,p_t,f_j\)。
- 选型原则:索引匹配查询;Logs/Traces 占账单主体。
十二、常见误解
“Tempo 不能 attribute 搜索”——能,用 TraceQL,秒级非毫秒级;设计换成本。
“Prometheus 内存只和 series 数有关”——churn 带来非线性 GC;稳态 80 万 series 可能比 churn 高的 40 万更稳。
“JSON 日志只是格式不同”——JSON 有 schema,管道可 tokenize;纯文本每层 regex,CPU 与维护成本成倍。
“块越大越好”——大 block 降低元数据开销但增大 compaction 压力和单次查询 IO;各系统有 sweet spot(Loki 1–2 MB,Prometheus 小时级–天级)。
十三、下一步
理解了存储端内部表达后,工程问题回到管道与 Trace 栈:日志管道、Traces 栈与采样。
参考资料
- T. Pelkonen et al., Gorilla: A Fast, Scalable, In-Memory Time Series Database, VLDB 2015
- Prometheus, TSDB Format, v2.45+, https://github.com/prometheus/prometheus/tree/main/tsdb
- Grafana Loki, Architecture, https://grafana.com/docs/loki/latest/architecture/
- Grafana Tempo, Architecture, https://grafana.com/docs/tempo/latest/architecture/
- Jaeger, Storage Backends, https://www.jaegertracing.io/docs/latest/storage/
- ClickHouse, MergeTree Engine, https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree
- Google, pprof format, https://github.com/google/pprof
- CNCF, CloudEvents Specification, v1.0
- VictoriaMetrics, Technical Papers, https://docs.victoriametrics.com/
- OpenTelemetry, OTLP protobuf, https://github.com/open-telemetry/opentelemetry-proto
- 本系列 时序数据库内核
- 本系列 Logs 技术栈对比
下一篇:日志管道:Fluent Bit、Vector、Logstash、Cribl 的取舍
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【可观测性工程】存储与成本:采样、下采样、冷热分层、对象存储
可观测性数据量持续增长,存储成本常超过计算成本。拆解四大支柱的成本结构、采样与保留期策略、冷热分层架构,以及带显式假设的成本估算 worksheet。
【可观测性工程】时序数据库内核:TSM、TSI、倒排索引与 Gorilla 压缩
深入时序数据库的存储内核:Prometheus TSDB 的 WAL 与块管理、InfluxDB 的 TSM 引擎与 TSI 倒排索引、Gorilla 压缩算法的数学原理、VictoriaMetrics mergeset 架构、ClickHouse MergeTree 作为 metrics 后端,以及国内大厂在 series churn 和 compaction 风暴上踩过的坑。
【可观测性工程】真实事故复盘剧本:从指标抖动到根因的全链路追查
虚构但可复现的 checkout 服务事故全链路:SLO Burn Rate 告警后按 Golden Minute→Metrics→Traces→Logs→Profile→Events 五阶递进排障,含 PromQL/LogQL/kubectl 命令与三条分级剧本,交叉引用系列 01–22。
【可观测性工程】Traces 栈与采样:Jaeger、Tempo、Zipkin、SkyWalking
拆解 Jaeger、Tempo、SkyWalking 架构差异与采样策略(头部/尾部/自适应),给出 W3C TraceContext 传播、OpenTelemetry tail_sampling 配置与选型框架。