多级抽象
flink为开发streaming/batch 应用提供了不同层级的抽象。
- 最底层的抽象提供了有状态的流(stateful streaming)。它通过Process Function内置在DataStream API中。它允许用户自由的处理一个或多个流中的数据,并且使用统一的故障容忍的状态(state)。除此之外,用户可以注册事件时间(event time)的回调与处理时间(processing time)的回调函数,允许程序实现更精细化的操作。
- 在实践中,大多数应用不会用到上面描述的等层级的抽象api,而是使用 Core APIS,如 DataStream API(有界/无界流)与DataSet API(有界流)。链式API提供了构造数据流的常用构造块,如多种用户可指定的transformation,joins,aggregations,windows,state等。被这些api所处理的数据类型,便是使用相应编程语言写api的对应语言的类(理解:如使用java写map这个api,那么map所处理的入参,便是一个java类)。
底层的 Process Function 集成在 DataStream API中,可以使得仅在某个有需要的操作符中使用更底层的api。
DataSet API为有界数据提供了额外的基础操作,如loops/iterations。, - Tabel API是一个关于表(table)的领域特定语言(DSL),这些表可能是一个动态变化的表(如在流场景中)。Table API遵循了(也扩展了)关系型模型:每张表都有schema(和关系型数据库类似),并且API提供了类似的操作符,如:select,project,join,group-by,aggregate等。使用Table API的应用重点关注应该使用哪一个操作符,而不是关注如何实现作这个操作。尽管Table API可以使用很多的UDF(自定义函数),但是与Core API比起来表达能力还是差了许多,但是使用起来更简洁(更少的代码)。除此之外,Table API的应用会通过一个优化器,以在真正执行前,对程序进行优化。
使用者可以无缝的在Table 和 DataStream/DataSet 中转化,这样的特定是的程序可以使得应用将 Tabel API , DataStream/DataSet API 混合起来使用。 - Flink提供的最高等级的抽象是SQL。这层抽象和Table API的应用场景与表达能力类似,但是不同的是其使用SQL来编写程序。SQL与Table API紧密的交互,并且SQL可以查询使用Table API定义的表。
程序与数据流
Flink程序中的基础构造块是streams与transformations。(注意的是使用DataSet API构造的数据集,在flink内部也被看做是一个流)stream的概念是指源源不断的数据(可能不会中断),transformation是指一个操作,它将一个或多个流作为输入,并且将一个或多个流作为输出结果。
当执行时,Flink应用会映射为streaming dataflows(数据流),它有streams以及转化操作符组成。每一个数据流都由一个或多个source开始,并以一个或多个sink结束。数据流一般来说是一个DAG(有向无环图),尽管某些特殊的“环”是允许的如iteration,但对大部分我们将要讨论的情况,我们暂时忽略这种特殊情况。
一般来说,数据流中的操作符与程序中的transformation是一一对应的。但是有时候,一个transformation可能由多个转化操作符组成。
并发数据流
Flink程序本就是并行分布式的计算。在执行时,一个流有一个或多个分区(stream partitions),每个操作符也是有一个或多个operator subtasks(操作符子任务)。 每个operator subtask之间都是独立的,并且在不同的线程甚至是不同的容器/机器上执行。
某个operator的subtask的数量就是这个operator的并发度(parallelism)。一个流的并发度总是产生该流的操作符的并发度。同一个程序的不同的操作符可能拥有不同的并发度。
流中的数据发送到下一个操作符有两种模式:一对一(转发)模式,重分配(redistributing) 模式
- 一对一:一个流(如在上图的source和map操作符之间)保留了数据所在的分区与顺序。这意味着map操作符的subtask[1]中所接收到的数据与source操作符的subtask[1]的数据完全相同且顺序也相同。
- 重分配模式:一个流(如上图map和keyBy/window之间,或者keyBy/window与sink之间)的分区被改变。每个操作符的subtask会根据所选的转化逻辑,将数据发送到不同的许多subtask中。如keyBy()会根据key的hash值来选择该数据的分区,然后发送;broadcast()(广播,将数据发送到所有下游分区);rebalance()(随机重分配)。这种情况下,元素间的顺序,仅在发送与接收的subtask间保持(如,mapd的subtask[1]与keyBy/window的subtask[2])。这个例子中,keyBy/window中的所有key的元素顺序都保留了,但这并不说明并发度不会有其他问题,考虑到keyBy/window最后每个key聚合的结果发送到sink节点的顺序不确定,这说明并发度也会带来不确定性。
windos
在流上对时间进行聚合(如,计数,求和)与批处理是很不一样的。例如,不可能统计流中的所有元素,因为数据流式无限的。因此,在流上的统计都会指定window,例如“统计最新5分钟的数据”或者“对最新的100个元素求和”。
window可以是时间驱动(time driven)(如:每30秒)或者数据驱动(data driven)(如:每100个数据)。有这么几种类型的window:tumbling window滚动窗口(窗口间没有重叠),sliding window滑动窗口(窗口间有重叠),session window(由一段不活跃时间段来分隔两个window)
时间
当在流程序中说道时间时(如定义一个window),有几种不同概念的时间:
- event time:代表事件产生的时间。它通常是指事件数据中的时间戳。Flink中使用 timestamp assigner 来指定数据中的时间戳。
- ingestion time:数据通过source操作符进入flink数据流的时间。
-
processing time:操作符处理数据时的所在机器的机器本地时间。
有状态的操作符
尽管有些数据流中的操作符看起来就是在同一时间仅对一个数据做处理(如数据解析),但是许多操作符还能够根据多个数据记住某些信息(如window操作符)。这种操作符称为有状态的操作符。
这些操作符的状态(state)是由内置的key/value来存储维持。某个state会随同它所在的操作符所处理的流一起被分区与分配。因此,只有通过keyBy操作符产生keyedStream后,才可以在keyedStream中访问key/value的state,并且访问的state的key被当前流中的数据的key所限制(即不能访问不再这个keyedStream分区中的key的value)。这样的限制(流中的key与state中的key要“对齐”)保证了state的更新都是本地操作,保证了state的一致性,减少了更新state且保持一致性导致的事务开销。这样的对齐也实现了调整流的分区时,state也会显然的同时被调整。
检查点与故障容忍
Flink通过结合流回放(stream reply)与检查点(checkpointing)实现了故障容忍。检查点代表某个时刻,流上所有操作符的状态。通过存储操作符的状态与回放从检查点开始的数据,流应用可以从检查点处重启且保持一致性(精确一次处理的语义 exactly-once processing semantics)。
检查点间隔代表着一种权衡:执行时管理故障容忍的开销 与 故障恢复时的恢复时间。
Batch on Streaming
Flink将batch程序当做流来处理,一个有界的流(有限的元素数量)。在内部,DataSet会被看做是一个数据流。上面的概念放到批处理上与在流处理上是一样的,除了一下几点:
- 批处理的故障容忍不使用检查点。批处理的故障恢复通过全部重新回放流数据来实现。因为输入是有限的,因此这样做是可以实现的。这样做在恢复时会有更多的开销,但是使得内部的数据处理更简单,因为内部不需要进行检查点的操作。
- 有状态的操作符的状态使用的是简单的 内存/out-of-core 数据结构,而不是key/value
- DataSet API 引入了特殊的同步的仅可在有界流中使用的迭代iteration