晚到了五十秒的那次点击,算在哪一分钟
每分钟的点击数报表,与事后从明细表里按时间聚合出来的数字对不上:有的分钟多几个,有的分钟少几个,总数倒是一样的。移动网络差的时段差得更多。
事件有两个时间:它发生的时间,和它到达处理程序的时间。两者之间隔着客户端的缓冲、网络、消息队列的积压,差几秒到几分钟都正常。按哪个时间把事件归到窗口里,统计结果就不一样;按发生时间归窗,又要回答另一个问题:晚到的事件等不等,等多久。
本文用 Kafka Streams 4.3.1 把这几种选择各跑一遍。实验见文末。
一、按到达时间,还是按发生时间
五次点击,括号里是「发生时刻 → 到达时刻」,单位是 08:00:00 之后的秒数:10→11、20→21、55→105、70→71、80→81。第三次点击发生在第 55 秒,晚了 50 秒才到。按发生时间,08:00 这一分钟有 3 次,08:01 有 2 次。
按到达时间归窗:{08:00=2, 08:01=3}
按发生时间归窗,允许晚到 5 分钟:{08:00=3, 08:01=2}按到达时间归窗,那次点击被算进了下一分钟。总数没变,每个窗口都错了。这就是开头报表对不上的原因:明细表里存的是发生时间,实时统计用的是到达时间。延迟越大、越不稳定,偏差越大;消费者积压之后追数据的那段时间,几分钟的事件会全部堆进同一个窗口。
按到达时间统计并非不能用。它简单、结果立刻确定,适合回答「系统此刻处理了多少」。回答「用户在那一分钟做了什么」,要用发生时间。
二、晚到的事件等多久
按发生时间归窗有一个代价:窗口什么时候算「结束」。08:00 这一分钟过去之后,属于它的事件还可能陆续到来。
按发生时间归窗,不允许晚到:{08:00=2, 08:01=2}
按发生时间归窗,允许晚到 10 秒(晚到的那个到达时,流里见过的最大时间戳是第 80 秒):{08:00=2, 08:01=2}
按发生时间归窗,允许晚到 30 秒:{08:00=3, 08:01=2}不允许晚到时,那次点击被丢弃:两个窗口加起来只有 4 次。丢弃是静默的,输出里看不出少了什么,只有运行时的一个指标(丢弃记录数)和一行日志会记下来。
允许晚到 10 秒仍然丢了,30 秒才算上。这里的「晚」不是按墙上时钟算的,而是按流时间:到目前为止见过的最大事件时间戳。那次点击到达时,流里已经见过第 80 秒的事件;08:00 的窗口在第 60 秒结束,加 10 秒宽限是第 70 秒,已经过了。加 30 秒宽限是第 90 秒,还没过。
宽限时间的选择是在两头取舍:
- 太短,晚到的事件被丢弃,统计偏少。
- 太长,窗口迟迟不关闭,状态占用变大,「最终结果」出来得晚。
它应该按实际的延迟分布来定:看事件的到达时间减发生时间的分位数,覆盖到比如 99.9%,并且把丢弃记录数作为监控指标。
三、结果什么时候输出
窗口计数有两种输出方式。默认是每来一条事件就输出一次当前的计数,下游会看到同一个窗口的多个版本(1、2、3)。下游是可以覆盖写的存储时没有问题;下游是「每个窗口发一次告警」或者追加写的日志时,就需要只在窗口关闭后输出一次。
只在窗口关闭后输出(suppress),事件 10、20、70,之后再没有新事件:{}
同样的设置,后面又来了 95、155 两个事件:{08:00=2, 08:01=2}第一行是空的:三个事件都处理了,没有任何输出。窗口「关闭」同样由流时间决定,而流时间只有新事件到来才会前进。最后一个事件是第 70 秒,08:00 的窗口要等流时间超过第 90 秒才关闭。没有新事件,它就一直开着。
这在两种情况下会成为问题:
- 流量很小或者有间歇的主题,最后一个窗口的结果会一直卡着,直到下一个事件到来。夜间没有数据的业务,最后一个窗口的结果可能要到第二天早上才输出。
- 测试里只发了几条数据就断言输出,会一直等不到。
需要「到点就出结果」时,不能只依赖事件驱动的流时间,要么由上游定期发送心跳事件来推进时间,要么在处理逻辑里用按墙上时钟触发的定时器兜底。
四、一个时间戳错了的事件
流时间取的是见过的最大时间戳,所以它只进不退。一个时间戳错得很远的事件会带来这样的后果:
事件 10、20、30、40 中间混进一个时间戳在一小时之后的事件(设备时钟错了),允许晚到 5 分钟:{08:00=1, 09:00=1}一台时钟快了一小时的设备发来一个事件,流时间直接跳到 09:00。之后到达的第 20、30、40 秒的正常事件,对应的窗口早已「关闭」,全部被当作晚到丢弃。08:00 这一分钟只剩下那个坏事件之前的 1 次。
一个坏事件影响的是它所在分区上之后所有的正常事件,直到真实时间追上那个错误的时间戳。客户端上报的时间不可信时,需要在进入窗口计算之前处理:
- 丢弃或修正明显不合理的时间戳,例如比服务端收到的时间晚出一个阈值的。
- 时间戳改用服务端收到事件时打上的时间,牺牲一点精度,换来可控。
本机时钟不可靠带来的其他问题,见订单号重复了:雪花算法依赖的三件事。
五、检查清单
- 明确报表要回答的问题用的是发生时间还是处理时间,实时与离线两条链路用同一种。
- 宽限时间按实际的延迟分布设置;丢弃记录数有监控。
- 下游能否接受同一个窗口的多次更新,决定要不要只在窗口关闭后输出。
- 流量有间歇时,窗口的关闭有别的推进手段。
- 事件时间戳在使用前做合理性检查。
- 重放历史数据时,流时间会快速前进,实时场景下调好的宽限在重放时可能丢弃更多或更少的事件,重放结果要单独核对。
同一个键的消息在分区内的顺序问题,见按订单号发到同一个分区,为什么还是乱序了。本文没有覆盖状态的检查点与故障恢复。
恢复进度和在线可见性还需分别约定:checkpoint 恢复与结果时间用单机算子恢复和迟到特征说明这两个边界。
配套实验
- codesphere-labs/messaging/event-time-windows:Kafka Streams 4.3.1 的
TopologyTestDriver(JDK 25.0.4.1):一分钟滚动窗口计数,按到达时间与按发生时间归窗、不同的宽限时间、只在窗口关闭后输出、混入一个时间戳在一小时之后的事件(验证记录)
参考资料
- Kafka Streams 文档:Core Concepts,Time
- Kafka Streams 文档:Windowing(宽限时间、窗口关闭后输出)
- KIP-328:Ability to suppress updates for KTables
- Akidau 等:The Dataflow Model(事件时间、水位线与触发的一般框架)
- Apache Flink 文档:Timely Stream Processing(同一组概念在另一个框架里的表达)