3. 缓冲与落盘的系统边界定义
在实时逐笔数据系统中,“实时接入—内存缓冲—批量落盘”并不是三个可以随意合并的步骤,而是一条职责清晰、契约明确、故障域相互隔离的三段式流水线。很多系统在早期为了图快,把接入、缓冲、落盘写在一个线程或一个模块里,结果一旦磁盘抖动,接入线程被阻塞,行情数据开始堆积甚至丢包;或者反过来,接入端解析异常直接把脏数据写进了落盘文件,事后无法追溯。本节要解决的核心问题就是:把这三段的边界划清楚,让每一段只做自己该做的事,并且用明确的数据契约把上下游粘起来。
三段式流水线的职责划分
第一段是实时接入。它的唯一职责是从上游(交易所行情网关、券商柜台、TCP/UDP 组播源等)把原始报文收进来,完成最小必要的校验(长度、校验和、序号连续性),然后尽快交给缓冲层。这一段的关键指标是“接收延迟”和“零阻塞”——它不应该做复杂的业务解析,更不应该直接碰磁盘。接入段的故障域是网络与协议层:断连、乱序、重复包、半包,都应在这一段被识别并打上标记,而不是把问题甩给下游。
第二段是内存缓冲。它是整条流水线的“蓄水池”和“解耦器”,负责吸收接入速率的瞬时尖峰,同时按落盘策略攒批。缓冲段要维护队列水位、批次边界、序号窗口,并在内存压力上升时做出背压决策(丢弃、降级还是阻塞上游)。它的故障域是内存与并发:队列溢出、锁竞争、批次切分错误。缓冲段不负责持久化,也不负责业务语义,它只保证“进来的有序数据,按批、按序、可追溯地交出去”。
第三段是批量落盘。它把缓冲层交来的批次写成持久化文件(如按分钟切分的二进制日志或 Parquet 文件),并维护写入偏移、文件索引和落盘确认。落盘段的故障域是磁盘与文件系统:写满、IO 抖动、fsync 超时、文件句柄耗尽。它必须做到“写失败可重试、重试不重复、重复可识别”,并且把落盘进度(已持久化的最大序号)反馈给缓冲层,作为批次可以释放内存的依据。
数据契约:让上下游“只认格式不认实现”
边界清晰的前提是契约清晰。三段之间至少需要三份契约。契约 A 是接入段交给缓冲段的对象:原始报文字节、接收时间戳、源标识、序号,以及一个异常标记位(如“校验失败”“序号跳变”)。缓冲段不需要理解报文的业务含义,它只按序号排序、按批切分。契约 B 是缓冲段交给落盘段的批次:批次起始序号、条数、字节长度、批次校验值、批次生成时间。落盘段据此可以独立完成写入和校验,而不必回查缓冲状态。契约 C 是落盘段回传给缓冲段的确认:已持久化的最大序号、文件偏移、写入耗时。缓冲段收到确认后才释放对应批次的内存,这就是“落盘确认驱动内存回收”的闭环。
契约的价值在于可测试。比如契约 B 中要求“批次内序号连续且升序”,那么缓冲段就可以用一个单元测试构造乱序输入,验证输出批次是否满足该断言;落盘段也可以用一个假批次验证写入后偏移是否正确推进。没有契约,边界就只是口头约定,联调时互相甩锅。
故障域隔离与背压设计
三段式划分最实际的收益是故障域隔离。假设磁盘 IO 突然从 2ms 抖到 200ms,落盘段写入变慢,缓冲队列水位上升。此时正确的行为是:缓冲段检测到水位超过高水位线(例如队列容量 80%),向接入段发出背压信号,接入段可以选择丢弃非关键数据或降低接收速率,而绝不能让落盘线程反向阻塞接入线程。反过来,如果接入端出现大量校验失败,异常标记会随契约 A 进入缓冲,缓冲段可以按策略把异常批次单独路由到“隔离区”而不是混入正常落盘文件,避免污染主数据。
一个常见的工程参数示例(属于合理假设,非实测):缓冲队列容量设为 20 万条,高水位 16 万条触发背压,批次大小 5000 条或 200ms 先到先切,落盘采用“写临时文件 + fsync + 原子重命名”的方式保证文件完整性。这些参数不是拍脑袋,而是由接入峰值速率和磁盘持续写带宽共同决定的:若峰值 5 万条/秒、单条 200 字节,则 200ms 批次约 1 万条、2MB,磁盘顺序写完全可以承受,而队列 20 万条提供了约 4 秒的抖动缓冲窗口。
落地时的边界检查清单
在编码前,建议把边界固化成可检查的约束:接入段只依赖网络库和校验工具,不引入存储 SDK;缓冲段只暴露“入队/出队批次”两个接口,内部队列实现可替换;落盘段只接收批次对象,不感知报文格式。监控上,三段各自暴露独立指标:接入段的收包速率与异常率、缓冲段的队列水位与批次等待时长、落盘段的写入延迟与 fsync 次数。这样一旦端到端延迟升高,可以快速定位是哪一段的故障域出了问题,而不是在一锅粥的日志里猜。
把这三段边界定义清楚,后续的缓冲结构选型、批量策略调参、落盘格式设计才有稳定的落脚点;边界模糊的系统,任何优化都只是在给耦合打补丁。