TL;DR · 本文论证:可观测体系永远是 lossy 的,每次采样、聚合、保留期决策都是认识论选择——它决定了你能回答什么问题、不能回答什么问题、什么问题永远问不出来。 · 写给:被可观测成本压住、被基数爆炸打过、做过 retention 取舍的 SRE / 平台工程师 / 架构师。 · 不读会错过:“为什么我们投入这么多还是看不清"的真正答案,以及为什么"完备性"是个伪命题。

打开任何一个生产可观测后端的容量规划表,你都会看到工程师在和同一个问题搏斗:怎么存得下

一家中等规模的互联网公司——每秒五万请求、每个请求产出 50 个属性的宽事件、每个事件平均 2 KB——一天近 9 TB 原始数据。十年近 32 PB。这还没算 trace 的扇出、metric 的高基数维度、日志的扁平字段。

工程师们的应对策略大致就那么几种:上压缩、上列存、下沉冷数据、采样、降采样、聚合。每一种都被讨论过几千次。但很少有人把这个问题写到这一层:

它不是工程优化能解决的问题,是物理约束。

你不可能存下全部状态,只能选择存哪一部分。这个选择不只是工程师在某个 design doc 里写下的一个 trade-off——它构成了整个可观测体系的认识论基础,决定了你能回答什么问题、不能回答什么问题、什么问题永远问不出来。

这一篇要把这件事说清楚。


1. 一个不愉快的算术:状态空间的体积

回到 L1-1 留下的回声三:基数等价于状态空间分辨率

什么意思?给定一个系统,它在每一时刻的"状态"可以被理解为所有可能字段值的一个组合:

state = (service.name, region, version, user_id, http.status, error.type, ...)

每加一个维度,状态空间的体积就乘上这个维度的基数。每加一个 user_id(基数百万),状态空间就放大一百万倍。

如果你想做到 Kalman 意义上的"完全可观测”——即从输出可以唯一反演内部状态——你需要存储覆盖这整个状态空间的样本。

状态空间体积的公式很简单:

V  =  ∏ (每维度的基数)  ×  时间窗口长度

把数字代进去:一个微服务有 service.name(基数 1)、region(基数 10)、version(基数 5)、user_id(基数 10⁶)、http.status(基数 30)、error.type(基数 50)——光这六个维度的笛卡尔积就有 7.5 × 10¹⁰ 个状态。再乘以一天的时间窗口(按秒计 8.6 × 10⁴),你需要存约 6.5 × 10¹⁵ 个时间序列才能完整覆盖。

存不下。即使存得下,也查不动。

