流计算相关的基础概念

已经了解到流计算是如何解决了传统批处理的局限以及如何支持了新的应用和架构,并且熟悉了开源流计算技术的演进,接下来介绍流计算的基础。

数据流图(Dataflow graphs)

字面上理解,一个数据流程序描述了数据在各种操作之间的流动。数据流程序通常用一个有向图来表示,其中节点是运算符代表了计算,边表示数据的依赖关系。算子是一个数据流程序的基本函数单元,从输入中消费数据、执行计算,以及输出结果用于进一步的处理。没有输入端的算子称为data source,而没有输出端的算子称为data sink。一个数据流图至少有一个data source和一个data sink。下图描述的是一个数据流程序从tweet的输入流中抽取并计算标签。

img

上图中的数据流图是逻辑上的,是因为描绘了计算逻辑的高级视图。为了执行数据流程序,该逻辑会被转换为一个物理的数据流图,这其中包含了执行计划的细节。例如,如果你使用一个分布式的流计算引擎,每个算子都可能拥有运行在不同的物理机器上的多个并行任务。下图展示了上图中逻辑数据流图的物理实现。在逻辑数据流图中,节点代表算子,而在物理视图中,节点代表任务。“Extract hashtags”和“Count”算子有两个并行任务,每个任务都执行一个输入子集的计算。

img

数据并行与任务并行

在数据流图中你可以用不同的方式进行并行处理。首先你可以将输入数据分区,然后以并行的方式在不同的数据子集上执行相同的计算任务。这种并行叫做数据并行。数据并行很有用,因为它支持了处理海量的数据并且将计算任务负载到不同的计算节点上去。其次,一些计算在相同甚至不同的数据上的不同算子的任务以并行方式运行。这种并行称为任务并行。使用任务并行可以更好地利用集群地计算资源。

数据交换策略

数据交换策略决定数据如何被分配到物理数据流图的任务中。数据交换策略可以由执行引擎根据算子的语义自动选择,也可以被数据流的开发者显式指定。这里,我们简单介绍下一些常见的数据交换策略。

  • forward策略将数据从一个任务发送到另一个接收的任务。如果这两个任务都处在相同的物理机器(这通常由任务调度器所决定)中,那么这个交换策略可以避免网络通讯。
  • broadcast策略发送数据给一个算子的所有并行任务。因为这个策略会复制数据并涉及到网络通讯,所以成本会较高。
  • key-based策略通过key的属性值对数据分区,并保证相同key的数据由相同的任务处理。在上图中,“Extract hashtags”算子的输出由key(标签)来分区,因而count算子的任务可以正确计算每个标签的出现次数。
  • random策略将数据均匀地分发给各个任务,在计算任务中均匀地分配负载。

forward和random策略也是一种key-based策略forward和random策略也可以被视为key-based策略的一种变体,前者保留上游元组的key,而后者执行key的重新随机分配。

img

现在你已经熟悉了数据流编程的基础,是时候学习这些概念如何应用于并行数据流的处理了。首先,我们给数据流下个定义:

数据流是一个可能无界的事件序列

数据流中的事件可能代表着一个监控数据、传感器度量、信用卡交易、气象站监测、在线用户交互、网站搜索等等。接下来你将学习使用数据流编程范式并行处理无限流的一些概念。

延迟与吞吐

在上一篇文章中我们看到流式程序与传统批处理程序有着不同的操作要求。同时在性能评估方面的需求也是不同的。对于批处理来说,通常关心的是一个任务的总运行时长,和处理引擎在读取输入、执行计算以及输出结果方面花费的时间。而流计算程序持续地在运行而输入可能是无界的,所以在数据流的处理中没有总运行时长的概念。流计算程序为了能够处理大量频发的事件需要尽可能快地处理到来的数据。我们把这种性能需求用专业描述为延迟吞吐

延迟

