系统架构

Flink是一个用于状态化并行流处理的分布式系统。它的搭建涉及多个进程,这些进程通常会分布在多台机器上。分布式系统需要应对的常见挑战包括分配和管理集群计算资源,进程协调,持久且高可用的数据存储及故障恢复等。

Flink并没有依靠自身实现所有上述功能,而是在已有集群基础设施和服务之上专注于它的核心功能–分布式数据流处理。Flink和很多集群管理器(如Apache Mesos、YARN及Kubernetes)都能很好地集成;同时它也可以通过配置,作为独立集群来运行。Flink没有提供分布式持久化存储,而是利用了现有的分布式文件系统(如HDFS)或对象存储(如S3)。它依赖Apache Zookeeper来完成高可用性设置中的领导选举工作。

搭建Flink所需组件

Flink的搭建需要四个不同组件,它们相互协作,共同执行流式应用。这些组件是:JobManagerResourceManagerTaskManagerDispatcher。Flink本身是用Java和Scala实现的,因此所有组件都基于Java虚拟机(JVM)运行。它们各自的职责如下:

  • 作为主进程(master process),JobManager控制着单个应用程序的执行。换句话说,每个应用都由一个不同的JobManager掌控。JobManager可以接收需要执行的应用,该应用会包含一个所谓的JobGraph,即逻辑Dataflow图,以及一个打包了全部所需类、库以及其他资源的JAR文件。JobManager将JobGraph转化成名为ExecutionGraph的物理Dataflow图,该图包含了那些可以并行执行的任务。JobManager从ResourceManager申请执行任务的必要资源(TaskManager处理槽)。一旦它收到了足够数量的TaskManager处理槽(slot),就会将ExecutionGraph中的任务分发给TaskManager来执行。在执行过程中,JobManager还要负责所有需要集中协调的操作,如创建检查点。
  • 针对不同的环境和资源提供者(resource provider)(如YARN、Mesos、Kubernetes或独立部署),Flink提供了不同的ResourceManager。ResouceManager负责管理Flink的处理资源单元–TaskManager处理槽。当JobManager申请TaskManager处理槽时,ResourceManager会指示一个拥有空闲处理槽的TaskManager将其处理槽提供给JobManager。如果ResourceManager的处理槽数无法满足JobManager的请求,则ResourceManager可以和资源提供者通信,让它们提供额外容器来启动更多TaskManager进程。同时,ResourceManager还负责终止空闲的TaskManager以释放计算资源。
  • TaskManager是Flink的工作进程(worker process)。通常在Flink搭建过程中要启动多个TaskManager。每个TaskManager提供一定数量的处理槽。处理槽的数目限制了一个TaskManager可执行的任务数。TaskManager在启动后,会向ResourceManager注册它的处理槽。当接收到ResourceManager的指示时,TaskManager会向JobManager提供一个或多个处理槽。之后,JobManager就可以向处理槽中分配任务来执行。在执行期间,运行同一应用不同任务的TaskManager之间会产生数据交换。(Note that multiple operators may execute in a task slot.)后面会进一步讨论任务执行和处理槽的概念。
  • Dispatcher会跨多个作业运行,它提供了一个REST接口来让我们提交需要执行的应用。一旦某个应用提交执行,Dispatcher会启动一个JobManager并将应用转交给它。Dispatcher同时还会启动一个WebUI,用来提供有关作业执行的信息。

本书是基于Flink 1.7版本写的,最新的稳定版1.12官网文档如下:

JobManager
The JobManager has a number of responsibilities related to
coordinating the distributed execution of Flink Applications: it
decides when to schedule the next task (or set of tasks), reacts to
finished tasks or execution failures, coordinates checkpoints, and
coordinates recovery on failures, among others. This process consists
of three different components:

  • ResourceManager
    The ResourceManager is responsible for resource de-/allocation and
    provisioning in a Flink cluster — it manages task slots, which are the
    unit of resource scheduling in a Flink cluster (see TaskManagers).
    Flink implements multiple ResourceManagers for different environments
    and resource providers such as YARN, Mesos, Kubernetes and standalone
    deployments. In a standalone setup, the ResourceManager can only
    distribute the slots of available TaskManagers and cannot start new
    TaskManagers on its own.
  • Dispatcher
    The Dispatcher provides a REST interface to submit Flink applications
    for execution and starts a new JobMaster for each submitted job. It
    also runs the Flink WebUI to provide information about job executions.
  • JobMaster
    A JobMaster is responsible for managing the execution of a single
    JobGraph. Multiple jobs can run simultaneously in a Flink cluster,
    each having its own JobMaster.

hadoop flink 单机 hdfs flink_hadoop flink 单机

应用部署

Flink应用可以通过两种模式进行部署。

  • 框架模式
    在该模式下, Flink应用会打包成一个JAR文件,通过客户端提交到运行的服务上。这里的服务可以是Flink Dispatcher,Flink JobManager或是YARN的ResourceManager。无论哪种情况,运行的服务都会接收Flink应用并确保其执行。如果应用提交到JobManager,会立即开始执行;如果应用提交到Dispatcher或YARN ResourceManager,它们会启动一个JobManager并将应用转交给它,随后由JobManager负责执行该应用。
  • 库模式
    在该模式下,Flink应用会绑定到一个特定应用的容器镜像(如Docker镜像)中。镜像中还包含着运行JobManager以及ResorceManager的代码。当容器从镜像启动后会自动加载ResourceManager和JobManager,并将绑定的作业提交执行。另一个和作业无关的镜像负责部署TaskManager容器。容器通过镜像启动后会自动运行TaskManager,后者可以连接ResourceManager并注册处理槽。通常情况下,外部资源管理框架(如Kubernates)负责启动镜像,并确保在发送故障时容器能够重启。

基于框架的模式采用的是传统方式,即通过客户端提交应用(或查询)到正在运行的服务上;而在库模式中,Flink不是作为服务,而是以库的形式绑定到应用所在的容器镜像中。后者常用于微服务架构。我们会在第10章中详细讨论。

任务执行

一个TaskManager允许同时执行多个任务。这些任务可以属于同一个算子(数据并行),也可以是不同算子(任务并行),甚至还可以来自不同的应用(作业并行)。TaskManager通过提供固定数量的处理槽来控制可以并行执行的任务数。
任务执行详细过程见P51

高可用性设置

流式应用通常都会设计成7*24小时运行,因此对于它很重要的一点是:即便内部进程发送故障时也不能终止运行。想要从故障中恢复,系统首先要重启故障进程,随后需要重启应用并恢复其状态。

TaskManager故障

如前所述,为了执行应用的全部任务,Flink需要足够数量的处理槽。应用的重启策略决定了JobManager以何种频率重启应用以及重启尝试之间的等待间隔。(第10章会详细讨论)

JobManager故障

和TaskManager相比,JobManager发生故障会更为棘手。它用于控制流式应用执行以及保存该过程中的元数据(如已完成检查点的存储路径)。如果负责管理的JobManager进程消失,流式应用将无法继续处理数据。这就导致JobManager成为Flink应用中的一个单点失效组件。为了解决该问题,Flink提供了高可用模式,支持在原JobManager消失的情况下将作业的管理职责及元数据迁移到另一个JobManager。

Flink中的高可用模式是基于能够提供分布式协调和共识服务的Apache Zookeeper来完成的。它在Flink中主要用于“领导”选举以及持久且高可用的数据存储。JobManager在高可用模式下工作时,会将JobGraph以及全部所需的元数据(例如应用的JAR文件)写入一个远程持久化存储系统中。此外,JobManager还会将存储位置的路径地址写入Zookeeper的数据存储。在应用执行过程中,JobManager会接收每个任务检查点的状态句柄(存储位置)。在检查点即将完成的时候,如果所有任务已经将各自状态成功写入远程存储,JobManager就会将状态句柄写入远程存储,并将远程位置的路径地址写入Zookeeper。因此所有用于JobManager故障恢复的数据都在远程存储上面,而Zookeeper持有这些存储位置的路径。

