[Flink][2][流处理基础]

第2章 流处理基础

本章的目标是介绍流处理的基本概念以及对其处理框架的要求。

2.1 Dataflow编程概述

2.1.1 Dataflow图

Dataflow程序通常表示为有向图

  • 其中节点称为算子,代表计算,代表数据依赖
  • 算子数据流应用程序基本功能单元。它们从输入中获取数据,对数据进行计算,然后将数据输出到输出端进行进一步处理。
  • 没有输入端的算子称为数据源没有输出端的算子称为数据汇
  • 数据流图必须至少一个数据源一个数据汇

图2-1显示了一个数据流程序,它从文章的输入流中提取并计数一些标签。

image-20201029121349342

像图2-1中的数据流图被称为逻辑图,因为它们传达了计算逻辑的高级视图。为了执行数据流程序,它的逻辑图被转换成物理Dataflow图,该图详细说明了程序是如何执行的。例如,如果我们使用分布式处理引擎,每个 算子可能有几个并行任务在不同的物理机器上运行。

图2-2显示了图2-1的逻辑图的物理数据流图。在逻辑Dataflow图中,节点代表算子,而在物理Dataflow图中,节点代表任务。每个任务负责计算一部分的输入数据。

2.1.2 数据并行和任务并行

可以以不同的方式利用数据流图中的并行性。

首先,可以对某个算子输入数据进行分区,并在数据子集并行执行相同操作的任务。这种类型的并行称为数据并行。数据并行非常有用,因为它允许将大量计算数据分布到多个不同的物理节点上并行执行。

其次,可以让不同算子任务 并行执行相同或不同数据的计算。这种类型的并行称为任务并行。使用任务并行,可以更好地利用集群的计算资源。

2.1.3 数据交换策略

数据交换策略定义了数据项如何被分配给物理Dataflow图中的不同任务。在这里,我们简要介绍一些常见的数据交换策略,如图2-3所示。

  • 转发策略发送端任务接收端任务之间一对一地进行数据传输。如果两个任务位于同一个物理机器上(这通常由任务调度器来保证),这种交换策略避免了网络通信。
  • 广播策略每个数据项发送给算子的所有并行任务。因为这种策略复制数据并涉及网络通信,所以成本相当高。
  • 基于键值的策略通过键属性划分数据,并保证具有相同键的数据项由相同的任务处理
  • 随机策略将数据项均匀随机分配给任务,以便负载均衡

2.2 并行流处理

下面看看如何将Dataflow的概念运用到并行数据流处理中。我们先给出数据流的定义:数据流是一个长度可能无限长的事件序列

数据流的例子如下:监控器产生的监控数据、传感器产生的测量数据、信用卡交易数据、气象站观测数据、搜索引擎搜索记录等

2.2.1 延迟和吞吐

对于批处理应用程序,我们通常关心作业的总执行时间,或者我们的处理引擎读取输入、执行计算和写回结果需要多长时间。由于流应用程序连续运行,并且输入可能是无限的,因此在流处理没有总执行时间的概念。取而代之的是,流处理必须尽可能地为传入数据提供结果(延迟),同时还要应对很高的事件输入速率(吞吐)。我们用延迟和吞吐来表示这两方面的性能需求。

2.2.1.1 延迟

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

在数据流中,延迟以时间为单位进行衡量,例如毫秒。根据应用程序的不同,可能会关心平均延迟最大延迟百分比延迟。例如,10ms的平均延迟意味着平均在10ms内处理事件。或者,10毫秒的95%延迟值意味着95%的事件在10毫秒内得到处理。

像Apache Flink这样的现代流处理引擎可以提供低至几毫秒的延迟。

2.2.1.2 吞吐

吞吐量是对系统处理能力的一种度量——它的处理速率。也就是说,吞吐量告诉我们系统每单位时间可以处理多少个事件

需要注意的是,处理的速率取决于事件到达速率;低吞吐量不一定表示性能差。在流式系统中,通常希望确保系统能够处理最大的预期事件到达速率。也就是说,主要关心的是确定峰值吞吐量,即系统处于最大负载时的性能限制。