延迟表示一个事件被处理所耗费的时长。实质上就是接收一个事件到看到它被处理后输出结果的时间间隔。为了直观地理解,想想你每天最喜欢去的咖啡店。当你进入咖啡店的时候,里面可能已经有一些其他的顾客了。因此你需要排队直到轮到你,然后收银员收到你的付款并按顺序传递给你准备饮料的咖啡师。一旦你的咖啡做好了,咖啡师会叫你的名字,你再从工作台上取走你的咖啡。你的服务延迟就是从你进入咖啡店到喝上第一口咖啡所经历的时间。

在流计算中,延迟是用时间单位进行度量的,比如毫秒。根据你的程序,你有可能关心平均延迟、最大延迟或者百分位延迟。例如一个平均延迟为10ms意味着事件平均处理时间为10ms。而一个95%延迟为10ms指的是95%的事件可以在10ms内处理。平均值掩盖了处理延迟的真实分布情况,因而难以用来检测问题。如果咖啡师在准备你的卡布奇诺之前刚好把牛奶用完了,那么你需要等他们从储备室里拿一些出来。对于这个延迟你可能会很生气,但是其他顾客可能依然是开心的状态。

对于许多流计算程序来说低延迟非常重要,比如欺诈监测、报警、网络监控以及提供服务等级协议(SLA)的服务。低延迟是流计算的一个关键特性,有了它才有所谓的实时程序。现代的流处理器(如Apache Flink)可以提供低至毫秒级别的延迟。相对地,传统的批处理延迟可能要几分钟甚至几个小时。在批处理中你首先要收集齐所有的事件,然后才能处理它们,因而它的延迟决定于最后一个事件到达的时间,也就自然而然依赖于批的大小了。真正的流计算不会引入这种人为延迟,因此才能达到真正的低延迟。在一个现实的流计算模型中,事件可以在它到达系统后尽快地处理,这个时候延迟真正地反映了每个事件上执行的实际工作。

吞吐

吞吐是一个系统处理能力的度量,即处理的速度。也就是说,吞吐告诉我们的是在一个时间单元内能够处理多少事件。回顾下咖啡店的例子,如果店铺从早上7点到下午7点之间营业,一天内服务了600名顾客,那么平均吞吐即是每个小时50个客户。我们一般希望延迟要尽可能的低,而吞吐要尽可能的高。

吞吐是由每个单位时间的事件或者操作来衡量的。需要注意的是处理的速度决定于事件到来的速度;低延迟并不一定表示性能不好。在流计算系统中,你一般希望明确的是你的系统能够处理多大速度的事件。也就是说,你主要关心的是吞吐的峰值,即你的系统最大负载时的性能极限。为了更好地理解吞吐峰值的概念,让我们想下开始时系统的资源完全处于空闲。当第一个事件到来的时候,它会尽可能最低延迟地被最快处理。如果你是咖啡店早上开门的第一个出现的顾客,那么你会被立刻服务。理想情况下,你希望这个速度不变并且独立于到来的事件。然而,一旦到达事件的速度导致系统资源全部被占用,则我们需要开始缓冲事件。在咖啡店的例子中,你可能会在午后看到这一情形的发生。许多人同时出现,你则需要排队等候。这个时候便是系统到达了吞吐峰值,继续增加事件则会带来更高的延迟。如果系统继续以超出自己处理能力的速度来接收数据,缓冲区则可能不可用,数据也会丢失。这种情形一般称为背压,有几种不同的策略来处理这种情况。

延迟 VS 吞吐

此时,已经明确了延迟和吞吐不是两个独立的指标。如果流经数据处理管道的事件花费了很长时间,则不会轻易达到高吞吐。简单来说,如果一个系统的容量比较小的话,则事件会被缓冲,然后一直等待被处理。

