一个团队把"每个接口的错误数"做成了指标,维度是 user_id。上线两天后监控系统开始告警内存不足——因为用户维度是百万级基数,指标序列数量爆炸。同一批人后来把所有信息都退回日志,于是"错误率上升"这件事再也没能被自动发现。
指标与日志不是"两种存法",而是两种查询模式:指标适合回答"现在有多少",日志适合回答"刚才发生了什么"。放错位置,要么把监控系统拖垮,要么让排障失去证据。
1. 三种数据的定位
| 形态 | 适合回答 | 成本结构 |
|---|---|---|
| 指标(metric) | 聚合与趋势(多少、多快) | 按序列数计费 |
| 日志(log) | 单次事件的细节(谁、为什么) | 按存储量计费 |
| 追踪(trace) | 一次请求跨服务的路径 | 按采样数计费 |
三者还有一层"采样语义"的差别:指标天然是聚合后的(每分钟一个点),日志天然是逐条,追踪则通常按比例采样。这意味着指标能"看见趋势",追踪能"看见路径",日志能"看见细节",而三者都不能互相替代。
指标的成本随维度基数增长,日志的成本随写入量增长。这个差别决定了所有取舍:一个高基数维度放进指标会拖垮监控,而大量重复的日志会拖垮存储。
2. 指标该放什么
| 内容 | 为什么适合 |
|---|---|
| 请求数、错误数、耗时分位 | 低基数、可聚合 |
| 资源水位 | 数值型、按实例 |
| 业务关键量(下单数、支付成功率) | 需要趋势与告警 |
| 队列长度、积压量 | 反映健康度 |
基数估算有个简单公式:各维度取值数的乘积 × 实例数 × 指标数。三个 3 值维度、20 个实例、10 个指标,就是 5,400 条序列;一旦加入一个百万值的维度,直接到亿级。
// 基数估算:加维度前先算一遍
series = Π(维度取值数) × 实例数 × 指标数
// 3×2×3 × 20 × 10 = 5,400 可控
// 3×2×3 × 20 × 10 × 1,000,000 = 5.4e9 不可控
关键约束是维度基数:service、status_code、endpoint(模板化后)属于低基数;user_id、order_id、trace_id 属于高基数,绝不该做指标维度。
// 指标定义:维度基数可控,才有意义
metric: http_requests_total
type: counter
labels:
service: [order, user, pay] # 3 个值
method: [GET, POST] # 2 个值
status_class: [2xx, 4xx, 5xx] # 3 个值 —— 用类别而不是具体码
# 总序列数 ≈ 3×2×3×实例数,可控
// 禁止:user_id / order_id / path(未模板化的原始路径)
第三行的做法很实用:用 status_class 而不是具体状态码,把 20 个码压成 3 类,序列数立刻降一个量级,而排障时仍能从日志里拿到具体码。
3. 日志该放什么
| 内容 | 为什么只能进日志 |
|---|---|
| 具体错误信息与堆栈 | 文本、不可聚合 |
| 单次请求的参数 | 高基数、涉敏 |
| 状态变化的完整上下文 | 需要事后还原 |
| 外部系统的原始响应 | 结构不固定 |
日志还有一个指标不具备的性质:它能承载"当时是怎么想的"。一条 event=retry.decision reason=timeout 的记录,说明的是系统在那一刻做出的判断——这类信息只能在日志里,因为它不是数值。
这些内容的共同点是只在"查某一件事"时被需要,而不需要实时聚合。把它们做成指标既没必要也不可行。
{
"ts": "2026-11-10T10:20:31.204Z",
"level": "error",
"service": "order",
"trace_id": "9f2c1a...",
"event": "pay.callback.failed",
"stage": "verify_sign",
"upstream": "pay-gateway",
"upstream_code": "SIGN_MISMATCH",
"order_id": "O-7781",
"retry": 2
}
// 这些字段几乎全部是高基数或文本:适合日志,不适合指标
4. 两者的交叉点:从指标到日志
| 步骤 | 做什么 |
|---|---|
| 1 指标告警 | 看到错误率上升 |
| 2 缩小窗口 | 定位到分钟级与具体服务 |
| 3 拿关联键 | 从该时段的日志取 trace_id 样本 |
| 4 追到单次 | 用 trace 看完整路径 |
第二与第三步之间有一个易断的环节:如果指标与日志没有共同的关联键,就无法从"错误率上升"走到"哪个请求错了"。所以 service、env、时间精度三者必须在两侧一致。
// 关联键必须一致,否则指标与日志互相找不到
指标标签:service=order, env=prod, window=1m
日志字段:service=order, env=prod, ts 精确到 ms
// 断点常见原因:日志缺 env、指标用了分钟桶而日志按秒存、
// 服务名在两侧不一致(order-service vs order)
排障时从指标跳到日志,还要注意时间桶的对齐:指标按分钟聚合,日志按毫秒记录,把"10:20 那一分钟"当成"10:20:00–10:20:59"是最容易犯的错——有些系统的桶是 10:19:30–10:20:29。
// 抓日志时先确认指标的时间桶边界,再取窗口
指标桶:[10:19:30, 10:20:30) ← 若是这种对齐
日志窗口:应取 10:19:30–10:20:30,并向前后各留 30s 余量
第三行的"服务名不一致"是最常见的断点:指标用部署名、日志用应用名,两边都正确,但就是连不上。
5. 成本与保留期
| 数据 | 保留期 | 原因 |
|---|---|---|
| 高精度指标 | 数周 | 排障需要,长期看趋势用降采样 |
| 降采样指标 | 数年 | 容量与增长分析 |
| 错误日志 | 数月 | 复盘与取证 |
| 全量日志 | 数天到数周 | 成本高,按需 |
分级采样的判断标准只有一条:这条日志会不会被查。会被查的(错误、超时、慢请求、状态变化)全留;不会被查的(正常请求的完整入参)按比例留,因为它们的价值是统计分布,不是单条追溯。
第四项几乎是所有团队的瓶颈:全量日志的存储成本随流量线性增长,而它被查询的概率极低。合理做法是"错误全留、正常采样、慢请求全留"。
// 分级采样:把存储花在会被查的地方
sampling:
level: error # 全留
slow: { threshold_ms: 500 } # 全留
sample_ratio: 0.05 # 其余 5%
always_keep:
- "http.status >= 500"
- "retry > 0"
6. 排错:五种"监控有数但查不出原因"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 指标告警但日志里搜不到 | 服务名或时间窗口不一致 | 统一关联键 |
| 指标序列数爆炸 | 高基数维度 | 去掉 ID 类维度 |
| 日志量太大被采样掉 | 全量记录 | 分级采样 |
| 指标看不出是哪类请求 | 维度太少 | 加低位基数维度(如 endpoint 模板) |
| 排障要拼三个系统 | 缺 trace | 引入追踪或统一 trace_id |
排错时还有个顺序建议:先看指标确认"有没有问题",再看日志确认"是什么问题",最后用追踪看"为什么"。反过来的话,日志量会把排查拖成几小时。
第二项与第四项是一对:维度太少查不出原因,维度太多拖垮系统。正确的平衡点是"能区分业务路径,不能区分个体"。
7. 边界:什么情况不必区分
| 场景 | 为什么 | 做法 |
|---|---|---|
| 单服务、无聚合需求 | 只查日志就够 | 不做指标 |
| 极小流量 | 指标成本高于收益 | 日志里统计即可 |
| 一次性排查 | 临时加日志 | 不建永久指标 |
| 已有完整 APM | 两者已被统一 | 按 APM 的模型走 |
判断口径一句话:这个问题需要"实时聚合"还是"事后取证"。前者进指标,后者进日志,两者都要就都记,但用不同的基数与保留期。
8. 自测
- 指标与日志的成本分别随什么增长?这个差别决定了哪些取舍?
- 为什么
user_id、trace_id不能做指标维度?"低基数"的判据是什么? - 用
status_class代替具体状态码,牺牲了什么、换来了什么? - 从指标走到日志,中间最容易断的环节是什么?三个必须一致的关联键是哪三个?
- 为什么"全量日志"几乎总是先被砍掉的成本项?分级采样的三条规则怎么定?
- "维度太少"与"维度太多"各会导致什么问题?平衡点在哪?