关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

错误追踪自己把磁盘写满了:Sentry 的事件吞吐和去重该怎么算

发布时间:2026-10-30

观测故障的系统在故障中先倒下:这不是机器小,是算法错了

错误追踪平台最尴尬的失效方式是这样的:线上服务大面积报错,工程师打开监控后台,看到的不是那批异常本身,而是队列积压、写入超时、磁盘使用率告警。在 Sentry、GlitchTip 这类自建栈里,最先撑不住的往往不是业务应用,而是事件接收后面的消息队列、关系库与检索存储。它们平时安静地跑在很低的利用率上,故障发生后的几十秒内被灌进平日几百倍的事件量。

原因通常不在机器规格。容量是按平均事件速率算出来的:把一天的总事件数除以 86400 秒,得到一个看起来很宽裕的数字,再乘一个安全系数,就定下了存储与队列的大小。这个算法在平稳期是对的,在故障期是错的,因为它默认输入速率是一个常量,而异常事件的输入速率恰恰是与故障强相关的变量——故障越严重,上报越猛,两者同向放大。

于是形成了一个反向耦合:系统越需要它工作的时候,喂给它的东西越多。这不是某个参数没调好,是容量模型选错了。而更隐蔽的一点是,这个错误在上线后很长一段时间里都不会暴露,因为验收时跑的是平稳期流量,故障期的输入形态从来没被测过。

事件与问题是两层:分组解决的是噪音,不是写入量

事件(event)与问题(issue/group)是两层数据结构。SDK 上报的是一条条事件,处理流水线按指纹(fingerprint,通常基于异常类型、堆栈帧、模块等计算出的分组标识)把同源事件归并到同一个问题下。前端看到的是一个问题卡片,上面写着出现次数、影响用户数、首次出现时间与最近出现时间。

很多人误以为归并之后只存一条。在多数实现里,每条事件仍然会作为一个独立记录写入事件存储,分组改变的是展示与告警的组织方式,不改变落库的条数。也就是说,去重解决的是「人要不要看这么多条」,不解决「磁盘要不要装这么多条」。

这个区别直接决定了容量模型要分两本账:问题的数量决定索引规模与展示层的复杂度,事件的数量决定写入量、队列积压与磁盘增长速度。前者增长缓慢,后者会在故障窗口里垂直拉升。用问题数去估存储,是自建栈最常见的一次低估。

影响用户数这类字段的维护也依赖事件流。它通常需要按用户标识做去重统计,而统计本身是每次事件都要读写的状态,事件速率越高,这部分的读写放大越明显。这也是关系库往往先于检索存储出问题的一个原因:它不是被容量压垮的,是被同一行的高频更新压垮的。

峰值放大倍数从哪来:链式报错、重试回放、崩溃循环

峰值放大倍数不是一个玄学系数,它可以拆成几个可观测的来源,每一项都能在架构里找到对应的开关。

链式报错。一个用户请求穿过网关、业务服务、下游服务与数据访问层,底层一次超时会让上游每一层各抛出一个自己的异常,每一层各自上报一次。一次根因,在 N 层调用链上产生 N 条事件,调用层数越深,放大越多。微服务拆得越细,这个乘数越大。

重试与队列回放。客户端超时重试、网关重试、消息队列的重投与死信回放,都会把同一根因的错误重复触发。重试次数是配置出来的,但故障期重试会被自动放大:下游慢的时候,超时重试的触发频率反而更高,形成「慢导致重试、重试导致更慢」的正反馈。

崩溃循环。移动端或桌面端进程崩溃后自动重启,重启后再次命中同一缺陷再次崩溃,形成一个在无人干预下持续上报的循环。这类放大的特点是持续时间长,且每个客户端都在独立贡献,与服务端是否已经修好无关。

还有一类容易被忽略:定时任务或批处理在循环里捕获并上报异常,一个周期任务每轮抛同样的错,等于给自己造了一个恒定速率的放大器。它在故障期不增加,却长期抬高基线,让平均速率好看、让峰值余量变小。

