Apache Flink 数据流编程模型
抽象等级(Levels of Abstraction)
Flink提供不同级别的抽象来开发流/批处理应用程序。
Statefule Stream Processing: 是最低级别(底层)的抽象,只提供有状态的流。它通过ProcessFunction嵌入到DataStream API之中。它使得用户可以自由处理来源于一个或者多个流的事件
DataStream/DataSet API: 在我们的实际工作中,大多数的应用程序是不需要上文所描述的低级别(底层)抽象,而是相对于诸如DataStream API(有界/无界流)和DataSet API(有界数据集)的Core API进行编程。这些API提供了用于数据处理的通用模块,如各种指定的transformations, joins, aggregations, windows, state等。在API中,这些处理的数据类型都是一个具体的实体类(class)。
底层的Process Function与DataStream API集成在一起,可以仅对一些操作进行底层抽象。
Table API: 是围绕着table的申明性DSL,可以被动态的改变(当其表示流时)。Table API遵循(扩展)关系模型:表有一个模式链接(类似与在关系数据库中的表),API也提供了一些类似的操作:select, project, join, group-by, aggregate等。Table API程序申明定义了怎么做是规范的,而不是明确指定应该是什么样子的。虽然Table API可以通过各种类型的用户定义的函数进行扩展,但它比Core API表达的更少,但使用起来更简洁(少写代码)。另外,Table API程序也会通过一个优化器,在执行之前应用优化规则。
可以在表和DataStream / DataSet之间进行无缝转换,允许程序混合使用Table API和DataStream 和DataSet API。
Flink提供的最高级抽象是SQL。 这种抽象在语义和表现力方面与Table API类似,但是将程序表示为SQL查询表达式。在SQL抽象与Table API紧密地相互作用,另外,SQL查询可以在Table API中定义的表上执行。
程序和数据流(Programs and Dataflows)
Flink可以说是由流(streams)和转换(transformations)为基础构建的(请注意,Flink的DataSet API中使用的数据集也是内部的流 )。从概念上讲,流是数据记录(可能是永无止境的)流,而转换是将一个或多个流作为输入,并产生一个或多个输出流。
执行时,Flink程序被映射到由流和转换运算符组成的流式数据流。每个数据从一个或多个源(sources)开始,并在一个或者多个接收器(sinks)中结束。数据流类似于一个任意有向无环图(DAG)。尽管通过迭代构造允许特殊形式的循环,但是为了简单起见,我们姑且先忽视这种情况。
程序中的转换与数据流中的操作符通常是一一对应的。然而,有时候,一个转换可能由多个转换操作符组成。
信号源(sources)和接收器(sinks)记录在流式连接器和批量连接器文档中。DataStream运算符和DataSet转换中记录了转换。
并行数据流(Parallel Dataflows)
Flink中的程序本质上是并行和分布的。在执行过程中,一个流有一个或者多个流分区,每个运算符有一个或者多个子任务。操作符子任务彼此独立,并且在不同的线程中执行,并且可能在不同的机器或容器上执行。
操作符子任务的数量是该特定操作符的并行度。流的并行性总是由生产它的操作符决定。同一个程序的不同运算符可能有不同的并行级别。
流可以以一对一(One-to-one)或者重新分配(Redistributing)的模式在两个操作符之间传输:
One-to-one: 保留了元素的分区和顺序,如上图中source —>map。这意味着map运算符的subtask[1]将按照源运算符的subtask[1]所产生顺序相同。
Redistributing: 如上图所示,map和keyBy/window之间,以及keyBy/window和Sink之间重新分配流,将会改变流的分区。每个操作符子任务根据所选的转换将数据发送到不同的目标子任务。比如 keyBy()(其通过哈希重新分区),broadcast(), or rebalance() (其随机重新分区)。在重新分配 交换中,元素之间的排序只保存在每对发送和接收的子任务中(例如map() 的subtask[1] 和 keyBy /window的subtask [2])。所以在这个例子里,每个关键字中的排序都被保留下来,但是并行性确实造成了不同关键字汇总结果后顺序的非确定性。
有关配置和控制并行的细节可以在并行执行的文档中找到。
窗口(Windows)
聚合事件(如:sum,count,etc)在流上的工作方式与批处理中的不同。例如,我们不能够去统计流中的所有元素,因为流一般是无限的(无界的)。因而,流中的一些aggregate操作,是由Windows控制的,例如:计算过去五分钟或者最后100个元素的总和。
Windows可以是由时间驱动的(例如,每30秒)或者数据驱动(例如每100个元素)。这可以用来区分不同类型的Windows,例如:tumbling windows (no overlap), sliding windows (with overlap), and session windows (punctuated by a gap of inactivity).
更多的窗口示例可以在这篇博客文章中找到。更多细节在窗口文档。
时间(Time)
当我们在流式编程中谈及时间时,可以参考不同的时间概念:
Event Time, 是事件创建的时间,通常用时间戳表示。Flink通过时间戳分配器来访问事件时间戳。
Ingestion time, 是事件进入Flink的时间,在源操作中每个记录都会获得源的当前时间作为时间戳,后续基于时间的操作(如: time window)会依赖这个时间戳
Processing Time, 是指each operator 执行程序时对应的物理机的系统时间
有关如何处理时间的更多细节,请参阅event time 文档。
有状态的操作(Stateful Operations)
尽管数据流中很多操作看起来像一个单独的事件,但是一些操作会跨越几个事件记下相关的的信息(比如像window operators)。这种操作被称为有状态的(stateful)。
这种有状态的操作,被保存在一种key/value的存储结构之中。状态与有状态操作符读取的流严格分区和分配。只有在keyed()函数之后才能访问key/value状态。并且仅限于与当前事件的键相关的值。流和状态的keys的匹配保证了所有状态更新都是本地操作,保证了一致性,所以不需要事务的开销。这种匹配还允许flink重新分配状态,并公开的调整分区。
有关更多信息,请参阅有关状态的文档。
检查点容错(Checkpoints for Fault Tolerance)
Flink使用流重播(stream replay) 和 检查点(checkpointing) 的组合来实现容错。检查点与每个输入流中的特定点以及每个操作元的相应状态有关。数据流可以从检查点恢复,同时保持一致性(exactly-once processing语义),方法是恢复operators 的状态并从检查点重放事件。
检查点间隔是在执行恢复时间(需要被重放的事件的数量)的情况下折衷的容错开销手段。
容错内部的描述提供了有关Flink如何管理检查点和相关主题的更多信息。有关启用和配置检查点的详细信息位于检查点API文档中。
批处理流(Batch on Streaming)
Flink执行批处理程序作为流程序的特殊情况,它是有限的(元素是有限的)。A*DataSet *在内部视为数据流。因此,上述概念同样适用于批处理程序,就像适用于流式处理程序一样,但有一点例外:
-
批处理程序的容错不使用检查点。通过完全重放流来恢复。这是可能的,因为输入是有限的。这将成本更多推向recovery,但是使常规地处理更便宜,因为它避免了检查点。
-
DataSet API中的有状态操作使用简化的内存/外核数据结构,而不是键/值索引。
-
DataSet API引入了特殊的同步(超级)iterations,这只能在有界的流上进行。有关详细信息,请查看iterations文档。