当JobManager发生故障时,其下应用的所有任务都会自动取消。新接手工作的JobManager会执行一下步骤:

  1. 向Zookeeper请求存储位置,以获取JobGraph、JAR文件及应用最新检查点在远程存储的状态句柄。
  2. 向ResourceManager申请处理槽来继续执行应用。
  3. 重启应用并利用最近一次检查点重置任务状态

如果是在容器环境(如Kubernetes)中以库模式部署运行应用,容器编排服务(orchestration service)通常会自动重启故障的JobManager或TaskManager容器。当运行在YARN或Mesos上面时,Flink的其余进程会触发JobManager或TaskManager进程重启。而独立集群模式下则没有重启故障进程的工具,因此有必要运行一些后备JobManager及TaskManager来接管故障进程的工作。

Flink中的数据传输

在运行过程中,应用的任务会持续进行数据交换。TaskManager负责将数据从发送任务传输至接收任务。它的网络传输模块在记录传输前会先将它们收集到缓冲区中。换言之,记录并非逐个发送的,而是在缓冲区中以批次形式发送。该技术是有效利用网络资源、实现高吞吐的基础。它的机制类似于网络以及磁盘I/O协议中的缓冲技术。(请注意,将记录放入缓冲区并不意味着Flink的处理模型是基于微批次的。)

每个TaskManager都有一个用于收发数据的网络缓冲池(每个缓冲默认32KB大小)。如果发送端和接收端的任务运行在不同的TaskManager进程中,它们就要用到操作系统的网络栈进行通信。流式应用需要以流水线方式交换数据,因此每对TaskManager之间都要维护一个或多个永久的TCP连接来执行数据交换。在Shuffle连接模式下,每个发送端任务都需要向任意一个接收任务传输数据。对于每一个接收任务,TaskManager都要提供一个专用的网络缓冲区,用于接收其他任务发来的数据。

当发送任务和接收任务处于同一个TaskManager进程时,发送任务会将要发送的记录序列化到一个字节缓冲区中,一旦该缓冲区占满就会被放到一个队列里。接收任务会从这个队列里获取缓冲区并将其中的记录反序列化。这意味着同一个TaskManager内不同任务之间的数据传输不会涉及网络通信

Flink采用多种技术来降低任务之间的通信开销。

基于信用值的流量控制

通过网络连接逐条发送记录不但低效,还会导致很多额外开销。若想充分利用网络连接带宽,就需要对数据进行缓冲。在流处理环境下,缓冲的一个明显缺点是会增加延迟,因为记录首先要收集到缓冲区中而不会立即发送。

Flink实现了一个基于信用值的流量控制机制,工作原理如下:接收任务会给发送任务授予一定的信用值,其实就是保留一些用来接收它数据的网络缓冲。一旦发送端收到信用通知,就会在信用值所限定的范围内尽可能多地传输缓冲数据,并会附带上积压量(已经填满准备传输的网络缓冲数目)大小。接收端使用保留的缓冲来处理收到的数据,同时依据各发送端的积压量信息来计算所有相连的发送端在下一轮信用的优先级

由于发送端可以在接收端有足够资源时立即传输数据,所以基于信用值的流量控制可以有效降低延迟。此外,信用值的授予是根据各发送端的数据积压量来完成的,因此该机制还能在出现数据倾斜(data skew)时有效地分配网络资源。不难看出,基于信用值的流量控制是Flink实现高吞吐低延迟的重要一环。

任务链接