让我们再看下咖啡店的例子,从而理解延迟和吞吐是如何相互影响的。首先很明显在无负载的情况下延迟最低。也就是,如果你是咖啡店里的唯一的顾客,那么你会得到最快的服务。然而,在繁忙的时候,顾客需要排队等候,这时延迟也会增加。另外影响延迟和吞吐的因素就是处理一个事件所需的时间,即咖啡店的顾客服务的时间。想象一下在圣诞节期间,咖啡师需要在每一杯咖啡里画出一个圣诞老人。这种情况下,准备一杯饮料的时间会增加,导致每个顾客在咖啡店里耗费更多的时间,因此降低了总体的吞吐。

那么你如何才能同时获取低延迟和高吞吐呢,还是说无法企及?有一种降低延迟的方式就是雇佣更熟练的咖啡师,即可以更快地制作咖啡。这样当高负载的时候便能增加吞吐,因为在同样多的时间内会有更多顾客得到服务。另外一个方法就是再雇佣一个咖啡师,也就是利用并行。这带来的主要结果是降低了延迟也即增加了吞吐。通常来说,如果一个系统能够更快地执行操作的话,也就可以在相同的时间内执行更多操作。这实际上也就是你在流计算管道中利用并行获得的效果。通过并行地处理多个流,你可以在相同的时间内处理更多事件时降低负载。

数据流操作

流计算引擎通常会提供内置操作来进行抽取、转换以及输出流。这些算子可以组合成数据流图,从而实现流计算程序的逻辑。本单元我们会介绍最常见的流操作。

操作可以是无状态的也可以是有状态的。无状态的操作不会维护任何内部状态。也就是说一个事件的处理不会依赖于过去的任何事件,并且不会保存历史。无状态的操作很容易实现并行,因为这些事件可以独立地进行处理,而且与它们到达的顺序无关。而且,在发生故障的情况下,无状态的操作很容易重启并从中断的地方继续处理。相对地,有状态的操作可能包含了之前接收的事件的一些信息。这个状态会被即将到来的事件更新,并被处理逻辑中未来的某些事件所使用。实现有状态的流计算程序的并行和容错具有很大的挑战,因为为了防止故障的发生需要对状态进行有效地分区和可靠地恢复。在本章地末尾,你会学到更多关于有状态的流计算、失败场景以及一致性等知识。

数据提取和数据输出

流处理器通过数据提取和输出操作与外部系统进行交互。数据提取是从外部数据源读取原始数据,并转换成适合处理的格式。实现数据提取逻辑的算子叫做data source。一个data source可以从一个TCP套接字、一个文件、一个Kafka topic或者一个传感器数据接口提取数据。数据输出即一种将数据以一种适合外部系统的方式输出的操作。执行数据输出的算子叫做data sinks,例如文件、数据库、消息队列和监控接口。

转换操作

转换操作时独立处理单个事件的单通道操作。它们一个一个地消费事件,将转换作用在事件数据上并产生一个新的输出流。转换逻辑有的集成在算子上,也有的是有用户自定义函数(UDF)提供的,如图所示。UDF是由程序员开发并且实现了自定义计算逻辑。

img

算子可以接受多个输入流,也可以产生多个输出流。而且可以通过将一个流切分成多个,或者将多个流合并成一个,从而修改了数据流(dataflow)的结构。

滚动聚合

滚动聚合是一种根据每个输入事件持续更新的聚合,如求和、最小值和最大值。聚合操作时有状态的,它将当前状态和未来的事件组合起来产生一个新的聚合值。注意,为了高效地组合当前状态和一个事件并产生一个聚合值,聚合函数需要满足结合律和交换律。否则这项操作需要存储所有的历史数据。下图描述了一个最简单的滚动聚合。算子维护当前的最小值并根据每个到来的事件进行更新。

img

窗口

转换和滚动聚合一次处理一个事件并输出事件,同时不断更新状态。而有些操作需要收集或缓存一些记录来计算结果。现在考虑一种场景需要对流进行连接操作或者整体进行聚合,比如中值。为了在无界流上有效地仅需这些操作,则需要限制该操作所包含的数据量。本单元我们讨论窗口相关的操作,它提供了一种机制去解决这类问题。

