多读书多实践,勤思考善领悟

Flink概念

本文于1823天之前发表,文中内容可能已经过时。

一、数据流编程模型

抽象层次

Flink提供不同级别的抽象来开发流/批处理应用程序。

  • 最低级抽象只提供有状态流。它 通过Process Function(过程函数)嵌入到DataStream API(流式传输)中。它允许用户自由处理来自一个或多个流的事件,并使用一致的容错状态。此外,用户可以注册事件时间和处理时间回调,允许程序实现复杂的计算。

  • 实际上,大多数应用程序不需要上述低级抽象,而是针对Core API编程, 如DataStream API(有界/无界流)和DataSet API(有界数据集)。这些流畅的API提供了用于数据处理的通用构建块,例如各种形式的用户指定的转换,连接,聚合,窗口,状态等。在这些API中处理的数据类型在相应的编程语言中表示为类。

    低级Process FunctionDataStream API集成,因此只能对某些 算子操作进行低级抽象。该数据集API提供的有限数据集的其他原语,如循环/迭代。

  • Table API是为中心的声明性DSL ,其可被动态地改变的表(表示流时)。该Table API遵循(扩展)关系模型:表有一个模式连接(类似于在关系数据库中的表)和API提供可比的 算子操作,如选择,项目,连接,分组依据,聚合等 Table API程序以声明方式定义应该执行的逻辑 算子操作,而不是准确指定 算子操作代码的外观。虽然 Table API可以通过各种类型的用户定义函数进行扩展,但它的表现力不如Core API,但使用更简洁(编写的代码更少)。此外, Table API程序还会通过优化程序,在执行之前应用优化规则。

    可以在表和DataStream / DataSet之间无缝转换,允许程序混合 Table API以及DataStreamDataSet API。

  • Flink提供的最高级抽象是SQL。这种抽象在语义和表达方面类似于 Table API,但是将程序表示为SQL查询表达式。在SQL抽象与 Table API紧密地相互作用,和SQL查询可以通过定义表来执行 Table API

程序和数据流

Flink程序的基本构建块是转换。(请注意,Flink的DataSet API中使用的DataSet也是内部流 - 稍后会详细介绍。)从概念上讲,是(可能永无止境的)数据记录流,而转换是将一个或多个流作为一个或多个流的 算子操作。输入,并产生一个或多个输出流。

执行时,Flink程序映射到流数据流,由和转换 算子组成。每个数据流都以一个或多个开头,并以一个或多个接收器结束。数据流类似于任意有向无环图 (DAG)。尽管通过迭代结构允许特殊形式的循环 ,但为了简单起见,我们将在大多数情况下对此进行掩饰。

DataStream程序及其数据流。

通常,程序中的转换与数据流中的 算子之间存在一对一的对应关系。但是,有时一个转换可能包含多个转换 算子。

源流和接收器记录在流连接器和批处理连接器文档中。DataStream 算子和DataSet转换中记录了转换。

并行数据流

Flink中的程序本质上是并行和分布式的。在执行期间,具有一个或多个流分区,并且每个 算子具有一个或多个 算子子任务。 算子子任务彼此独立,并且可以在不同的线程中执行,并且可能在不同的机器或容器上执行。

算子子任务的数量是该特定 算子的并行度。流的并行性始终是其生成 算子的并行性。同一程序的不同 算子可能具有不同的并行级别。

流可以以一对一(或转发)模式或以重新分发模式在两个算子之间传输数据:

  • 一对一流(例如,在上图中的Sourcemap() 算子之间)保存数据元的分区和排序。这意味着map() 算子的subtask [1] 将以与Source 算子的subtask [1]生成的顺序相同的顺序看到相同的数据元。
  • 重新分配流(在上面的map()keyBy / window之间,以及 keyBy / windowSink之间)重新分配流。每个 算子子任务将数据发送到不同的目标子任务,具体取决于所选的转换。实例是 keyBy() (其通过散列Keys重新分区),广播() ,或Rebalance () (其重新分区随机地)。在重新分配交换中,数据元之间的排序仅保存在每对发送和接收子任务中(例如,map()的子任务[1] 和子任务[2]keyBy / window)。因此,在此示例中,保存了每个Keys内的排序,但并行性确实引入了关于不同Keys的聚合结果到达接收器的顺序的非确定性。

窗口

聚合事件(例如,计数,总和)在流上的工作方式与批处理方式不同。例如,不可能计算流中的所有数据元,因为流通常是无限的(无界)。相反,流上的聚合(计数,总和等)由窗口限定,例如“在最后5分钟内计数”“最后100个数据元的总和”

Windows可以是时间驱动的(例如:每30秒)或数据驱动(例如:每100个数据元)。一个典型地区分不同类型的窗口,例如翻滚窗口(没有重叠), 滑动窗口(具有重叠)和会话窗口(由不活动的间隙打断)。

时间

当在流程序中引用时间(例如定义窗口)时,可以参考不同的时间概念:

  • 事件时间是创建事件的时间。它通常由事件中的时间戳描述,例如由生产传感器或生产服务附加。Flink通过时间戳分配器访问事件时间戳。
  • 摄取时间是事件在源算子处输入Flink数据流的时间。
  • 处理时间是执行基于时间的 算子操作的每个算子的本地时间。

有状态的 算子操作

虽然数据流中的许多 算子操作只是一次查看一个单独的事件(例如事件解析器),但某些 算子操作会记住多个事件(例如窗口算子)的信息。这些 算子操作称为有状态

