2307 字
约 7 分钟
18
S16-A29 · 指标与日志的边界:什么该进指标、什么只能进日志
2024-12-31
2026-11-10

一个团队把"每个接口的错误数"做成了指标,维度是 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. 自测

  1. 指标与日志的成本分别随什么增长?这个差别决定了哪些取舍?
  2. 为什么 user_id、trace_id 不能做指标维度?"低基数"的判据是什么?
  3. 用 status_class 代替具体状态码,牺牲了什么、换来了什么?
  4. 从指标走到日志,中间最容易断的环节是什么?三个必须一致的关联键是哪三个?
  5. 为什么"全量日志"几乎总是先被砍掉的成本项?分级采样的三条规则怎么定?
  6. "维度太少"与"维度太多"各会导致什么问题?平衡点在哪?
S16-A29 · 指标与日志的边界:什么该进指标、什么只能进日志
https://www.clxhxhhr.top/posts/6441/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。