放大倍数是可以被观测出来的,不需要靠猜。方法是在一次已知根因的故障结束后,用故障窗口内的事件总数除以受影响请求数,得到的比值就是这一次的实际放大倍数。建议把这个数按每次故障记录下来,并标注故障类型(超时、参数错误、依赖不可用之类),几次之后就得到属于你自己的经验区间,比任何通用建议都准。

把这些来源相乘,就得到故障期的瞬时事件速率。这个乘数在平稳期接近 1,在故障期可能达到两位数的量级,具体数值取决于调用链深度、重试策略与客户端规模,需以自有压测与历史数据为准。

为什么不能按平均速率算:平均速率、P99 速率、故障瞬时速率三者的差

平均速率、P99 速率、故障瞬时速率,是三个不同量级的数,用任何一个去代表另两个都会算错。

平均速率等于一段时间内的事件总量除以时长。它的作用是算成本:磁盘增长、月度存储账单、清理周期都按它估。它不能用来定峰值保护,因为峰值被平均掉了。

P99 速率代表平稳期的高位,是日常抖动的上界,比如定时批量任务、日常流量高峰带来的短时抬升。队列与处理能力的常规水位可以按它来定,再留出一到两倍余量。

故障瞬时速率是另一条曲线,它不由流量决定,而由故障的严重程度决定。它的形状是阶跃:故障发生瞬间从基线跳到峰值,故障恢复后骤降。平稳期的 P99 对它没有参考价值,一个平时 P99 只有基线几倍的系统,故障期可能跳到几十倍。

这里还要和另一种数据流划清界限。指标时序的写入量由采集目标数与采集间隔决定,基本恒定,扩缩容可以按线性外推;异常事件流不是时序,它的写入量由故障决定,是随机变量,不能用同一套外推方法。把这两类数据放在同一套容量模型里估,是第二个常见错误。

正确的做法是把三个数分开估:用平均速率估存储与成本,用 P99 估处理与队列的常规水位,用故障瞬时速率乘放大倍数估入口保护阈值与队列上限。三者的比值需要用自己的历史数据测,不同业务形态差别很大,不存在通用常数。

采样放在哪一级决定了保护范围:客户端采样 vs 服务端采样

采样放在哪一级,决定了它保护的是哪一段链路,这是整篇文章里最需要记牢的一条。

客户端采样在 SDK 里生效,事件还没离开应用进程就被按比率丢弃。它省下的是整条链路:出口带宽、接收端的入站连接与解析开销、消息队列的积压、处理流水线的 CPU,以及最终存储。这是唯一能在源头上减少「到达量」的手段。

服务端采样在事件被接收之后的某个环节生效,比如规整之后、落库之前按比率或按规则保留。它省下的是存储写入与索引膨胀,也可能省掉一部分处理开销,但入站带宽、接收端连接数、队列的瞬时积压一点没少。换句话说,服务端采样保护的是下游,不保护入口。

这个区别在故障期是决定性的:如果入口先被打满,下游的采样规则根本没有机会执行,事件要么卡在队列里,要么连接收端都进不来。很多团队配置了采样却仍然被峰值打穿,原因就是采样全在服务端,入口那一层裸奔。

按采样时机分,客户端采样属于在链路开头决策,也就是 head-based 的思路;服务端基于完整事件内容的规则属于在链路后段决策,接近 tail-based 的思路。两者不是互斥的:入口用一个基础比率把量级压下来,下游再按事件内容做差异化保留,比如对未捕获崩溃保留更高比例,对已知噪声异常保留更低比例。

采样率怎么定,不要抄默认值,本文也不给默认值。它应该由可接受的漏检代价反推:如果一个缺陷每天只出现几次,采样率过低会让它在统计上消失;如果它每秒出现几千次,即使只保留很小比例,也足够定位根因。所以更合理的做法是分层设定,崩溃类与首次出现的新指纹给高保留率,高频已知问题给低保留率。这个分层需要在 SDK 侧或接收端规则里明确配置,具体支持哪些规则、默认行为如何,需以实际版本文档为准。

