50.1 三支柱与成本模型
一句话
日志、链路、指标不是三种工具,而是三种数据形状——一条事件、一棵调用树、一个数值序列;形状决定了它能回答什么问题,也决定了钱花在哪:日志按「字节 × 保留期」付费,链路按「span 数 × 采样率」付费,指标按「活跃序列数」付费。
三支柱是三种形状,不是三种产品
| 维度 | 日志 Logs | 链路 Traces | 指标 Metrics |
|---|---|---|---|
| 最小单位 | 一条事件(时间 + 字段 + 文本) | 一棵调用树(trace = N 个 span) | 一个样本(name + labels + ts + value) |
| 是否预聚合 | 否,原样落盘 | 否,但按请求成树 | 是,写入前已聚合成序列 |
| 写入量随请求量 | 线性(KB/请求) | 线性(span 数 × 采样率) | 不随请求量增长,只随序列数增长 |
| 强项 | 细节、上下文、异常栈、审计 | 跨服务因果、耗时归属、调用拓扑 | 趋势、比率、分位、告警(最便宜) |
| 弱项 | 聚合贵、基数不可控、检索慢 | 采样即丢、无细节 | 无个体、多一个维度就多一批序列 |
| 成本第一驱动 | 字节 × 保留天数 | span 数 × 采样率 × 保留天数 | 活跃序列数(基数)× 保留天数 |
| 典型后端 | Elasticsearch、Loki、ClickHouse | Jaeger、Tempo、SkyWalking | Prometheus、VictoriaMetrics、Mimir |
🔑 一句话记法:指标记住「多少」,链路记住「哪一跳」,日志记住「为什么」。
三条容易记错的边界:
- 日志不是「越多越好」:它没有预聚合,任何统计都要扫原文,所以从日志算 QPS 的成本是指标的几百倍(50.2 日志)。
- 链路不是「采样就废」:它对单个请求的还原度是 100%,对被采样到的那个请求没有任何信息损失。
- 指标不是「精度低」:它的精度问题只在分位数和高基数两处,计数类信号(QPS、错误数)是精确的(50.4 度量)。
同一次请求,在三个系统里留下的痕迹
请求 req-7f3a(trace_id = 4bf92f35…4736)
指标 http_server_duration_bucket{service="order", route="/checkout", code="500"} +1
└─ 只有计数,没有「这一次」;成本 2 B / 请求
链路 trace 4bf92f35…4736 = 7 个 span,入口 320ms
└─ 其中 order→mysql 210ms(串行两次查询),链路本身可下钻到 span
日志 3 条,每条都带 trace_id=4bf92f35…4736
└─ 其中 1 条 error:"context deadline exceeded",带 db.statement 前缀
同一个故障,三种数据各说一句话:指标说「错误率 3%」,链路说「慢在 mysql」,日志说「拿连接超时」。缺任何一种,都要靠猜。
排障顺序 = 三支柱的顺序
| 你要回答 | 先看 | 为什么是它 |
|---|---|---|
| 现在有没有问题、多严重、从几点开始 | 指标 | 秒级、可告警、单位成本最低 |
| 集中在哪个服务、哪一跳、哪一次 | 链路 | 把端到端耗时拆到 span,能定位到跨进程边界 |
| 这一跳为什么慢/为什么错 | 日志 | 只有日志有参数、堆栈、SQL、异常文本 |
| 只有 0.1% 的请求异常 | 直方图 + 尾采样 | 平均值看不见,全采样太贵 |
| 容量还够不够 | 饱和度指标 | USE 类信号,趋势可外推 |
反过来做(先翻日志、再猜服务、最后才看指标)是最常见的低效排障:每次都要拉几百 MB 日志,还得在时间轴上手工对齐。
把三者缝起来的三根线
| 线 | 怎么缝 | 缺了会怎样 |
|---|---|---|
trace_id | 日志里作为字段写入;span 里天然有;入口统一注入 | 有日志定位不到链路,有链路查不到日志 |
| exemplar | 指标样本挂一个 trace_id(OpenMetrics exemplar) | 从 P99 尖刺要手工翻时间窗找那条 trace |
| 统一资源属性 | service.name / k8s.pod.name / host.name 三处同名同值 | 三个系统各叫各的名字,关联全靠人脑和 Excel |
三根线都指向同一件事:关联不是靠「时间差不多」,是靠同一个 ID 和同一套维度。语义约定的主场见 50.9 OpenTelemetry 标准。
成本模型:三条计价公式
日志日成本 ≈ EPS × 平均字节 × 86400 × (1/压缩比) × 副本数 × 单价
指标日成本 ≈ 活跃序列数 × (86400/抓取间隔) × 每样本字节 × 单价
链路日成本 ≈ QPS × 采样率 × 每请求 span 数 × 每 span 字节 × 86400 × (1/压缩比) × 单价
三个式子长得像,但变量不同:日志的变量是字节,指标的变量是序列数,链路的变量是采样率。所以「降本」在三个系统里是三套完全不同的动作。
算一笔账:1000 QPS 的三支柱
下面这段程序把三条公式跑成一张表(可直接 go run):
package main
import "fmt"
const (
qps = 1000.0 // 稳态 QPS
secsPerD = 86400.0
days = 30.0
gb = 1e9
reqPerDay = qps * secsPerD
)
func unit(b float64) string { // 自动换单位,避免一屏 0.0 TB
if b >= 1e12 {
return fmt.Sprintf("%.2f TB", b/1e12)
}
return fmt.Sprintf("%.1f GB", b/gb)
}
func main() {
fmt.Printf("稳态 %.0f QPS = 每天 %.2f 亿请求\n\n", qps, reqPerDay/1e8)
// 每请求写入量(落到存储上的字节,已含聚合;不含压缩)
metricB := 20000 * (secsPerD / 15) * 1.5 / reqPerDay // 2 万活跃序列, 15s 抓取, 1.5B/样本
fmt.Println("| 数据形状 | 每请求写入 | 倍数 | 每天原始量 |")
fmt.Println("|---|---|---|---|")
row := func(n string, b float64) {
fmt.Printf("| %s | %.0f B | %.0f× | %s |\n", n, b, b/metricB, unit(b*reqPerDay))
}
row("指标(2 万活跃序列)", metricB)
row("日志(2 条 × 500B)", 1000)
row("链路(10 span × 500B,全采样)", 5000)
row("链路(同上,1% 采样)", 50)
fmt.Println("\n| 30 天存储(压缩 + 副本后) | 量 |")
fmt.Println("|---|---|")
fmt.Printf("| 日志(压缩 5×、2 副本) | %s |\n", unit(reqPerDay*1000/5*2*days))
fmt.Printf("| 链路全采样(压缩 3×) | %s |\n", unit(reqPerDay*5000/3*days))
fmt.Printf("| 链路 1%% 采样(压缩 3×) | %s |\n", unit(reqPerDay*50/3*days))
fmt.Printf("| 指标 2 万序列 | %s |\n", unit(20000*(secsPerD/15)*1.5*days))
fmt.Printf("| 指标 200 万序列(多一个只有 100 取值的标签) | %s |\n", unit(2e6*(secsPerD/15)*1.5*days))
}
输出(本机实测):
稳态 1000 QPS = 每天 0.86 亿请求
| 数据形状 | 每请求写入 | 倍数 | 每天原始量 |
|---|---|---|---|
| 指标(2 万活跃序列) | 2 B | 1× | 0.2 GB |
| 日志(2 条 × 500B) | 1000 B | 500× | 86.4 GB |
| 链路(10 span × 500B,全采样) | 5000 B | 2500× | 432.0 GB |
| 链路(同上,1% 采样) | 50 B | 25× | 4.3 GB |
| 30 天存储(压缩 + 副本后) | 量 |
|---|---|
| 日志(压缩 5×、2 副本) | 1.04 TB |
| 链路全采样(压缩 3×) | 4.32 TB |
| 链路 1% 采样(压缩 3×) | 43.2 GB |
| 指标 2 万序列 | 5.2 GB |
| 指标 200 万序列(多一个只有 100 取值的标签) | 518.4 GB |
三个读法:
- 指标是唯一「每请求成本趋近于零」的形状:2 B/请求,是日志的 1/500、全采样链路的 1/2500。所以告警和趋势判断必须走指标,这不是偏好,是成本结构决定的。
- 链路的价格由采样率决定,日志的价格由字节和保留期决定。全采样链路比日志还贵 5 倍;1% 采样后比日志便宜 20 倍。链路开采样几乎不损失「结构」,日志开采样损失的是「证据」。
- 指标的成本与 QPS 无关,只与维度组合数有关。哪怕新增的标签只有 100 个取值,序列数也从 2 万变 200 万,30 天存储从 5.2 GB 变 518 GB:×100,和流量一点关系都没有。换成
user_id(百万级取值)就是 ×100 万——那不是贵,是不可用。
⚠️ 数字的边界:这里用 1.5 B/样本、压缩 5×/3× 是量级假设,不同后端差异可达 2–3 倍。结论不受影响——三者的成本结构差异是两个数量级以上,量级估算足以做决策。
三个降本杠杆,各自的副作用
| 杠杆 | 省多少 | 代价 | 什么时候不能用 |
|---|---|---|---|
| 日志:保留期 30 → 7 天 | 存储 ×0.23 | 7 天前的事故无法回溯 | 合规/审计有最低保留期(见 3.5 审计与取证) |
| 日志:写入端瘦身(2 条 500B → 1 条 300B) | ×0.3 | 排查时少一条上下文 | 关键路径的入参与错误码 |
| 链路:全采样 → 尾采样 1%(保错保慢) | 432 GB/天 → 4.3 GB/天 | 需要 Collector 缓冲、按 trace 路由 | 低 QPS、要求全量留存的场景 |
| 指标:删掉高基数标签 | 200 万 → 2 万序列,×0.01 | 失去按用户/请求下钻 | 需要个体维度时改用日志或 exemplar |
| 全链路:热/温/冷分层 | 冷数据单价降到 1/10 量级 | 冷查要等、跨层查询复杂 | SLO 排障窗口内的热数据 |
🔑 降本的正确顺序:先决定「哪些数据必须留」,再决定「留多久」,最后才是「换更便宜的存储」。顺序反过来,通常省了 20% 的钱,丢了 80% 的排障能力。
三个旋钮:保留、抓取、采样
| 旋钮 | 建议起点 | 依据 | 超了会怎样 |
|---|---|---|---|
| 指标保留时长 | 15d(--storage.tsdb.retention.time=15d) | 覆盖至少 2 个发布周期 + 一次复盘 | 磁盘线性涨,压缩与查询变慢 |
| 指标保留上限 | 200GB(--storage.tsdb.retention.size=200GB) | 先设硬顶,防止打满磁盘 | 磁盘 100% → Prometheus 崩 |
| 抓取间隔 | 15s | 告警窗口至少 4 倍于间隔,点数才够 | 改 5s = 样本成本 ×3 |
| trace 采样 | 1%(尾采样保错误/慢) | 见 50.9 OpenTelemetry 标准 | 100% 采集 = 成本随 QPS 线性涨 |
# 确认最终生效值(flag 与配置文件合并后的结果)
curl -s localhost:9090/api/v1/status/flags | tr ',' '\n' | grep retention
# 基数排查:哪 10 个指标名的序列最多,先砍它们
curl -s localhost:9090/api/v1/status/tsdb | python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(*d["seriesCountByMetricName"][:10], sep="\n")'
# 实际占用
du -sh /var/lib/prometheus
三个旋钮里,基数是最容易被忽略的:序列数由「指标名 × 标签组合」决定,加一个 user_id 标签就能把成本放大几个数量级。先看 seriesCountByMetricName,再谈保留时长。
采样经济学:同样的预算,尾采样买到更多信息
100 万请求、真实错误率 0.1%(1000 条错误):
| 策略 | 留下的量 | 留下的错误 | 能回答什么 |
|---|---|---|---|
| 全量 | 100 万 | 1000 | 一切 |
| 均匀采样 1% | 1 万 | 10 | 能估 P99;分不清 0.1% 与 0.2% |
| 尾采样:错误 100% + 成功 1% | 1.09 万 | 1000 | 错误案例可逐个看,成功率近似无偏 |
| 按级别:ERROR 全留 + INFO 1% | 1.09 万 | 1000 | 同上,但不含慢请求的链路 |
成本几乎一样(1.09 万 vs 1 万,只多 9%),错误样本的信息量差 100 倍。这就是为什么「采样」不是省钱手段,而是在同等预算下提高信息密度的设计:先把「必须留的」定义清楚(错误、慢请求、关键租户),剩下的按比例砍。
代价也要说清:尾采样要求同一 trace 的所有 span 到达同一个 Collector,且要缓冲一个决策窗口(实现细节在 50.3 链路)。
全栈监控 = 三支柱 × 三层
单看支柱会漏「深度」,单看分层会漏「形状」。两维交叉才是完整的观测面:
| 层 | 主要用哪个支柱 | 典型信号 | 缺了会怎样 |
|---|---|---|---|
| 基础层 | 指标(USE) | CPU 饱和度、内存、磁盘剩余、网卡丢包、IO 等待 | 服务变慢但不知道是不是机器的问题 |
| 中间层 | 指标 + 日志 | 连接池占用、慢查询、复制延迟、MQ 积压、GC 停顿、缓存命中率 | 应用指标正常,瓶颈全在依赖上 |
| 应用层 | 指标 + 链路 + 日志 | RED(速率/错误/时长)、trace、结构化日志、业务漏斗 | 知道机器没事,但不知道哪个接口坏了 |
💡 「标准化」不必背四条口号:三支柱要能互相关联,只需要两件事——同一套资源命名(service/host/pod 三处同名)和同一套语义约定。日志结构化、指标有单位、span 有
service.name,都是这两件事的推论。
反模式
| 反模式 | 后果 |
|---|---|
| 三支柱各买一套、各起一套命名 | 排障要在三个界面之间手工对齐时间和主机名 |
| 用日志算指标(grep 出 QPS、错误率) | 成本高 2 个数量级,且做不到秒级告警 |
| 链路 100% 采样(把开发环境的配置带上生产) | 1000 QPS 就是 432 GB/天,大部分正常请求没人看 |
指标里放 user_id / request_id | 基数爆炸,一条标签把存储放大 100 倍 |
| 保留期一刀切 30 天 | 热数据查不动、冷数据白花钱 |
| 只算存储单价,不算查询成本与人力 | 省下的钱小于排障变慢的损失 |
| 上了三支柱但没有 trace_id 贯通 | 三个孤岛,故障时仍然靠猜 |
自检
Q1 为什么「告警一律用指标」,而不是用日志或链路?
▸ 答案:因为只有指标的成本不随请求量增长(2 B/请求 vs 日志 1 KB/请求),可以长期保留并做秒级判定。若用日志告警,要么全量存(贵 500 倍),要么采样(错误样本被采样掉,漏报)。链路的错误率还会被采样率污染——尾采样保留 100% 错误后,trace 里的错误率远高于真实值,只能用来「看案例」,不能用来「判程度」。
Q2 1000 QPS、每请求 10 个 span、500 B/span,全采样一天多少原始数据?改 1% 尾采样省多少?省掉的为什么不是「证据」?
▸ 答案:1000 × 10 × 500 × 86400 = 432 GB/天;1% 后 4.3 GB/天,省 99%。尾采样按规则先保留「错误」和「慢请求」(100%),只对正常请求按 0.1%–1% 采,所以省掉的是「正常请求的样本」,事故证据反而一条不少。
Q3 给指标加一个只有 100 个取值的标签,为什么存储涨 100 倍而不是 1 倍?
▸ 答案:一条序列 = 一个标签组合。新增标签的 100 个取值,每个都让原有的每条序列分裂成 100 条,序列数是各标签取值数的乘积;序列数 ×100,抓取点数 ×100,存储也就 ×100。它和 QPS 无关——流量不变,账单照样涨。换成 user_id(百万级取值)就是 ×100 万,直接不可用。正确做法是保持指标低基数,个体维度用日志、链路或 exemplar。
Q4 老板说「日志保留期从 30 天砍到 7 天,省 77%」。这个决策缺了什么信息?
▸ 答案:缺三样:① 排障窗口——事故平均多久被发现、复盘是否需要 30 天前的日志;② 合规底线——资金/审计类日志可能有 6 个月以上的最低保留期(见 3.5 审计与取证);③ 是否已做冷存兜底。正确做法是先分层(热 7 天 + 冷 180 天,冷存单价低一个量级),再压缩热数据保留期,而不是直接把窗口砍掉。