抽象层次
levels_of_abstraction
- 最低级的抽象接口是状态化的数据流接口(stateful streaming)。这个接口是通过 ProcessFunction 集成到 DataStream API 中的。该接口允许用户自由的处理来自一个或多个流中的事件,并使用一致的容错状态。另外,用户也可以通过注册 event time 和 processing time 处理回调函数的方法来实现复杂的计算。
- 大部分程序通常会使用以 DataStream API(有界/无界数据流)、DataSet API(有界数据集)为代表的 Core APIs,并不会使用低级抽象接口。这些API为数据处理提供了大量的通用模块(common building block),包括用户定义的各种各样的变换(transformations)、连接(joins)、聚合(aggregations)、窗口(windows)、状态(state)等等。DataStream API 集成了 low level 处理函数,使得对一些特定的操作提供更低层次的抽象。此外,DataSet API 也为有界数据集提供了一些补充的编程原语,例如循环(loops)、迭代(iterations)等。
- Table API 是一种以数据表为核心地声明式 DSL,能够动态地修改表(表示流时)。Table API 的是一种(扩展的)关系型模型:每个都有一个 schema(类似于关系型数据库中的表结构),API也提供以下操作: select,project,join,group by,aggregate等。Table API 程序定义的是应该执行什么样的逻辑操作,而不是直接准确地指定程序代码运行的具体步骤。尽管 Table API 可以通过各式各样的自定义函数进行扩展,但是它在表达能力上仍然比不上 Core APIs,不过在使用上更简练(可以减少很多代码)。此外,Table API 程序在运行之前也会使用一个优化器对程序进行优化。用户可以在 tables 与 DataStream/DataSet 之间进行无缝切换,程序也可以混合使用 Table API 和 DataStream/DataSet APIs。
- Flink 提供的最高级接口是 SQL。这个层次的抽象接口和 Table API 非常相似,包括语法和接口的表现能力,唯一的区别是通过 SQL 查询语言实现程序。SQL 抽象接口和 Table API 的交互非常紧密,而且 SQL 查询也可以在 Table API 中定义的表上执行。
程序与数据流(Dataflow)
Flink 程序的基础构建单元是流(streams) 与转换(transformations)。DataSet API 中使用的数据集也是一种流。数据流(stream)就是一组永远不会停止的数据记录流,而转换(transformation)是将一个或多个流作为输入,并生成一个或多个输出流的操作。
执行时,Flink程序映射到 streaming dataflows,由流(streams)和转换操作(transformation operators)组成。每个 dataflow 从一个或多个源(source)开始,在一个或多个接收器(sink)中结束。dataflow 类似于有向无环图(DAG)。虽然可以通过迭代构造器生成某些特殊形式的环,但为了简化说明,大部分情况下我们不考虑这种结构。
通常情况下程序中的 transformation 和数据流图(dataflow)中的 operator 是一一对应的。不过有的时候也会出现一个 transformation 由多个operators 组成的情况。
Dataflow
并发数据流
Flink 程序是分布式、并发执行的。运行过程中,一个数据流可能会有一个或多个流分区(stream partitions),而一个 operator 也可能会有一个或多个运算子任务(operator subtasks)。运算子任务之间都是相互独立的,在不同的线程中运行的,可能在不同的机器或容器(container)上执行。运算子任务的数量由指定运算符(operator)的并发数确定。数据流的并发数就是它所生成的运算符的并发数。同一程序中不同的运算符可以有不同等级的并发量。
parallel_dataflow
数据流可以使用一对一的模式(one-to-one pattern)在两个运算符之间传输,也可以使用重分发模式(redistributing pattern):
- 一对一模式,例如上图中 Source 和 map() 运算符之间的数据流。数据流中元素的分组和顺序会保持不变。也就是说,map() 运算符的子任务[1]所看见的元素与 Source 运算符的子任务[1]所生成的元素的顺序完全一致。
- 重分发模式,例如上图中 map() 和 keyBy/window 运算符之间的数据流,以及 keyby/window 和 Sink 运算符之间的数据流。数据流所在的分区会改变。每个运算子任务会发送数据到不同的下游运算子任务(根据不同的运算转换)。例如 keyby() 运算符(通过对 key 进行哈希计算来重分区),boradcast() 和 rebalance()(随机重分区)就是重分发模式的例子。在此模式下,元素间的顺序只保留在每对发送-接收子任务中(例如 map() [1]和 keyBy/window[2])。尽管在子任务之间每个 key 的顺序都是确定的,但是由于程序的并发引入了不确定性,最终到达 Sink 的元素顺序就不能保证与一开始的元素顺序完全一致。
窗口
聚合事件(counts、sums 等)在流和批处理过程的工作模式完全不同。例如,计算数据流中的所有元素的个数是无法实现的,因为流在概念上是无限无边界的。因此,数据流的聚合操作都是由窗口(window)限定了范围来计算的,例如,“计算过去五分钟的元素个数”,“对最后100个元素求和”等。窗口可以时间驱动(例如以30秒为单位)或者数据驱动(例如以100个元素为单位)。有多种不同类型的窗口,例如,数据不重叠的滚动窗口(tumbling window)、数据重叠的滑动窗口(sliding window),以及以非活动状态为间隔的会话窗口(session window)。
window
时间
流式计算程序中的时间概念(例如在定义窗口时经常会用到的时间)有以下几种含义:
- 事件时间(Event Time),是指事件发生的时间,通常在事件的内容中由时间戳表示。
- 接入时间(Ingestion Time),是指事件在 source 运算符中进入 Flink 数据流的时间。
- 处理时间(Processing Time),是指运算符在执行时间类操作时的本地时间。
time_model
有状态操作
虽然数据流中的许多操作一次只查看一个单独的事件,但有些操作会记录多个事件之间的信息(例如窗口运算符)。这种操作就称为有状态的操作(stateful)。
有状态操作的状态可以理解成是以键值对(key/value)形式存储。这个状态的分区和分布是和数据流严格绑定在一起的。因此,在 keyBy() 函数执行之后,只能在带键的数据流中访问 key/value 状态,而且也只能获取与当前事件的主键相对应的值。数据流的键和状态的严格对应,确保了所有状态更新都是本地操作,同时也保证了事务的一致性。这种方式也使得Flink可以透明地重分发状态,调整数据流分区。
stateful
容错检查点
Flink 通过流重放(stream replay)和检查点机制(checkpoint)结合的方式实现了容错能力。检查点与每个输入流中的特定一点以及每个操作的相应状态相关。数据流可以从检查点恢复来保持一致性(exactly-once的语义),通过恢复操作的状态和从检查点开始重放事件。检查点间隔是对程序的容错能力与恢复时间(需要重发的事件数量)的平衡手段。
批处理操作
Flink 将批处理看作一种特殊的流处理,只是数据流有界的特例。DataSet 也被看作一种流数据。流式计算程序中的很多概念也能应用到批处理程序中,除了以下几处不同:
- 批处理程序的容错机制不使用检查点机制。而是通过完全重发所有数据流实现的,因为输入数据是有界的。这样,恢复过程中的开销可能更大一些,但是由于没有了检查点,正常处理过程的开销反而更小。
- DataSet API 中的有状态操作没有使用键/值(key/value)索引存储,而是使用了简化的 in-memory/out-of-core 数据结构,
- DataSet API 引入了特殊的同步(基于 superstep)迭代接口,该接口仅能用于有界数据流。