一旦事件到达速率超过了预期的最大值,我们就不得不开始缓冲事件。如果系统继续以超过其处理能力的接收速率接收数据,缓冲区可能会变得不可用,数据可能会丢失。这种情况通常被称为背压

2.2.1.3 延迟与吞吐

此时,应该清楚的是,延迟吞吐 不是独立的指标

  • 如果事件需要很长时间才能在数据处理管道中传输,我们就无法轻松确保高吞吐量(延迟影响了吞吐)。
  • 同样,如果系统的处理能力过低,事件将被缓冲,必须等待才能得到处理(吞吐影响了延迟)。

降低延迟可提高吞吐量。如果一个系统可以更快地执行操作,它可以在相同的时间内执行更多的操作。而一个很好的方式就是并行处理

2.2.2 数据流上的操作

流处理引擎通常提供一组内置操作来接收、转换和输出数据流。这些操作可以用来构成Dataflow图代表流式应用的逻辑。在本节中,我们将介绍最常见的流式操作

操作可以是无状态的,也可以是有状态的。

  • 无状态操作不维护任何内部状态。也就是说,一个事件的处理 不依赖于 任何历史事件,也不保留历史数据。无状态操作很容易并行化
  • 有状态操作会维护他们以前接收到的事件的信息状态通过传入的事件更新,并且在未来事件处理逻辑中使用。有状态流处理应用程序在并行化容错方面更具挑战性
2.2.2.1 数据接入和数据输出

数据接入和数据输出操作允许流处理器与外部系统通信。

数据接入是从外部系统 获取原始数据并将其转换为适合处理格式的操作。实现数据接入逻辑算子称为数据源

数据输出是以适合外部系统使用的形式产生输出的操作。实现数据输出算子称为数据汇

2.2.2.2 转换操作

转换操作是单程操作(single-pass),每个事件独立处理。操作一个接一个处理事件,并对事件数据进行一些转换,产生一个新的输出流。一般来说,转换操作比较简单,不用维护内部状态

转换操作的算子可以接受多个输入并产生多个输出流。他们还可以通过将一个流分成多个流或将多个流合并成一个流来修改数据流图的结构。

2.2.2.3 滚动聚合

滚动聚合是针对每个输入事件不断更新聚合操作,比如总和最小值最大值聚合操作有状态的,并将当前状态传入事件相结合生成新的聚合值。图2-5显示了一个滚动最小聚合。操作符保持当前的最小值,并针对每个传入事件相应地更新它。

2.2.2.4 窗口操作

转换滚动聚合 每次处理一个事件,以生成输出事件并更新状态。但是,有些操作必须收集缓存事件。例如求中位数的函数。为了在无限流高效地计算这些操作,需要限制这些操作维护的数据量。在本节中,我们将讨论窗口操作

窗口还支持在数据流进行一些有趣的查询。例如:如果有一个为司机提供实时交通信息的应用程序。在这个场景中,您想知道在过去几分钟内某个位置是否发生了拥堵。这时候我们只关注过去几分钟这个窗口的数据。

窗口操作不断地从一个无界事件流创建 长度有限的事件集(称为),并让我们对这些 执行计算。事件通常根据数据属性或时间分配到桶中。窗口的行为一组策略定义窗口策略决定何时创建新的存储桶,将哪些事件分配给哪些存储桶,以及何时计算桶中的数据。窗口策略指定可以基于时间数量其他数据属性

下面介绍常见的窗口类型的语义

2.2.2.4.1 滚动窗口

滚动窗口事件分配长度固定不重叠的桶中。当窗口边界通过时,所有事件都被发送到一个计算函数进行处理。基于计数的滚动窗口定义了在触发评估之前收集了多少事件。图2-6显示了一个基于计数的滚动窗口,它将输入流分到四个元素组成的桶。基于时间的滚动窗口定义了桶中事件的时间间隔。图2-7显示了一个基于时间的滚动窗口,它将事件收集到桶中,并每10分钟触发一次计算。

2.2.2.4.2 滑动窗口