窗口除了拥有实用价值之外,还在语义上支持一些有意思的流查询。我们已经可以通过滚动聚合将流的历史归结为一个聚合值并且为每个事件提供低延迟。这可以满足一些应用的需求,不过如果你只对最近的数据感兴趣呢?现在有一款提供实时交通信息的应用,可以让司机绕开拥堵的路段。在这种场景下,你想知道的是在某个地方最近几分钟是否有事故发生。而另一方面,知道所有发生过的事故没有多大意义。而且,将流的历史数据归结为一个聚合值,会丢失随时间变化的数据信息。例如,你可能想知道每五分钟有多少车辆穿过十字路口。

窗口操作从一个无界事件流中持续创建事件的有限集合,并在上面执行一些计算。事件一般会根据数据属性或者时间被分配到不同的桶。为了准确定义窗口操作法的语义,我们需要回答两个主要问题:“怎么将事件分配到桶”和“窗口多久产生一个结果”。窗口的行为是由一组策略定义的。窗口的策略决定了什么时候创建一个新桶,事件该被分配到哪个桶,什么时候计算桶的内容。后续的操作都是基于一个触发条件的。当触发条件发生时,桶中的内容会被发送给计算函数,将计算逻辑应用到桶中的每个元素。计算函数可以时聚合函数如sum、min或者是对桶中元素进行聚合的自定义操作。策略也可以基于时间(如最近5分钟接收的事件),或者基于计数结果(如最近100个事件),又或者基于一个数据属性。本单元,我们介绍常见的窗口类型的语义。

  • 滚动窗口将事件分配到固定大小的非重叠桶中。当窗口的边界到达,所有的事件会被发送到计算函数进行处理。基于计数的滚动窗口定义了在触发计算之前有多少事件会被收集。下面第1个图展示了一个基于计数的滚动窗口将输入流分散到包含4个元素的桶中。基于时间的滚动窗口定义了事件缓存到桶中的时间间隔。下面第2个图展示了一个基于时间的滚动窗口,将事件收集到桶中,且每十分钟触发计算。

img

img

  • 滑动窗口将事件分配到固定大小的重叠桶中。因此,一个事件可能属于多个桶。我们通过长度和滑动条来定义滑动窗口。滑动值定义了一个新桶创建的间隔。下图中的基于计数的滑动窗口的长度为4个事件,滑动条为3个事件。

img

  • 会话窗口经常在一些实际场景中很有用,而滚动窗口和滑动窗口通常派不上用场。假设有一个分析在线用户行为的程序。这种程序中,我们喜欢将来自用户相同的行为周期或者会话的事件组合在一起。会话是由发生在相邻时间的事件以及紧随着一段不活动的周期所组成。例如,用户与一系列新闻文章的交互可以被认为是一个会话。由于会话的长度不能事先定义而决定于真实的数据,滚动窗口和滑动窗口在这种场景下不能应用。我们需要的是一个窗口操作,能够分配属于相同会话的事件到相同的桶中。会话窗口根据会话间隔值对于会话中的事件进行分组,其中会话间隔值定义了不活动时间,即活动的关闭时间。下图展示了一个会话窗口。

img

目前你看到的所有的窗口类型都是全局窗口并且运行在整个流上。实际中你可能想将一个流分区到多个逻辑流中然后定义并行窗口。例如,你要从不同的传感器中接收度量值,你希望在进行窗口计算前按照传感器的序号来分组。在并行窗口中,每个分区独立于其他分区应用窗口策略。下图展示了一个并行的基于计数的滚动窗口,其长度为2且由事件颜色分区。

img

