1.5. 流处理基础概念#
前文已经多次提到,在某些场景下,流处理打破了批处理的一些局限。Flink 作为一款以流处理见长的大数据引擎,相比其他流处理引擎具有众多优势。本节将对流处理的一些基本概念进行细化,这些概念是入门流处理的必备基础,至此你将正式进入数据流的世界。
延迟和吞吐#
在批处理场景中,我们主要通过一次计算的总耗时来评价性能。在流处理场景,数据源源不断地流入系统,大数据框架对每个数据的处理越快越好,大数据框架能处理的数据量越大越好。例如 1.2.3 小节中提到的股票交易案例,如果系统只能处理一两只股票或处理时间长达一天,那说明这个系统非常不靠谱。衡量流处理的 “快” 和“量”两方面的性能,一般用延迟(Latency)和吞吐(Throughput)这两个指标。
延迟#
延迟表示一个事件被系统处理的总时间,一般以毫秒为单位。根据业务不同,我们一般关心平均延迟(Average Latency)和分位延迟(Percentile Latency)。假设一个食堂的自助取餐流水线是一个流处理系统,每个就餐者前来就餐是它需要处理的事件,从就餐者到达食堂到他拿到所需菜品并付费离开的总耗时,就是这个就餐者的延迟。如果正赶上午餐高峰期,就餐者极有可能排队,这个排队时间也要算在延迟中。例如,99 分位延迟表示对所有就餐者的延迟进行统计和排名,取排名第 99% 位的就餐者延迟。一般商业系统更关注分位延迟,因为分位延迟比平均延迟更能反映这个系统的一些潜在问题。还是以食堂的自助餐流水线为例,该流水线的平均延迟可能不高,但是在就餐高峰期,延迟一般会比较高。如果延迟过高,部分就餐者会因为等待时间过长而放弃排队,用户体验较差。通过检查各模块分位延迟,能够快速定位到哪个模块正在 “拖累” 整个系统的性能。
延迟对于很多流处理系统非常重要,比如欺诈检测系统、告警监控系统等。Flink 可以将延迟降到毫秒级别。如果用 mini-batch 的思想处理同样的数据流,很可能有分钟级到小时级的延迟,因为批处理引擎必须等待一批数据达到才开始进行计算。
吞吐#
吞吐表示一个系统最多能处理多少事件,一般以单位时间处理的事件数量为标准。需要注意的是,吞吐除了与引擎自身设计有关,也与数据源发送过来的事件数据量有关,有可能流处理引擎的最大吞吐量远小于数据源的数据量。比如,自助取餐流水线可能在午餐时间的需求最高,很可能出现大量排队的情况,但另外的时间几乎不需要排队等待。假设一天能为 1 000 个人提供就餐服务,共计 10 小时,那它的平均吞吐量为 100 人 / 小时;仅午间 2 小时的高峰期就提供了 600 人,它的峰值吞吐量是 300 人 / 小时。比起平均吞吐量,峰值吞吐量更影响用户体验,如果峰值吞吐量低,会导致就餐者等待时间过长而放弃排队。排队的过程被称作缓存(Buffering)。如果排队期间仍然有大量数据进入缓存,很可能超出系统的极限,就会出现反压(Backpressure)问题,这时候就需要一些优雅的策略来处理类似问题,否则会造成系统崩溃,用户体验较差。
延迟与吞吐#
延迟与吞吐其实并不是相互孤立的,它们相互影响。如果延迟高,那么很可能吞吐较低,系统处理不了太多数据。为了优化这两个指标,首先提高自助取餐流水线的行进速度,加快取餐各个环节的进程。当用户量大到超过流水线的瓶颈时,需要再增加一个自助取餐流水线。这就是当前大数据系统都在采用的两种加速方式,第一是优化单节点内的计算速度,第二是使用并行策略,分而治之地处理数据。如果一台计算机做不了或做得不够快,那就用更多的计算机一起来做。
综上,延迟和吞吐是衡量流处理引擎的重要指标。如何保证流处理系统保持高吞吐和低延迟是一项非常有挑战性的工作。
窗口与时间#
不同窗口模式#
比起批处理,流处理对窗口(Window)和时间概念更为敏感。在批处理场景下,数据已经按照某个时间维度被分批次地存储了。一些公司经常将用户行为日志按天存储,一些开放数据集都会说明数据采集的时间始末。因此,对于批处理任务,处理一个数据集,其实就是对该数据集对应的时间窗口内的数据进行处理。在流处理场景下,数据以源源不断的流的形式存在,数据一直在产生,没有始末。我们要对数据进行处理时,往往需要明确一个时间窗口,比如,数据在 “每秒”“每小时”“每天” 的维度下的一些特性。窗口将数据流切分成多个数据块,很多数据分析都是在窗口上进行操作,比如连接、聚合以及其他时间相关的操作。
图 1.14 展示了 3 种常见的窗口形式:滚动窗口、滑动窗口、会话窗口。
滚动窗口(Tumbling Window):模式一般定义一个固定的窗口长度,长度是一个时间间隔,比如小时级的窗口或分钟级的窗口。窗口像车轮一样,滚动向前,任意两个窗口之间不会包含同样的数据。
滑动窗口(Sliding Window):模式也设有一个固定的窗口长度。假如我们想每分钟开启一个窗口,统计 10 分钟内的股票价格波动,就使用滑动窗口模式。当窗口的长度大于滑动的间隔,可能会导致两个窗口之间包含同样的事件。其实,滚动窗口模式是滑动窗口模式的一个特例,滚动窗口模式中滑动的间隔正好等于窗口的大小。
会话窗口(Session Window):模式的窗口长度不固定,而是通过一个间隔来确定窗口,这个间隔被称为会话间隔(Session Gap)。当两个事件之间的间隔大于会话间隔,则两个事件被划分到不同的窗口中;当事件之间的间隔小于会话间隔,则两个事件被划分到同一窗口。
时间语义#
Event Time 和 Processing Time#
“时间” 是平时生活中最常用的概念之一,在流处理中需要额外注意它,因为时间的语义不仅与窗口有关,也与事件乱序、触发计算等各类流处理问题有关。常见的时间语义如下。
Event Time:事件实际发生的时间。
Processing Time:事件被流处理引擎处理的时间。
对于一个事件,自其发生起,Event Time 就已经确定不会改变。因各类延迟、流处理引擎各个模块先后处理顺序等因素,不同节点、系统内不同模块、同一数据不同次处理都会产生不同的 Processing Time。
“一分钟” 真的是一分钟吗?#
在很多应用场景中,时间有着不同的语义,“一分钟” 真的是一分钟吗?很多手机游戏中多玩家在线实时竞技,假设我们在玩某款手机游戏,该游戏将数据实时发送给游戏服务器,服务器计算一分钟内玩家的一些操作,这些计算影响用户该局游戏的最终得分。当游戏正酣,我们进入了电梯,手机信号丢失,一分钟后才恢复信号;幸好手机在电梯期间缓存了掉线时的数据,并在信号恢复后将缓存数据传回了服务器,图 1.15 展示了这个场景的流处理过程。在丢失信号的这段时间,你的数据没有被计算进去,显然这样的计算不公平。当信号恢复时,数据重传到服务器,再根据 Event Time 重新计算一次,那就非常公平了。我们可以根据 Event Time 复现一个事件序列的实际顺序。因此,使用 Event Time 是最准确的。
Watermark#
虽然使用 Event Time 更准确,但问题在于,因为各种不可控因素,事件上报会有延迟,那么最多要等待多长时间呢?从服务器的角度来看,在事件到达之前,我们也无法确定是否有事件发生了延迟,如何设置等待时间是一个很难的问题。比如刚才的例子,我们要统计一分钟内的实时数据,考虑到事件的延迟,如何设置合理的等待时间,以等待一分钟内所有事件都到达服务器?也正因为这个问题,流处理与批处理在准确性上有差距,因为批处理一般以更长的一段时间为一个批次,一个批次内延迟上报的数据比一个流处理时间窗口内延迟上报的数据相对更少。比如电商平台上,对于计算一件商品每分钟点击次数,使用一天的总数除以分钟数,比使用一分钟时间窗口实时的点击次数更准确。可以看到,数据的实时性和准确性二者不可得兼,必须取一个平衡。
Watermark 是一种折中解决方案,它假设某个时间点上,不会有比这个时间点更晚的上报数据。当流处理引擎接收到一个 Watermark 后,它会假定之后不会再接收到这个时间窗口的内容,然后会触发对当前时间窗口的计算。比如,一种 Watermark 策略等待延迟上报的时间非常短,这样能保证低延迟,但是会导致错误率上升。在实际应用中,Watermark 设计为多长非常有挑战性。还是以手机游戏为例,系统不知道玩家这次掉线的原因是什么,可能是在穿越隧道,可能是有事退出了该游戏,还有可能是坐飞机进入飞行模式。
那既然 Event Time 似乎可以解决一切问题,为什么还要使用 Processing Time?前文也提到了,为了处理延迟上报或事件乱序,需要使用一些机制来等待,这样会导致延迟提高。某些场景可能对准确性要求不高,但是对实时性要求更高,在这些场景下使用 Processing Time 就更合适一些。
状态与检查点#
状态是流处理区别于批处理的特有概念。如果我们对一个文本数据流进行处理,把英文大写字母都改成英文小写字母,这种处理是无状态的,即系统不需要记录额外的信息。如果我们想统计这个数据流一分钟内的单词出现次数,一方面要处理每一瞬间新流入的数据,另一方面要保存之前一分钟内已经进入系统的数据,额外保存的数据就是状态。图 1.16 展示了无状态和有状态两种不同类型的计算。
状态在流处理中经常被用到。再举一个温度报警的例子,当系统在监听到 “高温” 事件后 10 分钟内又监听到 “冒烟” 的事件,系统必须及时报警。在这个场景下,流处理引擎把 “高温” 的事件作为状态记录下来,并判断这个状态接下来十分钟内是否有 “冒烟” 事件。
流处理引擎在数据流上做有状态计算主要有以下挑战。
设计能够管理状态的并行算法极具挑战。前文已经多次提到,大数据需要在多节点上分布式计算,一般将数据按照某个 Key 进行切分,将相同的 Key 切分到相同的节点上,系统按照 Key 维护对应的状态。
如果状态数据不断增长,最后就会造成数据爆炸。因此可使用一些机制来限制状态的数据总量,或者将状态数据从内存输出到磁盘或文件系统上,持久化保存起来。
系统可能因各种错误而出现故障,重启后,必须能够保证之前保存的状态数据也能恢复,否则重启后很多计算结果有可能是错误的。
检查点(Checkpoint)机制其实并不是一个新鲜事物,它广泛存在于各类计算任务上,主要作用是将中间数据保存下来。当计算任务出现问题,重启后可以根据 Checkpoint 中保存的数据重新恢复任务。在流处理中,Checkpoint 主要保存状态数据。
数据一致性保障#
流处理任务可能因为各种原因出现故障,比如数据量暴涨导致内存溢出、输入数据发生变化而无法解析、网络故障、集群维护等。事件进入流处理引擎,如果遇到故障并重启,该事件是否被成功处理了呢?一般有如下 3 种结果。
At-Most-Once:每个事件最多被处理一次,也就是说,有可能某些事件直接被丢弃,不进行任何处理。这种投递保障最不安全,因为一个流处理系统完全可以把接收到的所有事件都丢弃。
At-Least-Once:无论遇到何种状况,流处理引擎能够保证接收到的事件至少被处理一次,有些事件可能被处理多次。例如,我们统计文本数据流中的单词出现次数,事件被处理多次会导致统计结果并不准确。
Exactly-Once:无论是否有故障重启,每个事件只被处理一次。Exactly-Once 意味着事件不能有任何丢失,也不能被多次处理。比起前两种保障,Exactly-Once 的实现难度非常高。如遇故障重启,Exactly-Once 就必须确认哪些事件已经被处理、哪些还未被处理。Flink 在某些情况下能提供 Exactly-Once 的保障。