还有一处容易算错:采样之后,展示层上的出现次数是采样后的计数,不等于真实发生量。如果要对外汇报影响面,要么按采样率做还原估算,要么单独维护一个不采样的计数指标,用聚合数值承担统计职责。还原估算在采样率很低时误差会被放大,事件分布越不均匀误差越大,这一点在写周报和做容量复盘时都要说清楚。

速率限制与配额是最后一道:被丢弃的量本身要监控

速率限制与配额是最后一道保护,位置在接收端入口,通常按项目或按密钥(per-project / per-key)设置。超过配额的事件会被丢弃,而不是排队等待,这是刻意的设计:排队等待等于把峰值转化为延迟,而延迟在故障期没有意义。

关键点是,被丢弃的量必须被计数并作为指标暴露出来。丢弃计数器是唯一能告诉你「保护生效了」的信号,也是唯一能告诉你「采样率是不是设错了」的信号。如果丢弃量长期为零,说明配额可能过松,保护没有真正兜底;如果平稳期就非零,说明入口容量或采样配置有问题;如果故障期丢弃量飙升而问题列表仍然完整,说明保护是有效的,可以放心。

配额的单位选择也有讲究。按事件条数限制和按字节数限制,保护的不是同一种资源:前者防的是处理与索引的条数压力,后者防的是带宽与磁盘的体量压力。事件体大小差异很大时,只限条数可能被大事件打穿,只限字节可能被海量小事件打穿,两条都设更稳。

配额还应该分层:全局配额保护整机,项目级配额防止单个服务的故障拖垮所有服务的可观测性。没有项目级配额时,一个崩溃循环会把其他所有项目的事件一起挤掉,等于一次局部故障让全局失去了观测能力。

单条事件成本的构成:堆栈深度、上下文、面包屑、source map、附件

单条事件的成本不是一个常数,它是几个可配置项的和,而这几项在多数 SDK 里都是默认全开的。

堆栈深度是最基本的项。每一帧包含函数名、文件路径与行号,深栈的字节数可以是浅栈的数倍。多数 SDK 允许限制保留的帧数,代价是定位信息变少,需要在排查效率与体积之间取一个明确的配置值。

局部变量与请求上下文。是否采集栈帧上的局部变量值,是否附带 HTTP 请求头、查询参数、请求体、用户信息,这些开关每一个都在往单条事件里加字段。上下文全的事件排查起来快,但单条成本高,且更容易涉及敏感数据,脱敏规则要一并考虑,不能只看体积。

面包屑(breadcrumb)记录异常发生前的操作轨迹,条数上限直接决定这部分体积。轨迹长有助于复现,但对高频事件来说它是纯成本,收益随频率迅速衰减。

source map 与前端产物。前端错误需要 source map 把压缩后的堆栈还原成可读形式,还原发生在处理端,消耗的是 CPU 与临时存储,产物本身也要单独存放。这部分不进事件体,却占处理资源,估算时容易漏掉。

附件与大对象。部分 SDK 支持附带截图、日志片段或小型转储文件,这类附件单条可以是普通事件的几个数量级大。是否允许、上限多少,必须作为一项明确的配额,而不是留给默认值。

所以容量估算里的「单条成本」应该用加权平均而不是拍一个数:按事件类型分别估计体积,再按各类型的占比加权。堆栈深、上下文全、带附件的事件哪怕只占几个百分点,也可能主导总体积。

依赖组件各自是独立的桶:队列、关系库、检索、缓存,谁先满

自建栈不是一整块资源,是几个独立的桶,任何一个先满都会让整体不可用,而且满的顺序在不同故障形态下不一样。

消息队列(Kafka、RabbitMQ 之类)承接接收端的写入尖峰,容量单位是积压条数与分区磁盘。它的作用是把瞬时峰值削平成平稳的处理速率,但如果峰值持续超过处理速率,队列只会越积越多,直到保留期触发删除或者磁盘写满。队列的告警应该看积压的增长速率,而不只是当前积压量:持续增长比高位横盘危险得多,前者意味着处理速率跟不上,后者只是还没消化完。

