数据流编程模型
抽象层次
Flink提供不同级别的抽象来开发流/批处理应用程序。
抽象层次
-
stateful streaming提供最低层次的抽象,最低层次的抽象只提供有状态流。它通过Process Function嵌入到DataStream API中。它允许用户自由地处理来自一个或多个流的事件,并使用一致的容错状态。此外,用户可以注册事件时间和处理时间回调,让程序实现复杂的计算。
-
在实践中,大多数应用程序不需要上述低层抽象,而是根据核心API(如DataStream API(有界/无界流)和DataSet API(有界数据集)进行编程。这些连贯的api为数据处理提供了常见的构建块,比如各种形式的用户指定转换、连接、聚合、窗口、状态等。在这些api中处理的数据类型表示为各自编程语言中的类。
低级别抽象 Process Function与DataStream API集成,使得仅对某些操作进行低级抽象成为可能。数据集API在有界数据集上提供了额外的原语,比如循环/迭代。 -
Table API是以表为中心的声明性DSL,表可以是动态更改的表(在表示流时)。Table API遵循(扩展)关系模型:表有一个附加模式(类似于关系数据库表)和API提供了类似的操作,如select, project, join, group-by, aggregate等。表API程序以声明的方式定义逻辑操作应该做什么,而不是指定操作的代码看起来如何。尽管表API可由各种类型的用户定义函数扩展,但它的表达能力不如Core API,但使用起来更简洁(编写的代码更少)。此外,Table API程序还会通过优化程序,在执行之前应用优化规则。
Table和DataStream/DataSet之间可以无缝转换,允许程序混合Table API和DataStream和DataSet API。 -
Flink提供的最高级抽象是SQL。这种抽象在语义和表达方面类似于Table API,但是将程序表示为SQL查询表达式。在SQL抽象与table API紧密地相互作用,SQL查询可以通过定义表来执行Table API。
程序和数据流
Flink程序的基本构建块是streams和transformations。(请注意,Flink的DataSet API中使用的DataSet也是内部流 - 稍后会详细介绍。)从概念上讲,streams是(可能永无止境的)数据记录流,而transformations是将一个或多个流作为输入,并产生一个或多个输出流的一个或多个流操作。
执行时,Flink程序映射到streaming dataflows,包括流和转换运算符。每个数据流都以一个或多个sources开头,并以一个或多个sinks结束。数据流类似于任意有向无环图 (DAG)。虽然通过迭代构造允许特殊形式的循环,但为了简单起见,我们将在大多数情况下对此进行隐藏。
并行数据流
Flink中的程序本质上是并行和分布式的。 在执行期间,流具有一个或多个流分区(stream partitions),并且每个运算符具有一个或多个运算符子任务(operator subtasks)。 运算符子任务彼此独立,并且可以在不同的线程中执行,并且可能在不同的机器或容器上执行。
运算符子任务的数量是该特定运算符的并行度。 流的并行性始终是其生成运算符的并行性。 同一程序的不同运算符可能具有不同的并行级别。
流可以以一对一(或转发)模式在两个操作之间传输数据,或以重新分配模式:
- 一对一流保留元素的分区和排序。这意味着map()操作符的子任务[1]将以与Source操作符的子任务[1]生成的元素相同的顺序看到相同的元素。
- 重新分布流更改流的分区。每个操作子任务根据选择的转换将数据发送到不同的目标子任务。例如keyBy()(通过散列键重新分区)、broadcast()或rebalance()(随机重新分区)。在重分发交换中,元素之间的顺序仅保存在每一对发送和接收子任务中。
Windows
聚合事件(例如,计数,总和)在流上的工作方式与批处理方式不同。例如,不可能计算流中的所有元素,因为流通常是无限的(无界)。相反,流上的聚合(计数,总和等)由窗口限定,例如“在最后5分钟内计数”或“最后100个元素的总和”。
Windows可以是时间驱动的(例如:每30秒)或数据驱动(例如:每100个元素)。一个典型地区分不同类型的窗口,例如翻滚窗口(tumbling windows)(没有重叠), 滑动窗口(sliding windows)(具有重叠)和会话窗口(session windows)(由不活动的间隙打断)。
Time
当在流程序中引用时间(例如定义窗口)时,可以参考不同的时间概念:
-
Event Time:创建事件的时间。它通常由事件中的时间戳描述,例如由生产传感器或生产服务附加。Flink通过时间戳分配器(timestamp assigners)访问事件时间戳。
-
Ingestion Time: 事件在 source operator 处输入Flink数据流的时间。
-
Processing Time: 执行基于时间的操作的每个操作的本地时间。
有状态操作
虽然数据流中的许多操作只是一次查看一个单独的事件(例如事件解析器),但某些操作会记住多个事件(例如窗口操作符)的信息。这些操作称为有状态。
有状态操作的状态是在可以看作是嵌入式键/值存储区中维护的。状态与由有状态操作符读取的流一起严格地划分和分布。因此,只有在keyBy()函数之后的键控流上才能访问键/值状态,并且只能访问与当前事件的键关联的值。对流和状态的键进行对齐可以确保所有状态更新都是本地操作,从而保证一致性,而不会产生事务开销。这种对齐还允许Flink重新分配状态并透明地调整流分区。
容错检查点(Checkpoints for Fault Tolerance)
Flink使用流重放(stream replay)和检查点(checkpointing)的组合实现容错。检查点与每个输入流中的特定点以及每个操作符的对应状态相关。通过恢复操作符的状态并从检查点的点重放事件,可以从检查点恢复流数据流,同时保持一致性(exactly-once处理语义)。
检查点间隔是在执行期间用恢复时间(需要重放的事件的数量)来折衷容错开销的手段。
Batch on Streaming
Flink将批处理程序作为流程序的一种特殊情况执行,其中流是有界的(元素数量有限)。数据集在内部被视为数据流。因此,上述概念同样适用于批处理程序,也适用于流程序,但有少数例外:
-
批处理程序的容错不使用检查点。恢复通过完全重播流来实现。这是可能的,因为输入是有界的。这将使成本更接近于恢复,但会降低常规处理的成本,因为它可以避免检查点。
-
DataSet API中的有状态操作使用简化的 in-memory/out-of-core 数据结构,而不是键/值索引。
-
DataSet API引入了特殊的同步(superstep-based)迭代,这只在有界流上是可能的。
分布式运行环境
任务和操作符链(Tasks and Operator Chains)
对于分布式执行,Flink将操作符子任务链接到任务中,每个任务由一个线程执行。 将操作符链接到任务是一项有用的优化:它可以减少线程到线程切换和缓冲的开销,并在降低延迟的同时提高整体吞吐量。
Job Managers, Task Managers, Clients
Flink运行时包含两种类型的进程:
- JobManagers(也称为masters)协调分布式执行。安排任务,协调检查点,协调故障恢复等。
至少有一个Job Manager。高可用性设置场景具有多个JobManagers,其中一个始终是leader,其他人处于standby状态。 - TaskManagers(也叫workers)执行任务(或者更具体地说,子任务)的数据流,以及buffer和交换数据流。
必须始终至少有一个TaskManager。
JobManagers和TaskManagers可以通过多种方式启动:直接在计算机上作为独立集群,在容器中,或由YARN或Mesos等资源框架管理。TaskManagers连接到JobManagers,宣布自己可用,并被分配工作。
client不是运行时和程序执行的一部分,但用于准备数据流并将数据流发送到JobManager。 之后,客户端可以断开连接或保持连接以接收进度报告。 客户端作为触发执行的Java / Scala程序的一部分运行,或者在命令行进程中运行./bin/flink run ....
任务槽和资源(Task Slots and Resources)
每个worker(TaskManager)都是一个JVM进程,可以在不同的线程中执行一个或多个子任务。为了控制worker接受的任务数量,worker有所谓的任务槽(至少一个)。
每个任务槽代表TaskManager的固定资源子集。例如,具有三个槽的TaskManager将其1/3的内存专用于每个槽。切换资源意味着子任务不会与来自其他作业的子任务竞争内存,而是具有一定量的保留内存。请注意,这里没有CPU隔离; 当前槽只分离任务的内存。
通过调整任务槽的数量,用户可以定义子任务如何相互隔离。每个TaskManager有一个槽意味着每个任务组在一个单独的JVM中运行(例如,可以在一个单独的容器中启动)。拥有多个槽意味着更多子任务共享同一个JVM。同一JVM中的任务共享TCP连接(通过多路复用)和心跳消息。还可以共享数据集和数据结构,从而减少每任务开销。
默认情况下,Flink允许子任务共享槽,即使它们是不同任务的子任务,只要它们来自同一个Job。 结果是一个槽可以保存作业的整个管道。 允许槽共享有两个主要好处:
- Flink集群需要的任务槽与作业中使用的最高并行度一样多。不需要计算一个程序总共包含多少任务(具有不同的并行度)。
- 更容易得到更好的资源利用。如果没有槽共享,非密集型source/map()子任务将阻塞与资源密集型窗口子任务一样多的资源。使用槽共享,可以充分利用槽资源,同时确保繁重的子任务在任务管理器中得到公平分配。
状态后端(State Backends)
存储键/值索引的确切数据结构取决于所选的状态后端。一个状态后端将数据存储在内存中的哈希映射中,另一个状态后端使用RocksDB作为键/值存储。除了定义保存状态的数据结构之外,状态后端还实现逻辑以获取键/值状态的时间点快照,并将该快照存储为检查点的一部分。
保存点(Savepoints)
用Data Stream API编写的程序可以从保存点恢复执行。保存点允许更新程序和Flink群集,而不会丢失任何状态。
保存点是手动触发的检查点,它捕获程序的快照并将其写入状态后端,他们依靠常规的检查点机制。在执行期间,程序会定期在工作节点上创建快照并生成检查点。对于恢复,仅需要最后完成的检查点,并且一旦有新检查点完成,就可以安全地丢弃旧检查点。
保存点与这些定期检查点类似,不同之处在于它们由用户触发,并且在较新的检查点完成时不会自动过期。可以从命令行创建保存点,也可以通过REST API取消作业。
网友评论