第2章 流处理基础

Dataflow图

Dataflow程序描述了数据如何在不同操作之间流动。Dataflow程序通常表示为有向图。图中顶点称为算子,表示计算;而边表示数据依赖关系算子是Dataflow程序的基本功能单元,它们从输入获取数据,对其进行计算,然后产生数据并发往输出以供后续处理。没有输入端的算子称为数据源,没有输出端的算子称为数据汇。一个Dataflow图至少要有一个数据源和一个数据汇。

为了执行Dataflow程序,需要将逻辑图转化为物理Dataflow图,后者会指定程序的执行细节。在逻辑Dataflow图中,顶点代表算子;在物理Dataflow图中,顶点代表任务。
Dataflow图

数据并行和任务并行

Dataflow图的并行性可以通过多种方式加以利用。首先,你可以将输入数据分组,让同一操作的多个任务并行执行在不同数据子集上,这种并行称为数据并行(data parallelism)。数据并行非常有用,因为它能够将计算负载分配到多个节点上从而允许处理大规模的数据。再者,你可以让不同算子的任务(基于相同或不同的数据)并行计算,这种并行称为任务并行(task parallelism)。通过任务并行,可以更好地利用集群的计算资源。

数据交换策略

数据交换策略定义了如何将数据项分配给物理Dataflow图中的不同任务。这些策略可以由执行引擎根据算子的语义自动选择,也可以由Dataflow编程人员显式指定

  • 转发策略(forward strategy)在发送端任务和接收端任务之间一对一地进行数据传输。如果两端任务运行在同一物理机器上(通常由任务调度器决定),该交换策略可以避免网络通信。
  • 广播策略(broadcast strategy)会把每个数据项发往下游算子的全部并行任务。该策略会把数据复制多份且涉及网络通信,因此代价十分昂贵
  • 基于键值的策略(key-based strategy)根据某一键值属性对数据分区,并保证键值相同的数据项会交由同一任务处理。
  • 随机策略(random strategy)会将数据均匀分配至算子的所有任务,以实现计算任务的负载均衡。

并行流处理

数据流:是一个可能无限的事件序列。

延迟和吞吐

批处理应用而言,我们通常会关心作业的总执行时间,或者说处理引擎读取输入、执行计算、写回结果总共需要多长时间。而流式应用的输入可能是无限的,所以在数据处理上无总执行时间的概念。取而代之的是,流式应用需要针对到来数据尽可能快地计算结果,同时还要应对很高的事件接入速率。我们用延迟吞吐来表示这两方面的性能需求。

延迟

延迟表示处理一个事件所需的时间。本质上,它是从接收事件到在输出中观察到事件处理效果的时间间隔。

在流处理中,延迟是以时间片(例如毫秒)为单位测量的。根据应用的不同,你可能会关注平均延迟,最大延迟或延迟的百分位数值。

保证低延迟对很多流式应用(例如:诈骗识别、系统告警、网络监测,以及遵循服务级别协议(SLA)的服务)而言至关重要。低延迟是流处理的一个关键特性,他滋生出了所谓的实时应用。像flink这样的现代化流处理引擎可以提供低至几毫秒的延迟。

吞吐

吞吐是用来衡量系统处理能力(处理速率)的指标,它告诉我们系统每单位时间可以处理多少事件。吞吐的衡量方式是计算每个单位时间的事件或操作数。
但要注意,处理速率取决于数据到来速率,因此吞吐低不一定意味着性能差。首要的关注点是确定峰值吞吐,即系统满负载时的性能上限。
现实中一旦事件到达速率过高致使系统没有空闲资源,系统就会被迫开始缓冲事件。此时系统吞吐已到达极限,一味提高事件到达速率只会让延迟更糟。如果系统持续以力不能及的高速率接收数据,那么缓冲区可能会用尽,继而导致数据丢失。这种情形通常称为背压(backpressure),我们有多种可选策略来处理它。

数据流上的操作

流处理引擎通常会提供一系列内置操作来实现数据流的获取、转换,以及输出。这些算子可以组合生成Dataflow处理图,从而实现流式应用所需的逻辑。
这些操作既可以是无状态的(stateless),也可以是有状态的(stateful)的。无状态的操作不会维持内部状态,即处理事件时无需依赖已处理过的事件,也不保存历史数据。由于事件处理互不影响且与事件到来的时间无关,无状态的操作很容易并行化。此外,如果发生故障,无状态的算子可以很容易地重启,并从中断处继续工作。相反,有状态算子可能需要维护之前接收的事件信息。它们的状态会根据传入的事件更新,并用于未来事件的处理逻辑中。有状态的流处理应用在并行化和容错方面会更具挑战性,因为它们需要对状态进行高效划分,并且在出错时需进行可靠的故障恢复。