关系型存储(通常是 PostgreSQL)承载项目、问题、用户、分组这类结构化元数据,瓶颈往往是写入的事务数与索引膨胀,而不是磁盘容量。问题表的高频更新在这里形成热点:每来一条事件都要更新计数与最近出现时间,同一行的写冲突会随事件速率线性上升。

检索与列式存储承载事件明细与检索,是体量增长最快的一块,也是最先被「每条事件都要落库」这个事实拖垮的一块。

缓存承担限流计数、去重判定与热点查询,是内存型资源。它满了会直接让限流与去重失效,等于在最需要保护的时候把保护关掉。这一类失效最容易被忽略,因为它的症状是「别的组件突然都变慢了」,看起来像是别处出了问题。

这四个桶的告警阈值要分别设,并且分别做扩容与故障演练。只看总磁盘使用率,会漏掉最先满的那一个。

还要看清楚组件之间的传导方向。接收端处理不过来会反压到 SDK,SDK 侧通常有本地队列与丢弃策略,本地队列满了就在客户端直接丢;这条链路如果不通,峰值会变成客户端的同步等待,进而影响到业务请求本身。反压的路径必须提前确认清楚:是丢弃、是异步重试,还是阻塞业务线程。自建栈里最常见的问题不是反压设计错了,而是默认行为从来没人检查过,直到故障那天才知道它是阻塞的。

另外要注意资源指标不止磁盘一种。连接数、文件句柄、分区数量、内存占用都可能先于磁盘打满,而它们的告警往往没有默认配置。队列分区写满和磁盘写满的症状很接近,处理方式却完全不同,排查时先确认是哪一种。

保留期与清理:明细过期、趋势保留

事件明细的保留是有期限的,过期数据按批次删除。清理任务本身也是资源项:批量删除会产生大量写入与索引变更,如果清理窗口和业务的写入高峰重叠,清理自己会成为一次负载事件。所以清理应该错峰执行,并且限速。

长期价值不在这批明细里。故障复盘之后,真正被反复使用的是趋势:某类问题在变多还是变少,每次发布的异常率变化,影响用户数的走势。这些是聚合结果,体量远小于明细,可以保留得更久。

因此保留策略应该分两层:明细按较短周期滚动删除,聚合趋势按较长周期保留。清理的批量大小与并发度需要按自有数据量调,删除批太大容易拖垮存储,太小又追不上写入速度,具体取值需以自有压测为准。

典型场景一:发布窗口内的事件洪峰

以下为典型部署思路,并非特指某一真实客户。

假设一个由十几个服务组成的系统,按周发布,发布窗口集中在夜间低峰。正常时段事件速率很低,日常告警基本为零,容量看起来非常宽裕。

风险点在于,发布引入的缺陷不会均匀分布在十几个服务上,它通常集中在被改动的那一两个服务,而这些服务的下游会因为超时或参数不兼容跟着报错。于是放大倍数在这一两条调用链上叠加,一条深链的放大就是十几倍起。

更麻烦的是发布期的事件指纹大量是新的。新异常没有历史分组可以归并,每个新指纹都要单独建组、单独触发首次出现告警,处理流水线的建组开销与告警通道的压力会在同一时刻抬升。这是发布期特有的一次性成本,平稳期测不出来,压测如果只灌老指纹也测不出来。

应对思路不是扩容,而是把窗口期的保护提前打开:发布前临时提高客户端采样率的丢弃比例,收紧项目级配额,给新指纹的首报做合并窗口(在短时间内把同一新指纹的多次首报合并成一条告警)。发布结束后再恢复常态。这三项都是可配置的策略,具体支持形式需以实际版本文档为准。

这里还要注意一件事:发布期恰恰是最不能丢失新指纹的时期。所以采样率的调整应该是「降低高频已知问题的保留率」而不是「全局一刀切降低」,否则新缺陷会被自己的保护策略挡在门外。

典型场景二:移动端崩溃循环

以下为典型部署思路,并非特指某一真实客户。