Flink采用一种名为任务链接的优化技术来降低某些情况下的本地通信开销。任务链接的前提条件是,多个算子必须有相同的并行度且通过本地转发通道(local forward channel)相连。(将多个算子合并为一个算子,通过方法调用进行数据传输

Flink在默认情况下会开启任务链接。虽然任务链接可以有效地降低本地任务之间的通信开销,但有的流水线应用反而不希望用到它。在第10章的“控制任务链接”中,再详细介绍。

事件时间处理

处理时间:基于处理器的本地时间,结果无法重现,不一致。
事件时间:可重现且一致,但需要一些额外的配置。

时间戳

在事件时间模式下,Flink流式应用处理的所有记录都必须包含时间戳。时间戳将记录和特定时间点进行关联,这些时间点通常是记录所对应事件的发生时间。但实际上应用可以自由选择时间戳的含义,只要保证流记录的时间戳会随着数据流的前进大致递增即可。基本上所有现实应用场景都会出现一定程度的时间戳乱序。

当Flink以事件时间模式处理数据流时,会根据记录的时间戳触发时间相关算子的计算。例如,时间窗口算子会根据记录关联的时间戳将其分配到窗口中。Flink内部采用8字节的Long值对时间戳进行编码,并将它们以元数据(metadata)的形式附加在记录上。内置算子会将这个Long值解析为毫秒精度的Unix时间戳(自1970-01-01-00:00:00.000以来的毫秒数)。但自定义算子可以有自己的时间戳解析机制,如将精度调整为微秒。

水位线

除了记录的时间戳,Flink基于事件时间的应用还必须提供水位线(watermark)。水位线用于在事件时间应用中推断每个任务当前的事件时间。基于时间的算子会使用这个时间来触发计算并推动进度前进。

在Flink中,水位线是利用一些包含Long值时间戳的特殊记录来实现的。水位线像带有额外时间戳的常规记录一样在数据流中移动。

水位线拥有两个基本属性:

  1. 必须单调递增。这是为了确保任务中的事件时间时钟正确前进,不会倒退。
  2. 和记录的时间戳存在联系。一个时间戳为T的水位线表示,接下来的所有记录的时间戳一定都大于T。

第二个属性可用来处理数据流中时间戳乱序的记录。对基于时间的算子任务而言,其收集和处理的记录可能会包含乱序的时间戳。这些算子只有当自己的事件时钟(由接收的水位线驱动)指示不必再等那些包含相关时间戳的记录时,才会最终触发计算。当任务收到一个违反水位线属性,即时间戳小于或等于前一个水位线的记录时,该记录本应参与的计算可能已经完成。我们称此类记录为迟到记录(late record)。为了处理迟到记录,Flink提供了不同的机制,后续在第6章中讨论。

水位线的意义之一在于它允许应用控制结果的完整性和延迟。如果水位线和记录的时间戳非常接近,那结果的处理延迟就会很低,因为任务无须等待过多记录就可以触发最终计算。但同时结果的完整性可能会受到影响,因为可能有部分相关记录被视为迟到记录,没能参与运算。相反,非常“保守”的水位线会增加处理延迟,但同时结果的完整性也会有所提升。

水位线传播和事件时间

下面讨论算子对水位线的处理方式。Flink内部将水位线实现为特殊的记录,它们可以通过算子任务进行接收和发送。任务内部的时间服务(time service)会维护一些计时器(timer),它们依靠接收到水位线来激活。这些计时器是由任务在时间服务内注册,并在将来的某个时间点执行计算。

当任务接收到一个水位线时会执行以下操作:

  1. 基于水位线记录的时间戳更新内部事件时间时钟
  2. 任务的时间服务会找出所有触发时间小于更新后事件时间的计时器。对于每个到期的计时器,调用回调函数,利用它来执行计算或发出记录。
  3. 任务根据更新后的事件时间将水位线发出

Flink对通过DataStream API访问时间戳和水位线有一定限制。普通函数无法读写记录的时间戳或水位线,但一系列处理函数(process function)除外。所有函数的API都无法支持设置发出记录的时间戳、调整任务的事件时间时钟或发出水位线。为发出记录配置时间戳的工作需要由基于时间的DataStream算子任务来完成,这样才能确保时间戳和发出的水位线对齐。举例而言,时间窗口算子任务会在发送触发窗口计算的水位线时间戳之前,将所有经过窗口计算所得结果的时间戳设为窗口的结束时间

任务在收到一个新的水位线之后,如何发送水位线和更新其内部事件时钟?

一个任务会为它的每个输入分区都维护一个分区水位线(partition watermark)。当收到某个分区传来的水位线后,任务会以接收值和当前值中较大的那个去更新对应分区水位线的值。随后,任务会把事件时间时钟调整为所有分区水位线中最小的那个值。如果事件时间时钟向前推动,任务会先处理因此而触发的所有计时器,之后才会把对应的水位线发往所有连接的输出分区,以实现事件时间到全部下游任务的广播。

对于那些有着两条或多条输入数据流的算子,如Union或CoFlatMap,它们的任务同样是利用全部分区水位线中的最小值来计算事件时间时钟,并没有考虑分区是否来自不同的输入流。这就导致所有输入的记录都必须基于同一个事件时间时钟来处理。如果不同输入流的事件时间没有对齐,那么该行为就会导致一些问题。

时间戳分配和水位线生成

时间戳和水位线通常都是在数据流刚刚进入流处理应用的时候分配和生成的。 由于不同的应用会选择不同的时间戳,而水位线依赖于时间戳和数据流本身的特征,所以应用必须显式地分配时间戳和生成水位线。Flink DataStream应用可以通过三种方式完成该工作:

  1. 在数据源完成:我们可以利用SourceFunction在应用读入数据流的时候分配时间戳和生成水位线。源函数会发出一条记录流。每个发出的记录都可以附加一个时间戳,水位线可以作为特殊记录在任何时间点发出。如果源函数(临时性地)不再发出水位线,可以把自己声明成空闲。Flink会在后续算子计算水位线的时候把那些来自于空闲源函数的流分区排除在外。第8章“实现自定义数据源函数”再详谈。
  2. 周期分配器(periodic assigner):DataStream API提供了一个名为AssignerWihPeriodWatermarks的用户自定义函数,它可以用来从每条记录提取时间戳,并周期性地响应获取当前水位线的查询请求。提取出来的时间戳会附加到各自的记录上,查询得到的水位线会注入到数据流中。该函数在第6章“分配时间戳和水位线”中介绍。
  3. 定点分配器(punctuated assigner):另一个支持从记录中提取时间戳的用户自定义函数叫做AssignerWithPunctuatedWatermarks。它可用于需要根据特殊输入记录生成水位线的情况。和AssignerWihPeriodWatermarks函数不同,这个函数不会强制你从每条记录中都提取一个时间戳(虽然这样也行)。同样在第6章介绍。

用户自定义的时间戳分配函数通常都会尽可能地靠近数据源算子,因为在经过其他算子处理后,记录顺序和它们的时间戳会变得难以推断。这也是为什么不建议在流式应用中途覆盖已有的时间戳和水位线(虽然这可以通过用户自定义函数实现)。

状态管理

在第2章我们指出,大部分的流式应用都是有状态的。很多算子都会不断地读取并更新某些状态,例如:窗口内收集的记录,输入源的读取位置。

通常意义上,函数里所有需要任务去维护并同来计算结果的数据都属于任务的状态。你可以把状态想象成任务的业务逻辑所需要访问的本地或实例变量。

算子状态

键值分区状态

状态后端

有状态算子的扩缩容

检查点、保存点及状态恢复

一致性检查点

从一致性检查点中恢复

Flink检查点算法

检查点对性能的影响

保存点

保存点的使用

从保存点启动应用