乱序容忍时间用分位数定,别用最大值也别用平均

一、乱序和迟到是两件事,先分开才有处置
实时计算里最容易被混用的一对词,是乱序与迟到。乱序数据指顺序不对但还来得及:它比同批次的邻居晚到,但仍在窗口关闭之前落进来。迟到数据指来不及了:窗口已经按水位线推过去了,它才到。两者的处置完全不同,前者靠窗口语义接住,后者只能靠额外通道和口径分离。把它们合成一个词来讨论,团队的问题就会停在”要不要再放宽一点”这种没有答案的地方。
工程上唯一可算的量,是到达时间减去事件时间的差。这个值为负说明数据比当前水位线还新,为正且越大说明越迟。所有关于容忍时间的争论都必须回到这一列数字上,不接受”感觉延迟挺大”这类陈述。也因此,事件时间字段本身要先把三件事定下来:它由谁写入、写入方是谁的时钟、缺失时怎么处理。第三件事尤其重要——大量所谓乱序,实际是事件时间被上游用了处理时间填充,差值恒为零,看起来一切正常,窗口却聚错了数据。
二、六个成因决定了分布不会对称
来源把乱序的成因归为六类,我们按分布特征把它们分成两组,因为这两组在观测时的用法不一样。
| 成因 | 分布特征 | 观测时怎么处置 |
|---|---|---|
| 网络与转发延迟 | 一般在数百毫秒以内,右偏 | 计入常态分布 |
| 消息队列重试 | 离散尖峰,间隔固定 | 计入常态,但单独看尖峰频次 |
| 多分区并行消费 | 与分区积压量相关,方差大 | 按分区分别统计 |
| 隔日批量补数 | 时点集中,一次涌入大批 | 打补数标记,剔出常态样本 |
| 终端离线延迟 | 小时量级积压,重新上线时倒空 | 单独一个分位 |
| 回调与批量同步并发 | 双峰,取决于对端排期 | 与对端约定后再定 |
后两类是结构性长尾,不是随机抖动:批量补数会在某个时点集中涌入,离线终端重新上线时会一次性倒出几个小时之前的数据。这个区分直接影响定值。如果把这些样本混在总体里算分位,高分位会被拉到分钟甚至小时级,窗口永远关不上;如果一律剔除,日常链路里真实的迟到又会被判成过期。我们的做法是分开算两个分位:常态链路算一个,突发批次算一个,容忍时间按常态那条定,突发那条走侧输出与批量回填。
三、定值三步:先看分布,再对齐窗口语义,最后算状态代价
第一步是观测。观测作业不要只输出一个平均延迟,要落这几列:事件时间、到达时间、两者差值、来源与分区标识、批次标识、是否为补数或回填的标记。有了标记列,才能在做分位统计时把突发样本剔除出去。看的时候按小时切片看分布形状,不看单一数字——一条链路的延迟分布在白天与夜间可能是两个形态,用一天聚合出来的分位去定生产参数,等于用平均值做决策。
第二步是与窗口语义对齐。容忍时间的物理含义很窄也很明确:事件时间上比当前水位线晚到的数据,还能不能被这个窗口接住。定完之后要检查两件事。一是它是否小于等于业务能接受的结果产出延迟,二是它是否与源端的实际分布匹配。有界乱序策略要配合一路按处理时间的观测,原因是水位线按事件时间推进,源端一旦停流,事件时间不动,从作业指标上看是”一切正常”,只有处理时间这一路能告诉你”其实是没有数据”。
第三步是算状态代价。窗口状态按聚合键与时间累积,容忍期长一倍,存活的窗口数量就长一倍以上,再叠上键基数,状态体积往往是数倍增长而不是线性增长。状态膨胀的表现为检查点变慢、恢复时间变长、单任务反压。所以容忍值不是越大越保险,它是一个用状态成本换取正确率的折中值。来源给的定值方法在这种情况下特别好用的地方,是它把”取多少”变成了一个可讨论的公式:取观测到的 P99,再乘一点二到一点五之间的系数。系数靠近哪一头,取决于你愿意让多少比例的迟到数据走侧输出。
为什么不用最大值:最大值被长尾绑架,一次补数就能把窗口拖成永远不关,结果产出延迟等于全链路最长那条路径。为什么不用平均值:平均值系统性低估真实延迟,用它定窗口会让多数本可接住的数据被判成过期,表现为数字偏小且不报错。P99 的意义不在于它更精确,而在于它把决策显式化了:接受约百分之一的迟到数据需要走别的通道,这个数字是主动选的,不是撞上的。
四、五种方案各有适用边界,四个坑各自的表现
五种常见手段不是替代关系,是分层。
- 有界乱序水位线:默认选择,适用于绝大多数单源链路,代价是窗口关闭被推迟一个容忍时长。
- 允许迟到:适用于结果可以被同一聚合键反复修正的场景,前置条件在下一节展开。
- 侧输出:适用于迟到数据必须进最终结果但不能污染实时结果的场景。
- 自定义水位线:适用于多源混合、需要按源分别推进或需要外部信号强制关闭的场景。
- 源端排序:只在同一分区键内有效,跨分区无法保证全局顺序,因此它是缓解手段不是解决方案。
四个坑里最难发现的是第一个。容忍值设得太小的表现是结果偏小,不抛错、不报警,只能靠与离线批处理结果做对账才看得出来。第二个坑是水位线只能单调前进,某个分区长时间没有数据,整体水位线就被它卡住不再推进,表现为”数据不延迟但结果不出”,看监控会觉得一切正常。第三个坑是忘记设空闲超时,这是第二个坑眼下最省事的处置办法:给源加空闲判定,超时后用一个显式的推进动作把水位线抬起来,两条要一起检查,只设其一往往无效。第四个坑是允许迟到时间过长导致状态膨胀,它和第三个坑是同一份配置的两面,设定时要同时给出状态体积的观测。
五、迟到数据的口径分离:实时口径与最终口径
允许迟到有两段语义,前一段是技术上的:晚到的数据仍然参与计算并更新结果。后一段是系统上的:更新后的结果会被下游接受。第二段做不到,前面就白做。
下游能不能接住”同一个键的结果被改写”,要逐个确认。对内看板与内部对账可以接;对外的报表快照、被缓存过的开放接口、进计费与结算链路的数据,接不住——同一指标会出现两个值,而且没人知道哪一个是被引用的那个。判断标准写成一个问题就够:这个数一旦被别人复制出去,我们还能不能收回。收不回的场景,就必须走侧输出而不是允许迟到。
侧输出的正确用法有三个动作。迟到的数据原样进侧通道,不做丢弃也不做修补;由离线任务定期与主结果合并,合并的判定按事件时间而不是处理时间;指标上显式区分实时口径与最终口径两个值,两个都落库、两个都出图。只留一个指标名的系统,上线三个月后没人记得那个数是哪一口径。
补数与回看的策略要在此刻定死:批量补数不进实时口径,单独跑批回填;归窗按业务日期而不是到达日期。按到达日期归窗的后果是,一次补数会让某个当期窗口的数字凭空拔高、另一期凭空缺失,任何看环比的人都会被误导。这条要在设计文档里写成一句明确的话,并在回填作业的日志里带上它所影响的业务日期区间,方便追溯。
六、指标定义、报警与对甲方的沟通口径
五条指标的定义要写进设计文档,每条给公式而不是给名字。
- 事件时间:数据所描述事实的发生时刻,注明由上游哪个字段提供、以谁的时钟为准。
- 水位线:当前认为不会再有更早事件到达的分界值,注明推进来源与是否受空闲超时干预。
- 迟到率:超出容忍时间才到达的条数除以该窗口总条数。
- 迟到数据的量级与占比:迟到样本的条数、其承载的业务量占窗口总量的比例,两者都要有。
- 窗口延迟:从事件时间上窗口结束,到结果实际产出,中间经过多久。
报警阈值不要照抄别人的秒数。我们的做法是按上面的观测作业跑一段基线期,取常态分布的高分位作为告警线,同时保留一条”迟到率连续多个窗口上升”的趋势告警,因为单位或链路改动往往先表现为趋势变化而不是绝对值超标。
最后是给甲方的沟通口径,这件事必须写在验收材料里而不是等出问题时口头解释。要讲清三句:某些指标会先出一个数、之后被修正;修正窗口等于我们约定的迟到处理周期,它是一个明确的工程参数而不是随机行为;什么时候可以当作结论使用——以最终口径产出并且带完成标记的那一版为准。把这三句写进文档,实时看板上的数字才不会被当成”算错了”;不写,再准的口径分离也会在信任上输掉。
洹拓在几个流式项目里做过同一件事的复盘:真正把结果做错的团队,几乎都不是水位线配错,而是容忍时间是从别的项目抄来的一个秒数。抄来的数字没有对应的分布、没有对应的状态预算、也没有对外的口径约定,于是三个问题会分别在上线后的不同时间点浮出来,且每一次都像新问题。