移动端崩溃循环的事件形态和服务端完全不同。服务端事件的速率由请求量决定,请求停了事件就停;客户端崩溃循环的速率由客户端进程自己决定,用户不卸载、不重启设备,它就会一直上报。

第一个特征是速率不随服务端修复立即下降。即使服务端已经发布修复,未更新的老版本客户端仍会持续贡献事件,衰减曲线由版本更新率决定,而不是由修复时间决定。这意味着峰值持续时间可能是天级而不是分钟级,队列与磁盘要按「持续超处理速率」而不是「短时尖峰」来估。

第二个特征是事件体偏大。移动端事件常带设备信息、系统版本、内存状态、面包屑,有时还带附件,单条成本高于服务端事件。速率高且体量大,会同时打在带宽与存储上,只限条数的配额在这里尤其不够用。

这种情况下客户端采样几乎无效:崩溃循环的事件通常在崩溃处理器里上报,进程即将终止,按比率随机丢弃只能减少一部分量,无法阻止循环本身。真正有效的手段在更外层——项目级的字节配额,针对特定指纹的抑制规则(同一指纹在单位时间内只保留有限条),以及在应用侧做启动期的自我熔断(连续多次启动即崩溃时,停止上报并降级到本地记录)。

这里也能看出错误追踪与指标的边界。崩溃率本身应该是一个指标,用聚合数值监控;错误追踪回答的是「这类崩溃的堆栈是什么、影响多少设备、首次与最近出现时间」,它不替代指标,也不替代日志与链路追踪。

保护手段 生效位置 能省下什么 省不下什么 什么时候才起作用
客户端采样(SDK 侧按比率丢弃) 应用进程内,事件离开进程之前 出口带宽、接收端入流量、队列积压、处理 CPU、存储 被丢弃事件的可见性;低频长尾问题可能漏检 从第一秒起持续生效,与流量大小无关
服务端采样(接收后按规则保留) 接收端或处理流水线,规整之后、落库之前 存储写入量、索引膨胀、部分处理开销 入站带宽、接收端连接数、队列瞬时积压 事件已经进站之后才生效
指纹分组(fingerprint 归并去重) 事件规整阶段 展示层噪音、告警条数、人工排查成本 写入与存储,每条事件通常仍要落库 事件已进入处理流水线之后
按项目与密钥的速率限制与配额 接收端入口,配额校验环节 极端峰值下的整体可用性,隔离单项目影响 被丢弃的事件本身,超额部分不可恢复 超过设定阈值的那一刻才生效
事件体裁剪(堆栈深度、上下文、面包屑、附件) SDK 序列化之前 单条事件字节数,连带带宽与存储总量 事件条数与处理调用次数 每一条事件都生效,与流量无关
保留期与批量清理 存储层定期任务 长期磁盘占用与索引规模 峰值瞬间的写入压力 到达保留边界后的清理窗口内

一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。

容量与保护清单(方案章节)

上线前把下面几项写成明确的数字,而不是留在默认值里。这份清单的顺序,就是排查时应该检查的顺序。

  • 基线事件速率与故障期放大倍数的估计值,两者分开记录,不要混成一个数。
  • 客户端采样比率,以及按事件类型分层的保留规则,明确哪些类不降采样。
  • 服务端采样与指纹抑制规则,写明它对哪类事件生效、对哪类不生效。
  • 项目级与全局的条数配额、字节配额,以及配额超限后的丢弃行为与返回码。
  • 队列的积压上限与降级策略:超限之后是丢弃、落盘,还是反压到接收端。
  • 单条事件的平均体积估算方法,包含堆栈帧数、上下文开关、面包屑上限与附件策略的实际配置状态。
  • 各依赖组件(队列、关系库、检索、缓存)独立的告警阈值与扩容步骤。
  • 明细保留期、清理窗口与清理限速,清理必须错开写入高峰。
  • 丢弃计数器的监控与告警,这一项最容易被漏掉,却是最重要的一项。