窗口操作与流计算中两个很重要的概念关系紧密:时间语义和状态管理。时间可能是流计算中最重要的概念。虽然低延迟是流计算中很迷人的一个特性,但是它的价值不仅仅是提供快速分析的能力。现实世界的系统、网络以及连接的管道都远非完美,因此流数据可能会经常延迟或者乱序到达。所以重要在于如何在这种情形下分发准确且确定的结果。更重要的是,流计算还要像处理即时生产的事件那样来处理历史的事件,从而支持离线分析和跨时间分析。当然,如果系统不能保证状态以防故障的话,这些目标都不能达成。所有之前看到的窗口类型都需要在执行计算之前缓存数据。事实上,如果你想在一个流计算程序中计算一些有意思的东西,就算是一个简单的计数,你也需要维持状态。考虑到流计算程序可能要运行几天、几个月甚至几年,你需要确保故障时能够恢复状态,并且保证在出问题时仍然有准确的结果。

时间语义

本单元,我们会介绍时间语义,以及流中时间的不同概念。我们会讨论流处理器如何针对乱序事件提供准确的结果,以及如何来处理历史事件甚至时间旅行。

如何定义一分钟?

在处理一个包含持续事件的无界流时,时间是程序中的一个核心的概念。假定你要持续计算一个结果,比如一分钟一次。那么在我们的流计算程序中一分钟意味着什么?

现在假设有一款程序,分析用户玩移动在线游戏产生的事件数据。用户组队参与游戏,程序收集队伍的行为数据并且在游戏中提供奖励,比如额外的生命或者升级,这取决于用户多快完成游戏的目标。例如,如果一个队伍中的用户在一分钟之内打破500个气泡则可以升一级。爱丽丝是一个忠实的玩家,每天在上下班的路上都会玩。问题在于她住在柏林并且乘坐地铁去上班。众所周知柏林地铁上的移动网络信号很差。假设当她的手机联网后开始玩打气泡,同时将事件发送到分析程序。突然,火车经过一段隧道导致她的手机断网。而她继续玩,而游戏相关事件则缓存在她的手机上。当或者离开隧道,她又连上网络并且发送事件给分析程序。此时程序该如何处理?这个时候如何定义一分钟?包不包括爱丽丝离线的时间呢?

img

在线游戏作为一个简单的场景,展示了操作符语义应该决定于事件实际发生的时间而非事件接收时的时间。在移动游戏中,如果处理不好则结果可能很糟糕,爱丽丝和她的团队可能会很失望并放弃这款游戏。事实上有很多关乎时间的应用,我们需要保证其语义。如果我们仅仅考虑一分钟能够接收多少数据,则结果会大不相同,并且这决定于网速以及处理速度。事实上,定义一分钟内事件数量的是数据本身的时间。

在爱丽丝的游戏案例中,流处理程序可以处理两种不同的时间,处理时间和事件时间。接下来将详细介绍这两者。

处理时间

处理时间是被操作的流真正被执行时机器上的本地时钟的时间。处理时间窗口包含了在一个时间周期内到达窗口操作符的所有事件,它由机器上的时钟来测量。如下图所示,在爱丽丝的例子中处理时间窗口在她手机断网后会继续计时,因而不会考虑那段时间她的行为。

img

事件时间

事件时间是流中的事件真实发生的时间。事件时间是依附于流事件的时间戳。一般在进入处理管道之前,这个时间戳(例如事件产生时间)就已经和事件绑定在一起了。下图显示了事件时间窗口会正确地摆放事件,且反映事情发生的实际情况,即使有些事件延迟了。

img

事件时间将处理速度与结果完全解耦。基于事件时间的操作是可预测的而且结果是确定的。无论流处理速度多快或者事件何时到达操作符,事件时间窗口总会计算出相同的结果。

使用事件时间,不仅仅可以处理延迟事件这种挑战。除了面对网络的延迟,流计算程序可以受其他因素影响导致事件乱序到达。假设鲍勃是那款移动在线游戏的另一个玩家,他也和爱丽丝在同一辆或者上。他们玩一样的游戏但是移动运营商不同。当爱丽丝在隧道中断开了连接,鲍勃手机还在联网并且分发事件到游戏程序中。

