Apache Flink 的体系架构 (三)
写在前面:
大家好,我是强哥,一个热爱分享的技术狂。目前已有 12 年大数据与 AI 相关项目经验, 10 年推荐系统研究及实践经验。平时喜欢读书、暴走和写作。
业余时间专注于输出大数据、AI 等相关文章,目前已经输出了 40 万字的推荐系统系列精品文章,强哥的畅销书「构建企业级推荐系统:算法、工程实现与案例分析」已经出版,需要提升可以私信我呀。如果这些文章能够帮助你快速入门,实现职场升职加薪,我将不胜欢喜。
想要获得更多免费学习资料或内推信息,一定要看到文章最后喔。
内推信息
如果你正在看相关的招聘信息,请加我微信:liuq4360,我这里有很多内推资源等着你,欢迎投递简历。
免费学习资料
如果你想获得更多免费的学习资料,请关注同名公众号【数据与智能】,输入“资料”即可!
学习交流群
如果你想找到组织,和大家一起学习成长,交流经验,也可以加入我们的学习成长群。群里有老司机带你飞,另有小哥哥、小姐姐等你来勾搭!加小姐姐微信:epsila,她会带你入群。
第二章介绍了分布式流处理的重要概念,比如并行化、时间和状态。在本章中,我们将对 Flink 的体系架构做更高层次的介绍,并阐述 Flink 是如何处理我们之前讨论过的关于流处理方面的问题的。我们将展示 Flink 的体系架构以及了解 Flink 是如何处理流应用程序中的时间和状态,并讨论其容错机制。本章提供了使用 Apache Flink 成功实现和操作高级流处理应用程序的相关背景信息,它将有助于你理解 Flink 的内部原理以及高性能的秘密。
系统架构
Flink 是一个用于有状态并行数据流处理的分布式计算引擎,一个 Flink 基本架构通常由分布在不同机器的多个进程组成,对于分布式计算系统来说,最大的挑战就是解决集群计算资源的分配和管理、协调进程、数据持久化以及故障恢复这些核心问题。
Flink 本身并没有实现以上所有提到的功能,恰恰相反,它专注于其核心的功能--分布式数据流处理,通过利用现有的集群基础设施和服务来发挥作用。Flink 支持很多种资源管理器,比如 Apache Mesos、YARN 和 Kubernetes,能够非常友好地进行集成,同时 Flink 也支持基于 standalone 模式部署,自己管理资源,无需借助其他资源组件,但是这种方式的资源管理效率相对来说不高。Flink 作为分布式流式计算引擎,本身不提供数据持久化,但是他利用了分布式文件系统实现数据持久化,常见的优秀分布式文件系统有 HDFS 和 S3,如果需要实现 Flink 的高可用,那就需要依赖于 Apache Zookeeper 进行 leader 的选举。
在本节中,我们将重点介绍 Flink 的架构组成,以及组件之间是如何进行通信和执行任务的,不仅如此,我们还会讨论 Flink 两种不同的部署模式,了解基于不同的部署模式下,应用程序是如何分配的以及任务执行的过程是怎样的,最后,我们还会解释 Flink 的高可用模式是如何运行的。
Flink 架构组件
Flink 通常由四大组件构成,每个组件有不同的功能,共同管理流应用程序的运行,四大组件分别是:JobManager、ResourceManager、TaskManager 和 Dispatcher。由于 Apache Flink 是基于 Java 和 Scala 开发实现的,因此所有的组件都是在 Java 虚拟机上运行的,接下来我们针对每个组件的功能以及组件之间是如何进行交互的进行讨论。
作业管理器(JobManager):是单个应用程序的主线程,每个应用程序有单独的 JobManager 控制,JobManager 会接收要执行的应用程序,一般来说,一个应用程序包含了一个 JobGraph、一个逻辑数据流图(参见第二章)和一个 JAR 文件这三个要素,JAR 文件打包了程序运行所必需的类、库以及其他资源。JobManager 将 JobGraph 转换为 ExecutionGraph,即物理数据流图,其中包含了可以并行执行的任务,JobManager 向 ResourceManager 请求执行任务所需要的资源(slot),一旦它接收到足够的 TaskManager slots,就会将 ExecutionGraph 中的任务分配给执行它们的 TaskManager,同时在任务执行期间,JobManager 负责所有需要中心协调的操作,比如 checkpoint、savepoint 和 state recovery 等。
资源管理器(ResourceManager):Flink 提供了多种资源管理器,常见的有 YARN、Mesos、Kubernetes 和 standalone 模式,资源管理器负责管理 TaskManager 的 slots,即 Flink 的计算资源基本单元,当 JobManager 申请 TaskManager slots 时,ResourceManager 会将空闲的 TaskManager slot 分配给 JobManager,如果 ResourceManager 没有足够的资源来满足 JobManager 的请求时,资源管理器还可以向资源提供方申请容器用来启动 TaskManager 进程,除此之外,资源管理器还负责终止空闲的 TaskManager 进程,从而释放计算资源。
任务管理器(TaskManager):Flink 的工作进程,通常情况下,一个 Flink 集群下会运行多个 TaskManager 进程,每个 TaskManager 会提供一定量的 slots,slots 的数量决定了 TaskManager 能够运行多少个计算任务,TaskManager 启动之后,就会向 ResourceManager 注册它的 slots,当收到 ResourceManager 的指令时,TaskManager 会向 JobManager 提供一个或多个 slot 资源,然后 JobManager 就可以将 slot 分配到任务中去执行了,在执行任务的过程中,同一个应用程序中不同的 TaskManager 进程可以进行数据交换。关于任务的执行流程以及 slot 的概念留到后面的内容再做讨论。
分发器(Dispatcher):用于跨作业执行任务 ,并且提供了 REST 接口去提交应用程序,一旦应用程序被提交执行,它会启动一个 JobManager 去管理提交的应用程序,REST 接口使 dispatcher 能够作为集群(位于防火墙之后)的 HTTP 入口对外提供服务。此外,Dispatcher 也会启动一个 WEB UI 面板,用来展示作业的执行信息,在 Flink 的架构中,Dispatcher 不是必需的,取决于应用提交的方式,关于这点,我们会在“应用部署”这一小节进行讨论。
图 3-1 展示了当应用程序提交执行后,Fink 组件之间是如何进行交互的。
注意,图 3-1 只是大致描述了 Flink 各个组件的职责以及组件之间交互的情况。根据部署环境的不同,比如采用不同的资源管理器,如基于 YARN、Mesos、Kubernetes 或者 standalone 部署,在执行步骤上会存在差别,基于这些差别组件或许可以在相同的 JVM 进程中运行。例如,在 standalone 集群中,集群是没有资源管理器的,ResourceManager 只能分发可用的 TaskManager 的 slots,而无法单独启动新的 TaskManager。在“部署模式”的章节中,我们将讨论在不同环境下部署和配置 Flink。
应用部署
Flink 应用程序有两种部署方式。
框架模式(Framework):在这种模式下,Flink 应用程序会被打包到一个 JAR 文件中,并且由客户端提交到正在运行的服务中,该服务可以是一个 Flink Dispatcher,Flink JobManager 或者 YARN 资源管理器等等,在任何情况下,必须要有这样的一个正在运行的服务,确保能够接收应用程序并且执行。如果一个应用程序被提交到 JobManager,它会立即启动并开始执行应用程序,如果这个应用程序是提交到 Dispatcher 或者 YARN 资源管理器,那么它会启动一个 JobManager,并将应用程序移交给 JobManager 处理,然后 JobManager 就会开始执行该应用程序。
库模式(Library):在这种模式下,Flink 应用程序会绑定在指定应用程序的容器镜像中,比如 Docker 镜像,这个镜像同样包含了运行 JobManager 和 ResourceManager 的代码,当启动镜像中的容器后,它会自动启动 ResourceManager 和 JobManager,并且提交绑定的应用程序去执行,此外,还会有单独用于部署 TaskManager 从而进行独立作业的镜像,当镜像中的容器启动时,会自动启动一个 TaskManager,随后与 ResourceManager 建立连接并注册 slots 信息。通常来说,基于 Kubernetes 这样的外部资源管理器可以用于启动镜像并且确保发生故障时自动重启容器。
以上这两种部署方式,相对而言,框架模式(Framework)比较偏向于传统方法,通过客户端将应用程序提交给正在运行的服务,而库模式(Library)则完全不同,不存在 Flink 服务,相反是将 Flink 和容器镜像中的应用程序绑定在一起,这种部署模式常见于微服务架构中。我们会在“运行和管理流式应用”这一章节内容中讨论更多关于应用部署的细节。
任务执行
一个 TaskManager 可以同时执行多个任务,这些任务可以是同一个算子的子任务,也就是 Flink 中的数据并行度,或者分属不同的算子,即任务并行度,甚至可以是其他应用程序的子任务,即作业并行度。一个 TaskManager 通过提供一定数量的 slot 从而控制任务的并发数,一个 slot 可以执行应用程序的一个切片,即应用程序中的一个算子的一个并行任务,下图 3-2 展示了 TaskManager、slots、tasks 和 operators 之间的关系。
从图 3-2 左边部分中你可以看到一个由 5 个算子操作构成的作业图(JobGraph),A 和 C 是数据源(source operator),E 是数据输出端(sink operator),C 和 E 有 2 个并行度,其他的算子有 4 个并行度,因为最大的并行度是 4,所以该应用程序至少需要 4 个 slot 用于执行任务,给定两个 TaskManager,每个 TaskManager 各有两个 slot,这种情况下是可以满足应用程序需求的。JobManager 将 JobGraph 转化为 ExecutionGraph,并将任务分配到 4 个可用的 slot 上。对于有 4 个并行任务的算子,它的任务会分配到每个 slot 上。对于有 2 个并行任务的算子 C 和 E,它们的任务被分配到 slot 1.1、2.1 以及 slot 1.2、2.2。将任务调度到 slots 上,可以让多个任务跑在同一个 TaskManager 上,这样的好处是可以使得任务之间的数据交换不需要进行网络传输,显得更高效。但是会存在另一个的问题,将太多任务调度到同一个 TaskManager 上会导致 TaskManager 超负荷运行,从而降低计算的效率。在接下来的“控制任务调度”这一节中我们会讨论如何控制任务调度。
在同一个 JVM 中 TaskManager 以多线程的方式执行任务。相对进程来说,线程粒度更细,通信成本更低,更轻量级,但是线程之间并没有非常严格的将任务互相隔离。因此,单个误操作的任务可能会 kill 掉整个 TaskManager 进程,以及运行在此进程上的所有任务。通过为每个 TaskManager 配置一个的 slot,即 TaskManager 只运行一个应用程序的任务,可以实现应用程序相互隔离。通过 TaskManager 内部的多线程特性,以及在一台服务器上部署多个 TaskManager 进程这样的功能特性,Flink 在部署应用程序时提供了很大的灵活性来权衡性能和资源隔离,我们将在第 9 章详细讨论 Flink 集群的配置和设置的更多细节。
高可用配置
流处理应用程序通常设计为 24/7 运行,因此很重要的一点是,即使出现的进程失败的情况,也要确保任务的执行不会被停止。要从失败中恢复包括两个方面,系统首先需要重新启动失败的进程,其次,重新启动应用程序并恢复其状态。在本节中,我们将解释 Flink 如何重新启动失败的进程,至于如何恢复应用程序状态这一点在本章后面的“一致性 checkpoint 恢复”部分进行详细讨论。
TaskManager 进程失败
如上所述,Flink 需要足够数量的 slots 资源来执行应用程序的所有任务,如果 Flink 设置有 4 个 TaskManager,每个 TaskManager 提供两个 slot,那么流应用程序的最大并行度为 8。如果其中一个 TaskManager 运行失败,可用 slot 的数量将下降到 6 个。在这种情况下,JobManager 将要求资源管理器(ResourceManager)提供更多的 slot 资源。如果无法满足 slot 资源的要求,例如应用程序在 standalone 集群中运行,在没有获得足够可用的 slot 资源之前 JobManager 无法重新启动应用程序。应用程序的重新启动策略决定了 JobManager 重新启动应用程序的频率以及在尝试重新启动的等待时间。
JobManager 进程失败
比 TaskManager 运行失败更具挑战性的问题是 JobManager 运行失败。JobManager 是 Flink 的大脑,控制流应用程序的执行,并保存关于其执行的元数据,例如指向已完成检查点的指针。如果 JobManager 进程宕机,则流应用程序无法继续处理任务,这样的问题使得 JobManager 成为 Flink 中的单点故障。为了解决这个问题,Flink 提供了一个高可用的模式,该模式将作业的元数据迁移到另一个 JobManager,以防原来的 JobManager 宕机而引起应用程序执行失败的情形。
Flink 的高可用模式是基于 Apache ZooKeeper 实现的,Apache Zookeeper 是一个提供协调和一致性的分布式服务系统。Flink 依赖 ZooKeeper 进行 leader 选举,并作为一个高可用和持久的数据存储。在高可用性模式下,JobManager 将 JobGraph 和所有需要的元数据(如应用程序的 JAR 文件)写入远程持久存储系统。另外,JobManager 将指向存储位置的指针写入 ZooKeeper 的数据存储中。在应用程序执行期间,JobManager 接收各个任务检查点的状态句柄(存储位置)。当完成一个检查点,即所有任务都成功地将它们的状态写入远程存储时,JobManager 将状态句柄写入远程存储,并将指向此位置的指针写入 ZooKeeper。因此,从 JobManager 故障恢复所需的所有数据都存储在远程存储中,ZooKeeper 持有指向存储位置的指针。图 3-3 说明了这种设计。
当 JobManager 失败时,所有属于该应用程序的任务都会自动被取消,一个新的 JobManager 会接管失败 JobManager 的工作,执行以下步骤。
1. 向 ZooKeeper 请求存储位置,以便从远程存储获取应用程序最后一个检查点的 JobGraph、JAR 文件和状态句柄。
2. 从资源管理器(ResourceManager)请求可用的 slots 来继续执行应用程序。
3. 重新启动应用程序,并将所有任务的状态重置为最后一个完成的检查点。
当一个应用程序是以库部署的形式运行(如 Kubernetes),失败的 JobManager 或 TaskManager 容器会由容器服务自动重启。当运行在 YARN 或 Mesos 之上时,JobManager 或 TaskManager 进程会由 Flink 自动触发重启。在 standalone 模式下,Flink 并未提供为失败进程重启的工具。所以运行 standby JobManager 和 TaskManager 对于接管失败的进程是非常有必要的,我们将在后面章节中的“高可用的设置”中讨论 Flink 的高可用配置。
Flink 数据传输
正在运行的应用程序的任务不断地交换数据。TaskManager 负责将数据从发送任务发送到接收任务。TaskManager 的网络组件在数据发送之前将收集数据放到缓冲区中,也就是说记录不是一条一条进行发送的,而是以批次的形式写入缓冲区中,达到一定条件之后再进行发送。该技术是有效利用网络资源以及实现高吞吐量的基础。这样的机制类似于网络或磁盘 IO 协议中使用的缓冲技术。注意:缓冲区中的记录暗示了 Flink 的处理模型是基于微批量的。
每个 TaskManager 都有一个网络缓冲区池(默认大小为 32KB),用于发送和接收数据。如果发送任务(sender tasks)和接收任务(receiver tasks)在单独的 TaskManager 进程中运行,它们通过操作系统的网络堆栈进行通信。流式应用程序通过数据管道进行数据交换,即,每对 taskmanager 维护一个永久的 TCP 连接来交换数据。在 shuffle 连接模式下,每个发送任务需要能够向每个接收任务(receiver tasks)发送数据。TaskManager 需要为每个接收任务(receiver tasks)提供一个专用的网络缓冲区,任何任务都需要将数据发送给这个缓冲区。一旦缓冲区被填满,它就通过网络传送到接收任务(receiver tasks)。在接收端,每个接收任务(receiver tasks)对应的每个发送任务(sender tasks)都需要一个网络缓冲区。图 3-4 显示了这个架构。
正如图 3-4 所示,有 4 个发送任务(sender tasks)和 4 个接收任务(receiver task),每个发送任务至少需要 4 个网络缓冲区来接收数据,用于发送数据到接收任务,同理,每个接收任务至少需要 4 个网络缓冲区来接收发送任务推送过来的数据。TaskManager 之间的网络缓冲区在同一个网络连接中进行多路复用。为了更加顺畅地实现数据交换,TaskManager 必须能够提供足够的缓冲区为所有并行出入的连接提供服务。在 shuffle 或广播(broadcast)连接的情况下,每个发送任务(sender tasks)需要为每个接收任务(receiver tasks)提供一个缓冲区。即,所需要的缓冲区数量是所涉及算子的并行度的平方。Flink 对网络缓冲区的默认大小是 32kb,可以配置多个,默认是 2048,对于中小型的任务来说已经足够了。如果需要设置更大的配置值,可以参考“主内存和网络缓冲区”章节中的描述调整配置大小。
当发送任务和接收任务在同一个 TaskManager 进程中运行时,发送任务将输出记录序列化到一个字节缓冲区中,并在缓冲区填满后将其放入队列中。接收任务从队列中取出缓冲区并反序列化为输入的记录。因此,在同一个 TaskManager 中运行的任务之间涉及的数据传输不涉及网络通信,这样一来,数据的传输和计算效率更高。
Flink 采用不同的技术来降低任务之间的通信成本。 在下面的章节中,我们简要讨论了基于信用(credit-based)的流量控制和任务链。
基于信用(Credit-Based)的流量控制
通过网络连接发送的单条记录是比较低效的处理方式,而且还会引起大量的开销。因此需要借助缓冲(Buffering)来充分利用网络连接的带宽。在流处理上下文中,缓冲的一个缺点是增加了延迟,因为记录是先收集到缓冲区中,等到触发阈值才会进行发送,而不是立即发送,所以这个过程会存在一定的延迟。
Flink 实现了一个基于信用的流控制机制,其工作原理如下。一个接收任务授予发送任务一定的信任,即保留一定数量的网络缓冲区用于接收发送任务发送的数据。一旦发送方接收到信用通知(credit notification),它就会发送与所授缓冲区数量一致且大小相同的缓冲区——即已填充并准备发送的网络缓冲区的数量。接收方使用保留的缓冲区处理已发送的数据,并使用发送方的积压大小(backlog)来考虑其所有连接发送方确定信用(credit)优先级。
基于信用(credit-based)的流量控制减少了延迟,因为发送方可以在接收方有足够的资源接收数据时发送数据。更重要的是,在数据分布不均匀的情况下,即数据倾斜,它是分配网络资源的一种有效机制,因为信用是根据发送方的积压大小(backlog)来授予的。因此,基于信用的流量控制是 Flink 实现高吞吐量和低延迟的一个重要组成部分。
任务链
Flink 提供了一种称为任务链的优化技术,它可以在一定条件下减少本地通信的开销。为了满足任务链的要求,必须使用两个或多个具有相同并行度算子操作,通过本地转发通道进行连接。图 3-5 中显示的算子操作管道就满足了这些要求。包括了三个算子操作,同时所有算子操作的任务并行度都为 2,并通过本地转发通道进行连接。
图 3-6 描述了如何使用任务链执行管道(pipeline)。算子函数被整合到单个线程执行的任务中。一个函数产生的记录通过一个简单的方法调用被单独传递给下一个函数。因此,在函数之间传递记录基本上不需要序列化和通信成本
任务链技术可以显著降低本地任务之间的通信成本,但是在某些情况下,不使用任务链执行管道也是没问题的。例如,可以将一个长管道的链式任务断开,拆分成两个任务,进而将一个性能要求高的任务调度到不同的 slots。图 3-7 显示了在没有任务链的情况下执行的相同管道,所有函数都运行在各自独立线程中,在每个线程中对单个任务进行计算。
默认情况下 Flink 会启用任务链。在“控制任务链”中,我们展示了如何禁用应用程序的任务链,以及如何控制单个算子的任务链行为。
事件时间处理
在“时间语义”中,我们强调了时间语义对于流处理应用程序的重要性,并解释了处理时间和事件时间之间的区别。虽然处理时间很容易理解,因为它基于机器的本地时间进行处理,但是它会产生一些任意的、不一致的和无法重现的结果。相反,事件时间语义产生可重复的和一致的结果,这是大部分流处理场景所严格要求的。然而,与基于处理时间语义的应用程序相比,基于事件时间语义的应用程序需要额外的配置,Flink 默认是采用处理时间语义。此外,支持事件时间语义的流处理器的内部机制比单纯使用处理时间语义的内部机制更复杂。
Flink 为常见的事件处理操作提供了直观且易于使用的原语,同时提供了功能丰富的 api,通过实现更高级的事件时间内部底层接口可以自定义算子逻辑。对于这样的高级应用程序,充分理解 Flink 的内部时间处理对我们进一步领会 Flink 一些更高级的特性是非常有帮助,有时候也是必需的。前一章介绍了 Flink 利用两个概念来提供事件时间语义:记录时间戳和水位线。下面,我们将描述 Flink 如何在内部实现和处理时间戳和水位线,以支持基于事件时间语义的流应用程序。
时间戳
所有由 Flink 事件时间流应用程序处理的记录必须附有事件本身的时间戳。时间戳将记录与特定的时间点关联起来,通常指向的时间点表示的是事件发生的时间点。但是,应用程序可以自由选择时间戳的语义,只要流记录的时间戳大致随着流的前进而增加即可。正如在“时间语义”中所看到的,基本上在所有实际用例中都会出现一定程度的时间戳的异常,常见的是时间戳乱序。
当 Flink 基于事件时间模式处理数据流时,它是根据事件记录的时间戳基于时间算子做计算的。例如,时间窗口算子根据相关的时间戳将记录分配到对应划分好的窗口。Flink 将时间戳编码为 16 字节 Long 值,并将它们作为元数据附加到记录中。它的内置算子操作将 Long 值解释为精度为毫秒的 Unix 时间戳(从 1970-01-01-00:00:00.000 开始的毫秒数)。但是,自定义算子可以进行转换,例如,可以将精度调整为微秒。
水位线(Watermark)
除了记录时间戳外,Flink 事件时间应用程序还必须提供水位线(watermark)。在事件时间应用程序中,使用水位线获取每个任务的当前事件时间。基于时间的算子使用这个时间来触发计算并取得进展。例如,时间窗口任务完成窗口计算,并且在任务事件时间窗口的结束边界(window end time)时输出结果。
在 Flink 中,水位线作为带有一个 Long 值时间戳的特殊记录。如图 3-8 展示的是带有事件时间戳的常规事件数据流中的水位线。
水位线有两个基本的属性:
1. 它们必须是单调地增加,以确保任务的事件时间向前推进,而不是向后推进。
2. 它们与记录的时间戳相关。带有时间戳 T 的水位线表示所有后续记录都应该具有时间戳 > T。
第二个属性用于处理具有无序记录时间戳的流,例如图 3-8 中具有时间戳 3 和 5 的记录。基于时间算子的任务会收集和处理可能存在无序时间戳的记录,并在其事件时间表示不需要更多具有相关时间戳的记录时,完成计算。当一个任务接收到一条违反了水位线属性的记录,并且该记录的时间戳比之前接收到的水位线小,那么它所属的计算任务可能已经完成。这样的记录称为“延迟记录”。Flink 提供了处理延迟记录的不同方法,这些方法将在“处理延迟数据”中讨论。
水位线还有一个有趣的特性,它们允许应用程序控制结果的完整性和延迟。设置与记录的时间戳非常接近的水位线,能保证较低的处理延迟,因为任务在完成计算之前只会保留比较短暂的时间用于等待更多记录的到来,但是呢,结果的完整性可能会受到影响,因为相关的记录可能还没被进入计算窗口中,结果就已经计算处理了,这样的记录我们认为是延迟记录。与此相反的是,如果设置了比较大的水位线,则会增加处理延迟,但是可以提高结果的完整性。
水位线传播和事件时间
在本节中,我们将讨论算子操作如何处理水位线。Flink 将水位线当做特殊的记录,由算子任务接收和发出。任务本身有一个内部时间服务,用于维护计时器,并在收到水位线时激活。并且任务可以在定时器服务中注册定时器,以便在将来某个特定的时间点执行计算。例如,窗口算子操作为每个活动窗口注册一个定时器,当事件时间超过窗口的结束时间时,定时器将清除窗口的状态。
当任务收到水位线时,会执行以下操作:
1. 任务根据水位线的时间戳更新其内部事件时间。
2. 任务的时间服务用小于更新事件时间的时间标识所有定时器。对于每个过期的定时器,该任务调用一个回调函数,用于执行计算并释放结果。
3. 任务使用更新后的事件时间,赋值并释放一个水位线。
这里需要注意的是,Flink 限制通过 DataStream API 访问时间戳或水位线。函数不能读取或修改记录时间戳和水位线,但处理函数可以读取当前处理的记录的时间戳,请求当前事件时间,并注册定时器。这些函数都没有公开 API 来设置输出记录的时间戳、操作任务的事件时间或生成水位线。相反,基于时间的 DataStream 算子任务配置记录的时间戳,以确保它们与生成的水位线正确对齐。例如,timewindow 算子任务,在其使用触发窗口计算的时间戳输出水位线之前,将窗口的结束时间作为时间戳附加到窗口计算输出的所有记录上。
接下来,让我们更详细地解释任务在接收新水位线时如何生成水位线并更新其事件时间的。正如“数据并行性和任务并行性”中所看到的,Flink 将数据流分割为分区,并通过一个单独的算子任务并行处理每个分区。每个分区都是带有时间戳的记录和水位线的流。根据算子操作与前一个或后一个算子操作的连接方式,其任务可以从一个或多个输入分区接收记录和水位线,并将记录和水位线发送到一个或多个输出分区。在下面,我们将详细描述一个任务如何向多个输出任务生成水位线,以及如何使用从输入任务接收的水位线推进其事件时间。
一个任务为每个输入分区维护一个分区水位线。当它从一个分区接收到一个水位线时,它将相应的分区水位线更新为接收值和当前值这两者中的最大值,并设置为当前值。随后,任务将其事件时间更新为所有分区水位线的最小值。如果事件时间继续推进,任务将处理所有触发的定时器,最终,通过向所有连接的输出分区生成相应的水位线的方式,将其新的事件时间广播给所有下游任务。
图 3-9 显示了具有四个输入分区和三个输出分区的任务如何接收水位线、更新其分区水位线和事件时间并生成水位线。
具有两个或多个输入流(如 Union 或 CoFlatMap,请参阅“多流转换”)的算子的任务还将其事件时间计算为所有分区水位线的最小值——它们不区分不同输入流的分区水位线。因此,两个输入流的记录都基于相同的事件时间进行处理。如果应用程序的各个输入流的事件时间不一致,则此行为可能导致问题。
Flink 的水位线处理和传播算法确保算子 task 输出正确对齐的时间戳记录和水位线。然而,它依赖于这样一个事实,即所有的分区不断地提供递增的水位线。一旦一个分区没有推进它的水位线,或者变得完全空闲,没有发送任何记录或水位线,任务的事件时间将不会推进,任务的定时器也不会触发。对于依赖于前进执行计算并清理其状态的基于时间的算子操作来说,这种情况是有问题的。因此,如果任务没有定期从所有输入任务接收新水位线,则基于时间的算子操作的处理延迟和状态大小可能会显著增加。
类似的情况也出现在两个输入流的算子操作上,它们的水位线明显不同。具有两个输入流的任务的事件时间将对应于较慢流的水位线,而较快流的记录或中间结果通常处于缓冲状态,直到事件时间允许处理它们。
时间戳分配与水位线生成
到目前为止,我们已经解释了什么是时间戳和水位线,以及它们是如何在 Flink 内部处理的。然而,我们还没有讨论它们的起源。时间戳和水位线通常是在流应用程序接收流时分配和生成的。因为时间戳的选择是特定于应用程序的,而水位线依赖于时间戳和流的特征,所以应用程序必须显式地分配时间戳并生成水位线。Flink DataStream 应用程序可以通过三种方式分配时间戳和生成流水位线:
1. 在源(source)端:时间戳和水位线可以由 SourceFunction 在将流注入应用程序时分配和生成。源函数发出记录流。记录可以与相关的时间戳一起发出,而水位线可以作为特殊记录在任何时间点发出。如果源函数某段时间内不再生成任何水位线,它可以声明自己为空闲状态。Flink 将从后续算子操作的水位线计算中排除空闲源函数产生的流分区。如前所述,源的空闲机制可用于解决不推进水位线的问题。在“实现自定义源函数”一节中更详细地讨论了源函数。
2. 周期性 assigner: DataStream API 提供了一个名为 AssignerWithPeriodicWatermarks 的用户定义函数,该函数从每个记录中提取时间戳,并定期查询当前水位线。提取的时间戳被分配给相应的记录,被查询的水位线附加到流中。这个函数将在“分配时间戳和生成水位线”中讨论。
3. Punctuated assigner:AssignerWithPunctuatedWatermarks 是另一个用户定义的函数,它从每个记录中提取时间戳。它可用于生成特殊输入记录中编码的水位线。与 AssignerWithPeriodicWatermarks 函数不同,这个函数可以(但不需要)从每个记录中提取水位线。我们在“分配时间戳和生成水位线”中也详细讨论了这个函数。
用户定义的时间戳分配函数通常应用于尽可能靠近源算子的地方,因为在算子处理记录及其时间戳之后,很难对记录的顺序及其时间戳进行推断。这也是为什么在流应用程序中间覆盖现有的时间戳和水印不是一个好主意,尽管用户定义的函数可以这样做。
状态管理
在第 2 章中,我们指出大多数流应用程序是有状态的。很多算子不断地读取和更新某种状态,如窗口中收集的记录、输入源的读取位置,或自定义的、特定于应用程序的算子状态(如机器学习模型)。Flink 处理所有状态 -- 不管内置的还是用户定义的算子,都是一样的。在本节中,我们将讨论 Flink 支持的不同类型的状态。我们将解释状态如何由状态后端存储(state backends)和维护,以及如何通过重新分布状态来扩展有状态的应用程序。
通常来说,由任务维护并用于计算函数结果的所有数据都属于任务的状态。你可以将状态看做是任务的业务逻辑访问的本地变量或实例变量。图 3-10 显示了典型的任务及其状态之间的交互过程。
任务接收一些输入数据,在进行数据处理时,任务可以读取和更新数据状态,并根据输入数据和状态计算结果。举个简单的例子:一个任务连续地计算它接收了多少条记录。当任务接收到一条新记录时,它将访问该状态以获取当前计数、增加计数、更新状态并输出新的计数。
读写状态的应用程序逻辑通常比较简单明了,难度在于如何实现高效可靠的状态管理,这是比较有挑战性的难点,其中包括了处理非常大的状态时可能会出现内存溢出,要确保在发生故障时不会丢失任何状态。所有与状态一致性、故障处理及有效存储和访问相关的问题都由 Flink 处理,这样的话开发人员就可以专注于他们的应用程序的逻辑。
在 Flink 中,状态总是与特定的算子相关联。为了让 Flink 内部运行环境获得算子的状态,算子需要向 Flink 注册状态。Flink 有两种状态类型,算子状态(operator state)和键控状态(keyed state),分别应用在不同的作用域,在本文接下来的部分中我们会针对这两种状态进行讨论。
非键控状态(Operator State)
也称为 non-key state,与 key 无关的状态类型,算子状态的作用域是算子任务,这意味着由同一并行任务处理的所有记录都可以访问相同的状态,算子状态不能被使用相同或不同算子的另一个任务访问。图 3-11 显示了任务之间如何访问算子状态。
Flink 提供了三种非键控状态类型:
List state
顾名思义,就是将状态用列表的数据结构作存储,并发度在改变的时候,会将并发上的每个 List 都取出,然后把这些 List 合并到一个新的 List,然后根据元素的个数在均匀分配给新的任务。
Union list state
相比于 ListState 更加灵活,把划分的方式交给用户去做,当改变并发的时候,会将原来的 List 拼接起来。然后不做划分,直接交给用户,广泛使用在 kafka connector 中,但它与常规 List State 的不同之处在于,当程序发生故障或从保存点(savepoint)启动应用程序时,它是如何恢复的。我们将在本章后面讨论这一差异。
Broadcast state
广播状态,如大表和小表做 Join 时,小表可以直接广播给大表的分区,在每个并发上的数据都是完全一致的。做的更新也相同,当改变并发的时候,把这些数据复制到新的任务即可针对算子的每个任务的状态相同的特殊情况而设计,该属性可在检查点期间和扩展算子时使用。这两个方面都将在本章后面的章节中讨论。
键控状态(Keyed State)
表示与 key 相关联的状态类型,键控状态是根据算子输入流记录中定义的 key 来维护和访问的。Flink 为每个 key 维护一个状态实例,并使用与维护该 key 状态的算子任务相同的 key 对所有记录进行分区。当一个任务处理一条记录时,它会自动归类当前记录的 key 所要访问的状态。最终,所有具有相同 key 的记录会访问同一个状态。图 3-12 显示了任务如何与键控状态交互。
你可以将键控状态看作是一个键值映射,它跨算子的所有并行任务对键进行分区(或分片)。Flink 为键控状态提供了不同的原语,这些原语决定了为这个分布式键值映射中的每个键存储的值的类型。我们将简要讨论最常见的键控状态原语。
Value state
每个键存储任意类型的单个值。支持自定义复杂的数据架构,都可以存储为值状态。
List state
存储每个键的值列表。列表项可以是任意类型的。
Map state
为每个 key 存一个 key-value 映射。映射中的 key 和 value 可以是任意类型。
Flink 状态原语为状态提供多种不同数据结构的状态,可以进行更为高效的状态访问。我们将在“在 RuntimeContext 中声明键控状态”中进一步讨论。
状态后端(State Backends)
有状态算子的任务通常读取并更新每个传入记录的状态。由于高效的状态访问对于处理低延迟的记录至关重要,因此每个并行任务都在本地内存中维护其状态,以确保快速的状态访问。状态存储、访问和维护的确切方式由称为状态后端(state backend)的可插拔组件决定,支持三种类型,状态后端主要做两件工作:本地状态管理和远程位置的检查点状态。
对于本地状态管理,状态后端存储所有 key 状态,并确保所有访问都正确地限定到当前 key。Flink 提供状态后端,将键控状态作为对象存储在 JVM 堆的内存数据结构中。此外,状态后端序列化状态对象并将它们放入 RocksDB,后者将它们写入本地磁盘。虽然第一中方式基于内存可以实现非常快速的状态访问,但它受到内存大小的限制。第二种方式是基于本地磁盘,访问 RocksDB 状态后端存储的状态相对比较慢,但是好处就是可以支持更大的状态存储。
状态的检查点是非常重要的重要,因为 Flink 是一个分布式系统,状态仅仅是在本地维护。TaskManager 进程(以及在其上运行的所有任务)可能在任何时候出现故障,因此,它的存储是容易出现丢失的。状态后端负责将任务的状态检查点指向远程持久存储。检查点的远程存储可以是分布式文件系统(HDFS)或数据库系统(Redis)。不同的状态后端生成状态检查点的方式是不同的。例如,RocksDB 状态后端支持增量检查点,这可以显著减少非常大的状态检查点的开销。
我们将在“选择状态后端”中更详细地讨论不同的状态后端及其优缺点。
有状态算子的扩缩容
流处理应用程序的一个常见需求是,根据输入数据的速率,调整算子的并行度。对于无状态算子来说,扩展是很简单的,但是对于有状态算子却比较有挑战性,因为它们的状态需要重新分区并分配给变多或变少的并行任务。Flink 支持四种模式来扩展不同类型的状态。
对于键控状态(keyed state)的算子通过将键重新划分为更少或更多任务的方式来扩展。但是,为了提高任务之间状态传输的效率,Flink 不会重新分配单个 key。相反,Flink 在所谓的键组(key groups)中组织 key。key group 是 key 的分区,同时也是 Flink 将 key 分配给任务的方式。图 3-13 显示了如何在 key 组中重新划分键控状态。
在扩展算子列表状态(List state)的算子时,List 中的条目会重新分发进行。从概念上讲,所有并行算子任务的列表条目都被收集起来,并均匀地重新分配给更小或更大数量的任务。如果列表项比算子的调整后的并行度小,那么一些任务将以空状态开始。图 3-14 显示了算子列表状态的重新分配。
算子联合列表状态(union list state)的算子通过向每个任务广播状态的完整列表来扩展。然后,任务可以选择使用哪些条目,丢弃哪些条目。图 3-15 显示了如何重新分配算子联合列表状态。
算子广播状态(broadcast state)的算子通过将状态复制到新任务来进行扩展。这是因为广播状态能确保所有任务具有相同的状态。在缩容的情况下,多余的任务被简单地取消,因为状态已经被复制,不会丢失。图 3-16 显示了算子广播状态的重新分配。
检查点、保存点和状态恢复(Checkpoints, Savepoints, and State Recovery)
Flink 是一个分布式数据处理系统,因此,它必须处理各种异常情况,比如进程终止、机器故障和网络连接中断等故障。因为任务在本地维护它们的状态,所以 Flink 在异常的情况下不会丢失状态并且保证状态一致性。
在本节中,我们将介绍 Flink 的检查点和恢复机制,以确保精确一次(exactly-once)的状态一致性。我们还讨论了 Flink 独特的 savepoint 特性,这是一种类似“瑞士军刀”的工具,它解决了流处理应用程序的许多挑战。
一致性检查点
Flink 的恢复机制基于应用程序状态的一致性检查点。有状态流应用程序的一致性检查点是在所有任务都处理了完全相同的输入时,其每个任务的状态的副本。这可以通过查看应用程序一致性检查点的简单算法的步骤来解释。这个朴素算法的步骤是:
1. 暂停读取数据的所有输入流,不接受数据输入。
2. 等待所有处理中的数据被完全处理,这意味着所有的任务已经处理了所有的输入数据。
3. 通过将每个任务的状态复制到远程持久存储,来得到一个检查点,所有任务完成拷贝操作以后,检查点就完成了
4. 恢复读取数据的所有输入流。
这里要注意的是,Flink 并不是实现这种简单的机制。我们将在本节的后面介绍 Flink 更复杂的检查点算法。
图 3-17 显示了一个简单应用程序的一致性检查点。
应用程序有一个单一的源任务(source task),它使用一个递增的数字流——1、2、3,等等。数字流被分成偶数和奇数流。sum 算子的两个任务是计算所有偶数和奇数的和。源任务将其输入流的当前偏移量存储为状态。sum 任务将当前 sum 值保存为状态。在图 3-17 中,当输入偏移量为 5 时,Flink 采用检查点,此时两个任务的总和为 6 和 9。
从一致性检查点恢复
在流应用程序执行过程中,Flink 会为应用程序状态定期做一致性检查点。如果出现故障,Flink 使用最新的检查点一致性地恢复应用程序的状态并重新进行数据处理。图 3-18 显示了恢复过程。
Figure 3-18. Recovering an application from a checkpoint
应用程序可分三步恢复:
1. 重新启动整个应用程序。
2. 将所有有状态任务的状态重置为最新的检查点。
3. 恢复所有任务的数据处理。
这种检查点和恢复机制可以提供应用程序状态的精确一次一致性,因为所有算子都做了检查点并恢复它们的所有状态,并且所有输入流都被重置到检查点所在的位置。数据源是否可以重置其输入流取决于其实现和使用该流的外部系统或接口。例如,像 Apache Kafka 这样的事件日志系统会保留一定时间内的记录,并且带有对应的偏移量。相反,从 Socket 读取数据的流不能被重置,因为 Socket 一旦数据被消费,就会丢弃它。因此,如果所有的输入流消费的数据都来自可重置的数据源,那么应用程序就可以做到精确一次一致性。
当应用程序从检查点重新启动后,其内部状态与做检查点时的状态完全相同。然后应用程序就开始消费并且处理从检查点到发生故障之间的所有数据。尽管这意味着 Flink 会对某些消息进行两次处理(在失败之前和失败之后),但是该机制仍然能够实现精确一次状态一致性,因为所有算子的状态相当于被重置到它未见到这些数据之前的时间点。
在这里必须指出的一点是,Flink 的检查点和恢复机制只重置流应用程序的内部状态。取决于应用程序的 sink 算子的不同,在恢复期间,某些结果记录可能会多次发送到下游系统,例如事件日志、文件系统或数据库。对于某些存储系统,Flink 的 sink 函数可以提供 exactly-once 的输出,例如,在检查点完成时才提交释放的记录。另一种适用于许多存储系统的方法是幂等更新。在“应用程序一致性保证”中详细讨论了端到端的一次性应用程序所面对的挑战和解决的办法。
Flink 的检查点算法
Flink 的恢复机制基于一致性检查点。从流应用程序获取检查点的原始方法--暂停, 做检查点, 然后恢复应用程序进行数据处理,但它的理念是“停止一切(stop-the-world)”,这对于即使是中等延迟要求的应用程序而言也是不实用的。相反,基于 Chandy-Lamport 算法实现了分布式快照的检查点,该算法并不会暂停整个应用程序,而是将检查点的保存与数据处理分离,这样就可以实现在其它任务做检查点状态保存状态时,让某些任务继续进行而不受影响。接下来,我们将分解这个算法是如何工作的。
Flink 的检查点算法用到了一种称为“检查点分界线”(checkpoint barrier)的特殊数据形式。与水位线(watermark)类似,检查点分界线由 source 算子注入到常规的数据流中,它的位置是限定好的,不能超过其他数据,也不能被后面的数据超过。检查点分界线带有检查点 ID,用来标识它所属的检查点;这样,这个分界线就将一条流逻辑上分成了两部分。分界线之前到来的数据导致的状态更改,都会被包含在当前分界线所属的检查点中;而基于分界线之后的数据导致的所有更改,就会被包含在之后的检查点中。
我们使用一个简单的流处理应用程序的例子来逐步对算法进行说明。应用程序由两个源任务组成,每个源任务消费一个不递增的数字流。源任务的输出被划分为偶数和奇数流。每个分区由一个任务处理,该任务计算所有接收到的数字的和,并将更新后的总和转发给接收器(sink)。应用程序如图 3-19 所示。
JobManager 通过向每个数据源任务发送带有新检查点 ID 的消息来初始化检查点,如图 3-20 所示。
当 source 任务收到消息时,它会暂停发出新的数据,在状态后端触发本地状态的检查点,并向所有传出的流分区广播带着检查点 ID 的分界线(barriers)。状态后端在状态检查点完成后会通知任务,而任务会向 JobManager 确认检查点完成。在发出所有分界线后,source 任务就可以继续常规操作,发出新的数据了。通过将分界线注入到输出流中,源函数(source function)定义了检查点在流中所处的位置。图 3-21 显示了两个源任务生成它们的本地状态检查点并发出检查点 barriers 之后的流应用程序。
源任务发出的检查点分界线(barrier),将被传递给所连接的任务。与水位线(watermark)类似,barrier 会被广播到所有连接的并行任务,以确保每个任务从它的每个输入流中都能接收到。当任务收到一个新检查点的 barrier 时,它会等待这个检查点的所有输入分区的 barrier 到达。在等待的过程中,任务并不会闲着,而是会继续处理尚未提供 barrier 的流分区中的数据。对于那些 barrier 已经到达的分区,如果继续有新的数据到达,它们就不会被立即处理,而是先缓存起来。等待所有 barriers 到达的过程称为“分界线对齐(barrier 对齐)”,如图 3-22 所示。
图 3-22 任务等待在每个输入分区上接收一个 barrier;已经到达 barrier 的输入流记录被缓冲;所有其他记录都定期处理,一旦一个任务从它的所有输入分区接收到全部的 barriers,它就在状态后端启动一个检查点 barriers,并将这个检查点 barriers 广播给它的所有下游连接的任务,如图 3-23 所示。
一旦发出了所有检查点 barriers,任务就开始处理缓冲的记录,发出所有缓冲记录之后,任务将继续处理输入流。图 3-24 显示了此时的应用程序。
最终,检查点 barriers 到达 sink 任务。当 sink 任务接收到一个 barriers 时,它执行一个 barriers 对齐,生成自己的检查点状态,并向 JobManager 确认接收到的 barriers。JobManager 从应用程序的所有任务接收到检查点确认后,将应用程序的检查点标记为完成。图 3-25 显示了检查点算法的最后一步。如前所述,完成的检查点可用于从故障中恢复应用程序。
检查点对性能的影响
Flink 的检查点算法在不停止整个应用程序的情况下,从流应用程序生成一致性的分布式检查点。但是,它相应地会增加应用程序的处理延迟。对此 Flink 做了一些调整,可以在某些条件下减轻性能影响。
当任务生成状态检查点时,它被阻塞,并且缓冲输入。由于状态可能变得非常大,而且检查点需要通过网络将数据写入远程存储系统,所以生成检查点很容易花费几秒钟到几分钟的时间,这部分时间对于延迟敏感的应用程序来说太长了。在 Flink 的设计中,状态后端负责做检查点。如何精确地复制任务的状态取决于状态后端的实现。例如,文件系统状态后端和 RocksDB 状态后端支持异步检查点。当触发检查点时,状态后端创建状态的本地副本。当本地副本完成后,任务将继续其常规处理。后台异步线程地将本地快照复制到远程存储,并在完成检查点后通知任务。异步检查点显著减少了任务继续处理数据之前的等待时间。此外,RocksDB 状态后端还具有增量检查点功能,可以减少了传输的数据量。
另一种减少检查点算法对处理延迟影响的技术是调整 barrier 对齐步骤。对于需要非常低的延迟、并且可以容忍“至少一次”(at-least-once)状态保证的应用程序,Flink 可以将检查点算法配置为,在等待 barrier 对齐期间处理所有到达的数据,而不是把 barrier 已经到达的那些分区的数据缓存起来。一旦一个检查点的所有 barrier 都到达了,算子任务就会将状态写入检查点,当然,现在的状态中,就可能包括了一些“提前”的更改,这些更改本该由属于下一个检查点的数据到来时触发。如果发生故障,从检查点恢复时,就将再次处理这些数据:这意味着检查点现在提供的是“至少一次”(at-least-once)而不是“精确一次”(exactly-once)的一致性保证。
保存点(Savepoint)
Flink 的恢复算法基于状态检查点。检查点根据可配置的策略定期获取并自动丢弃。由于检查点的目的是确保在出现故障时可以重新启动应用程序,所以当应用程序被明确终止时,检查点将被删除。然而,除了故障恢复之外,应用程序状态的一致性快照还可以用于更多的事情。
保存点(savepoints)是 Flink 最有价值和最独特的特性之一。原则上,保存点是使用与检查点相同的算法创建的,因此基本上是带有一些附加元数据的检查点。Flink 不会自动生成保存点,所以用户(或外部调度器)必须显式地触发创建保存点,Flink 也不会自动清除保存点。第 10 章描述了如何触发和释放保存点。
使用保存点
给定一个应用程序和一个兼容的保存点,我们可以从该保存点启动应用程序。这可以将应用程序的状态初始化为保存点的状态,并从保存点处运行应用程序。虽然这种行为看起来与使用检查点从故障中恢复应用程序完全相同,但故障恢复实际上只是一种特殊情况。使用保存点可以在相同的集群上以相同的配置启动相同的应用程序。从保存点启动应用程序可以让你做更多的事情。
可以从保存点启动一个不同但兼容的应用程序。因此,你可以修复应用程序逻辑中的 bug,并重新处理尽可能多的事件,以便修复结果。修改后的应用程序还可以用于运行具有不同业务逻辑的 A/B 测试或假设场景。注意,应用程序和保存点必须是兼容的,也就是说应用程序必须能够从保存点加载状态。
可以使用不同的并行度启动相同的应用程序,对程序做扩缩容。
可以在不同的集群上启动相同的应用程序。这允许你将应用程序迁移到更新的 Flink 版本或不同的集群或数据中心。
可以使用保存点暂停应用程序,稍后再继续运行。这为更高优先级的应用程序或输入数据不连续产生时释放集群资源提供了可能性。
还可以使用版本保存点来存档应用程序的状态。
由于保存点拥有如此强大的功能,许多用户会定期创建保存点,以便应用程序可以还原到之前的某个时间点。我们看到的一种最有趣的用法是,使用保存点将应用程序是不断地将流应用程序迁移到低成本的数据中心,降本增效。
从保存点启动应用程序
前面提到的所有保存点用例都遵循相同的模式。首先,获取正在运行的应用程序的保存点,然后将其用于恢复启动应用程序中的状态。在本节中,我们将描述 Flink 如何初始化从保存点开始的应用程序的状态。
一个应用程序由多个算子组成。每个算子可以定义一个或多个键控状态和非键控状态。算子操作由一个或多个算子任务并行执行。因此,典型的应用程序由多个状态组成,这些状态分布可以分布在不同 TaskManager 进程上运行的多个算子任务上。
图 3-26 显示了一个具有三个算子的应用程序,每个算子运行两个任务。一个算子(OP-1)有一个算子状态(OS-1),另一个算子(OP-2)有两个键控状态(KS-1 和 KS-2)。在生成保存点时,将所有任务的状态复制到持久存储位置。
保存点中的状态副本由算子标识符和状态名组成。算子标识符和状态名需要能够将保存点的状态数据映射到启动应用程序的算子的状态。当从保存点启动应用程序时,Flink 将保存点数据重新分发给相应算子的任务。
注意,保存点不包含有关算子任务的信息。这是因为当应用程序以不同的并行度启动时,任务的数量可能会发生变化。在本节的前面,我们讨论了 Flink 扩展有状态算子的策略。
如果修改后的应用程序是从保存点启动的,则保存点中的状态只能在包含具有相应标识符和状态名的算子的情况下映射到应用程序。默认情况下,Flink 分配唯一的算子标识符。但是,算子的标识符是根据前面算子的标识符来确定生成的。因此,当一个算子的前一个版本算子改变时,算子的标识符也会改变,例如,当一个算子被添加或删除时。因此,具有默认算子标识符的应用程序,在不丢失状态的情况下进行更新这方面的功能支撑非常有限。因此,我们强烈建议手动为算子分配唯一标识符,不要依赖于 Flink 的默认分配。我们在“指定唯一算子标识符”中详细描述了如何分配算子标识符。
结束语
在本章中,我们讨论了 Flink 的高级体系结构及其网络堆栈的内部机制、事件处理模式、状态管理和故障恢复机制。这些内容在设计高级流应用程序、部署和配置集群、操作流应用程序以及对它们的性能进行推断时非常有用。
版权声明: 本文为 InfoQ 作者【数据与智能】的原创文章。
原文链接:【http://xie.infoq.cn/article/60eda84e3ea1e1c59c6a9c81c】。文章转载请联系作者。
评论