数据流编程模型
原文链接
博主理解篇
抽象层次
Flink提供不同级别的抽象来开发流/批处理应用程序。
- 这个最低级别的抽象提供了有状态的流式操作。它是通过处理函数嵌入到DataStream API。它允许用户自由的处理一个或者多个数据流中的事件,并且使用一致,容错的状态。此外,用户可以注册事件时间和处理时间回调,允许程序实现复杂的计算。
- 实际上,大多数应用不需要上面描述的低级别抽象,而是针对Core APIs(核心API),例如: DataStream API(有边界和无边界的数据流) 和 DataSet API(有边界的数据集)。这些流畅的API提供通用数据处理,像用户指定的各种形式的transformations(转换),joins(连接),aggregations(聚合),windows(窗口化操作),state(状态)等等。这些API表示在各自的编程语言中为类(class)中的数据类型进行处理。
低阶的处理函数集成了DataStream API,这样就可以针对特性的操作使用低层级的抽象。DataSet API 为有边界的 data sets提供了附加的原语,例如循环/迭代。
- Table API 是一种以表为中心的声明式的DSL,它可能会被动态的改变(当处理数据流的时候)。Table API 遵循扩展模型:Table 有一个附加模式(类似于关系型数据库表)并且API提供了类似的操作,例如:select,project,join,group-by,aggregate等等。Table API声明式的定义了逻辑操作应该怎么做 而不是确切的指定操作的代码看起来如何。尽管table API可以通过多种形式的用户自定义函数来扩展,它的表现还是不如Core APIs,但是用起来更加的简洁(写更少的代码)。此外,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所使用的DataSets内部也是流-更多内容将在以后解释)。从概念上讲流(可能没有结束)是一个数据流记录,而转换是一个操作,它取一种或者多个流作为输入,并产生一个或者多个输出流作为结果。
当执行的时候,Flink程序映射到streaming dataflows(流数据流),由streams和转换operators组成。每一个数据流开始于一个或者多个source,并且终止于一个或者多个sink。数据流类似于任意的有向无环图(DAGS)。虽然通过迭代构造允许特定形式的环,但是大多数情况下,简单起见,我们都不考虑这一点。
通常情况下,程序中的转换与数据流中的操作是一一对应的。有时,然而,一个转换可能有多个转换操作构成。
source和sink的文档在streaming connectors和 batch connectors 。Transformation的文档在DataStream operators和DataSet transformation。并行数据流
Flink程序本质上是并行的和分布式的。在执行过程中,一个流(stream)包含一个或多个流分区 (stream partition),而每一个operator包含一个或多个operator子任务 。操作子任务之间彼此独立,在不同的线程中执行,甚至有可能运行在不同的机器或容器上。
operator子任务的数量即是此特定operator的并行度 。一个流的并行度即其生产operator的并行度。相同程序中的不同的operator可能有不同级别的并行度。
流在两个operator之间传输数据,可以通过一对一(或称 forwarding )模式,或者通过redistributing模式: - 一对一流(例如上图中Source与map() opreator之间)保持了元素的分区与排序。那意味着 map() operator的子任务[1]将以与 Source 的子任务[1]生成顺序相同的顺序查看到相同的元素。
- Redistributing 流(如上图中 map() 与 keyBy/window 之间,以及 keyBy/window 与 Sink 之间)则改变了流的分区。每一个operator子任务根据所选择的转换,向不同的目标子任务发送数据。比如 keyBy() (根据key的哈希值重新分区), broadcast() ,或者 rebalance() (随机重分区)。在一次 redistributing 交换中,元素间的排序只保留在每对发送与接受子任务中(比如, map() 的子任务[1]与 keyBy/window 的子任务[2])。因此在这个例子中,每个键的顺序被保留下来,但是并行确实引入了不确定性–对于不同键的聚合结果到达sink的顺序。
配置和并行度的详细配置可以查看这个文档parallel execution。
窗口(Window)
聚合事件(比如计数、求和)在流上的工作方式与批处理不同。比如,对流中的所有元素进行计数是不可能的,因为通常流是无限的(无边界的)。相反,流上的聚合需要由窗口来划定范围,比如 “计算过去的5分钟” ,或者 “最后100个元素的和” 。
窗口可以是事件驱动的 (比如:每30秒)或者数据驱动的 (比如:每100个元素)。窗口通常被区分为不同的类型,比如滚动窗口 (没有重叠), 滑动窗口 (有重叠),以及会话窗口(由不活动的间隙所打断)
更多的窗口例子可以查看这个博客。更多的明细可以查看窗口文档window docs。
时间(Time)
当提到流程序(例如定义窗口)中的时间时,你可以参考不同的时间概念:
- 事件时间是事件创建的时间。它通常由事件中的时间戳描述,例如附接在生产传感器,或者生产服务。Flink通过时间戳分配器访问事件时间戳。
- 摄入时间是事件进入Flink数据流源算子的时间。
- 处理时间 是每一个执行时间操作的operator的本地时间。
操作时间的更多详细信息请查看文档event time docs。
有状态的操作
尽管数据流中的很多操作一次只查看一个独立的事件(比如事件解析器),有些操作却会记录多个事件间的信息(比如窗口算子)。 这些操作被称为有状态的 。
有状态操作的状态保存在一个可被视作嵌入式键/值存储的部分中。状态由有状态operator读取的流一起被严格地分区与分布。因此,只能访问一个 keyBy() 函数之后的 keyed streams 的键/值状态,并且仅限于与当前事件键相关联的值。调整流和状态的键确保了所有状态更新都是本地操作,以在没有事务开销的情况下确保一致性。这种对齐还使得Flink可以透明地重新分配状态与调整流的分区。
查看更多信息,请查看此文档有关state的内容。
容错检查点
Flink使用流重放与检查点的结合实现了容错。检查点与每个输入流的特定点及与相关的每一个operator的状态相关。一个数据流可以从一个检查点恢复出来,其中通过恢复operator状态并从检查点重放事件以保持一致性 (一次处理语义)
检查点间隔是以恢复时间(需要重放的事件数量)来消除执行过程中容错的开销的一种手段。
容错内部的描述提供了更多关于flink管理检查点和相关的话题。启用和配置检查点的详细信息请查看这个文档checkpointing API docs。
流式批处理
Flink将批处理程序作为流处理程序的特殊情况来执行,只是流是有界的(有限个元素)。 DataSet 内部被视为数据流。上述适用于流处理程序的概念同样适用于批处理程序,除了一些例外:
- 批处理程序的容错不再使用检查点。而是通过完全地重放流来恢复。因为输入是有界的,因此这是可行的。这种方法使得恢复的成本增加,但是由于避免了检查点,因而使得正常处理的开销更小。
- DataSet API中的有状态操作使用简化的in-memory/out-of-core数据结构,而不是键/值索引。
- DataSet API引入了特殊的同步(superstep-base)迭代,而这种迭代仅仅能在有界流上执行。细节可以查看迭代文档。
分布式运行时
原文链接
任务和Operator链
对于分布式运行,Flink将operator子任务链接在一起放入任务池。每个任务由一个线程执行。将operator链接到任务池中是一项有用的优化:它减少线程到线程的切换和缓冲的开销,并在降低延迟的同时提高整体吞吐量。可以配置链接行为,有关详细信息,请查阅链接文档。
下图中的示例数据流由五个子任务执行,因此有五个并行线程。
作业管理器,任务管理器,客户端
Flink运行时有两种类型的进程组成:
- 作业管理器(JobManagers,也称为主节点master)负责协调分布式运行时。它们调度任务,协调检查点,协调失败恢复,等。
至少有一个作业管理器节点,高可用的环境有多个作业管理器,其中一个节点是leader角色,其他节点是standby角色。 - 任务管理器(TaskManagers,也称为工作节点worker)执行数据流的任务(更特定一些,子任务),缓冲以及转换数据流。
同样至少有一个任务管理器节点。
作业管理器与任务管理器可以以多种方式启动:以standalone集群的方式直接在主机上启动,或者被资源管理器YARN或Mesos管理启动。任务管理器连接到作业管理器上,声明它们自己是可用状态并且可被分配任务。
客户端不是程序运行时的一部分,但是经常用来准备以及发送数据流程序到作业管理器上。此后,客户端可以断开连接,或者保持连接结束进程报告。客户端可以以Java/Scala程序的方式触发执行,或者在命令行"./bin/flink"中运行。
Task Slot和资源
每个Worker节点(任务管理器)是一个JVM进程,在分开的线程中可以执行一个或多个子任务。一个Worker通过控制task slots(至少一个)来控制节点接受多少任务。
每个task slot代表任务管理器固定大小的资源子集。例如:一个拥有3个slot的任务管理器,将会分配它管理的1/3内存到每个slot。对资源进行分槽(slot)意味着子任务不会与其他作业的子任务竞争管理的内存,而是具有一定数量的保留管理内存。注意此处没有CPU隔离发生,现在只能分隔任务的管理内存。
通过调整task slot数目,用户可以定义子任务之间如何隔离。每个任务管理器拥有一个slot意味着任务组运行在隔离的JVM(例如,可以在隔离的容器上启动)上。拥有多个slots意味着更多的子任务共享相同的JVM。在相同的JVM上的任务共享TCP连接(通过多路复用)和心跳信息。它们还可以共享数据集和数据结构,从而减少每个任务的开销。
默认情况下,Flink允许子任务共享slot,即使它们是不同任务的子任务,只要它们来自同一个作业。结果就是一个slot拥有这个作业的所有管道操作(pipeline)。允许这种slot共享有两个主要的好处: - Flink集群需要与作业中使用的最高并行度同样的task slots。无需计算程序总共包含多少任务(在不同的并行度之上)。
- 更容易获得更好的资源利用率。没有共享slot的情况下,非密集型的source与map()子任务将会阻塞与资源密集型的窗口子任务同样多的资源。通过共享slot,将并行度从2增加到6可以充分利用the slotted(时隙)资源,同时确保繁重的子任务在任务管理器上公平的分配。
APIs同样还包括用于防止不期望的slot共享的资源组机制。
根据经验,一个很好的默认任务槽(task slot)数就是CPU核心数。使用超线程,每个slot需要2个或更多硬件线程上下文。
状态后端(State Backends)
存储键/值对索引的确切数据结构取决于所选的状态后端。一个状态后端将数据存储在内存中的哈希映射中,另一个状态后端使用[RocksDB]()作为键/值对存储。除了定义保存状态的数据结构之外,状态后端还实现逻辑以获取键/值对状态的时间点快照,并将该快照存储为检查点的一部分。
保存点Savepoints
用Data Stream API编写的程序可以从保存点恢复执行。保存点允许更新程序和Flink集群,而不会丢失任何状态。
保存点是手动触发的检查点,它会获取程序的快照并将其写入状态后端。他们依靠常规的检查点机制。在执行期间,程序会定期在工作节点上创建快照并生成检查点。对于恢复,仅仅需要最后完成的检查点,因此一旦新的检查点完成,就可以安全地丢弃旧的检查点。
保存点与这些定期检查点类似,不同之处在于它们由用户触发,并且在较新的检查点完成时不会自动过期。可以从命令行或通过REST API取消作业时创建保存点。
原创文章,作者:3628473679,如若转载,请注明出处:https://blog.ytso.com/196002.html