flowchart LR
    D1["service.name
基数 1"] D2["region
基数 10"] D3["version
基数 5"] D4["user_id
基数 10⁶"] D5["http.status
基数 30"] D6["error.type
基数 50"] T["时间窗口
1 天 ≈ 8.6×10⁴ 秒"] D1 --> P((笛卡尔积)) D2 --> P D3 --> P D4 --> P D5 --> P D6 --> P T --> P P --> R["≈ 6.5 × 10¹⁵
时间序列"] style D4 fill:#ffe3e3 style R fill:#fff4d6

图 1·六个维度的状态空间体积。user_id 一项就贡献 10⁶ 倍——这就是为什么 TSDB 工程师听到"user_id label"会立刻翻脸。


2. 基数爆炸不是 bug,是数学

在生产可观测系统里,“基数爆炸”(cardinality explosion)通常被描述成一类故障——一个开发者把 user_id 当 label 上线了,Prometheus 内存爆掉。事故复盘里这通常被归类为"使用不当"。

但这种叙事掩盖了一件事——它不是滥用,是数学。

任何对状态空间的细粒度划分都会产生指数级成本,差别只在于哪一维爆得最快。当代 TSDB 的所有优化(稀疏索引、字典编码、Roaring Bitmap、tiered storage、列存)都是在和这个数学常数搏斗——它们不能消除它,只能延缓它。

这就是为什么"加一个 label 就好"在可观测领域永远比看起来贵:每加一个 label,等于在重新定义状态空间的维度。

文献佐证:Brian Brazil(Prometheus 联合创作者)多次在 Robust Perception 博客上写过这件事的工程后果——具有讽刺意味的是,多数事故的触发条件几乎一模一样:某个工程师"无意中"把高基数标识符(user_id、request_id、container_id、trace_id 之一)放进了 metric label,导致 Prometheus 内存爆炸或 series count 突破百万。这些事故的相似性本身就是一个证据:它们不是"开发者粗心",它们是数学约束在工程现场的反复显形。


3. 存不下:四种"丢弃数据"的策略

既然全存不下,工程实践就发展出了四种扔掉数据的策略。理解每一种各自扔掉了什么、保留了什么,是理解可观测体系认知极限的基础。

策略一·聚合(aggregation):把多个事件压成一个统计量。 丢掉的:每个个体事件的身份。 保留的:分布、计数、分位数。 代价:你能问"p99 延迟是多少",但问不出"那个 p99 是谁的请求"。

策略二·保留期(retention):只保留最近的数据,旧数据删除。 丢掉的:历史细节。 保留的:近期完整信息。 代价:三个月前的奇怪现象——你只能依赖事后判断。

策略三·采样(sampling):只保留部分事件,其余丢弃。 丢掉的:未被采到的具体事件。 保留的:被采样到的事件的完整原貌。 代价:低频但关键的事件可能被错过。

策略四·降采样(downsampling):对历史数据降低时间分辨率(每秒变每分钟)。 丢掉的:精细的时间结构。 保留的:长期趋势。 代价:你能看到一年趋势,看不到去年某天某秒发生过什么。

四种策略在任何生产可观测后端都是同时使用的。它们不是替代关系,是组合关系——每一种处理状态空间公式中的不同一项。

flowchart TD
    EVT["理想:所有事件全保留"]
    EVT -.->|"压维度"| AGG["聚合
保留统计量
丢失个体身份"] EVT -.->|"压时间"| RET["保留期
保留近期
丢失历史"] EVT -.->|"压数量"| SMP["采样
保留子集
丢失未采样事件"] EVT -.->|"压时间分辨率"| DS["降采样
保留长期趋势
丢失瞬时细节"] style EVT fill:#e0eaff

图 2·四种 lossy 策略对应状态空间公式的不同坐标。任何生产体系都是这四者的组合。


4. 采样不是工程技巧,是认识论决定

四种策略里,采样最容易被低估。它通常被描述为"一种降成本手段"。但它真正在做的事情严重得多。

每次你做出一个采样决策,其实是在预测未来你会想问什么问题。

  • 头采样按固定概率丢弃:假设"未来的问题在事件分布上是均匀的"。
  • 尾采样保留出错或慢请求:假设"未来的问题集中在异常事件上"。
  • 自适应采样动态调节:假设"未来的问题取决于当前流量结构"。

这些假设可能对,也可能错。当它们错的时候——比如一个"看起来正常的请求"实际上携带了某个隐藏 bug 的种子——这个事件被采样掉了,之后无论花多少钱都问不回来。

我把这一节叫"认识论决定"而不是"工程权衡",区别就在这里:工程权衡可以事后修复,认识论决定一旦做出就不可逆——你已经把某些问题永久地排除在可问范围之外。


5. 三种采样的真正张力

把三种主流采样策略的本质张力摊开:

Head Sampling(头采样,先验)

在 trace 起点根据预设概率决定保留与否。优点:无状态、便宜、实现简单。缺点:在 trace 开始时,你根本不知道这条 trace 会不会出错——你的"采样决定"被迫在没有任何信息的情况下做出。

它只反映你对全局分布的预设。

Tail Sampling(尾采样,后验)

等 trace 完整结束后,根据它的最终特征决定保留与否。优点:能精准保留"出错的、慢的、含某关键 span 的" trace。缺点:需要在 collector 缓存完整 trace、内存开销大、跨节点协调复杂(一条 trace 可能横跨多个 collector 实例)。

它根据观察到的事实做决定,但代价是必须先观察。

Adaptive Sampling(自适应采样,条件)

采样率随流量、错误率、延迟动态调节——低流量时全采、高流量时只采异常。优点:成本可控、对异常敏感。缺点:你看到的数据取决于当时的系统状态——同一条事件在不同时刻的命运不同,使数据集本身带上了观察者效应。

它的偏差是结构化的、可解释的,但也是难以纠正的。

flowchart TB
    subgraph H["Head Sampling 头采样 (先验)"]
        direction LR
        H1["trace 起点
掷骰子"] --> H2{保留 ?} H2 -->|"是"| H3["完整记录"] H2 -->|"否"| H4["丢弃"] end subgraph T["Tail Sampling 尾采样 (后验)"] direction LR T1["trace 完整结束
缓存全部 span"] --> T2{异常 / 慢 ?} T2 -->|"是"| T3["保留"] T2 -->|"否"| T4["丢弃"] end subgraph A["Adaptive Sampling 自适应 (条件)"] direction LR A1["当前流量结构
错误率 / 延迟"] --> A2["动态采样率"] A2 --> A3["低流量全采
高流量只采异常"] end

图 3·三种采样的本质区别:先验 / 后验 / 条件。每一种都是对"未来问题分布"的不同假设。


6. 完备性是个伪命题

很多团队在做可观测预算讨论时会问:“要怎样才算够完备?”

这是一个无解的问题。

完备性意味着"能回答未来的所有问题"。但未来的问题集是开放的——你不知道明天有人会基于哪个奇怪的属性组合去问一个反直觉的问题。你今天预留的存储和维度只能覆盖你今天能想到的问题集。

你的可观测体系永远是关于未来问题的一个隐含假设。

当那个假设错了——比如一个新业务上线,引入了一种你没预留 label 的维度——你的可观测体系在那个新维度上是结构性盲的。不是"没看清楚",是"根本看不见"。

Charity Majors 强调的 explore-driven workflow(高基数、高维度、可探索)是对这个问题的一种回应:尽可能保留宽事件的原貌,把"问什么问题"的决定推迟到查询时。但这条路仍然受物理约束——你只能保留有限时间窗口内的有限维度。

我能想到的唯一诚实的态度,是承认每个可观测体系都有结构性盲区,并定期审视它。


7. 回声三的真正含义

把上面这些拼起来,L1-1 留下的回声三可以重新表述:

可观测的代价 = 状态空间分辨率的代价。 不可观测的部分 = 你放弃分辨率的那些维度。

这不是工程缺陷,是物理学。

它的工程推论是:

  1. 任何"我们要存所有事件"的承诺都是技术债务,迟早被基数爆炸打破。
  2. 任何采样策略都是对未来问题分布的赌注。
  3. 成熟的可观测体系不是"什么都存",而是清楚地知道自己存了什么、丢了什么、为什么这么选。

这是把可观测性从工具问题升级为治理问题的入口——L3 主线会继续展开。


8. 两种"坏的死亡"

实践中,一个可观测体系常以两种方式失败——它们都和这一篇的认识论选择有关。

死法一·过存储(death by overstorage):什么都存下来,但基数爆炸让查询变得极慢或不可能。系统名义上是"完整的",实际上没人能用。这是一种"我们没做任何认识论选择"的失败。

死法二·过采样(death by oversampling):高效采样让成本看起来漂亮,但当一个真正的事故来临时,你需要的那个事件被采掉了。事故复盘的结论会是"我们的可观测覆盖不够",但根因是"我们对未来问题的假设错了"。

两种死法都很常见。它们的共同点是:当问题出现时,做出错误选择的人通常已经离职了。可观测体系的认识论选择往往是匿名、隐含、长尾的——这是它最危险的地方。

治理它的方式不是技术的,是组织的:定期 cardinality 审计、采样策略评审、retention 等级与业务挂钩、“我们放弃了哪些问题"作为可观测路线图的明文项。这些 L3 会详细讨论。


9. 自我证伪

如果以下任一条件成立,本文的核心论证就会动摇: ① 如果存储 / 计算成本在未来 5 年内崩溃到趋近于零(极端压缩、新型存储介质、量子)——那么"存不下全部"的物理约束被弱化。部分有效但有限: 历史上每次"存储变便宜”,数据规模都同步增长,相对约束没变。 ② 如果可观测的真实问题集是封闭的——即所有未来问题都能被今天预设——那么采样的"认识论"框架确实是矫情。反驳: 这恰恰是 monitoring 和 observability 的本质区分,封闭问题集就是 monitoring;可观测的存在前提就是问题集开放。 ③ 如果有某种数学结构可以让事件流被无损压缩到任意小——那么"什么都存"突然可行。理论可能性: 不为零(信息论的 Kolmogorov 复杂度允许部分场景),但对一般业务事件流无效。


10. Key Takeaways

  • 基数爆炸不是 bug,是数学——任何细粒度的状态划分都会产生指数级成本,TSDB 优化只是延缓而不是消除。
  • 四种 lossy 策略(聚合 / 保留期 / 采样 / 降采样)在任何生产体系都同时使用,每种对应状态空间公式的一项。
  • 采样是认识论决定而非工程权衡——每次采样都在预测未来想问什么问题,错了就不可逆。
  • “完备性"在可观测语境下是伪命题——你的体系永远是关于未来问题的隐含假设,永远有结构性盲区。
  • 可观测体系的失败模式有两种:什么都存(查不动)或采得太狠(缺关键事件)——两者根因都是没明确做认识论选择。
  • 治理它的方式更偏组织而非技术——cardinality 审计、采样评审、retention 与业务挂钩,都是 L3 议题。

11. 通向下篇

把"事件即真相"加上"事件存不下"放在一起,我们得到一个清晰但尴尬的事实:

可观测性永远是带损失的,永远是关于未来问题的赌注。

这个事实在传统系统里已经够难处理。在 L1-4 要讨论的 LLM 系统里,它会变得更难——因为 LLM 的"状态"在某种意义上是无限维度的:每个 token 在每一步推理时的注意力分布、每个 prompt 的语义嵌入、每次 retrieval 召回的 chunk 组合,都构成新的状态轴。传统采样策略对它失效。

L1-4 处理这个新的边界条件。


参考

  • Brazil, B. Cardinality is key. Robust Perception blog——经典的"label 加错了就炸"系列。
  • Brazil, B. (2018). Prometheus: Up & Running. O’Reilly——TSDB 内部机制权威解释。
  • Vitter, J. S. (1985). Random sampling with a reservoir. ACM Transactions on Mathematical Software, 11(1)——reservoir sampling 的奠基论文,至今是采样工程的理论基础。
  • Sigelman, B. H., et al. (2010). Dapper, a Large-Scale Distributed Systems Tracing Infrastructure. Google Technical Report——首次在大规模分布式追踪里系统化使用概率采样的论文。
  • OpenTelemetry Specification. Tail Sampling Processor. opentelemetry.io——尾采样在工程实践中的当代标准实现。