错误追踪平台最尴尬的失效方式是这样的:线上服务大面积报错,工程师打开监控后台,看到的不是那批异常本身,而是队列积压、写入超时、磁盘使用率告警。在 Sentry、GlitchTip 这类自建栈里,最先撑不住的往往不是业务应用,而是事件接收后面的消息队列、关系库与检索存储。它们平时安静地跑在很低的利用率上,故障发生后的几十秒内被灌进平日几百倍的事件量。
原因通常不在机器规格。容量是按平均事件速率算出来的:把一天的总事件数除以 86400 秒,得到一个看起来很宽裕的数字,再乘一个安全系数,就定下了存储与队列的大小。这个算法在平稳期是对的,在故障期是错的,因为它默认输入速率是一个常量,而异常事件的输入速率恰恰是与故障强相关的变量——故障越严重,上报越猛,两者同向放大。
于是形成了一个反向耦合:系统越需要它工作的时候,喂给它的东西越多。这不是某个参数没调好,是容量模型选错了。而更隐蔽的一点是,这个错误在上线后很长一段时间里都不会暴露,因为验收时跑的是平稳期流量,故障期的输入形态从来没被测过。
事件(event)与问题(issue/group)是两层数据结构。SDK 上报的是一条条事件,处理流水线按指纹(fingerprint,通常基于异常类型、堆栈帧、模块等计算出的分组标识)把同源事件归并到同一个问题下。前端看到的是一个问题卡片,上面写着出现次数、影响用户数、首次出现时间与最近出现时间。
很多人误以为归并之后只存一条。在多数实现里,每条事件仍然会作为一个独立记录写入事件存储,分组改变的是展示与告警的组织方式,不改变落库的条数。也就是说,去重解决的是「人要不要看这么多条」,不解决「磁盘要不要装这么多条」。
这个区别直接决定了容量模型要分两本账:问题的数量决定索引规模与展示层的复杂度,事件的数量决定写入量、队列积压与磁盘增长速度。前者增长缓慢,后者会在故障窗口里垂直拉升。用问题数去估存储,是自建栈最常见的一次低估。
影响用户数这类字段的维护也依赖事件流。它通常需要按用户标识做去重统计,而统计本身是每次事件都要读写的状态,事件速率越高,这部分的读写放大越明显。这也是关系库往往先于检索存储出问题的一个原因:它不是被容量压垮的,是被同一行的高频更新压垮的。
峰值放大倍数不是一个玄学系数,它可以拆成几个可观测的来源,每一项都能在架构里找到对应的开关。
链式报错。一个用户请求穿过网关、业务服务、下游服务与数据访问层,底层一次超时会让上游每一层各抛出一个自己的异常,每一层各自上报一次。一次根因,在 N 层调用链上产生 N 条事件,调用层数越深,放大越多。微服务拆得越细,这个乘数越大。
重试与队列回放。客户端超时重试、网关重试、消息队列的重投与死信回放,都会把同一根因的错误重复触发。重试次数是配置出来的,但故障期重试会被自动放大:下游慢的时候,超时重试的触发频率反而更高,形成「慢导致重试、重试导致更慢」的正反馈。
崩溃循环。移动端或桌面端进程崩溃后自动重启,重启后再次命中同一缺陷再次崩溃,形成一个在无人干预下持续上报的循环。这类放大的特点是持续时间长,且每个客户端都在独立贡献,与服务端是否已经修好无关。
还有一类容易被忽略:定时任务或批处理在循环里捕获并上报异常,一个周期任务每轮抛同样的错,等于给自己造了一个恒定速率的放大器。它在故障期不增加,却长期抬高基线,让平均速率好看、让峰值余量变小。
放大倍数是可以被观测出来的,不需要靠猜。方法是在一次已知根因的故障结束后,用故障窗口内的事件总数除以受影响请求数,得到的比值就是这一次的实际放大倍数。建议把这个数按每次故障记录下来,并标注故障类型(超时、参数错误、依赖不可用之类),几次之后就得到属于你自己的经验区间,比任何通用建议都准。
把这些来源相乘,就得到故障期的瞬时事件速率。这个乘数在平稳期接近 1,在故障期可能达到两位数的量级,具体数值取决于调用链深度、重试策略与客户端规模,需以自有压测与历史数据为准。
平均速率、P99 速率、故障瞬时速率,是三个不同量级的数,用任何一个去代表另两个都会算错。
平均速率等于一段时间内的事件总量除以时长。它的作用是算成本:磁盘增长、月度存储账单、清理周期都按它估。它不能用来定峰值保护,因为峰值被平均掉了。
P99 速率代表平稳期的高位,是日常抖动的上界,比如定时批量任务、日常流量高峰带来的短时抬升。队列与处理能力的常规水位可以按它来定,再留出一到两倍余量。
故障瞬时速率是另一条曲线,它不由流量决定,而由故障的严重程度决定。它的形状是阶跃:故障发生瞬间从基线跳到峰值,故障恢复后骤降。平稳期的 P99 对它没有参考价值,一个平时 P99 只有基线几倍的系统,故障期可能跳到几十倍。
这里还要和另一种数据流划清界限。指标时序的写入量由采集目标数与采集间隔决定,基本恒定,扩缩容可以按线性外推;异常事件流不是时序,它的写入量由故障决定,是随机变量,不能用同一套外推方法。把这两类数据放在同一套容量模型里估,是第二个常见错误。
正确的做法是把三个数分开估:用平均速率估存储与成本,用 P99 估处理与队列的常规水位,用故障瞬时速率乘放大倍数估入口保护阈值与队列上限。三者的比值需要用自己的历史数据测,不同业务形态差别很大,不存在通用常数。
采样放在哪一级,决定了它保护的是哪一段链路,这是整篇文章里最需要记牢的一条。
客户端采样在 SDK 里生效,事件还没离开应用进程就被按比率丢弃。它省下的是整条链路:出口带宽、接收端的入站连接与解析开销、消息队列的积压、处理流水线的 CPU,以及最终存储。这是唯一能在源头上减少「到达量」的手段。
服务端采样在事件被接收之后的某个环节生效,比如规整之后、落库之前按比率或按规则保留。它省下的是存储写入与索引膨胀,也可能省掉一部分处理开销,但入站带宽、接收端连接数、队列的瞬时积压一点没少。换句话说,服务端采样保护的是下游,不保护入口。
这个区别在故障期是决定性的:如果入口先被打满,下游的采样规则根本没有机会执行,事件要么卡在队列里,要么连接收端都进不来。很多团队配置了采样却仍然被峰值打穿,原因就是采样全在服务端,入口那一层裸奔。
按采样时机分,客户端采样属于在链路开头决策,也就是 head-based 的思路;服务端基于完整事件内容的规则属于在链路后段决策,接近 tail-based 的思路。两者不是互斥的:入口用一个基础比率把量级压下来,下游再按事件内容做差异化保留,比如对未捕获崩溃保留更高比例,对已知噪声异常保留更低比例。
采样率怎么定,不要抄默认值,本文也不给默认值。它应该由可接受的漏检代价反推:如果一个缺陷每天只出现几次,采样率过低会让它在统计上消失;如果它每秒出现几千次,即使只保留很小比例,也足够定位根因。所以更合理的做法是分层设定,崩溃类与首次出现的新指纹给高保留率,高频已知问题给低保留率。这个分层需要在 SDK 侧或接收端规则里明确配置,具体支持哪些规则、默认行为如何,需以实际版本文档为准。
还有一处容易算错:采样之后,展示层上的出现次数是采样后的计数,不等于真实发生量。如果要对外汇报影响面,要么按采样率做还原估算,要么单独维护一个不采样的计数指标,用聚合数值承担统计职责。还原估算在采样率很低时误差会被放大,事件分布越不均匀误差越大,这一点在写周报和做容量复盘时都要说清楚。
速率限制与配额是最后一道保护,位置在接收端入口,通常按项目或按密钥(per-project / per-key)设置。超过配额的事件会被丢弃,而不是排队等待,这是刻意的设计:排队等待等于把峰值转化为延迟,而延迟在故障期没有意义。
关键点是,被丢弃的量必须被计数并作为指标暴露出来。丢弃计数器是唯一能告诉你「保护生效了」的信号,也是唯一能告诉你「采样率是不是设错了」的信号。如果丢弃量长期为零,说明配额可能过松,保护没有真正兜底;如果平稳期就非零,说明入口容量或采样配置有问题;如果故障期丢弃量飙升而问题列表仍然完整,说明保护是有效的,可以放心。
配额的单位选择也有讲究。按事件条数限制和按字节数限制,保护的不是同一种资源:前者防的是处理与索引的条数压力,后者防的是带宽与磁盘的体量压力。事件体大小差异很大时,只限条数可能被大事件打穿,只限字节可能被海量小事件打穿,两条都设更稳。
配额还应该分层:全局配额保护整机,项目级配额防止单个服务的故障拖垮所有服务的可观测性。没有项目级配额时,一个崩溃循环会把其他所有项目的事件一起挤掉,等于一次局部故障让全局失去了观测能力。
单条事件的成本不是一个常数,它是几个可配置项的和,而这几项在多数 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 支持哪些采样配置与分层规则、接收端支持的配额维度与单位、事件保留期的默认行为、清理任务的执行方式与限速参数、指纹计算的具体规则。这些都属于随版本变化的实现细节,需以实际版本文档为准,本文不提供任何默认值与版本相关数字。
文中没有给出任何吞吐基准、事件体积数值、默认采样率与默认保留期,也没有引用任何实测数据与客户案例。所有数值关系都以变量形式表达,例如「平均速率乘以放大倍数」「加权平均单条体积乘以日事件量」,需要代入你自己的观测数据才能得出可用结果。
两个典型场景均为典型部署思路,并非特指某一真实客户,用于说明同一套方法在不同事件形态下的用法差异:服务端发布洪峰的关键是短时阶跃与新指纹建组开销,移动端崩溃循环的关键是天级持续与大体量单条。两者对应的保护手段并不相同。
最后需要说明本文的边界:错误追踪回答的是「哪类异常、影响多少用户、首次与最近出现时间」,它不回答指标的聚合趋势问题,也不替代日志与链路追踪。把崩溃率当指标、把明细当日志、把错误追踪当时序库用,都会让容量账越算越偏。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品