每一项都要有一次压测记录支撑。压测的注入方式必须是真实形态的事件洪峰,也就是短时间内的阶跃式灌入,而不是均匀速率的持续灌入。均匀灌入只能测出吞吐上限,测不出队列行为,而打满磁盘的从来是队列行为。

演练的方式也要明确下来。在预发环境注入一个底层超时,观察三件事:事件速率曲线的阶跃形态、队列积压曲线的斜率、丢弃计数器的变化。演练的通过标准不是「系统没挂」,而是保护按预期生效并且丢弃量有完整记录。一次没触发任何丢弃的演练说明不了什么,它可能只是峰值不够高。

还有一条容易被忽略的运维约定:保护策略的变更要走配置管理。发布窗口临时调低保留率、事后忘记恢复,是自建错误追踪里很常见的一类人为故障,它不会立刻出事,会在下一次真正的故障里出事。

错误追踪的容量账,上线前最该算清楚的几项

把全文收成一个可执行的顺序。先算放大倍数,再算入口保护,最后才算存储,顺序反了就会买错资源。

第一项是放大倍数。它由调用链深度、重试策略与客户端规模共同决定,不能凭感觉取一个值,应该用一次真实的故障演练测出来:在预发环境注入一个底层超时,统计这段时间内产生的事件总数与请求总数的比值,这个比值就是你的放大倍数。

第二项是入口保护阈值。用基线速率乘以放大倍数,再乘以一个安全余量,得到接收端必须能扛住的瞬时速率;配额应该设在这个值附近,而不是设在一个拍出来的整数上。同时确认保护的第一道在 SDK 里,而不是只在服务端。

第三项是存储与清理。用平均速率乘以加权平均的单条体积,得到日增长量;再乘以明细保留天数,得到稳态占用;最后加上一次故障期峰值的增量作为冗余。缺少最后这一项的容量规划,会在第一次大故障当天晚上被发现。

第四项是丢弃计数器。所有保护手段都会丢东西,区别只在于丢得有记录还是没记录。没有记录的保护,等于把可观测性问题变成了不可观测的问题。

这几项算清楚之后,剩下的才是机器规格问题。机器规格从来不是难点,难点在于知道自己要防的是什么量级。

错误追踪这篇的技术口径与来源,以及哪些需要你自己验证

本文讲的是自建错误追踪平台(Sentry、GlitchTip 这类栈)的容量规划与峰值保护方法,属于通用架构原理与工程经验,不针对某一具体版本的实现细节。文中提到的组件名(消息队列、关系型存储、检索与列式存储、缓存)指的是这类架构中常见的角色,具体用哪个产品、哪个版本,需要按你自己的部署情况对照。

需要你自己验证的部分包括:SDK 支持哪些采样配置与分层规则、接收端支持的配额维度与单位、事件保留期的默认行为、清理任务的执行方式与限速参数、指纹计算的具体规则。这些都属于随版本变化的实现细节,需以实际版本文档为准,本文不提供任何默认值与版本相关数字。

文中没有给出任何吞吐基准、事件体积数值、默认采样率与默认保留期,也没有引用任何实测数据与客户案例。所有数值关系都以变量形式表达,例如「平均速率乘以放大倍数」「加权平均单条体积乘以日事件量」,需要代入你自己的观测数据才能得出可用结果。

两个典型场景均为典型部署思路,并非特指某一真实客户,用于说明同一套方法在不同事件形态下的用法差异:服务端发布洪峰的关键是短时阶跃与新指纹建组开销,移动端崩溃循环的关键是天级持续与大体量单条。两者对应的保护手段并不相同。

最后需要说明本文的边界:错误追踪回答的是「哪类异常、影响多少用户、首次与最近出现时间」,它不回答指标的聚合趋势问题,也不替代日志与链路追踪。把崩溃率当指标、把明细当日志、把错误追踪当时序库用,都会让容量账越算越偏。


上一篇:2026 海外服务器租用 数据合规与回程线路 避坑问答全解

下一篇:时序库为什么要按时间分块:TDengine 这类库的写入与降采样怎么一起规划