第9章 可观测性数据管理与成本
采样、保留与查询优化
章节导读
前八章我们讨论了如何产生、采集、关联、查询可观测性数据,以及如何用它驱动告警与开发流程。但有一个问题始终悬在头顶,而且随着系统规模的增长越来越沉重:这些数据要花钱,而且花得比你想象的多。本章把镜头从「如何用数据」转向「如何管理数据」——采样、保留、查询优化、成本归因与数据治理,这是可观测性从「技术能力」变成「可持续工程实践」的必经之路。
先看量级。可观测性数据的增长速度系统性地快于业务数据,原因有三。第一,它与请求量成正比:业务增长一倍,遥测数据至少增长一倍,而埋点密度的提升会让它增长更多。第二,它有「成功悖论」:系统越可观测,工程师越依赖它,就会埋更多的点、开更多的 DEBUG、保留更长的历史,数据量进一步膨胀——这是一个正反馈回路。第三,三大支柱中日志与追踪的单体成本远高于指标:一条宽事件可能有上百个字段、几 KB 大小,而一个 Counter 无论背后发生多少请求都只占一个数值。行业调查(如 Splunk 的 State of Observability 报告)反复显示,可观测性支出已占许多公司云账单的 10%–30%,个别高流量公司甚至超过计算资源本身;Datadog、New Relic 等按量计费厂商的客户中,「账单休克」(bill shock)是最常见的投诉之一。Charity Majors 在书中直言:如果你的可观测性账单让你不敢埋点,那么这套体系已经在伤害你的工程能力了。
面对成本压力,行业里有两种截然不同的哲学。第一种是「少采多算」:在数据进入系统之前就大幅削减(激进采样、只留 ERROR 日志、短保留期),把成本控制在采集侧。它的优点是账单可预测、后端压力小;缺点是你在故障发生的那一刻就已经决定了哪些数据不存在——而被丢弃的往往正是那个 0.3% 的异常请求。第二种是「多采少存」:尽可能全量采集(尤其是高价值的宽事件),但用分层存储、降采样、列式压缩把长期存储成本压到极低,把「贵」的部分限制在短期热数据上。Honeycomb 是这一哲学的代表:它主张对宽事件不做采样、保留期可以长达数年,靠列式存储与查询时聚合把成本摊薄。两种哲学没有绝对对错,但有一个共识:无差别的削减是最糟糕的策略——它同时损失了诊断能力和成本确定性。
本章的核心思想可以概括为一句话:可观测性数据是一种资产,资产需要生命周期管理。采集侧用采样策略决定「什么值得记录」;存储侧用分层与降采样决定「记录多久、以什么精度记录」;查询侧用索引、预聚合与物化视图决定「怎么便宜地读出来」;治理侧用成本归因与数据质量审计决定「谁在用、值不值」。这四层缺一不可:只做采样不做归因,你不知道砍掉的是谁的关键数据;只做归因不做治理,你只是把账单分摊得更精确而已。
一个贯穿全章的类比是「图书馆」。原始遥测数据像源源不断送来的书:采样是入馆筛选(哪些书值得收藏),保留策略是书架分区(新书开架、旧书闭架、古籍入库),降采样是缩微胶片(牺牲细节换取空间),索引与预聚合是目录卡片与综述文摘(让查找变快),成本归因是借阅统计(哪些书没人借就该剔旧)。一座好的图书馆不是藏书最多的图书馆,而是「读者要的书找得到、找得快、养得起」的图书馆。可观测性平台同理。
我们还会讨论一个组织层面的问题:开源与商业的成本模型对比。自建 Prometheus + Loki + Tempo + Thanos 栈看似免费,实则把成本转移到了运维人力、硬件与隐性故障上;商业 SaaS 看似昂贵,实则把不确定性变成了可预测的账单。理解两种模型的真实 TCO(总拥有成本)构成,是做出理性选型的前提。最后,本章以数据质量治理收尾——垃圾进、垃圾出:再精妙的采样与存储策略,也救不了一堆无人查询的僵尸仪表盘和语义混乱的标签。让我们从数据爆炸的量化分析开始。
核心概念
- 尾部采样
- 数据分层
- 降采样
- 成本归因
- 数据治理
要点精析
可观测性数据的成本解剖:三大支柱的单位成本差异与「量纲错配」。要管理成本,先要理解成本从哪里来。可观测性数据的总成本可以分解为四个环节:采集(SDK 序列化、Agent 处理、网络传输)、处理(Collector 的解析、富化、路由)、存储(写入 IO、索引构建、介质占用)、查询(扫描、聚合、可视化)。三大支柱在这四个环节上的成本结构截然不同。指标是最便宜的:一个 Counter 无论背后发生一百万次还是一亿次请求,存储的都只是一个累加值加时间戳,成本与请求量解耦,这就是为什么 Prometheus 能用一台机器扛住数十万活跃时间序列。日志与事件是最贵的:每条记录都是独立的、变长的、需要索引的文档,成本与事件量严格成正比,一条 2KB 的宽事件在每秒一万事件的流量下就是每天 1.7TB 的原始写入量。追踪介于两者之间:单个 Span 比日志小,但一次请求可能产生几十个 Span,且需要额外的关联索引。理解这个「量纲错配」至关重要:很多团队把指标当日志用(给 Prometheus 塞 user_id 标签导致序列爆炸),或者把日志当指标用(用全文检索做聚合统计),都是在用最贵的方式做最便宜的事。工程决策框架:先问「这个数据的消费模式是聚合还是检索」,聚合类需求优先用指标或预聚合,检索类需求才用原始事件;再问「这个维度的基数是多少」,基数超过一万的维度不要进指标系统的标签。
头部采样(Head Sampling):在请求入口处做出的不可逆决定。头部采样指在 Trace 的根 Span 创建时就决定是否记录整条 Trace,通常由 SDK 侧的采样器实现,如 OpenTelemetry 的 TraceIdRatioBasedSampler(按 trace_id 的哈希值取模,保证同一条 Trace 的所有 Span 决策一致)或 ParentBasedSampler(跟随父 Span 的决策)。它的最大优点是成本极低且可预测:决策在本地完成,不需要缓冲、不需要跨服务协调,被丢弃的请求根本不产生网络传输和后端写入。缺点同样致命:决策时刻你只知道请求的「入口特征」(路径、租户、随机数),完全不知道它未来会不会出错、会不会慢。在 1% 的采样率下,一个只影响 0.3% 请求的故障,其 Trace 有 99.7% 的概率被丢弃——而这恰恰是最需要 Trace 的场景。头部采样的适用边界:流量极大且故障模式「大面积」而非「长尾」的系统(如 CDN、广告竞价),或者作为尾部采样之前的第一级粗筛(先头部采 10%,再尾部精细决策)。一个常见误区是把头部采样率当作「精度旋钮」随意调低来省钱——采样率减半,你丢失的不是「一半的数据」,而是「一半的长尾故障可见性」,而后者的价值远高于前者。另一个误区是各服务独立配置采样率,导致同一条 Trace 在不同服务上决策不一致,拼出残缺的调用链;正确做法是用 ParentBased + 统一的采样配置分发。
尾部采样(Tail Sampling):等 Trace 结束后再做决定,用缓冲换完整性。尾部采样的核心思想是:把采样决策推迟到整条 Trace 的所有 Span 都到达之后,此时你已经知道这条 Trace 是否包含错误、总耗时多少、经过了哪些服务,可以做出「价值感知」的决策——保留所有出错的、所有超过 P99 延迟的、所有涉及关键租户的 Trace,而对海量正常 Trace 只保留一个小比例。OpenTelemetry Collector 的 tailsamplingprocessor 是标准实现,它支持 latency、status_code、string_attribute、rate_limiting、composite 等多种策略并可组合。代价是必须缓冲:Collector 要在内存中持有每条 Trace 直到其「结束」(由 decision_wait 参数决定,通常 30 秒),这带来三个工程问题。第一,内存占用与流量和 decision_wait 成正比,高流量下需要水平扩展多个 Collector 实例,而同一 Trace 的 Span 可能落在不同实例上——必须用 loadbalancing exporter(按 trace_id 一致性哈希路由)先把同 Trace 的 Span 汇聚到同一实例。第二,decision_wait 是一个两难:设得太短,慢 Trace 的后续 Span 还没到就做了决策,导致截断;设得太长,内存压力大且数据延迟高。第三,尾部采样无法减少「采集侧」成本——所有 Span 仍然要从应用传到 Collector,网络与序列化开销一分不少,它省的是后端存储与索引成本。决策框架:如果你的成本瓶颈在后端(按 GB 计费的 SaaS、存储集群),尾部采样收益巨大;如果瓶颈在应用侧 CPU 或网络带宽,应该结合头部采样做两级过滤。
动态采样(Dynamic Sampling):让采样率随数据分布自适应。固定比例采样有一个隐藏问题:它对所有「类型」的流量一视同仁,但流量的价值密度极不均匀。假设 99% 的请求是「GET /health 200」这种完全同质的流量,1% 是各种罕见的错误组合,固定 10% 采样会留下大量重复的健康请求(信息冗余)却可能漏掉罕见组合。动态采样(Honeycomb 的 Refinery 是代表实现)的做法是:先对 Trace 按「相似性键」(如 status_code + route + error 类型的组合)分桶,然后对每个桶独立决定采样率——出现次数越多的桶采样率越低,罕见桶则 100% 保留,同时保证每个桶至少留下 N 条样本。其效果是「保留分布的形状」:采样后的数据集在统计上仍然能代表原始流量的多样性,而不是被高频模式淹没。Refinery 还支持基于「Trace 是否有后代 Span 出错」「是否跨越特定服务」的规则,以及把采样决策本身作为一个字段(SampleRate)写回数据,使得后续聚合查询可以通过乘以 SampleRate 还原真实计数——这一点极其重要,否则采样后的错误率统计会严重失真。动态采样的实现代价与尾部采样类似(需要缓冲与汇聚),但算法复杂度更高,且「相似性键」的选择需要工程判断:键选得太细,每个桶只有一两条,退化为全量保留;选得太粗,退化为固定比例采样。误区提示:动态采样不是「更聪明的省钱工具」,而是「在给定预算下最大化信息保留」的工具——如果你的预算允许全量保留(Honeycomb 对宽事件的立场),不采样才是最优解。
存储分层:热、温、冷三层的成本-可查询性权衡。可观测性数据的访问模式高度时间局部化:绝大多数查询集中在最近几小时(正在处理的故障),少量查询回溯几天(趋势对比),极少数查询回溯数月(合规审计、年度容量规划)。存储分层的思想就是按访问频率分配不同成本的介质与索引强度。热层(通常 3–14 天):SSD/NVMe、全字段索引、支持任意维度即席查询与秒级响应,承担 95% 以上的查询流量,单位成本最高。温层(14–90 天):HDD 或对象存储 + 本地缓存、部分索引(只索引常用维度)、查询延迟可接受到十秒级,用于趋势分析与事后复盘。冷层(90 天以上):对象存储归档(S3 Glacier 类)、压缩率最大化、几乎不建索引、查询需要分钟级甚至人工触发,主要服务合规与取证需求。分层的实现载体:Thanos 与 Cortex/Mimir 把 Prometheus 数据下沉到对象存储并支持按时间分块(block)的分级压缩;Elasticsearch 的 ILM(Index Lifecycle Management)用 rollover + shrink + force merge + 冻结索引实现自动流转;Loki 天然把索引与数据分离,历史数据可直接放对象存储。关键工程细节:分层策略必须与 SLO 和合规要求对齐——如果监管要求交易日志保留七年且可检索,那么「冷层不可查询」的设计就不合规;如果故障复盘通常在 72 小时内完成,热层设 30 天就是浪费。误区:分层不是「把旧数据扔掉」的委婉说法,冷层的价值在于「需要时还能拿到」,如果恢复流程从未演练过,冷层等于不存在。
降采样(Downsampling):用精度换空间,以及 Counter 语义的陷阱。降采样指把高精度的历史数据聚合为低精度版本:原始 15 秒一个点,30 天后只保留 5 分钟一个点,一年后只保留 1 小时一个点。对于「过去一年的日请求量趋势」这类查询,1 小时精度完全够用,而存储量降到原始的 1/240。Thanos 的 compactor 组件自动完成这件事,生成 5m 和 1h 两个降采样分辨率的块,查询时通过 max_resolution 参数选择。但降采样有几个必须理解的语义陷阱。第一,Counter 的降采样不能简单取平均:Counter 是单调递增的累加值,降采样时应保留区间内的最大值(或首末值以便下游计算 rate),取平均会破坏 rate/increase 函数的正确性。第二,Histogram 的降采样必须逐桶累加而不能对分位数再聚合:P99 的平均值不等于平均的 P99,这是分位数的不可加性;正确做法是保留原始 bucket 计数,查询时重新计算分位数。第三,Gauge 的降采样应同时保留 min/max/avg/count 四个统计量,只留 avg 会抹平毛刺——而毛刺往往正是你要找的东西。第四,降采样是不可逆的:一旦原始精度数据过期删除,任何「当初没想到」的问题都无法再回答,因此降采样的保留窗口应该保守设置,宁可多存一段时间。决策框架:对告警与 SLO 计算所依赖的序列,降采样窗口必须长于 SLO 周期(30 天 SLO 至少保留 30 天原始精度);对纯趋势观察的序列,可以激进降采样;对高基数诊断数据,降采样无意义(它本来就是事件而非序列),应该走分层归档路线。
查询优化:索引、预聚合与物化视图的三层加速体系。可观测性后端的查询成本主要来自「扫描量」:一个跨越七天、涉及上百个字段、过滤后只剩一百条记录的查询,可能需要扫描数十亿条原始数据。优化的第一层是索引:为高频过滤字段建立倒排索引(Elasticsearch/Loki)或稀疏索引(ClickHouse 的主键排序 + 跳数索引),把扫描范围从全量缩小到候选集。索引的代价是写入放大与存储膨胀,因此必须选择性建立——给所有字段建索引等于没有索引。ClickHouse 类列式存储的经验法则是:把最常用的过滤维度放在 ORDER BY 键的前缀位置,利用主键索引跳过数据块;对高基数字段用 bloom filter 跳数索引。第二层是预聚合:把「每次查询都重新计算」变成「写入时计算一次、查询时直接读」。Prometheus 的 recording rules 是典型:把复杂的 SLO 燃烧率计算、多维度的错误率聚合作为规则周期性求值,结果作为新的时间序列存储,仪表盘查询这些预计算序列而非原始数据,查询延迟从秒级降到毫秒级。第三层是物化视图:ClickHouse 的 Materialized View、Druid 的 rollup、Honeycomb 的 SLO 与 Triggers 都属于此类——在数据写入时同步或增量地维护聚合结果。三层的选择依据是查询模式的稳定性:模式固定且高频(仪表盘、告警)用预聚合;模式多变但过滤字段集中(即席探索)用索引;模式多变且聚合维度爆炸用列式存储 + 查询时聚合(牺牲单次查询成本换取灵活性)。误区:预聚合不是越多越好,每条 recording rule 都是持续的 CPU 与存储开销,且规则本身需要维护——为「可能有人查」而建的预聚合,多数最终无人使用。
成本归因:让每一字节遥测数据都有主人。成本管理的最大障碍往往不是技术而是「无主」:可观测性账单是一个整体数字,没人知道哪个团队、哪个服务、哪类数据贡献了多少,于是既无法问责也无法优化。成本归因的做法是给遥测数据流本身打上「归属标签」并在管道中统计流量。第一步是标签规范:强制所有遥测数据携带 team、service、deployment.environment、cost_center 四个资源属性(通过 OTel SDK 的 Resource 或 Collector 的 resource processor 注入),这是归因的最小维度集。第二步是流量计量:在 Collector 中用 metrics generator 或自定义 exporter 统计每个 team/service 组合产生的 Span 数、日志字节数、指标序列数,形成「遥测的遥测」;商业平台如 Datadog 的 Usage Attribution、Honeycomb 的 query cost 面板提供类似能力。第三步是分摊与反馈:把计量数据换算为成本(自建按存储与 CPU 分摊,SaaS 按账单单价),生成团队级月度报表,并设置预算告警(某团队本周遥测量环比增长 50% 时通知其负责人)。归因的行为学效果远大于财务效果:当团队第一次看到「你们服务贡献了全公司 30% 的日志量,其中 80% 是重复的 DEBUG 输出」时,优化会自发发生——这就是「可见性驱动治理」。误区提示:归因不要做成惩罚机制,否则团队会倾向于少埋点、藏数据,最终损害的是整个组织的可观测性;正确的框架是「预算内自由、超预算需解释」,把成本意识与工程自主性平衡起来。
数据质量治理:垃圾进、垃圾出,以及「僵尸遥测」的清理。可观测性平台的质量问题通常不是「数据太少」而是「数据太脏」:同一个服务在不同团队手里叫三个名字,environment 标签有时是 prod 有时是 production,HTTP 状态码一会儿是整数一会儿是字符串,时间戳时区混乱。这些不一致会让跨服务查询失效——你按 service.name=checkout 过滤,却漏掉了一半叫 checkout-service 的数据。治理的第一项工作是建立语义约定(Semantic Conventions):OpenTelemetry 已经为常见属性定义了标准命名(http.request.method、db.system、k8s.pod.name 等),组织内部应在此基础上补充业务维度的约定(tenant.id、order.type),并用 Collector 的 transform/attributes processor 在管道中强制规范化(重命名、类型转换、枚举值映射),而不是指望每个团队自觉。第二项工作是字段级审计:定期统计每个字段的「查询命中率」与「取值分布」,识别三类问题字段——从未被查询的(考虑删除)、基数异常膨胀的(如把时间戳或 UUID 放进了索引字段)、取值高度集中的(如 99.9% 都是同一个值,作为过滤维度无意义)。第三项工作是仪表盘与告警的生命周期管理:给每个仪表盘记录「最后访问时间」,超过 90 天无人访问的自动标记为待归档,通知所有者确认;告警规则同理,长期未触发或触发后从未被处理的规则应被复审。治理的节奏建议是季度性的「遥测大扫除」,配合日常的准入审查(新服务接入时检查标签规范)。核心观念:数据质量是可观测性的复利资产,脏数据的成本不是线性的而是指数级的——它会污染下游每一个查询、每一个告警、每一次决策。
开源与商业的成本模型对比:TCO 的真实构成。「自建省钱」是一个需要严格检验的直觉。自建可观测性栈(Prometheus + Thanos + Loki + Tempo + Grafana,或 ClickHouse + Vector + 自研查询层)的显性成本是硬件与云资源:存储(对象存储 + 本地 SSD)、计算(Collector 集群、查询节点、Compactor)、网络(跨可用区流量)。但真正的大头是隐性成本:运维人力(一个中等规模的自建栈通常需要 1–3 名专职 SRE 维护,按全成本年薪计算远超硬件)、演进成本(版本升级、组件替换、容量规划)、故障成本(可观测性平台自身宕机时,业务团队失去诊断能力的机会成本)、以及「长尾功能缺口」(自建栈往往缺少商业产品的异常检测、智能降噪、企业级权限与审计,需要自研或忍受)。商业 SaaS 的成本结构相反:显性成本高(按 GB 或按事件计费,规模大时账单惊人且随业务线性增长),隐性成本低(零运维、功能完整、SLA 保障)。决策框架有三个维度:规模(小团队与初创公司几乎总应该选 SaaS,因为人力成本占比过高;超大规模公司自建可能省 50% 以上,因为议价与规模效应)、数据敏感性(金融、医疗、政务等合规场景可能强制自建或私有云)、可观测性成熟度(成熟度低的团队自建容易陷入「工具维护吞噬工程能力」的陷阱)。中间路线值得重视:用 OpenTelemetry 统一采集侧(避免厂商锁定),后端混合部署(核心指标自建 Prometheus,高基数事件用 SaaS),或者选择可自托管的商业产品(Grafana Enterprise、ClickHouse Cloud)。关键是保留「采集侧的可移植性」——只要数据格式是 OTLP,后端换起来就是配置问题而非重构问题。
数据生命周期管理的整合决策框架:把四层策略串成一条流水线。前面讨论的采样、分层、降采样、查询优化、归因、治理不是六个独立话题,而是同一条数据生命周期上的六个决策点,需要协同设计。一条完整的决策链是这样的:(1)在应用侧,用「价值密度」判断埋点必要性——这个事件能回答什么问题?谁会用?多久用一次?无人能答的事件不埋。(2)在采集侧,用两级采样控制流量——头部采样做粗筛(保护应用与网络),尾部/动态采样做精筛(保护诊断能力),并始终保留 100% 的错误与长尾。(3)在管道侧,用 Collector 做规范化与富化(统一标签、脱敏 PII、注入归属信息),同时计量各团队流量。(4)在存储侧,按访问模式分层:热层全索引短保留、温层部分索引、冷层归档;指标类数据叠加降采样。(5)在查询侧,为稳定的高频查询建预聚合,为探索性查询保留原始数据的即席能力。(6)在治理侧,季度审计字段命中率、仪表盘活跃度、告警有效性,把结果反馈回第(1)步的埋点决策。这条链上最常见的失败模式是「局部优化、全局失衡」:比如为了省存储把热层保留期砍到 3 天,结果故障复盘时数据已过期,工程师转而打开 DEBUG 日志全量采集,总成本反而上升。另一个失败模式是「策略与 SLO 脱节」:保留期、降采样窗口、采样率都应该从 SLO 周期与合规要求反推,而不是从预算正推。记住本章的核心命题:可观测性成本管理的目标不是最小化支出,而是最大化「每单位成本换来的诊断能力与决策价值」——这是一个投资回报率问题,不是一个削减问题。
代码示例
/* 示例 1:OTel Collector 两级采样管道(头部粗筛 + 尾部精筛 + 一致性哈希汇聚) */
/* 第一级:应用侧 SDK 头部采样 20%,减轻网络与 Collector 压力 */
/* Go SDK 侧配置示意 */
/* sampler := sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.2)) */
/* 第二级:Collector 网关层,先用 loadbalancing 把同一 trace_id 的 Span 汇聚到同一实例 */
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 25
/* 尾部采样:多策略组合,异常与长尾 100% 保留 */
tail_sampling:
decision_wait: 30s /* 等待 Trace 完整的最长时间,需大于最慢请求耗时 */
num_traces: 100000 /* 内存中缓冲的 Trace 上限,超出则提前决策 */
expected_new_traces_per_sec: 5000
policies:
- name: keep-all-errors
type: status_code
status_code:
status_codes: [ERROR, UNSET]
- name: keep-slow-traces
type: latency
latency:
threshold_ms: 2000
- name: keep-critical-tenants
type: string_attribute
string_attribute:
key: tenant.tier
values: [enterprise, premium]
- name: keep-cross-region
type: span_count
span_count:
min_spans: 12 /* Span 数多意味着调用链复杂,保留用于拓扑分析 */
- name: sample-normal-traffic
type: probabilistic
probabilistic:
sampling_percentage: 5
/* 把采样决策的倍率写回属性,供下游聚合还原真实计数 */
transform/writesamplerate:
error_mode: ignore
trace_statements:
- context: resource
statements:
- set(attributes["sample.rate_applied"], "true")
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, transform/writesamplerate]
exporters: [otlp/tempo]这份配置实现了「异常全留、正常抽样」的尾部采样策略,是可观测性成本控制中收益最高的一环。几个关键参数的工程含义值得展开。decision_wait 设为 30 秒,意味着 Collector 会为每条 Trace 在内存中保留最多 30 秒等待后续 Span 到达;如果你的系统存在耗时超过 30 秒的批处理或长轮询请求,这些 Trace 会被截断决策,需要单独用 string_attribute 策略按 route 白名单保留,或调大该值(代价是内存)。num_traces 是缓冲上限,按「每秒新 Trace 数 × decision_wait × 单 Trace 平均内存占用」估算,示例中 5000 × 30 = 15 万条量级,设为 10 万意味着高峰期会提前决策——这是有意的背压设计,宁可牺牲少量采样精度也不能让 Collector OOM。策略顺序很重要:tailsamplingprocessor 按声明顺序求值,任一策略命中即保留,因此把「必须保留」的规则放前面、「抽样」规则放最后。status_code 策略中包含 UNSET 是一个容易忽略的细节——OTel 中未显式设置状态的 Span 默认为 UNSET,很多客户端库不会主动设 ERROR,只保留 ERROR 会漏掉大量真实故障。最后,采样后必须解决「统计失真」问题:5% 采样下的错误率计算需要乘以采样倍率,生产环境建议用 Refinery 类支持 SampleRate 字段回写的实现,或在 spanmetrics 连接器生成指标时基于全量数据(在采样之前)计算,这样指标不受追踪采样影响。/* 示例 2:Prometheus + Thanos 长期存储与降采样配置 */
/* Prometheus 侧:只保留 2 天本地数据,通过 remote_write 上传到 Thanos */
/* prometheus.yml 关键片段 */
global:
scrape_interval: 15s
external_labels:
cluster: prod-cn-hangzhou-1
region: cn-hangzhou
cost_center: cc-platform
/* 启动参数(Prometheus) */
/* --storage.tsdb.retention.time=2d */
/* --storage.tsdb.path=/prometheus */
/* --enable-feature=exemplar-storage */
/* Thanos Sidecar:把本地 TSDB 块上传到对象存储 */
/* thanos sidecar --tsdb.path=/prometheus */
/* --objstore.config-file=/etc/thanos/bucket.yml */
/* --prometheus.url=http://localhost:9090 */
/* bucket.yml:对象存储配置 */
type: S3
config:
bucket: acme-observability-thanos
endpoint: s3.cn-hangzhou.amazonaws.com.cn
sse_config:
type: SSE-S3
/* Thanos Compactor:降采样与保留策略的核心 */
/* thanos compact --data-dir=/var/thanos/compact */
/* --objstore.config-file=/etc/thanos/bucket.yml */
/* --retention.resolution-raw=30d */
/* --retention.resolution-5m=180d */
/* --retention.resolution-1h=3y */
/* --downsampling.disable=false */
/* --compact.concurrency=4 */
/* Thanos Store Gateway:查询对象存储中的历史块 */
/* thanos store --data-dir=/var/thanos/store */
/* --objstore.config-file=/etc/thanos/bucket.yml */
/* --index-cache-size=500MB */
/* --chunk-pool-size=2GB */
/* Query 侧:查询时选择分辨率,避免不必要的高精度扫描 */
/* thanos query --store=dnssrv+_grpc._tcp.thanos-store.monitoring.svc */
/* --query.max-concurrent=32 */
/* --query.partial-response */
/* Grafana 数据源中按时间范围自动降分辨率的查询示例 */
/* 最近 6 小时用原始精度,最近 1 年用 1h 精度 */
/* /api/v1/query_range?query=...&max_resolution=auto */Thanos 的三档保留参数是本章「存储分层 + 降采样」思想的直接落地:原始精度(raw,15 秒一个点)保留 30 天,5 分钟降采样保留 180 天,1 小时降采样保留 3 年。为什么原始精度只留 30 天?因为绝大多数告警计算、SLO 燃烧率分析、故障复盘都发生在 30 天窗口内,而原始精度的存储量是 1h 精度的 240 倍。30 天这个数字还要与 SLO 周期对齐——如果你的 SLO 是 30 天滚动窗口,那么原始精度保留期必须 ≥ 30 天,否则窗口边缘的计算会因数据缺失而失真。Compactor 是这套体系中唯一有状态且不可并行部署的组件(多个 Compactor 同时操作同一 bucket 会导致数据损坏),它同时负责块的合并、降采样与过期删除,是运维上最需要监控的组件——Compactor 落后会导致查询性能下降与存储膨胀。查询侧的 max_resolution=auto 是一个容易被忽略的省钱开关:Grafana 面板查询一年范围时若强制原始精度,Store Gateway 需要拉取并聚合海量 chunk,延迟从毫秒级恶化到分钟级;auto 让查询层根据时间范围自动选择最粗的可用分辨率。external_labels 中的 cluster/region/cost_center 是成本归因的基础——没有这三个标签,多集群数据汇聚后你将无法回答「哪个集群的指标最多」。最后的 partial-response 参数允许部分 Store 不可用时返回不完整结果而非报错,这是可用性与正确性的权衡,在成本分析类查询中应关闭(宁可失败也不要错误的账单)。/* 示例 3:日志/事件数据的分层保留策略(Loki + 对象存储) */
/* loki-config.yaml 关键片段 */
schema_config:
configs:
- from: 2026-01-01
store: tsdb
object_store: s3
schema: v13
index:
prefix: loki_index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
shared_store: s3
aws:
s3: s3://acme-observability-loki/cn-hangzhou
s3forcepathstyle: false
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true /* 必须显式开启,否则 limits 中的 retention 不生效 */
delete_request_store: s3
limits_config:
retention_period: 720h /* 全局默认保留 30 天 */
ingestion_rate_mb: 10
ingestion_burst_size_mb: 20
max_streams_per_user: 100000 /* 限制活跃流数量,防止标签基数爆炸 */
max_global_streams_per_user: 500000
reject_old_samples: true
reject_old_samples_max_age: 168h
per_stream_rate_limit: 5MB
per_stream_rate_limit_burst: 20MB
/* 按流粒度的差异化保留:合规数据长留,调试数据短留 */
retention_stream:
- selector: '{namespace="audit"}'
priority: 1
period: 25560h /* 审计日志保留 3 年(合规要求) */
- selector: '{severity="debug"}'
priority: 2
period: 72h /* DEBUG 日志只留 3 天 */
- selector: '{namespace="kube-system", severity="info"}'
priority: 3
period: 336h /* 基础设施 INFO 留 14 天 */
/* S3 生命周期规则:把历史 chunk 转入低频/归档存储 */
/* aws s3api put-bucket-lifecycle-configuration */
/* {
"Rules": [
{ "ID": "to-ia", "Filter": {"Prefix": "cn-hangzhou/audit/"},
"Status": "Enabled",
"Transitions": [
{"Days": 90, "StorageClass": "STANDARD_IA"},
{"Days": 365, "StorageClass": "GLACIER"}
],
"Expiration": {"Days": 1095}
}
]
} */这份配置展示了「同一份数据、不同价值、不同寿命」的精细化保留策略,其核心是 retention_stream 的差异化规则。全局默认 30 天覆盖了绝大多数运维诊断需求;审计日志因合规要求保留 3 年(25560 小时),但注意它同时被 S3 生命周期规则在 90 天后转入低频存储、365 天后转入归档存储——这就是「逻辑保留期」与「物理存储层级」的解耦:数据在保留期内始终存在且可查询,但存储单价随时间下降一个数量级。DEBUG 日志只留 72 小时是一个高杠杆决策:多数系统的日志量中 DEBUG 占 50%–80%,而 72 小时之外的 DEBUG 几乎从未被查询过,仅此一条规则通常能削减 40% 以上的日志存储成本。max_streams_per_user 是 Loki 特有的保护机制——Loki 为每个唯一的标签组合维护一个「流」(stream),流数量等于标签基数,把 pod_name、request_id 这类高基数值放进 Loki 标签会导致流爆炸、索引膨胀乃至集群不可用;正确做法是把这些值放进日志正文(用 LogQL 的 json/regexp 解析器在查询时提取),标签只保留低基数的路由维度。retention_enabled 必须显式开启是 Loki 的一个著名陷阱:只在 limits_config 里写 retention_period 而忘记开启 compactor 的 retention_enabled,数据会永久保留,很多团队直到对象存储账单异常才发现。最后,reject_old_samples_max_age 防止时钟错乱的客户端写入远古时间戳的数据,这类数据会创建大量小 chunk 严重损害压缩率与查询性能。/* 示例 4:成本归因——统一归属标签注入 + 遥测流量计量 */
/* Collector 配置:强制注入归属维度,并统计各团队的数据量 */
processors:
/* 从 Kubernetes 元数据自动推导归属标签 */
k8sattributes:
extract:
metadata:
- k8s.namespace.name
- k8s.deployment.name
- k8s.pod.name
labels:
- tag_name: team
key: team
- tag_name: cost_center
key: cost-center
- tag_name: service
key: app.kubernetes.io/name
/* 规范化与兜底:缺失归属标签的数据打上 unowned 标记,便于治理 */
transform/ownership:
error_mode: ignore
trace_statements:
- context: resource
statements:
- set(attributes["service.name"], "unknown-service") where attributes["service.name"] == nil
- set(attributes["team"], "unowned") where attributes["team"] == nil
- set(attributes["cost_center"], "cc-unattributed") where attributes["cost_center"] == nil
/* 环境标签枚举值统一:production/prod/PROD -> prod */
- replace_pattern(attributes["deployment.environment"], "^(?i)production$", "prod")
/* 脱敏:删除高成本且含 PII 的字段 */
attributes/redact:
actions:
- key: http.request.header.authorization
action: delete
- key: user.email
action: hash
exporters:
/* 内部遥测指标暴露,用于成本计量 */
prometheus:
endpoint: 0.0.0.0:8888
service:
telemetry:
metrics:
level: detailed /* 开启详细内部指标,含每个 pipeline 的吞吐 */
address: 0.0.0.0:8889
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, transform/ownership, attributes/redact, batch]
exporters: [otlp/backend]
/* 成本分摊查询(PromQL):按团队统计追踪数据量并折算成本 */
/* 每个团队每秒导出的 Span 数 */
/* sum by (team) (rate(otelcol_exporter_sent_spans{exporter="otlp/backend"}[1h])) */
/* 30 天累计 Span 量(假设单价 0.35 元 / 百万 Span) */
/* sum by (team) (
increase(otelcol_exporter_sent_spans[30d])
) * 0.35 / 1000000 */
/* 无主数据占比(治理 KPI,目标 < 1%) */
/* sum(increase(otelcol_exporter_sent_spans{team="unowned"}[7d]))
/ sum(increase(otelcol_exporter_sent_spans[7d])) */成本归因的技术前提是「每条数据都知道自己属于谁」,这份配置用三层机制保证这一点。第一层是自动推导:k8sattributes processor 从 Pod 的命名空间与标签中提取 team、cost_center、service,开发者无需在代码里写任何东西——这是归因能落地的关键,任何依赖「开发者自觉填写」的方案最终覆盖率都不会超过 60%。第二层是兜底与可见化:transform processor 把缺失归属的数据显式标记为 unowned / cc-unattributed,而不是让它们悄悄混入。这个标记的价值在于它把「归因覆盖率」变成了一个可监控的 KPI——示例最后那条 PromQL 计算无主数据占比,目标小于 1%,一旦某个新服务上线忘记打 team 标签,这个比例会立刻上升并触发治理流程。第三层是规范化:replace_pattern 把 production/prod/PROD 统一为 prod,避免同一环境因标签值不一致而在成本报表中被拆成三行。注意正则末尾的锚点在 OTTL 字符串中需要写作转义形式,避免被解释器误读,这是编写 transform 语句时的常见坑。exporter 侧的 otelcol_exporter_sent_spans 是 Collector 自带的内部指标,按 exporter 维度统计发送量;如果需要按 team 维度统计,需要在 pipeline 中为每个团队建立独立 pipeline,或使用支持按属性分组的自定义 exporter(如 spanmetrics connector 配合 team 维度)。成本折算的单价应来自真实账单:SaaS 场景用合同单价,自建场景用「(存储成本 + 计算成本 + 人力成本)/ 总数据量」的全成本单价——只算硬件的单价会严重低估真实成本,导致「自建很便宜」的错误结论。/* 示例 5:预聚合 recording rules——把高频昂贵查询固化为廉价序列 */
/* prometheus-rules.yaml */
groups:
- name: cost-attribution.rules
interval: 60s
rules:
/* 各团队每小时的遥测数据量(Span 数),供成本报表直接读取 */
- record: telemetry:spans_per_team:rate1h
expr: sum by (team, service, cluster) (
rate(otelcol_exporter_sent_spans[1h])
) * 3600
labels:
aggregation_level: team
/* 各团队日志写入速率(MB/s) */
- record: telemetry:log_ingest_mb_per_team:rate5m
expr: sum by (team, namespace) (
rate(loki_distributor_bytes_received_total[5m])
) / 1024 / 1024
/* 序列基数监控:每个 job 的活跃时间序列数,基数爆炸的早期预警 */
- record: telemetry:active_series_per_job:count
expr: count by (job, cluster) ({__name__!=""})
/* 高基数标签 TOP 检测辅助:单个标签值的序列数 */
- record: telemetry:series_per_pod:topk
expr: topk(50, count by (pod) ({__name__=~"http_.+"}))
- name: slo-preaggregation.rules
interval: 30s
rules:
/* 服务级请求总量与错误量(SLO 计算的分子分母,避免每次查询扫描全量原始序列) */
- record: job:http_requests:rate5m
expr: sum by (job, cluster, status_class) (
rate(http_requests_total[5m])
)
labels:
purpose: slo
- record: job:http_errors:rate5m
expr: sum by (job, cluster) (
rate(http_requests_total{status_class="5xx"}[5m])
)
/* 延迟直方图预聚合:保留 bucket 而非分位数,遵守分位数不可加原则 */
- record: job:http_request_duration_seconds_bucket:rate5m
expr: sum by (job, cluster, le) (
rate(http_request_duration_seconds_bucket[5m])
)
/* 30 天 SLO 燃烧率所需的长窗口聚合(查询代价极高,必须预计算) */
- record: job:slo_error_ratio:30d
expr: |
sum by (job, cluster) (increase(http_requests_total{status_class="5xx"}[30d]))
/
sum by (job, cluster) (increase(http_requests_total[30d]))
- name: capacity.rules
interval: 5m
rules:
/* 存储容量趋势:对象存储用量与增长率,用于容量规划 */
- record: storage:thanos_bucket_bytes:sum
expr: sum by (bucket) (thanos_objstore_bucket_bytes_total)
- record: storage:thanos_bucket_growth:7d
expr: (storage:thanos_bucket_bytes:sum
- storage:thanos_bucket_bytes:sum offset 7d)
/ storage:thanos_bucket_bytes:sum offset 7dRecording rules 是查询优化中投入产出比最高的手段,其本质是「用固定的写入时计算换取可变的查询时计算」。这份规则文件体现了三类典型场景。第一类是成本报表类聚合:telemetry:spans_per_team:rate1h 把「按团队统计遥测量」这个每月要跑几百次、每次都要扫描全量 exporter 指标的查询,固化为一条每分钟更新一次的序列,仪表盘查询成本从秒级降到毫秒级。注意命名遵循 Prometheus 社区约定的 level:metric:operations 三段式(如 job:http_requests:rate5m),这个约定不是形式主义——它让规则的聚合层级与操作一目了然,是规则库可维护性的基础。第二类是 SLO 长窗口聚合:job:slo_error_ratio:30d 计算 30 天窗口的错误率,直接查询需要对 30 天的原始数据做 increase 聚合,在大规模集群上可能耗时数十秒甚至超时,而 SLO 仪表盘与燃烧率告警每分钟都要用它——不预聚合根本无法运行。这里有一个关键的语义细节:延迟数据预聚合的是 histogram bucket(保留 le 标签)而不是 histogram_quantile 的结果,因为分位数不可加,对已计算的分位数再做 sum/avg 会得到数学上错误的值;保留 bucket 后,任何时间窗口与任何聚合维度的分位数都可以在查询时正确重算。第三类是「元监控」:telemetry:active_series_per_job:count 监控每个 job 的活跃序列数,这是基数爆炸的早期预警——某个服务不小心把 request_id 放进标签时,这条序列会在几小时内暴涨,配合告警可以在造成集群故障前发现。规则的成本也要核算:每条规则在每个 interval 都要求值,interval=30s 的复杂规则可能消耗可观的 CPU;建议为规则组本身建立监控(prometheus_rule_evaluation_duration_seconds、prometheus_rule_evaluation_failures_total),并定期清理从未被查询的规则。/* 示例 6:Honeycomb Refinery 动态采样配置(按价值密度自适应) */
/* refinery-rules.yaml */
SampleRate: 1 /* 全局兜底采样率,1 表示不采样 */
Samplers:
__default__:
DeterministicSampler:
SampleRate: 1
production-traces:
/* 动态采样:按相似性键分桶,高频桶低采样、罕见桶全保留 */
DynamicSampler:
SampleRate: 10 /* 目标平均采样率 1/10 */
FieldSet: /* 相似性键:决定「什么样的 Trace 算同一类」 */
- name
- http.route
- http.status_code
- error.type
- service.name
AddSampleRateKeyToTrace: true
AddSampleRateKeyToField: meta.refinery.dynsampler_key
MaxKeys: 5000 /* 桶数量上限,防止键组合爆炸导致内存增长 */
AgeOutDuration: 300s /* 桶多久未出现后被回收 */
/* 关键业务链路:完全绕过采样 */
payment-critical:
RulesBasedSampler:
Rules:
- name: keep-all-payment-errors
SampleRate: 1
Conditions:
- field: service.name
operator: "="
value: payment-gateway
- field: error
operator: "exists"
- name: keep-all-slow-payments
SampleRate: 1
Conditions:
- field: duration_ms
operator: ">="
value: 1500
- name: keep-every-nth-normal
SampleRate: 20 /* 正常支付流量保留 5% */
- name: default-keep-all
SampleRate: 1 /* 兜底:未匹配任何规则的 Trace 全保留 */
/* 开发/测试环境:激进采样,成本敏感 */
staging-traces:
DeterministicSampler:
SampleRate: 100
/* Refinery 部署要点:多实例需共享 Redis 以协调同一 Trace 的决策 */
/* refinery -c refinery.yaml -r refinery-rules.yaml */
/* config.yaml 片段 */
/* RedisHost: redis.refinery.svc:6379
RedisPrefix: refinery
SampleCacheSize: 10000000
Timeout: 30s
MaxAlloc: 4GB */Refinery 的动态采样解决了固定比例采样的「信息冗余」问题,其核心是 FieldSet 定义的相似性键。示例中用 name + http.route + status_code + error.type + service.name 五个字段组合作为分桶依据,效果是:「GET /api/products 200」这类每秒出现上万次的同质流量被归入同一个桶,按 SampleRate=10 的目标只保留约十分之一(且桶越大采样率越低);而「POST /api/checkout 500 + error.type=InventoryTimeout」这种一天只出现三次的罕见组合,因为桶内计数极低而被 100% 保留。这就是「保留分布形状」的含义——采样后的数据集仍然覆盖原始流量的全部模式类型,而不是被高频模式淹没。MaxKeys=5000 是一个重要的安全阀:如果 FieldSet 中不小心包含了高基数字段(如 user.id 或 trace.id),桶数量会爆炸到百万级,Refinery 内存迅速耗尽;MaxKeys 限制桶总数,超出后退化为全局采样,AgeOutDuration 则回收长期不活跃的桶。AddSampleRateKeyToTrace 把采样决策的键与倍率写回 Trace,这带来两个价值:查询时可以按「采样桶」分组分析,更重要的是聚合计算时可以用 SampleRate 字段还原真实计数(Honeycomb 的查询引擎会自动做这个加权),避免采样导致的统计失真。RulesBasedSampler 展示了另一种模式——基于显式条件的规则采样,适合「某些链路必须全量」的合规或业务场景;注意最后一条 default-keep-all 规则,Refinery 的规则是「首个匹配生效」,没有兜底规则时未匹配的 Trace 会被丢弃,这是配置中最危险的疏漏。多实例部署必须配置 Redis:Refinery 需要跨实例协调同一 Trace 的采样决策(因为 Span 可能分散到达不同实例),没有 Redis 时每个实例独立决策会导致同一 Trace 的部分 Span 被保留、部分被丢弃,产生残缺的调用链。实验笔记
实验 9:可观测性数据成本治理实战——从「账单休克」到「按价值付费」
一、实验目标
在一个模拟的电商微服务系统上,完整走一遍可观测性数据的成本治理闭环:量化当前数据成本构成 → 部署两级采样并验证诊断能力不受损 → 配置分层保留与降采样 → 建立成本归因看板 → 执行一次数据质量审计。完成后你应当能够回答:「我们的遥测数据花了多少钱、花在哪、砍掉哪一部分损失最小」。
二、环境准备
需要 Docker Compose 环境,包含:(1) 三个模拟服务(frontend / checkout / inventory),用第 2 章实验的 demo 应用改造,支持通过环境变量 ERROR_RATE、LATENCY_P99、DEBUG_LOG_ENABLED 注入行为;(2) OpenTelemetry Collector(contrib 版,需包含 tailsamplingprocessor、k8sattributes 可用 transform 替代);(3) Tempo 或 Jaeger 作为追踪后端;(4) Prometheus + Thanos(sidecar/compactor/store/query 四组件)+ MinIO 作为对象存储;(5) Grafana。压测工具用 k6 或 locust,能够产生稳定可复现的流量(建议 200 RPS 基线,其中 5% 为慢请求、0.5% 为错误请求)。实验总时长约 4 小时,其中数据积累时间约 2 小时,可分两次进行。
三、实验步骤
步骤 1(成本基线测量):关闭所有采样(100% 全量),运行压测 30 分钟。测量并记录:Collector 接收的 Span 总数与字节数(otelcol_receiver_accepted_spans、otelcol_receiver_accepted_spans_bytes)、Tempo 存储目录大小、Prometheus TSDB 的块大小与活跃序列数(prometheus_tsdb_head_series)、日志总量。据此计算「每百万 Span 的存储成本」与「每天全量运行的推算成本」(假设对象存储 0.12 元/GB/月、SSD 0.8 元/GB/月)。这份基线是后续所有优化的对照。
步骤 2(头部采样的能力损失验证):在 SDK 侧配置 TraceIDRatioBased(0.1)(10% 头部采样),运行 20 分钟并注入 ERROR_RATE=0.005(0.5% 错误率,即每 200 个请求 1 个错误)。查询后端:错误 Trace 的实际留存数量是多少?理论预期是「错误请求总数 × 10%」,验证是否吻合。关键观察:在 10% 头部采样下,你是否还能找到那个 0.5% 的故障模式?记录你花了多久才找到一条错误 Trace,以及是否存在「某个特定租户的错误一条都没留下」的情况。
步骤 3(尾部采样的能力恢复验证):切换到示例 1 的 tail_sampling 配置(错误与慢请求 100% 保留、正常流量 5%),重跑步骤 2 的相同场景。对比三组数据:后端存储量(相对步骤 1 的百分比)、错误 Trace 的召回率(应为 100%)、慢 Trace 的召回率(应为 100%)。计算「诊断能力/成本」比值:尾部采样相比头部采样,在成本相近的情况下召回率提升了多少倍?同时验证 decision_wait 的截断效应:注入一个耗时 45 秒的慢请求(超过 30s 的 decision_wait),观察其 Trace 是否完整、是否被正确保留。
步骤 4(分层保留与降采样):配置 Thanos Compactor 的三档保留(raw=2h、5m=6h、1h=24h,实验用压缩时间尺度以便观察),运行 compaction 后用 thanos tools inspect 检查对象存储中各分辨率块的数量与大小,计算降采样带来的空间节省比例。验证语义正确性:对同一段时间范围,分别用 max_resolution=raw 和 max_resolution=1h 查询 rate(http_requests_total[5m]),对比结果差异并解释为什么降采样后的 rate 计算仍然近似正确(提示:Counter 的降采样聚合方式)。再对 Histogram 做一次实验:用 1h 分辨率数据计算 histogram_quantile(0.99, ...),与 raw 分辨率的结果对比,误差有多大?
步骤 5(成本归因看板):为三个服务分别打上 team=team-a / team-b / team-c 标签(故意让 inventory 服务缺失 team 标签,验证 unowned 兜底逻辑),部署示例 4 的 transform 配置与示例 5 的 recording rules。在 Grafana 建立成本看板:按团队的 Span 量堆叠图、按团队的推算月度成本、无主数据占比、活跃序列数 TOP 10。验证:无主数据占比是否准确反映了 inventory 服务的流量份额?
步骤 6(数据质量审计):编写一个查询脚本,统计过去 1 小时内:(a) 每个属性键的出现次数与不同取值数量(基数),找出基数 > 1000 的键;(b) 每个属性键被查询的频次(若后端不支持查询统计,用 Grafana 的 dashboard 使用情况 API 替代,找出 30 天无人访问的面板);(c) 取值集中度 > 99% 的键(作为过滤维度无价值)。产出一份「待清理字段清单」并估算清理后的成本节省。
四、检查清单
□ 步骤 1 的成本基线有具体数字(每百万 Span 的字节数与元数)
□ 步骤 2 能定量说明头部采样对长尾故障的召回损失
□ 步骤 3 的尾部采样在存储量下降的同时错误召回率达到 100%
□ 步骤 4 能解释降采样后 rate 与 histogram_quantile 的误差来源
□ 步骤 5 的无主数据占比与故意制造的标签缺失一致
□ 步骤 6 产出了至少 3 条可执行的清理建议及其节省估算
五、进阶挑战
(1) 实现两级采样的最优配比搜索:固定后端存储预算为基线的 15%,在「头部采样率 × 尾部正常流量采样率」的二维空间中搜索组合,找到使错误召回率最高的配置,并解释为什么单纯调低尾部采样率不如先做头部粗筛。(2) 用 Refinery 的 DynamicSampler 替换 tail_sampling,对比两者在相同存储预算下的「模式覆盖率」(采样后数据中出现了多少种不同的 route × status_code × error.type 组合),验证动态采样保留分布形状的能力。(3) 设计一个成本预算自动化闭环:当某团队的 7 天遥测量环比增长超过 50% 时,自动在 Collector 中为该团队注入更激进的采样配置(通过远程配置下发),并发送通知——把成本治理从人工审计变成自动反馈系统。
延伸阅读
- OpenTelemetry Collector 的 tailsamplingprocessor 与 loadbalancingexporter 官方文档(github.com/open-telemetry/opentelemetry-collector-contrib)——尾部采样的权威参考:全部策略类型(latency、status_code、string_attribute、numeric_attribute、span_count、composite、and、ottl_condition 等)的参数说明、多实例部署时的一致性哈希路由配置、decision_wait 与 num_traces 的调优指南。重点读 composite 策略的文档,它是实现「多条件组合采样」的关键,也是示例 1 的进阶形态。
- Thanos 官方文档的 Compactor 与 Store Gateway 章节(thanos.io)——长期存储与降采样的完整机制说明:三档分辨率的保留参数、块合并与降采样的执行流程、Compactor 的单实例约束与数据损坏风险、max_resolution 的查询语义。文档中「Downsampling」一节明确解释了为什么 Counter 与 Histogram 的降采样必须采用特定的聚合方式,是理解示例 2 语义陷阱的原始材料。
- Honeycomb 的 Refinery 文档与「Sampling」系列博客(docs.honeycomb.io/manage-data-volume/sampling)——动态采样算法的设计哲学与实现细节:DynamicSampler 的分桶机制、SampleRate 字段回写与查询时加权还原、RulesBasedSampler 的规则求值顺序。Honeycomb 团队的多篇文章(如「Dynamic Sampling and the Art of Not Losing Data」)从信息论角度论证了「为什么保留分布形状比保留固定比例更重要」,是本章采样思想的理论支撑。
- Grafana Loki 官方文档的 Retention 与 Limits 章节——日志分层保留的实操参考:retention_stream 的 selector 语法与优先级规则、compactor 的 retention_enabled 开关、max_streams_per_user 的流基数保护机制。文档中「Best Practices」一节关于「什么该放标签、什么该放日志正文」的论述,是避免 Loki 标签基数爆炸的必读材料,与本章数据质量治理直接相关。
- Prometheus 官方文档的 Recording Rules 与 Best Practices 章节,以及社区约定的 level:metric:operations 命名规范——预聚合的语法、求值间隔与性能影响、规则库的组织方式。配合阅读 Robust Perception 博客中关于「recording rules 何时该用、何时是浪费」的多篇文章,能建立对预聚合成本收益的量化直觉,避免把规则文件变成无人维护的坟场。
- 《Observability Engineering》第 11 章 "Observability for the Modern Enterprise" 与 Charity Majors 关于可观测性成本的系列演讲(如 "Your Observability Bill is a Choice")——本书作者对成本问题的直接论述:为什么「按事件计费」的商业模式会扭曲工程决策、宽事件模型如何在成本与诊断能力之间取得平衡、以及「可观测性支出应该占基础设施支出的多少」的经验区间。是本章商业与组织视角的原始出处。
- CNCF 的 FinOps Foundation 资料与《Cloud FinOps》(O'Reilly,J.R. Storment & Mike Fuller 著)中关于「可观测性成本」与「单位经济」(unit economics)的章节——把云成本管理的方法论(分摊、展示、优化三阶段)应用到遥测数据上的系统框架。FinOps 的「团队自治 + 集中治理」模型与本章的成本归因实践高度契合,适合需要在组织层面推动可观测性成本治理的工程师阅读。
- ClickHouse 官方文档的「Primary Key / Sparse Index」与「Materialized View」章节,以及 Uber、Cloudflare 等公司关于用 ClickHouse 构建可观测性后端的工程博客——列式存储在高基数遥测数据上的索引与预聚合机制:为什么稀疏索引 + 排序键能支持任意维度查询、跳数索引(bloom filter、minmax)的适用场景、物化视图的增量维护成本。是理解「多采少存」哲学技术可行性的关键材料。