通过事件时间,我们可以保证这种场景下结果正确性。而且,结合可重放流,时间戳的确定性让你可以追溯过去。也就是说,可以重放一个流,像处理实时一样来分析历史数据。 另外,你可以快速追溯数据到当前,一旦数据赶上当前发生的事件,可以使用相同的程序逻辑来作为一个实时程序来继续处理。

水印

我们已经讨论了很多关于事件时间窗口,现在来看一下一个重要的概念:我们如何决定什么时候触发一个事件时间窗口。也就是说,我们要等多久才能确定在某一个时间点之前已经接收到了所有的事件?而且我们如何知道数据将会延迟?由于分布式系统的不可预测和外部组件可能引起的延迟,该问题没有绝对正确的答案。本单元,我们将了解到如何使用水印的概念去定义事件时间窗口的行为。

水印是一个全局的度量,表明在某个时间点之后确定不会有延迟的事件到达。实质上水印就是提供了一个逻辑的时钟,通知系统当前的时间。当操作符接收到一个带有时间T的水印,即认为不会再有小于时间T的事件。水印对于事件时间窗口和处理乱序事件都是至关重要的。 一旦收到水印,操作符即被标记为某个时间间隔内的所有时间戳都已收到,此时要么触发计算,要么对接收到的事件排序。

水印在结果的可信度与延迟之间,提供了一种可配置的权衡。Eager水印确保了低延迟,但是也带来了较低的可信度。这种情况下,晚来的事件可能会在水印之后到达,此时我们额外去处理这些事件。另外,如果水印来得太慢,会获得较高的可信度,但同时增加不必要的延迟。

现实场景中,系统往往不知道如何确定一个水印。在之前移动游戏的例子中,其实很难预估一个用户会断开连接多久。他们可能在经过一个隧道,搭载一架飞机,甚至不再玩了。不管水印是用户定义的还是自动生成的,在分布式系统中跟踪一个全局的进度可能会有问题,因为毕竟存在一些零散的任务。因此,简单地依赖水印可能不是一个好办法。相反,让流处理系统来提供一些机制来处理可能在水印之后到达的事件才是更加重要的。根据具体应用的要求不同,你可能要忽略这些事件,或者记录日志,或者用来修正之前的结果。

处理时间 VS 事件时间

这里你可能会有疑问:*既然事件时间解决了所有的问题,那么为什么还要考虑处理时间呢?*答案就是处理时间在有些场景中确实很有用。处理时间窗口可以引入尽可能低的延迟。由于你不需要考虑迟到和乱序的事件,窗口只需要简单地缓冲事件,并且在特定的时间长度到达时立即触发计算。因此对于那些速度比准确性更重要的应用来说,处理时间更加方便。另外有一个场景是在需你要周期性地实时报告结果,而与准确性无关时。例如一个实时监控面板,需要在接收到事件时展示它们。最后,处理时间窗口提供了流本身可靠表示,对于有些案例来说可能是一个比较理想的特性。回顾下,处理时间提供了较低的延迟,但是结果取决于处理速度且结果不是确定的。另一方面,事件时间保证的确定的结果,并且允许你处理迟到和乱序的事件。

状态和一致性保证

现在我们再来看流处理中另外一个很重要的概念——状态。状态在流处理中无处不在。任何稍微复杂的计算都离不开它。为了计算某个结果,可能要基于一段时间或者一些事件来使用UDF收集状态,例如计算聚合或者模式检测。有状态的操作符使用到来的事件以及内部的状态来计算结果。例如,滚动聚合操作符会输出截止到目前收集到的所有事件的和。该操作符会保持sum的当前值,并且一旦收到一个新事件就立即更新。类似地,有一个操作符,当检测到在一个“烟雾”事件后10分钟内紧随一个“高温”事件时发出警报。该操作符需要在内部状态中存储“高温”事件,直到接收到一个“烟雾”事件或者超过了10分钟的有效期。