滑动窗口将事件分配到固定大小允许互相重叠的桶中。因此,一个事件 可能属于多个桶。我们通过指定桶的长度滑动间隔来定义滑动窗口。图2-8中的窗口长度为4,滑动间隔为3。

2.2.2.4.3 会话窗口

会话窗口在常见的现实场景中非常有用,在这些场景中,滚动窗口和滑动窗口都不能应用。考虑一个分析在线用户行为的应用程序。在这样的应用程序中,我们希望将来自同一会话事件分到一组

会话窗口根据会话间隔(session gap)对事件进行分组,会话间隔定义了认为会话已关闭的非活动时间。(也就是如果用户在很长的一段时间内没有与服务器通信就认为他的会话已经关闭了)

窗口操作与流处理中的两个主要概念密切相关:时间语义状态管理

  • 流数据通常会有延迟或者乱序到达,这时如何保证窗口正确划分就很重要
  • 此外,为了避免故障,需要在生成结果之前将窗口中的数据都采取安全措施保护起来

2.3 时间语义

2.3.2 处理时间

处理时间机器本地时钟的时间。处理时间窗口包括在一段时间内** 碰巧到达窗口所有事件,由机器的本地时钟测量。如图2-12所示

2.3.3 事件时间

事件时间是流中的事件实际发生时间。事件时间通过附加到流事件时间戳来判断。

图2-13显示:即使事件有延迟,事件时间窗口也能准确地把事件分配到正确的窗口中,从而反映事情发生的真实情况

无论数据流的处理速度有多快,事件到达算子的顺序是怎样的,事件时间窗口的计算将产生相同的结果。

通过依赖事件时间,即使是在无序数据的情况下,我们也可以保证结果的正确性。此外,当与可重放的流结合时,时间戳的确定性使你能够回到过去。也就是说,你可以重放一个流并分析历史数据,就像事件是实时发生的一样。

2.3.4 水位线

到目前为止,在我们关于事件时间窗口的讨论中,我们忽略了一个非常重要的方面:我们如何决定事件时间窗口触发时机(什么时候停止收集并开始计算)?也就是说,我们要等多久才能确定我们已经收到了某个时间点之前发生的所有事件?考虑到分布式系统的不可预测性和由外部带来的各种延迟,这些问题没有绝对正确的答案

水位线(watermark)是一种全局进度度量,它是一个时间点。它表明我们确信这个时间点之前的事件全部到达了。本质上,水位线提供了一个逻辑时钟,通知系统当前的事件时间。当操作员收到时间为T的水位线时,可以假设不会再收到时间戳小于T的事件。水位线对于事件时间窗口和处理无序事件的算子都是必不可少的。

水位线提供了结果可信度和延迟之间trade-off

  • 激进的水位线确保低延迟,但提供较低可信度
  • 保守的水位线带来高延迟,但同时带来较高可信度

流处理系统会提供某种机制处理水位线之后到达的事件。

2.3.5 处理时间与事件时间

此刻你可能会想,既然事件时间解决了我们所有的问题,为什么我们还要去关心处理时间?

事实是,在某些情况下,处理时间确实很有用。

  • 处理时间窗口引入了尽可能低的延迟
  • 当你需要定期实时报告结果时,但是不太关注结果的精度时,处理时间是更合适的。
  • 最后,处理时间窗口提供了流本身的真实情况,这对于一些用例来说可能是一个理想的属性。

2.4 状态和一致性模型

状态在数据处理中无处不在。任何复杂一点的计算都需要它。为了产生结果,函数在一段时间或多个事件上累积状态(例如,计算聚集或检测模式)。有状态算子使用传入事件内部状态计算它们的输出更新状态

在连续运行的流作业中,状态在事件之间是持久的,我们可以在编程模型中将其作为一级公民公开。而在之前的批处理中,后一个批次的数据是看不到前一个批次的数据的。

由于流操作引擎有可能处理的是无限流,因此应小心不要让内部状态无限增长。为了限制状态的大小,算子通常会对到目前为止看到的事件进行某种总结概要。这样的摘要可以是计数总和、迄今为止所看到的事件的抽样窗口缓冲区。