数据接入和数据输出

数据接入和数据输出操作允许流处理引擎和外部系统进行通信。数据接入操作是从外部数据源获取原始数据并将其转换成适合后续处理的格式。实现数据接入操作逻辑的算子称为数据源。

转换操作

转换操作逐个读取事件,对其应用某些转换并产生一条新的输出流。

滚动聚合

滚动聚合(如求和、求最小值和求最大值)会根据每个到来的事件持续更新结果。聚合操作都是有状态的,它们通过将新到来的事件合并到已有状态来生成更新后的聚合值。注意,为了更有效地合并事件和当前状态并生成单个结果,聚合函数必须满足可结合(associative)及可交换(commutative)的条件,否则算子就需要存储整个流的历史记录。

窗口操作

窗口操作会持续创建一些称为“桶”的有限事件集合,并允许我们基于这些有限集进行计算。事件通常会根据其时间或其他数据属性分配到不同桶中。为了准确定义窗口算子语义,我们需要决定事件如何分配到桶中以及窗口用怎样的频率产生结果。窗口的行为是由一系列策略定义的,这些窗口策略决定了什么时间创建桶,事件如何分配到桶中以及桶内数据什么时间参与计算。其中参与计算的决策会根据触发条件判定,当触发条件满足时,桶内数据会发送给一个计算函数(evaluation function),由它来对桶中的元素应用计算逻辑。策略的指定可以基于时间(例如最近5秒钟接收的事件)、数量(例如最新100个事件)或其他数据属性。

  • 滚动窗口(tumbling window) 将事件分配到长度固定且互不重叠的桶中。在窗口边界通过后,所有事件会发送给计算函数进行处理。基于数量的(count-based)滚动窗口定义了在触发计算前需要集齐多少条事件。基于时间的(time-based)滚动窗口定义了在桶中缓冲数据的时间间隔。
  • 滑动窗口(sliding window)将事件分配到大小固定且允许相互重叠的桶中,这意味着每个事件可能会同时属于多个桶。我们通过指定长度和滑动间隔来定义滑动窗口。滑动间隔决定每隔多久生成一个新的桶
  • 会话窗口(session window)假设有一个应用要在线分析用户行为,在该应用中我们要把事件按照用户的同一活动或会话来源进行分组。会话由发生在相邻时间内的一系列事件外加一段非活动时间组成。

时间语义

我们将讨论流处理引擎如何基于乱序事件产生精确结果,以及如何使用数据流进行历史事件处理并实现“时间旅行”(时间旅行式分析 time travel)。

流处理场景下一分钟的含义

当处理一个持续到达且可能无穷的事件流时,时间便成了应用中最为核心的要素。
如果我们仅考虑现实时间一分钟内收到多少数据,那结果可能会随网络连接速度或处理速度而改变。而事实上每分钟收到事件数目是由数据本身的时间来定义的。

处理时间

处理时间是当前流处理算子所在机器上的本地时钟时间。

事件时间

事件时间是数据流中事件实际发生的时间,它以附加在数据流中事件的时间戳为依据。这些时间戳通常在事件数据进入流处理管道之前就存在(例如事件的生成时间)。

事件时间将处理速度和结果内容彻底解耦。基于事件时间的操作是可预测的,其结果具有确定性

使用事件时间要克服的挑战之一是如何处理延迟事件。普遍存在的无序问题也可以借此解决。

水位线

怎样决定事件时间窗口的触发时机?换言之,我们需要等多久才能确定已经收到了所有发生在某个特定时间点之前的事件?此外,我们如何得知数据会产生延迟?

水位线是一个全局进度指标,表示我们确信不会再有延迟事件到来的某个时间点。本质上,水位线提供了一个逻辑时钟,用来通知系统当前的事件时间。当一个算子接收到时间为T的水位线,就认为不会再收到任何时间戳小于或等于T的事件了。水位线无论对于事件窗口还是处理乱序事件的算子都很关键。算子一旦收到某个水位线,就相当于接到信号:某个特定时间区间的时间戳已经到齐,可以触发窗口计算或对接收的数据进行排序了。