如果我们考虑使用批处理系统分析无界数据集的场景,状态的重要性则体现得更加明显。在现代的流处理工具出现之前,这是一种常见的实现选择。这时,一个任务要反复对到来的事件做批次运算。当任务结束时,结果会被写入持久化的存储,所有的操作状态随之丢弃。当任务下次调度运行时,不能访问之前的任务状态。这种问题一般通过将状态管理委托给外部系统来解决,如数据库。而相反,对于运行流任务,在程序代码中操作状态变得尤其简单。在流计算中,会有跨多个事件的持久状态,我们将其视为变成模型中的一等公民。当然,使用外部系统来管理流状态也是可以的,虽然这样可能会带来一些额外的延迟。

由于流处理需要持续计算无界数据,因此需要小心不能让内部状态无限增长。为了限制状态的大小,操作符一般只是维持到目前为止的一些概要信息。这种概要可能是一个count、sum、一个事件实例、一个窗口的缓存,或者应用程序感兴趣的一些自定义结构。

我们可以想象到,支持有状态的操作符会带来一些实现上的挑战。首先,系统需要有效地管理状态,并且确保不受并行更新的影响。其次,并行会复杂化,因为结果可能既依赖于状态,又依赖于到来的事件。侥幸的是,在很多场景下,你可以通过一个key对状态进行分区,然后独立地管理每个分区的状态。例如,你在处理来自一组传感器的度量数据,你可以使用分区的操作符状态来独立维持各个传感器的状态。第三点,也是最重要的挑战,就是有状态的操作符需要保证状态可以被恢复,在失败时能够正确计算出结果。在接下来的一个单元,你将了解任务失败和结果保证的细节。

任务失败

流计算任务的操作符状态是十分重要的,应该维护好用于防止失败。如果状态在一次失败中丢失了,则恢复后结果会不正确。流计算任务通常会运行很长周期,因而状态也可能已经手机了几天甚至数月。这样,在失败时从头开始处理将会带来很大的代价,并且十分耗时。

在上篇文章,你已经知道了如何将流计算程序定义为一个数据流图的模型。在运行之前,它们会被翻译成物理的数据流图,包含多个连接在一起的并行任务,每个都运行着一些操作逻辑,消费输入流,同时产生输出流传给其他流。实践中可以很容易地设置成百上千的这样的任务并行地运行在多个物理机器上。这些长期运行的流任务,每个任务在任何时候都有可能失败。如何确保透明地处理此类故障,以便流作业能够继续运行呢?实际上,你不但希望流处理器在遇到任务失败时继续处理,而且希望保证结果和操作符状态的正确性。下面我们将在本单元讨论这些问题。

什么叫任务失败

对于输入流的每个事件,任务的执行一般会有以下步骤:

  1. 接收事件,如存储在本地缓存中
  2. 更新内部状态
  3. 输出一条记录

失败可能发生在以上任意一个步骤中,而系统需要在失败时明确定义其行为。如果任务在第一步失败,事件会丢失吗?如果在更新内部状态后失败,恢复后会再次更新吗?这些场景中,输出还是确定的吗?

注:我们假定网络连接可靠,这样就不会记录就不会丢失或者重复,所有的事件最终会按照FIFO的顺序到达目的地。注意,Flink使用TCP连接,因此可以保证这些要求。同时我们假定有很好的失败检查,并且没有恶意任务执行;也就是说,所有未失败的任务都会遵循以上步骤。

在批处理场景下,由于输入数据都已经准备好,所以可以轻松解决这些问题。最简单的方式就是重启任务,但是这样的话需要重放所有数据。然而在流计算的世界,处理失败并非容易。流系统通过提供结果保证来定义失败时的行为。接下来,我们现代流处理器提供的几种保证类型,以及系统达成这些保证的一些语义。

