Flink批流合一的介绍
概况
DataStream API支持不同的运行时执行模式,我们可以根据实际的需求和任务的特征来选择这些模式。
STREAMING
执行模式是DataStream API的“经典”执行行为,应该用于需要持续增量处理并预期无限期在线的无限作业。
此外,还有一种批处理风格的执行模式,我们称之为 BATCH
执行模式。 它以一种像批处理框架(如MapReduce)的方式执行作业。 这应用于已知有限的数据源输入并且不会连续运行的有界作业。
Flink对流和批处理的统一方法意味着在有界输入上执行的DataStream应用程序将产生相同的最终结果,而不管配置的执行模式是什么。 注意final在这里的意思是很重要的:以 STREAMING
模式执行的作业可能会产生增量更新(类似数据库中的upserts),而批处理作业在最后只会产生一个最终结果。 如果解析正确,那么最终的结果应该是是相同的,只是实现的方式不同。
通过启用 BATCH
执行,我们可以配置Flink应用额外的优化,只有当我们知道输入的数据源是有限数据的时候才能这样做。 例如,除了允许更高效的任务调度和故障恢复行为的不同shuffle实现外,还可以使用不同的连接/聚合策略。 我们将在下面详细讨论执行行为。
什么时候可以使用BATCH执行模式?
BATCH执行模式只能用于有界数据的Flink Job或者Programs。 有界性是数据源的一个属性,它能说明数据源的所有输入在执行之前是否已知的,或者是否会无限期地出现新数据。 反过来,如果一个Job的所有数据源都是有界的,那么它就是有界的,否则就是无界的。
另一方面, STREAMING
执行模式既可以用于有界数据源的Job,也可以用于无界数据源的Job。
一般来讲,当程序的数据输入有边界时,应该使用 BATCH
执行模式,因为这样效率更高。 当您的程序的数据输入是无界的时,您必须使用 STREAMING
执行模式,因为只有这种模式足够通用,能够处理连续的数据流。
一个明显的异常情况是,当您想使用有界Job来引导一些作业状态时,然后想在无界作业中使用这些作业状态。 例如,通过使用 STREAMING
模式运行一个有界作业,获取一个savepoint,然后在这个savepoint上恢复一个无界作业。 这是一个非常特殊的案例,当我们允许生成一个Savepoint作为 BATCH
执行作业的额外输出时,这个用例可能很快就会过时。
使用STREAMING模式运行有界作业的另一种情况是: 为了测试无界数据输入的Job而使用了有界的数据源。 对于测试来说,在这些情况下使用有界源会更自然一些。
配置BATCH执行模式
通过设置execution.runtime-mode的值可以指定执行模式,有三种可能的情况:
STREAMING
经典的流处理模式(默认)BATCH
DataStream API上的批处理模式AUTOMATIC
系统自动决定
通过命令行执行Flink应用程序的时候,通过配置flink run的参数来指定;另一种方式是编程方式通过创建或者是配置执行上下文的方式。
下面我们先来看看通过flink run命令行来指定:
bin/flink run -Dexecution.runtime-mode=BATCH examples/streaming/WordCount.jar
下面是在code里面指定:
final StreamExecutionEnvironment env= StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
官方推荐在命令行使用,不推荐在代码中使用。在命令行使用能让Flink程序提供更大的灵活性,而非局限在某一个执行模式。
执行行为
下面将概述BATCH和STREAMING模式进行对比。
Task Scheduling And Network Shuffle
Flink job包含多个在工作流图中连接在一起的操作。系统统一决定怎么安排这些操作在不同的机器或者节点上执行,以及他们之间的数据交互。
多个操作或者算子可以链接在一起,我们称为工作链。Flink将一个或者多个(链)操作看做一个统一的调度单元,flink称为task.通常 subtask
这个术语是指在taskmanager上并行运行的task的单个实例。
task的调度和数据的网络shuffles在BATCH和STREAMING模式下是不一样的。 多数情况是因为我们知道程序的数据输入是有限流使用BATCH执行模式,这允许Flink使用更有效的数据结构和算法。
下面这个例子来解释任务调度和网络传输的区别:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> source = env.fromElements(...);
source.name("source")
.map(...).name("map1")
.map(...).name("map2")
.rebalance()
.map(...).name("map3")
.map(...).name("map4")
.keyBy((value) -> value)
.map(...).name("map5")
.map(...).name("map6")
.sinkTo(...).name("sink");
Operations之间就意味着一对一的连接模式,比如 map(), flatMap,或者是filter(). 这些operations会直接将数据转发到下一个operation,这种情况就允许将这些operation链接在一起称为一个task, Flink也不会在这些operation之间插入network shuffle的操作。
另一方面,像keyBy() 或者 rebalance() 等这样的操作要求数据在不同的并行任务之间传输,这就会导致一个network shuffle(我理解为task之间数据的交叉传输)。
看上面的代码为例:
- task1: source, map1,和map2
- task2: map3, map4
- task3: map5,map6 和 sink
在task1和task2之间, task2和task3之间会有一个network shuffle。可以参照下图:
STREAMING Execution Mode
在STREAMING的执行模式下,所有任务都需要一直在线运行。Flink利用这种特性通过pipeline及时处理最新的数据,这也是连续性和低延迟的流出特征需要的。另外这也要求taskmanager要有足够的系统资源来运行所有的task。
network shuffle是流水线运行的,也就是说数据会被立即发送到下游的任务,同时会在网络层产生一些缓冲。这个是很有必要的,因为在处理一个持续的流数据时,各任务之间不存在时间点来做数据的物化(具体化)。这种处理和BATCH模式不同,在BATCH模式中,中间结果是可以物化的,下面将会讲到。
BATCH Execution Mode
在BATCH执行模式中,Job的多个task可以被划分为多个阶段,然后按阶段执行。Flink能做到按阶段执行时因为它数据是有界的,同时它能在进入下一阶段之前完全处理这一阶段的数据。上面的例子中,Job有三个阶段,分别对应了三个有shuffle barrier分隔的tasks.
不像上面STREAMING模式中发数据立即发送到下游的task, 分阶段处理要求Flink将task的产生中间结果物化到一些非临时的存储中,着这样上游任务执行完成后,下游任务才能读取到这些数据。这将增加处理延迟,但是带来其他有趣的属性。首先,当中间某一个task执行失败的时候,可以回溯到上游最新的可用结果重新开始计算,而不是重新启动整个Job。另外就是BATCH的Job可以在更少的资源(这里的资源值taskmanager的slot数量)上执行,因为系统可以一个一个的执行task。
TaskManagers将保存中间结果,直到下游没有task再使用。(从技术上讲可以保存到消费这些结果的下游任务产生输出结果)。之后,这些中间结果在存储空间足够的情况下将会被保留,以便在出现谷中的时候可以回溯到早起的结果重新开始计算。
State Backends / State
在STREAMING模式下,Flink使用state backend来控制状态的存储方式和 checkpont
的工作方式。
在BATCH模式中,配置的后端状态将被忽略。 相反,keyed操作的输入数据按key分组(使用排序),然后依次处理key下面的所有记录。 这允许同时只保留一个key的状态。 当处理到下一个key的数据时,当前key的状态将被丢弃。
Order of Processing
operations(算子)或者用户自定义的算子处理数据的顺序在BATCH和STREAMING模式下是不同的。
STREAMING模式下,用户自定义的函数不会对输入数据的顺序做任何的假设,数据一到就会被处理。
而在STREAMING模式下,有一些operation,Flink会保证其顺序的。排序可能是特定任务的调度,network shuffle和状态后端的副作用,也可能是系统有意识的选择。
我们来区分三种不同类型的输入:
- broadcast input: 广播输入,来自广播流
- regular input: 常规输入, 既不是广播,也不是keyed的输入
- keyed input: keyed的输入,来执KeyedStream的输入
使用多种输入类型的操作或者算子将使用下面的处理顺序:
- 首先处理广播输入
- 然后第二部处理常规输入
- 最后处理keyed输入
对于消费多个常规或广播输入数据的函数(例如CoProcessFunction), Flink有权以任何顺序处理来自该类型的任何输入的数据。
对于使用多个keyed输入的函数(例如KeyedCoProcessFunction), Flink会处理来自所有键输入的单个键的所有记录,然后再转移到下一个keyed输入。
Event Time / Watermarks
当设计到event time支持的时候,Flink的流处理是建立在一个悲观的假设上的,即时间是无序的。如时间戳是t的事件可能在时间戳t+1之后才进入流处理。正因为如此,系统永远不能确定是否还有事件的时间戳t < T(T是未来的某一个时间点)。为了平摊这种无序对最终结果的影响,使系统更实用,在STREAMING模式下,Flink采用了一个方式叫Watermarks. 一个watermark带有一个时间戳T信号,表示在它后面没有t < T的数据出现。
在BATCH模式中,输入数据集是预先知道的,所以不需要watermark,我们可以按时间戳对元素排序,以便按时间顺序处理它们。 对于熟悉流处理的开发者来说,BATCH中我们可以假设有“完美的水印”。
如上所述,在批处理模式中,我们只需要每个键相关联的输入数据的末尾MAX_WATERMARK,或者non-keyed数据集的最后的时间。 基于该方案,所有注册的计时器将在时间结束时触发,用户定义的WatermarkAssigners或WatermarkGenerators将被忽略。 但是,指定一个WatermarkStrategy仍然很重要,因为它的TimestampAssigner仍将用于给事件分配时间戳。
Processing Time
Processing Time 是机器处理事件的时间,是处理事件的机器上的时钟时间。基于这个定义,我们可以知道基于processing time的计算结果是不可重复的,这是因为处理两次相同记录的processing time一定是不相同的。
尽管如此,processing time在STREAMING处理模式下还是很有用的。原因和流处理管道任务经常实时摄取无限流数据,所以event time和processing time是有关系的。此外,由于上述原因,processing time的1小时通常是非常接近event time的1小时。所以使用processing time可以提供源于预期结果的提示的提前触发。
这两种时间的相关性在BATCH模式式下是不存在的,因为在BATCH模式中,输入的数据集都是静态的,而且是已知的。所以,Flink允许用户使用processing time来注册计时器,但是像event time一样,所有的计时器都将在数据输入结束的时候触发。
从概念上讲,我们可以想象,在作业执行期间,processing time不会提前,而是快进到整个输入被处理时的时间结束。
Failure Recovery
在STREAMING模式下,Flink使用checkpoint检查点来进行故障恢复。
使用checkpoint进行故障恢复的一个特征是,再出现故障的是时候,Flink将从最新的checkpoint重新启动所有正在运行的任务。这可能比我们在BATCH模式下执行的操作代价更高,但是你这也是允许使用BATCH模式执行的原因。
在BATCH执行模式下,Flink会尝试回溯到之前处理阶段,该阶段的中间结果任然是可用的。一般而言,只有失败的任务需要重新启动,这与从checkpoint重启所有任务相比更高效,更节省时间。
重要的考虑
对比经典的STREAMING执行模式,在BATCH模式下,有些东西不像我们的预期那样工作。一些特性在两种模式下是互不支持的。
在BATCH模式下的行为的改变:
类似 reduce()
或者 sum()
这样的滚动操作在STREAMING模式下会做增量的更新,而在BATCH模式下,这些操作(reduce/sum)不会滚动,它们只会计算出最后的结果。
BATCH模式下不支持的行为:
- checkpointing和任务与checkpointing有关的操作在BATCH模式下都不会起作用。
- 不支持迭代流,参考迭代器流
实现自定义的操作符时要小心,否则会出现一些不当的行为。值得注意的是,自定义的操作符是Flink提供的高级用法,对大多数开发者来说,最好还是使用keyed process函数来处理。