参考书籍web
Stream Processing with Apache Flinkhttps://www.oreilly.com/library/view/stream-processing-with/9781491974285/算法
《基于Apache Flink的流处理》https://book.douban.com/subject/34912177/数据库
注:本文主要是针对《基于Apache Flink的流处理》的笔记apache
1-8章笔记下载地址后端
在这一章中,咱们对Flink的架构进行了一个高层次的介绍,并描述了Flink如何解决咱们以前讨论过的流处理相关问题。特别地,咱们重点解释Flink的分布式架构,展现它在流处理应用中是如何处理时间和状态的,并讨论了它的容错机制。缓存
Flink是一个用于状态化并行数据流处理的分布式系统。Flink设置由多个进程组成,这些进程一般分布在多台机器上运行。网络
分布式系统须要解决的常见挑战是数据结构
Flink自己并无实现全部这些功能。它只关注于其核心功能——分布式数据流处理,可是利用了不少现有的开源中间件和框架来实现其余非核心部分。多线程
Flink的搭建由四个不一样的组件组成,它们一块儿工做来执行流应用程序。这些组件是JobManager、ResourceManager、TaskManager和Dispatcher。因为Flink是用Java和Scala实现的,因此全部组件都运行在Java虚拟机(jvm)上。各组成部分的职责将在下面四个子小节分别介绍。架构
应用管理
JobManager是控制 单个应用程序执行的主进程,每一个应用程序由一个的JobManager控制。(一对一关系)
资源管理
Flink为 不一样的环境和资源提供者(如YARN、Mesos、Kubernetes和独立部署)提供了多个资源管理器。
工做进程,执行任务的
TaskManager是Flink的工做进程(worker process,工人)
与用户直接对话
Dispatcher提供一个REST接口让用户提交要执行的应用。
Flink应用程序能够以两种不一样的模式来部署。
在这种模式下,Flink应用程序被打包到一个JAR文件中,并由客户端提交给一个正在运行的服务。该服务能够是Flink Dispatcher、Flink JobManager或YARN的ResourceManager。
在这种模式下,Flink应用程序被绑定在一个应用程序特定的容器镜像中,好比Docker镜像。
第一种模式比较传统,第二种模式经常使用于微服务中。
TaskManager能够同时执行多个任务。
这些任务能够
TaskManager提供固定数量的处理槽来控制它可以并发执行的任务的数量。一个处理槽能够执行应用程序的某个算子的一个并行任务。下图是一个TaskManager、处理槽、任务以及算子关系的例子。
左侧是一个JobGraph(应用程序的非并行表示,逻辑图)。
它由5个算子组成。
算子A和C是数据源,算子E是数据汇。
右侧是一个ExecutionGraph(物理图)
每一个TaskManager是一个JVM,而每一个Slot是JVM中的一个线程。TaskManager在同一个JVM进程中以多线程方式执行它的任务。线程比单独的进程更轻量,通讯成本更低,但不会严格地将任务彼此隔离。所以,一个行为不正常的任务能够杀死整个TaskManager进程和运行在它上面的全部任务。
流式应用程序一般设计为24x7运行。所以,即便内部进程失败,也不能中止运行。
而要想从失败中恢复
本小节主要学习如何从新启动失败的进程。
下面举例说明TaskManager故障应该如何处理
假设咱们的应用程序要以最大并行度为8来执行,那么四个TaskManager(每一个TaskManager提供两个插槽)能够知足咱们对并行度的需求。
若是其中一个TaskManager发生故障,可用插槽的数量将减小到6个。
在这种状况下,JobManager将请求ResourceManager提供更多处理槽。
若是请求失败,JobManager会按照必定的时间间隔连续地重启应用。直到重启成功(有足够多的空闲插槽就能重启成功)。
比TaskManager失败更具挑战性的问题是JobManager失败。
JobManager控制流应用程序的执行,并保存有关其执行的元数据,例如指向已完成检查点的指针。
若是负责的JobManager进程失败,流应用程序将没法继续处理。
这使得JobManager成为Flink中的应用程序的一个单点失效组件(也就是若是这个组件失效,那么整个系统失效)。
为了克服这个问题,Flink支持一种高可用模式,该模式能够在原始JobManager失效时将应用的管理权和应用的元数据 迁移到另外一个JobManager。
Flink的高可用模式 基于 ZooKeeper
它是一个分布式系统,来提供分布式协调和共识服务。
Flink使用ZooKeeper进行领袖选举,并将其做为一个高可用性和持久的数据存储。
在高可用性模式下操做时,JobManager将JobGraph和全部必需的元数据(如应用程序的JAR文件)写入远程持久存储系统。
此外,JobManager将一个指向存储位置的指针 写入ZooKeeper的数据存储中。
在应用程序执行期间,JobManager接收各个任务检查点的状态句柄(存储位置)。当检查点完成后,JobManager将状态写入远程存储,并将指向此远程存储位置的指针写入ZooKeeper。
所以,从JobManager故障中恢复所需的全部数据都存储在远程存储中,而ZooKeeper持有指向存储位置的指针。
图3-3说明了这种设计。
当JobManager失败时,接管它工做的新JobManager执行如下步骤:
最后还有一个问题,当TaskManager或者JobManager失效时,谁会触发它们的重启?
在运行过程当中,应用的任务不断地交换数据。TaskManager 负责将数据从发送任务发送到接收任务。TaskManager的网络组件在发送记录以前在缓冲区中收集记录,就是说,记录不是一个一个发送的,而是先缓存到缓冲区中而后一批一批发送。这种技术是有效使用网络资源和实现高吞吐量的基础。
每一个TaskManager都有一个 网络缓冲池(默认大小为32 KB)用于发送和接收数据。
图3-4显示了这个架构。
当发送方任务和接收方任务在同一个TaskManager进程中运行时
经过网络链接发送单条记录很低效,而且形成很大的开销。缓冲是充分利用网络链接的带宽的关键。在流处理上下文中,缓冲的一个缺点是增长了延迟,由于记录是在缓冲区中收集的,而不是当即发送的。
Flink实现了一个基于信用值的流控制机制,其工做原理以下。
基于信用值的好处
Flink提供了一种被称为任务连接的优化技术,它能够减小特定条件下本地通讯的开销。
图3-6描述了如何在任务连接模式下执行管道。
Flink在默认状况下会开启任务连接,可是也能够经过配置关闭这个功能
正如上一节所述,事件时间语义会生成可重复且一致性的结果,这是许多流应用的刚性需求。下面,咱们将描述Flink如何在内部实现和处理事件时间戳和水位线,以支持具备事件时间语义的流应用。
Flink事件时间流应用处理的全部记录都必须带时间戳。时间戳将记录与特定的时间点关联起来,一般是记录所表示的事件发生的时间点。此外,在现实环境中,时间戳乱序几乎不可避免。
当Flink以事件时间模式处理数据流时,它会根据记录的事件时间戳来触发基于时间的算子操做。
水位线用于标注事件时间应用程序中每一个任务当前的事件时间。
在Flink中,水位线被实现为一种带时间戳的特殊记录。如图3-8所示,水位线像常规记录同样在数据流中移动。
水位线有两个基本特性:
第二个属性用于处理数据流中时间戳乱序的记录,例如图3-8中具备时间戳2和5的记录。
水位线的一个意义是,它们容许应用控制结果的完整性和延迟。
在本节中,咱们将讨论算子如何处理水位线。
当一个任务收到水位线时,会发生如下操做:
考虑到任务并行,咱们将详细介绍一个任务如何将水位线发送到多个下游任务,以及它从多个上游任务获取水位线以后如何推进事件时间时钟前进。具体的方式以下
下图举了一个有4个输入分区和3个输出分区的任务在接受到水位线以后是如何更新它的分区水位线和事件时间时钟的。
Flink的水位线传播算法确保算子任务所发出带时间戳的记录和水位线必定会对齐。
对于具备两个输入流且水位线差距很大的算子,也会出现相似的效果。具备两个输入流的任务的事件时间时钟将受制于较慢的流,一般较快的流的记录或中间结果将处于缓冲状态,直到事件时间时钟容许处理它们。
下面介绍时间戳和水位线是如何产生的。
时间戳和水位线一般是在流应用接收数据流时 分配和生成的。Flink DataStream应用能够经过三种方式完成该工做
大多数流应用是有状态的。许多算子不断读取和更新某种状态。无论是内置状态仍是用户自定义状态,Flink的处理方式都是同样的。
在本节中,咱们将讨论
一般,须要任务去维护并用于计算结果的数据都属于任务的状态。图3-10显示了任务与其状态之间的典型交互。
然而,高效可靠的状态管理更具挑战性。这包括处理很是大的状态(可能超过内存),并确保在发生故障时不会丢失任何状态。全部与状态一致性、故障处理、高效存储和访问相关的问题都由Flink处理,以便开发人员可以将重点放在应用程序的逻辑上。
在Flink中,状态老是与一个特定的算子相关联。为了让Flink的运行时知道算子有哪些状态,算子须要对其状态进行注册。根据做用域的不一样,有两种类型的状态:算子状态和键值分区状态
算子状态的做用域为算子的单个任务。这意味着由同一并行任务以内的记录均可以访问同一状态。算子状态不能被其余任务访问。以下图
Flink为算子状态提供了三类原语
键值分区状态是根据算子输入记录中定义的键来维护和访问的。Flink为每一个键维护一个状态实例,该状态实例老是位于那个处理对应键值记录的任务上。当任务处理一个记录时,它自动将状态访问范围限制到当前记录的键。所以,具备相同键值分区的全部记录都访问相同的状态。图3-12显示了任务如何与键值分区状态交互。
键值分区状态是一个在算子的全部并行任务上进行分区的分布式键值映射。键值分区状态原语以下
为了确保快速的状态访问,每一个并行任务都在本地维护其状态。至于状态的具体存储、访问和维护,则由一个称为状态后端的可拔插组件来完成。
状态后端负责两件事:
对于本地状态管理,Flink提供两种实现
状态检查点很重要,由于Flink是一个分布式系统,状态只能在本地维护。TaskManager进程可能在任什么时候间点失败。所以,它的存储必须被认为是易失的。状态后端负责将任务的状态检查点指向远程和持久存储。用于检查点的远程存储能够是分布式文件系统或数据库系统。状态后端在状态检查点的方式上有所不一样。例如,RocksDB状态后端支持增量检查点,这能够显著减小很是大的状态的检查点开销。
流应用的一个基本需求是根据输入速率的增长或减小而调整算子的并行性。有状态算子,调整并行度比较难。由于咱们须要把状态从新分组,分配到与以前数量不等的并行任务上。
带有键值分区状态的算子能够经过将键从新划分来进行任务的扩缩容。可是,为了提升效率,Flink不会以键为单位来进行划分。相反,Flink以键组做为单位来从新分配,每一个键组里面包含了多个键。
带有算子列表状态的算子在扩缩容时会对列表中的条目进行从新分配。理论上来讲,全部并行任务的列表项会被统一收集起来,并再均匀从新分配。若是列表项的数量少于算子的新并行度,一些任务将以空状态开始。图3-14显示了操做符列表状态的从新分配。
带有算子联合状态的算子会在扩缩容时把状态列表中的所有条目 广播到所有任务中。而后,任务本身来选择使用哪些项和丢弃哪些项。如图3-15显示。
带有算子广播状态的算子在扩缩容时会把状态拷贝到所有新任务上。这样作是由于广播状态要确保全部任务具备相同的状态。在缩容的状况下,直接简单地停掉多余的任务便可。如图3-16显示。
Flink是一个分布式的数据处理系统,且任务在本地维护它们的状态,Flink必须确保这种状态不会丢失,而且在发生故障时保持一致。
在本节中,咱们将介绍Flink的检查点和故障恢复机制,看一下它们是如何提供精确一次的状态一致性保障。此外,咱们还讨论了Flink独特的保存点(savepoint)功能,它就像一把瑞士军刀,解决了运行流式应用过程当中的诸多难题。
有状态流应用程序的一致检查点是在全部任务都处理完等量的原始输出后对所有任务状态进行的一个拷贝。咱们能够经过一个朴素算法来对应用创建一致性检查点的过程进行解释。朴素算法的步骤为:
下图展现了一个一致性检查点的例子,这个算法读取数据,而后对奇数和偶数分别求和
在流应用执行期间,Flink周期性为应用程序生成检查点。一旦发生故障,Flink会使用最新的检查点将应用状态恢复到某个一致性的点并重启应用。图3-18显示了恢复过程。
应用程序恢复分为三个步骤:
假设全部算子都将它们的状态写入检查点并从中恢复,而且全部输入流的消费位置都能重置到检查点生成那一刻,那么这种检查点和恢复机制能够为整个应用提供精确一次的一致性保障。输入流是否能够重置,取决于它的具体实现以及所消费外部系统是否提供相关接口。例如,像Apache Kafka这样的事件日志能够从以前的某个偏移读取记录。相反,若是是从socket消费而来则没法重置,由于socket一旦消耗了数据就会丢弃数据。
咱们必须指出,Flink的检查点和恢复机制只能重置流应用内部的状态。根据应用所采用的数据汇算子,在恢复期间,某些结果记录可能被屡次发送到下游系统,例如事件日志、文件系统或数据库。对于某些存储系统,Flink提供的数据汇能够保证了精确一次输出。
Flink基于Chandy-Lamport的分布式快照算法来实现检查点。该算法并不会暂停整个应用程序,在部分任务持久化状态的过程当中,其余任务能够继续执行。
Flink的检查点算法使用一种称为检查点分隔符的特殊类型的记录,它与水位线相似。检查点分隔符携带一个检查点ID来标识它所属的检查点,分隔符从逻辑上将流分割为两个部分。由检查点以前的记录 引发的全部状态修改都包含在分隔符对应的检查点中,而由屏障以后的记录引发的全部修改都不包含在分隔符对应的检查点中。
下面咱们经过一个简单的例子来解释这个算法
咱们使用一个简单的流应用程序示例逐步解释该算法。应用程序由两个数据源任务组成,每一个数据源任务消耗一个不断增加的数字流。数据源任务的分别输出奇数分区和偶数分区。每一个分区都由一个任务处理,该任务计算全部接收到的数字的总和,并将更新后的总和发送给下游数据汇。该应用程序如图3-19所示。
JobManager经过向每一个数据源任务 发送一个新的带有检查点编号的消息来启动检查点生成流程,如图3-20所示。
当数据源任务接收到检查点消息时,
数据源发出的检查点分隔符被广播给下游任务。当下游任务接收到新的检查点分隔符时,将继续等待来自全部其余上游任务的分隔符到达检查点。在等待期间,它继续处理那些还没有提供分隔符的上游任务的记录,而那些提供了分隔符的上游任务的记录会被缓存,等待稍后处理。等待全部检查点到达的过程称为检查点对齐,如图3-22所示。
一旦一个任务从它的全部上游任务收到分隔符,它就会让状态后端生成一个检查点,并将检查点分隔符广播给它的全部下游任务,如图3-23所示。
在发出检查点分隔符后,任务就开始处理缓冲的记录。在处理完全部缓冲记录以后,任务会继续处理其输入流。图3-24显示了此时的应用程序。
最后,检查点分隔符到达数据汇。当数据汇接收到分割符时,会先进行对齐操做,而后将自身状态写入检查点,并向JobManager确认接收到该分隔符。一旦应用的全部任务都发送了检查点确认,JobManager就会将应用程序的检查点记录为已完成。图3-25显示了检查点算法的最后一步。如前所述,已完成的检查点可用于从故障中恢复应用。
Flink的检查点算法从流应用中产生一致的分布式检查点,而不会中止整个应用。可是,它会增长应用的处理延迟。Flink实现了一些调整,能够在某些条件下减轻性能影响。
任务在将其状态写入检查点的过程当中,将被阻塞。一种好的方法是先将检查点写入本地,而后任务继续执行它的常规处理,另外一个进程负责将检查点传到远端存储。
此外,还能够在分隔符对齐的过程当中不缓存那些已经收到分隔符所对应分区的记录,而是直接处理。但这会让一致性保证从精确一次下降到至少一次
Flink最有价值和最独特的功能之一是保存点。原则上,保存点的生成算法与检查点生成算法同样,所以能够把保存点看做是带有一些额外元数据的检查点。Flink不会自动生成保存点,而是须要用户显式的调用来生成保存点。
给定一个应用和一个兼容的保存点,咱们能够从该保存点启动应用。这将把应用的状态初始化为保存点的状态,并从获取保存点的位置运行应用。
保存点能够用在不少状况
在本节中,咱们将描述Flink在从保存点启动时如何去初始化应用状态。
一个典型的应用程序包含多个状态,它们分布在不一样算子的不一样任务上。
下图显示了一个具备三个算子的应用程序,每一个算子各运行两个任务。其中一个算子(OP-1)有一个算子状态(OS-1),另外一个算子(OP-2)有两个键值分区状态(KS-1和KS-2)。当生成保存点时,全部任务的状态都会被复制到一个持久化存储位置上。
保存点中的状态副本会按照算子标识符和状态名称进行组织。该算子标识符和状态名须要可以将保存点的状态数据映射到应用启动后的状态上。当从保存点启动应用程序时,Flink将保存点数据从新分发给相应算子的任务。
若是应用发生了修改,只有那些算子标识符和状态名称没变的状态副本才能被成功还原。默认状况下,Flink会分配惟一的算子标识符。可是,算子的标识符是基于其前面算子的标识符生成的。这样,假如上游的算子标识符发生了变化,那么下游的算子也会变化。所以,咱们强烈建议为操做符手动分配惟一标识符,而不依赖于Flink的默认赋值。