故事

洪武年间,南京城设了一套水驿急递。各省灾荒、边警、漕运迟滞,都靠驿卒换马接力,昼夜兼程送入京师。兵部有位年轻主事姓周,新到任便想整顿——他嫌历年案卷混乱,决意按"到京时辰"编年:正月初三到的归正月初三,五月初七到的归五月初七,一目了然。

老吏张头劝他:"大人,驿报上盖的可是'事发时辰'——河南水灾是腊月起的事,驿路冰封,拖到正月才到京。您按到京时辰归档,开春调粮的文书就排到腊月前面去了。"周主事不听:"我只管京师收到什么、何时收到。事发在河南,我怎么知道?"

三月后,周主事按"到京时辰"汇总去岁全年边警,呈给尚书。尚书扫一眼便摔了册子:"六月到京的'辽东缺饷',你排在四月'宁夏换马'前面。可辽东那封是去年九月的事,因大雪阻路走了九个月;宁夏四月到京,却是三月才发生的事。你这叫兵部按你的收到顺序调兵?"

周主事辩解:"下官确实不知河南何时灾、辽东何时饥。驿报在路上快慢不一,有的三日到,有的九月到,下官只能按盖印进京的漏刻为准。"尚书冷笑:"那你告诉我,去岁全年到底哪天最不太平?按你的册子,正月初三最险——可那天到京的七件急报,六件是去年夏秋的旧事。真正正月初三发生的事,倒被大雪埋在路上,开春才到,混在四月案卷里没人注意。"

周主事这才明白:驿报在路上会"乱序"。他重新整理,在每件案卷旁注两列时辰——左列"事发漏刻"(驿报上盖的原始印),右列"到京漏刻"(兵部收到的印)。同一桩事,两列可能差三天,也可能差九个月。他再画图:横轴是事发时间,纵轴是到京时间,点越靠上说明在路上耽搁越久。一幅散点铺开来,他看见一条斜向的带子——大部分点落在"到京 = 事发 + 数日"附近,但总有几个点远远飘在上方,像被风吹离雁阵的孤雁。

"这些孤雁,"张头指着最上方一个点,"就是辽东缺饷——去年九月的事,今年六月才到。你若只看'到京顺序',它落在最后;可你若问'去年九月发生了什么',它必须出现在那个月的集合里。"

周主事再不敢用单一顺序。他做了两套索引:一套按事发月份聚类,看"那个月天下发生了什么";一套按到京月份聚类,看"这个月兵部忙了什么"。两套索引对不上才是常态——腊月到京的案卷里,混着去年春夏秋三季的事;而去年腊月发生的事,要散到今年正二三月才陆续到齐。

后来他在每册封面加了批注:"截至本月,去岁腊月事已到齐九成,十一月事到齐七成,十月事到齐五成——再早的月份,仍有孤雁未归,数目不详。"尚书再未骂过他。


概念解析

流处理系统面对的核心悖论是:数据产生的时间和数据被处理的时间,是两个独立流淌的时钟

事件时间(Event Time) 是数据本身携带的印记——传感器采样瞬间、用户点击时刻、交易发生秒数。它发生在系统之外,不可更改,像驿报上盖的事发漏刻。

处理时间(Processing Time) 是数据抵达计算节点、被算子消费的瞬间。它受网络延迟、队列积压、背压重试支配,像驿卒换马的速度——雨雪、疲病、劫匪,样样都能让它偏离预期。

朴素的做法是像周主事最初那样:按处理时间归档、按处理时间汇总、按处理时间做窗口统计。这在"低延迟、低乱序"的假设下能工作,但一旦事件在路上产生显著延迟,分析就失真——旧事件混入新窗口,新事件迟到漏计,趋势判断整体错位。

窗口(Window) 是流处理的基本单位:把无限流切分成有限块来做聚合。按处理时间开窗,窗口边界由本地时钟决定,实现简单,但语义脆弱。按事件时间开窗,窗口边界由数据自带的时间戳决定,语义正确,但实现复杂——因为窗口"闭窗"的时刻无法预先知道。去年腊月的事,今年三月还可能有一封迟到;窗口若按"到京时刻"强行关闭,那封迟到的驿报就无家可归。

Watermarks(水位线) 是 Flink、Beam 等系统给出的折中:一个单调递增的进度标记,声明"所有事件时间早于某值的事件,理论上都已到达"。它不是保证——"理论上"意味着仍有迟到可能——而是概率性的边界,像周主事封面上的批注"到齐九成"。水位线推进越快,延迟越低,但遗漏越多;推进越保守,结果越完整,但实时性越差。这是事件时间与处理时间之间的张力,没有通用最优解,只有场景权衡。

乱序(Out-of-Order) 不是异常,是流处理的常态。TCP 保证单连接内有序,但分布式系统有无数并行连接;传感器离线缓存后批量上报、移动端断网续传、跨数据中心复制延迟,都会把事件时间顺序彻底打乱。承认乱序的存在,是流处理设计的第一课。

Lambda 架构的历史教训。 早年 Twitter 的 Storm 只认处理时间,结果离线报表与实时看板永远对不上数——同一分钟,两个系统给出两个答案。后来 Lambda 架构用两套管道并行:Storm 给近似实时,Hadoop 给精确离线,日终合并。维护成本翻倍,语义裂缝仍在。Kappa 架构(Jay Kreps, 2014)提出统一日志层——Kafka 作为持久化的事件时间主干,实时与离线从同一源头重放。Watermarks 和窗口机制让这套统一层成为可能。

现代系统如 Flink 的 Watermark 策略、Google Dataflow 的"准确一次"事件时间处理,本质都是在回答周主事当年面对的同一道题:当驿报在路上会乱序、会迟到、会丢失,京师那套档案系统该怎么编,才能既不错过旧事的真相,也不耽误新事的响应?