结果保证

在讨论几种不同类型的保证之前,我们需要明确在讨论流处理器的任务失败时容易引起混淆的几个要点。本章接下来,当我们谈到结果保证时,指的是流处理器的内部状态的一致性。也就是说,我们关心的是失败恢复后应用程序看到的状态值。注意,流处理器通常只能保证流处理器本身内部状态的正确性。然而,保证仅仅一次地将结果分发给外部系统是非常有挑战性的。例如,一旦数据发送到接收器,很难保证结果的正确性,因为接收器可能不能提供给事务来还原之前写入的结果。

至多一次

任务失败时最简单的做法就是不去恢复丢失的状态,再重放丢失的事件。“至多一次”是保证每个事件至多处理一次。也就是说,事件可以被简单丢弃,而并没有机制来确保结果的正确性。这种类型的保证也被称为“无保证”,因为即使是丢弃所有事件的系统,也可以实现这种保证。没有任何保证听起来像是一个糟糕的主意,但有可能还好,如果能够接受近似结果,那么你最关心的就是提供尽可能低的延迟。

至少一次

在大多数实际应用中,最基本的要求是不丢失事件。这种类型的保证被称为“至少一次”,意思是所有的事件一定会被处理,虽然有些可能会被处理不止一次。如果应用程序的正确性依赖于信息的完整性,重复的处理也是可以接受的。例如,确定一个输入流中是否发生了特定的事件,可以通过至少一次的保证来正确实现。在最坏的情况下,将不止一次处理事件。在至少一次的保证的情况下,计算输入流中特定事件的发生次数可能会返回错误的结果。为了确保至少一次的结果的正确性,需要有机制来重放事件,要么是从源头,要么是从一些缓存中。一种方式是将所有事件写入持久化存储,以至于任务失败时可以重放。另一种实现同等功能的方式是使用记录确认。这种方法在缓存中存储所有的事件,直到它的处理被管道中的所有任务确认为止,此时便可以丢弃这些事件。

仅仅一次

这是最严格也是最有挑战性的一种保证。仅仅一次的结果保证意味着不仅没有事件会丢失,而且内部状态的更新会作用于每个事件仅仅一次。本质上说,仅仅一次的保证意味着我们的应用程序可以提供正确的结果,就像从来没有发生过故障一样。

提供仅仅一次的保证要求至少一次的保证,因此需要一种数据重放机制。另外,流处理器需要确保内部状态的一致性。也就是说,在失败恢复之后应该知道事件更新是否已经反映在状态上。事务更新也是一种实现方式,然而可能导致大量的性能开销。相反,Flink使用轻量级的快照机制来实现仅仅一次的结果保证。我们将在第三章讨论Flink的容错算法。

端到端的仅仅一次

以上的保证类型仅仅指的是流处理器组件。在实际的流处理架构中,常常见到多个连接的组件。最简单的场景下,也会至少有一个源和一个接收器。端到端的保证指的是跨整个数据处理管道的结果正确性。要想达到端到端的保证,需要考虑到管道的所有组件。每个组件都会提供自己的保证,而整个管道的端到端的保证其实是其中最弱的保证。重要在于,你有时可以通过弱保证达到强语义。常见的案例是任务执行的幂等操作,如最大值或者最小值。这种情况下,你可以通过至少一次保证来达到仅仅一次的语义。

总结

截止到目前,我们学习了数据流处理的基本概念和思想。了解了数据流编程模型,以及流计算程序如何表示为一个分布式的数据流图。紧接着,你了解了并行处理无限数据流的要求,并且认识到了流处理中延迟和吞吐的重要性。学到了基本的流操作,以及如何使用窗口对无界的输入数据计算有价值的结果。你对流处理中时间的概念曾感到困惑,并比较了事件时间和处理时间的概念。最后,你知道了为什么状态在流处理程序中很重要,并且该如何用它来应付失败以及保证正确的结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值