状态 算子操作的状态保持在可以被认为是嵌入式键/值存储的状态中。状态被分区并严格地与有状态算子读取的流一起分发。因此,只有在keyBy()函数之后才能在被Key化的数据流上访问键/值状态,并且限制为与当前事件的键相关联的值。对齐流和状态的Keys可确保所有状态更新都是本地 算子操作,从而保证一致性而无需事务开销。此对齐还允许Flink重新分配状态并透明地调整流分区。

容错检查点

Flink使用流重放检查点的组合实现容错。检查点与每个输入流中的特定点以及每个算子的对应状态相关。通过恢复 算子的状态并从检查点重放事件,可以从检查点恢复流数据流,同时保持一致性(恰好一次处理语义)

检查点间隔是在执行期间用恢复时间(需要重放的事件的数量)来折衷容错开销的手段。

流处理批处理

Flink执行批处理程序作为流程序的特殊情况,其中流是有界的(有限数量的数据元)。一个数据集在内部视为数据流。因此,上述概念以相同的方式应用于批处理程序,并且它们适用于流程序,除了少数例外:

  • 批处理程序的容错不使用检查点。通过完全重放流来恢复。这是可能的,因为输入有限。这会使成本更多地用于恢复,但使常规处理更便宜,因为它避免了检查点。
  • DataSet API中的有状态 算子操作使用简化的内存/核外数据结构,而不是键/值索引。
  • DataSet API引入了特殊的同步(超级步骤)迭代,这些迭代只能在有界流上进行。

二、分布式运行时环境

任务和算子链

对于分布式执行,Flink算子子任务一起放入任务。每个任务由一个线程执行。将算子链接到任务中是一项有用的优化:它可以Reduce线程到线程切换和缓冲的开销,并在降低延迟的同时提高整体吞吐量。可以配置链接行为;

下图中的示例数据流由五个子任务执行,因此具有五个并行线程。

TaskManager,JobManager,客户端

Flink运行时包含两种类型的进程:

  • JobManagers(也称为Masters )协调分布式执行。他们安排任务,协调检查点,协调故障恢复等。

    总是至少有一个Job Manager。高可用性设置将具有多个JobManagers,其中一个始终是Leader,其他人处于待机状态

  • TaskManagers(也叫工人)执行任务(或者更具体地说,子任务)的数据流,以及缓冲器和交换数据

    必须始终至少有一个TaskManager。

JobManagers和TaskManagers可以通过多种方式启动:作为独立集群直接在计算机上,在容器中,或由YARN或Mesos等资源框架管理。TaskManagers连接到JobManagers,宣布自己可用,并被分配工作。

客户端是不运行时和程序执行的一部分,而是被用来准备和发送的数据流的JobManager。之后,客户端可以断开连接或保持连接以接收进度报告。客户端既可以作为触发执行的Java / Scala程序的一部分运行,也可以在命令行进程中运行./bin/flink run ...

任务槽和资源

每个worker(TaskManager)都是一个JVM进程,可以在不同的线程中执行一个或多个子任务。为了控制工人接受的任务数量,工人有所谓的任务槽(至少一个)。

每个任务槽代表TaskManager的固定资源子集。例如,具有三个插槽的TaskManager将其1/3的托管内存专用于每个插槽。切换资源意味着子任务不会与来自其他作业的子任务竞争托管内存,而是具有一定数量的保存托管内存。请注意,此处不会发生CPU隔离; 当前插槽只分离任务的托管内存。

通过调整任务槽的数量,用户可以定义子任务如何相互隔离。每个TaskManager有一个插槽意味着每个任务组在一个单独的JVM中运行(例如,可以在一个单独的容器中启动)。拥有多个插槽意味着更多子任务共享同一个JVM。同一JVM中的任务共享TCP连接(通过多路复用)和心跳消息。它们还可以共享数据集和数据结构,从而Reduce每任务开销。

默认情况下,Flink允许子任务共享插槽,即使它们是不同任务的子任务,只要它们来自同一个作业。结果是一个槽可以保存作业的整个管道。允许此插槽共享有两个主要好处:

  • Flink集群需要与作业中使用的最高并行度一样多的任务槽。无需计算程序总共包含多少任务(具有不同的并行性)。
  • 更容易获得更好的资源利用率。如果没有插槽共享,非密集 源/ map()子任务将阻止与资源密集型窗口子任务一样多的资源。通过插槽共享,将示例中的基本并行性从2增加到6可以充分利用时隙资源,同时确保繁重的子任务在TaskManagers之间公平分配。

API还包括可用于防止不期望的时隙共享的资源组机制。

根据经验,一个很好的默认任务槽数就是CPU核心数。使用超线程,每个插槽然后需要2个或更多硬件线程上下文。

状态后台

存储键/值索引的确切数据结构取决于所选的状态后台。一个状态后台将数据存储在内存中的哈希映射中,另一个状态后台使用RocksDB作为键/值存储。除了定义保存状态的数据结构之外,状态后台还实现逻辑以获取键/值状态的时间点SNAPSHOT,并将该SNAPSHOT存储为检查点的一部分。

保存点

用Data Stream API编写的程序可以从保存点恢复执行。保存点允许更新程序和Flink群集,而不会丢失任何状态。

保存点是手动触发的检查点,它会获取程序的SNAPSHOT并将其写入状态后台。他们依靠常规的检查点机制。在执行期间,程序会定期在工作节点上创建SNAPSHOT并生成检查点。对于恢复,仅需要最后完成的检查点,并且一旦新的检查点完成,就可以安全地丢弃旧的检查点。

保存点与这些定期检查点类似,不同之处在于它们由用户触发,并且在较新的检查点完成时不会自动过期。可以从命令行或通过REST API取消作业时创建保存点。