水位线允许我们在结果的准确性延迟之间做出取舍。激进的水位线策略保证了低延迟,但随之而来的是低可信度。反之,如果水位线过于保守,虽然可信度得以保证,但可能会无谓地增加处理延迟。

流处理系统很关键的一点是能提供某些机制来处理那些可能晚于水位线的迟到事件。根据需求的不同,可以直接忽略这些事件,或将它们写入日志或利用它们去修正之前的结果。

总结:处理时间提供很低的延迟,但它的结果依赖处理速度,具有不确定性。事件时间则与之相反,能保证结果的准确性,并允许你处理延迟甚至无序的事件。

状态和一致性模型

状态在数据处理中无处不在,任何一个稍复杂的计算都要用它。为了生成结果,函数会在一段时间或基于一定个数的事件来累积状态(例如计算聚合或检测某个模式)。有状态算子同时使用传入的事件和内部状态来计算输出。

支持有状态算子将面临很多实现上的挑战:

  • 状态管理 系统需要高效地管理状态并保证它们不受并发更新的影响。
  • 状态划分 由于结果需要同时依赖状态和到来的事件,所以状态并行化会变得异常复杂。幸运的是,在很多情况下可以把状态按照键值划分,并独立管理每一部分。
  • 状态恢复 最后一个也是最大的挑战在于,有状态算子需要保证状态可以恢复,并且即使出现故障也要确保结果正确。

任务故障

在流式作业中,算子的状态十分重要,因此需要在故障时予以保护。如果状态在故障期间丢失,那恢复后的结果就会不准确。流式作业通常会运行较长时间,因此状态可能是经过数天甚至数月才收集得到。通过重新处理所有输入来重建故障期间丢失的状态,不仅代价高,而且还很耗时。

结果保障

在本章剩余部分,当提到“结果保障”,我们指的是流处理引擎内部状态的一致性。也就是说,我们关注故障恢复后应用代码能够看到的状态值。请注意,保证应用状态的一致性和保证输出的一致性并不是一回事儿。一旦数据从数据汇中写出,除非目标系统支持事务,否则结果的正确性将难以保证。

至多一次

至多一次是一种最简单的情况,它保证每个事件至多被处理一次。换言之,事件可随意丢弃,无任何机制来保证结果的正确性。这类保障也称为“没有保障”,因为即便系统丢掉所有事件也能满足其条件。无论如何,没有保障听上去都是个不靠谱的主意。但如果你能接受近似结果并仅关注怎样降低延迟,这种保障似乎也可以接受。

至少一次

对大多数现实应用而言,用户期望是不丢事件,这类保障称为至少一次。它意味着所有事件最终都会处理,虽然有些可能会处理多次。如果正确性仅依赖信息的完整度,那重复处理或许可以接受。

为了确保至少一次结果语义的正确性,需要想办法从源头或缓冲区中重放事件。持久化事件日志会将所有事件写入永久存储,这样在任务故障时就可以重放它们。实现该功能的另一个方法是采用记录确认(record acknowledgments)。该方法会将所有事件放在缓冲区中,直到处理管道中所有任务都确认某个事件已经处理完毕才会将事件丢弃。

精确一次

精确一次是最严格的,也是最难实现的一类保障,它表示不但没有事件丢失,而且每个事件对于内部状态的更新都只有一次。本质上,精确一次保障意味着应用总会提供正确的结果,就如同故障从未发生过一般。

提供精确一次保障是以至少一次保障为前提,因此同样需要数据重放机制。此外,流处理引擎需要确保内部状态的一致性,即在故障恢复后,引擎需要知道某个事件对应的更新是否已经反映到状态上。事务性更新是实现该目标的一个方法,但它可能带来极大的性能开销。Flink采用了轻量级检查点机制来实现精确一次结果保障。

端到端的精确一次

至今为止你看到的保障类型都仅限于流处理引擎自身的应用状态。在实际流处理应用中,除了流处理引擎也至少还要有一个数据来源组件和一个数据终点组件。端到端的保障指的是在整个数据处理管道上结果都是正确的。在每个组件都提供自身的保障情况下,整个处理管道上端到端的保障会受制于保障最弱的那个组件。注意,有时候你可以通过弱保障来实现强语义。一个常见情况就是某个任务执行一些诸如求最大值或最小值的幂等操作。该情况下,你可以用至少一次保障来实现精确一次的语义。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值