支持有状态算子会带来很多实现上的挑战:

  1. 状态管理:系统需要有效地管理状态,并确保它不受并发更新的影响
  2. 状态划分:并行化变得复杂,因为结果取决于状态和传入的事件。幸运的是,在许多情况下,您可以通过一个键来划分状态,并独立管理每个分区的状态。例如,比如正在处理来自一组传感器的测量流,可以用不同的分区来处理不同的传感器。
  3. 状态恢复:有状态操作符带来的第三个也是最大的挑战是确保状态可以恢复,并且即使在出现故障的情况下结果也是正确的。

2.4.1 任务故障

流式作业中的算子状态非常重要,应防止出现故障。如果状态在故障期间丢失,恢复后的结果将是不正确的。流处理引擎不仅需要保证在出现任务故障时可以正常运行,还需要保证结果和算子状态的正确性。

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

  1. 接收事件,将其存储在本地缓冲区中;
  2. 更新内部状态
  3. 产生输出记录。

在这些步骤中的任何一个都可能发生故障,系统必须清楚地定义其在每种故障场景中的如何处理。例如,一个定义完整的流式处理系统需要明确以下问题:如果任务在第一步失败,事件会丢失吗?如果在更新了内部状态后失败了,恢复后还会再更新吗?而在上面这些情况下,输出还是正确的吗?

2.4.2 结果保障

在批处理场景中,所有这些问题都得到了回答,因为批处理作业可以简单地从头开始重新启动。因此,没有事件丢失,状态完全是从零开始建立的。然而,在流处理中,这些问题很棘手。流式系统通过提供结果保障(result guarantee)来定义它们在出现故障时的行为。接下来,我们回顾了现代流处理引擎提供的几种不同级别的结果保障。

2.4.2.1 至多一次(AT-MOST-ONCE)

当任务失败时,最简单的方法就是不做任何事情来恢复丢失的状态和重放丢失的事件。至多一次只保证每个事件至多处理一次。换句话说,系统可以简单地丢弃事件,不做任何事情来确保结果的正确性。这种类型的保障也被称为“无保障”,因为即使是系统丢弃所有事件也可以提供这种保证。

2.4.2.2 至少一次(AT-LEAST-ONCE)

在大多数现实世界的应用程序中,人们期望事件不会丢失。这种类型的保证被称为至少一次这意味着所有事件都将被处理,并且其中一些事件有可能被处理多次。如果应用程序的正确性仅取决于信息的完整性,重复处理可能是可以接受的。

为了确保至少一次这种结果保障,需要有一种方法来重放(replay)事件——要么从源(source),要么从某个缓冲区(buffer)。

下面介绍两种保证至少一次的方式

  1. 持久事件日志将所有事件写入持久存储,以便在任务失败时可以重放(replay)。
  2. 另一种方法是使用记录确认。此方法将每个事件存储在缓冲区中,直到管道中的所有任务都确认这个事件已经处理过了,此时可以丢弃该事件。
2.4.2.3 精确一次(EXACTLY-ONCE)

精确一次是最严格的保证,也很难实现。它意味着不仅不会有事件丢失,而且每个事件只允许处理一次。从本质上来说,精确一次意味着我们的应用程序将提供完全正确的结果,就好像从未发生过失败一样

精确一次以至少一次为前提的,因此数据重放机制必不可少。

而且在故障恢复之后,处理引擎应该知道一个事件的更新是否已经反映在状态上。有两种实现方式:

  • 事务性更新是实现这一结果的一种方式,但是它们会导致大量的性能开销。

  • 相反,Flink使用轻量级快照机制来实现一次结果保证

2.4.2.4 端到端精确一次(END-TO-END EXACTLY-ONCE)

端到端保证指的是整个数据处理流水线上的结果正确性。流水线上的每个组件都提供自己的保证,完整管道的端到端保证将由所有组件中最弱的那个组件来决定。有时候弱的保障可能会表现出强的语义,比如,你使用至少一次来求最大值或者最小值,管道的其他组件都使用精确一次,那么这个管道也是端到端精确一次的。