4. 时间语义(Time Semantics)
这章我们会介绍时间语义,以及在流中,对于时间的各种不同的概念的描述。同时我们也会讨论一个流处理器在事件乱序的情况下,如何能提供精准的结果,以及如何使用流对历史events进行处理。
一分钟的含义
假设我们要持续的对流计算并生成结果,例如每一分钟。这里的“一分钟”在流处理应用里到底意味着什么呢?
考虑这么一个场景,一个程序用于分析手机网游用户的events。用户属于各个小队。基于小队成员达成游戏给定目标的速度,应用收集小队的信息并在游戏中给出奖励,例如升级、经验等。例如,如果一个小队的所有用户在一分钟内生产了500个泡泡,则他们获得一次升级。小明是其中一个玩家,他每天早上会在去上班的公交上玩这个游戏。可问题是,小明住在大山里,在出山的路上网络特别差。假设小明开始在游戏中生产泡泡,此时手机连接到了网络,并发送events到分析程序。突然,公交进入了一个隧道,导致他的手机断网了。小明继续玩游戏,此时events会缓存到他的手机里。当公交出了隧道,网络恢复后,pending的events会被发送到分析程序。此时分析程序怎么做?在这里一分钟意味着什么呢?是否考虑到了小明断网的时间?下图描述了此问题:
在线游戏是一个简单的场景,描述了 operator 语义如何依赖于时间。当events发生在某个时间段内,但是应用接收到events时已超过了这个时间段时,应用应如何处理时间语义?在这个手机游戏中,若是对此处理不当,会导致较差的客户体验。但是在一些非常看重时间的应用中,结果可能会更严重。如果仅考虑在一分钟内接收到多少个events,则结果会直接与于网络连接状况、或是处理速度等相关。所以,真正定义一分钟内的events时,应该与数据本身的时间相关。
例如在小明玩游戏这个例子中,流处理应用可以以两种时间概念进行操作:处理时间(processing time)或事件时间(event time)。接下来介绍这两种概念。
处理时间(Processing Time)
处理时间是处理流的operator在执行时,所属机器上的本地时间。Processing-time 窗口包含所有在一个时间周期内到达window operator 的events,以本地机器时间衡量。如下图所示,在小明游戏的案例中,在小明的手机断开连接后,processing-time 窗口仍会持续计时,所以不会将小明断网时间内的events计入到当前时间段。
事件时间(Event Time)
事件时间是一个event实际发生的时间,它基于事件流中event被打上的时间戳。时间戳一般存储与event 数据中。下图展示了一个event-time 窗口,可以正确的将events放入合适的窗口中,反应了事件实际发生的情况,即使事件的到达存在延迟。
事件时间将处理速度与结果完全解耦。基于事件时间的操作时可预测的(predictable)并且结果是明确的(deterministic)。使用事件时间窗口计算时,无论流处理的速度有多快,或是events到达operator的速度有多慢,它输出的一定是个相同的结果。
处理延迟的events仅仅是事件时间可以解决的众多问题中的一个。更普遍的数据乱序的问题,也可以由事件时间解决。假设小花是游戏的另一个玩家,她与小明都是做的同一班公交,在同一时间玩游戏,但是用的是不同的手机卡。在过隧道时,小明的手机没信号了,但是小花的手机还可以正常联网并向游戏应用继续发送events。
依赖于事件时间,我们可以在数据无序到达的情况下,依然保证事件的正确性。进一步的说,在结合了可重跑的流(replayable streams)时,明确的时间戳可以用于快速执行过去的数据(fast forward the past)。也就是说,你可以重跑(replay) 一个流并分析历史数据,就像events是实时发生的一样。同时,你也可以快进计算,直到到达当前的状态(fast forward the computation to the present),这样一旦应用获取了正在发生的事件时,它可以像一个实时应用一样,以同样的program logic继续工作。
水印(Watermarks)
在讨论event-time 窗口时,我们忽略了一个很重要的方面:我们如何决定何时触发一个event-time 窗口?也就是说,我们需要等待多长时间,才能确保已经收到了在某个特定时间点之前发生的所有事件呢?并且我们如何知道数据会有延迟呢?很遗憾,这些问题是没有一个完美的答案的,因为分布式系统会有各种预料之外的异常,并且可能会有多方面的外部组件影响延迟。在这章我们会引入如何使用水印(watermarks)配置event-time窗口的行为。
水印是一个全局的进度指标(progress metric),表示的是:在什么时间点,我们可以有信心判断,之后不会有更多的(延迟的)事件到达。本质上,水印提供了一个逻辑时钟,提醒系统关于当前event time的信息。当一个operator收到了一个水印,时间为T。它可以假设:不会再有更多时间戳小于T的事件被收到。水印对于event-time窗口以及operators处理乱序events至关重要。一旦一个水印被接收到后,operators即被通知到:对于某个时间周期,所有的时间戳已经被观察到(observed),接下来应触发计算,或是将收到的事件进行排序。
水印提供了一个配置,用于权衡结果可信度与延迟。Eager watermarks可以确保低延迟,但是提供较低的可信度。在这种情况下,延迟的events可能会在水印之后到达,我们需要提供处理这些events的代码。另一方面,如果水印特别宽松,则我们对结果的准确度会有更大的信心,但是可能会增加不必要的处理延迟。
在很多真实应用中,系统并没有足够的知识去完美地准确算出水印。在手机游戏的例子中,基本是不可能预测用户会有多长的时间丢失连接。无论是自定义的水印或是自动生成的水印,在分布式系统中,由于有落后的tasks,跟踪整个分布式系统的进度可能仍是个问题。所以,如果仅简单的依赖于水印,则可能并不是一个很好的方法。更重要的是,流处理系统应提供一些机制去处理这些落后于水印的events。根据应用的需求,可以简单的丢弃这些事件,或是记录日志,亦或是使用它们去修正之前的结果。
处理时间vs事件时间
这里你可能会好奇既然event time可以解决我们所有的问题,那为什么还要提出processing time。事实上,处理时间在某些情况下是非常有用的。Processing-time 窗口可以尽可能地引入最低延时。例如:不考虑延迟及乱序事件,window仅是用于缓存一些数据,在到达一段时间后立即触发一个计算。所以对于那些速度比准确度更重要的应用来说,processing time 会更方便。另一个案例是定期实时提供报告结果,不考虑精准度。最后,processing-time窗口是流自身行为的一个体现,可能在某些场景下是一个很好的属性。例如,计算每秒收集到的事件,用于检测outage。总而言之,processing time 提供了低延时,但是输出的结果取决于处理的速度,并且结果并不是明确的。而event time保证了明确的结果,并可以处理延时或乱序的事件。
References:
Vasiliki Kalavri, Fabian Hueske. Stream Processing With Apache Flink. 2019