系统架构对话(二):缓冲区加大了,为什么还是丢数据?
系统架构对话第 2 篇
有一类性能问题很容易让人困惑:平均处理速度看起来足够,数据却仍然会丢。于是我们扩大缓冲区、增加线程、提高优先级。改动越来越多,效果却未必跟着变好。
这一篇继续使用五路采集、每路约 26 MB/s 的设定。所有容量计算都是简化预算,统一使用十进制 MB,并不替代实际的接收和存储测试。
缓冲区买到的是时间
问:把回读缓冲区从小容量加到 64 MB,接收是不是就变快了?
答:缓冲区本身没有让消费者每秒多处理一个字节。它给短时积压留出了位置:消费者暂时没运行时,新到的数据还不至于立刻无处可放。
问:输入 26 MB/s,64 MB 能让消费者停多久?
答:如果缓冲区原本为空,而且暂停期间完全没有消费,理想时间预算约为 64 ÷ 26,也就是 2.46 秒。实际应使用当时剩余的可用容量计算,还要扣除元数据、对齐和实现上的保留空间。
问:如果消费者持续只能处理 25 MB/s 呢?
答:每秒净积压约 1 MB,从空缓冲开始,理想情况下约 64 秒填满。容量再翻倍,也只是把失败推迟。长期输入大于长期处理能力时,需要提升瓶颈能力、减少处理需求,或者在业务允许的地方限流和丢弃。
对于不能要求源端减速的 UDP 采集,“背压”未必能一直传回设备。此时必须约定超限策略,并明确记录缺失,不能让系统表面显示成功、文件却悄悄变得不可用。
问:那只要平均消费速度大于输入速度,就够了?
答:还不够。平均值只说明长期账面上可能追得上。一次停顿能否被容纳、停顿之后能否及时清空积压,以及积压期间的端到端延迟是否合格,都要另外检查。缓冲越大,也可能让旧数据在队列里等得越久。
另外,应用缓冲和网卡、驱动、套接字接收队列位于不同位置。扩大下游缓冲,无法挽回已经在上游队列中被丢弃的数据。容量预算应该沿整条链路分段计算。
多进程改变了什么
问:我们把五路采集拆成五个进程,是不是就能用上五个 CPU 核?
答:具备并行执行的机会,但一个进程里有多个线程,本来就可能使用多个核。要判断多进程的价值,需要分开看并发组织和故障、内存管理的边界。
答:原先界面、解析、推送和五路接收都在同一个托管进程中,分配行为和 GC 暂停可能影响同一进程内的接收工作。拆开以后,界面进程的一次回收不再是采集进程自己的回收;某一路崩溃,也不必直接终止其他路。
这不是“GC 总会暂停所有线程”的简单规则。.NET 有不同的 GC 模式和回收阶段,原生代码执行也有不同情况。设计时应测量实际的分配率和暂停,相关边界可对照 .NET GC 文档。
问:这样性能和可用性都提升了,代价在哪里?
答:配置如何交给子进程、进程间消息是否丢失、怎样启动和关闭、异常如何关联,都要设计。定位问题时,日志和时间戳也必须跨进程对齐。维护和测试的成本会增加。
而且五个进程仍可能争用同一块磁盘、同一个内存控制器和同一组 CPU。如果瓶颈在磁盘写入,增加进程可能只会让竞争更激烈。它是一种边界调整,效果需要沿瓶颈验证。
环形缓冲减少了什么
问:扩大缓冲区和使用环形缓冲,不都是空间换时间吗?
答:它们的主要作用不同。扩大容量是增加可用资源,容忍更长的积压;预分配并复用存储,则是在减少反复申请和释放资源的工作。
问:用了环形缓冲,就不会再触发 GC 了?
答:只能说它可能减少这条数据路径上的分配。日志、消息封装、回调和解析代码仍可能产生对象。缓存住数据数组,也不意味着周围代码已经没有分配。
复用还带来新的约束:生产者何时发布数据,消费者何时归还槽位,谁负责可见性和同步,缓冲满了怎么办?如果消费者还在使用一块内存,生产者就覆盖了它,得到的可能是内容损坏,而不只是统计上的丢帧。
问:单生产者、单消费者的队列能否直接换成多生产者?
答:不能只改调用数量。并发模型、读写索引和发布顺序都是协议的一部分。先明确使用模型,再选择经过验证的实现,并用乱序、满队列和慢消费者场景测试它。
优先级不会增加算力
问:把接收线程的优先级提到最高,让它总是先运行,不就能避免丢包?
答:优先级解决的是资源紧张时谁先获得运行机会。它不会增加 CPU 总量,也不能保证线程永远不被打断。如果接收工作压住了落盘消费者,数据只会从套接字队列挪到应用缓冲里,最后仍可能溢出。
因此,优先级、读写分离和环形缓冲应放在一张链路图上讨论:谁产生数据、谁消耗数据、共享什么资源、在哪一段积压。接收端的计数变漂亮,而最终文件缺失增加,不能算优化成功。
把办法按作用分开会更容易分析:减少重复计算和分配是在减少资源需求;并发、容量和资源池是在组织可用资源;优先级和调度是在仲裁竞争。一个实现可能同时包含几类作用,分类的目的是解释机制,不是给它贴唯一标签。
怎样证明改动有效
问:那优化前后,我应该看什么?
答:至少沿数据路径保留能相互核对的观测:逐路输入与缺失、各级队列高水位、消费速率、写入延迟,以及分配率和暂停。对于时延,除了平均值,还要看高分位和最长停顿的分布。
同一批数据、同一负载和相同的存储条件下,先测基线,再改变一个主要因素。扩大缓冲以后,若缺失降低但排队时间明显增加,就要回到场景确认延迟预算是否仍然允许。
性能优化的判断单位是完整业务链路。局部更快、队列更大,只有在最终数据质量和时延目标同时达标时,才算获得了需要的收益。
如果消费者因磁盘抖动停顿 4 秒,64 MB 缓冲够不够?
按单路输入 26 MB/s、期间不消费的简化假设,会积压约 104 MB,64 MB 不够。还要问停顿发生前已经积压了多少,以及恢复后的净消费余量有多大。不能仅凭一次“平均写入速度高于 26 MB/s”的测试,就判定方案合格。
持续过载和无边界排队也可能引发级联故障,进一步的工程案例可读 Google SRE 关于级联故障的讨论。下一篇则看另一类问题:这一路确实崩溃了,恢复到底从哪里开始,又在哪里才算结束?