做Flink做了好几年,让我感触最深的是,虽然写了Flink代码好多年,但是要让我通俗易懂地将Flink的重要的核心概念串起来,我一时还真找不到切入点,好多人可能跟我一样,脑子里对于Flink的知识是散的,没有一条很好的线把这些串起来。所以整理的几天的思绪,想把它写下来,分享给有同样困惑的。
很多人聊起Flink,都知道它是一个优秀的数据处理引擎,多用来处理实时数据。但是我们也知道,SparkStreaming也是用来处理实时数据,但是他们有什么区别,而Flink又是凭什么打败SparkStreaming成为当下最流行的实时数据处理引擎呢?
其实SparkStreaming和Flink在处理实时数据上用这两种截然不同的方式。我们说SparkStreaming是微批次架构,他其实是用传统批处理的方式来处理实时数据,也就是我们通常所说的有界流。传统批处理方式是持续地接收数据,用时间来划分多个批次,再周期性地执行批次运算。但如果我们需要计算每小时出现事件转换的次数,且事件转换跨越了所给定的时间划分,传统批处理会将中间的状态结果带到下一个批次进行计算,这显然对于处理实时数据来说是不合理的;除此之外,当接收到的事件出现前后乱序的问题,传统批处理仍会将中间的状态结果带到下一个批次进行计算,这种处理方式也不尽如人意。
这时我们就需要有一个真正的流处理引擎,一个无穷无尽的数据源在持续接受数据,以代码作为数据处理的基础逻辑,数据源的数据经过代码处理后产生出结果,然后输出,这就是流式处理的基本原理。这样的流处理引擎必须具备一下几个特点:
这个引擎必须要有能力可以累积和维护状态,可以累积在过去时间接收过的所有事件,从而影响最终结果输出。
时间,时间意味着这个引擎可以确定什么到什么时间所有数据都完全接受完成,输出计算结果。这个时间其实并不是我们广义上理解的时间,而是数据世界的时间。
这个引擎需要实时产生结果,即数据被接受到后需要立刻处理。我们不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。
所以Flink应运而生了,Flink其实就是一个有状态分布式的流式数据处理引擎。
Flink作为一个有状态分布式的流式数据处理引擎需要具备哪些重要的特质:
状态容错(State Fault Tolerance)
状态维护(State Management)
Event-Time处理(Event-time processing)
状态保存和迁移(Savepoints and Job Migration)
接下来我们一个一个来解释说明:
一、状态容错
当我们在考虑状态容错的时侯,我们往往希望我们的状态被精确一次(Exactly-once)计算,任务在运算时累积的状态,每接受一次事件都会进行状态计算,计算状态都应该是精确一次的,如果计算超过一次的话也意味着数据引擎产生的结果是不可靠的。
如何确保状态拥有精确一次(Exactly-once)的容错保证?
在简单的使用场景中,如果有无限流的数据进入,后面单一的 进程处理运算,每处理完一笔计算即会累积一次状态,这种情况下如果要确保产生精确一次的状态容错,每处理完一笔数据,更改完状态后进行一次快照,快照记录当前数据处理的位置以及当前的状态。如果下一次我处理数据失败了,为了能够确保精确一次,我直接拿取上一次记录的位置即状态进行恢复即可。也就是我们通过快照来保证数据被精确一次处理。
如何在分布式的场景下替多个拥有本地状态的算子产生一个全局一致性的快照(Global consistent snapshot)?
但是我们都知道Flink是一个分布式的的处理引擎,Flink如何在分布式的场景下替多个拥有本地状态的算子产生一个全局一致性的快照(Global consistent snapshot)?这其实就涉及到分布式的状态容错。
原理很简单,在分布式的环境中,算子在各个节点做运算,当一个时间流过所有的算子,更改完所有的状态值以后,将这些状态值及该事件运算的位置保存到FileSystem中即可。
Flink是如何产生全局一致性的快照的呢?
每个算子本地的statebackend在每次将产生checkpoint时会将它们传入共享的 DFS 中。当任何一个 Process 挂掉后,可以直接从上一个完整的 Checkpoint 中将所有的运算值的状态恢复,重新设定到相应位置。Checkpoint 的存在使整个程序能够实现分布式环境中的 Exactly-once。
但是出现的问题是,当当前事件经过所有算子进行状态保存的时候,我们必须停止下一个事件的处理,才能保证状态的保存准确。Flink是如何实现在不中断运算的前提下产生快照?
如何在不中断运算的前提下产生快照?
其实就是利用Checkpoint barrier。Flink 在某个 Datastream 中会一直安插 Checkpoint barrier 1,Checkpoint barrier 2 ...Checkpoint barrier N。
Checkpoint 被触发后开始从数据源持续不断地产生 Checkpoint barrier。Checkpoint barrier 会一一流经所有的算子,每流过一个算子会将自己的状态进行保存,类似以下表格:
Checkpoint barrier N负责part of checkpoint N,Checkpoint barrier N -1 负责part of checkpoint N - 1,以此类推。
如图,当部分事件标为红色,Checkpoint barrier N 也是红色时,代表着这些数据或事件都由 Checkpoint barrier N 负责。Checkpoint barrier N 后面白色部分的数据或事件则不属于 Checkpoint barrier N。
当数据源收到 Checkpoint barrier N 之后会先将自己的状态保存,以Kafka Source为例,数据源的状态就是目前它在 Kafka 的patition offset,这个状态也会写入到上面提到的表格中。下游的 Operator 1 会开始运算属于 Checkpoint barrier N 的数据,当 Checkpoint barrier N 跟着这些数据流动到 Operator 1 之后,Operator 1 也会计算属于 Checkpoint barrier N 的所有数据的状态,同时也会直接对 Checkpoint 去做快照。
当快照完成后继续往下游走,Operator 2 也会接收到所有数据,然后计算所有属于 Checkpoint barrier N 的数据的状态,当Operator 2收到 Checkpoint barrier N 之后也会直接将计算的状态写入到 Checkpoint N 中。以上过程到此可以看到 Checkpoint barrier N 已经完成了一个完整的表格,这个表格我们就叫做分布式快照。
分布式快照可以用来做状态容错,任何一个节点挂掉的时候可以在之前的 Checkpoint 中将其恢复。继续以上 Process,当多个 Checkpoint 同时进行,Checkpoint barrier N 已经流到 job manager 2,Flink job manager 可以触发其他的 Checkpoint,比如 Checkpoint N + 1,Checkpoint N + 2 等等也同步进行,利用这种机制,可以在不阻挡运算的状况下持续地产生 Checkpoint。
二、状态维护
Flink通过StateBackend来维护状态,Flink内置了三种StateBackend:
MemoryStateBackend(默认)
FsStateBackend
RocksDBStateBackend
MemoryStateBackend执行checkpoint的时候,会把state的快照数据保存到jobmanager的内存中。默认情况下,MemoryStateBackend配置为支持异步快照。 异步快照可避免可能导致流应用程序背压的潜在阻塞管道。
MemoryStateBackend的局限性:
默认情况下,每个个体的状态的大小限制为5 MB。 可以在MemoryStateBackend构造函数中进一步增加大小。
状态不能超过akka frame大小,akka frame大小(默认10M):jobmanager和taskmanger之间传递的消息的最大限制。
聚合状态不能超过jobmanager内存。
MemoryStateBackend使用场景:本地开发与调试,持有很小的状态。
FsStateBackend将正在运行的数据保存在TaskManager的内存中。在检查点时,它将状态快照写入配置的文件系统和目录中的文件。最小元数据存储在JobManager的内存中(或者,在高可用性模式下,存储在元数据检查点中)。FsStateBackend 默认使用异步快照,以避免在编写状态检查点时阻塞处理管道。
RocksDB跟上面的都略有不同,它会在本地文件系统中维护状态,state会直接写入本地rocksdb中。同时它需要配置一个远端的filesystem uri(一般是HDFS),在做checkpoint的时候,会把本地的数据直接复制到filesystem中。fail over的时候从filesystem中恢复到本地。RocksDB克服了state受内存限制的缺点,同时又能够持久化到远端文件系统中,比较适合在生产中使用
使用RocksDBStateBackend的场景:
RocksDBStateBackend最适合处理大状态,长窗口或大键/值状态的Flink有状态流处理作业。
RocksDBStateBackend最适合每个高可用性设置。
RocksDBStateBackend是目前唯一可用于支持有状态流处理应用程序的增量检查点的状态后端。
三、Event - Time
在 Flink出现之前,大数据处理引擎一直只支持 Processing-time 的处理。Processing Time 是来模拟我们真实世界的时间,其实就算是处理数据的节点本地时间也不一定是完完全全的真实世界的时间,所以说它是用来模拟真实世界的时间。而 Event Time 是数据世界的时间,即我们要处理的数据流世界里的时间。
时间的一个重要特性是:时间只能递增,不会来回穿越。 在使用时间的时候我们要充分利用这个特性。假设我们有这么一些记录,然后我们来分别看一下 Processing Time 还有 Event Time 对于时间的处理。
对于 Processing Time,因为我们是使用的是本地节点的时间(假设这个节点的时钟同步没有问题),我们每一次取到的 Processing Time 肯定都是递增的,递增就代表着有序,所以说我们相当于拿到的是一个有序的数据流。
而在用 Event Time 的时候因为时间是绑定在每一条的记录上的,由于网络延迟、程序内部逻辑、或者其他一些分布式系统的原因,数据的时间可能会存在一定程度的乱序,比如上图的例子。在 Event Time 场景下,我们把每一个记录所包含的时间称作 Record Timestamp。如果 Record Timestamp 所得到的时间序列存在乱序,我们就需要去处理这种情况。
如果单条数据之间是乱序,我们就考虑对于整个序列进行更大程度的离散化。简单地讲,就是把数据按照一定的条数组成一些小批次,但这里的小批次并不是攒够多少条就要去处理,而是为了对他们进行时间上的划分。经过这种更高层次的离散化之后,我们会发现最右边方框里的时间就是一定会小于中间方框里的时间,中间框里的时间也一定会小于最左边方框里的时间。
这个时候我们在整个时间序列里插入一些类似于标志位的特殊的处理数据,这些特殊的处理数据叫做 watermark。一个 watermark 本质上就代表了这个 watermark 所包含的 timestamp 数值以后到来的数据已经再也没有小于或等于这个时间的了。在确定要计算的数据可能会出现的延迟时间后,比如5s,我们可以将watermark设定延迟5接收数据,接受完所有数据后再进行数据计算。
我们要理解时间和watermark的本质:
时间:流处理中的时间本身就是一个普通递增的字段,不一定是时间。
watermark:watermark本质就是解决数据乱序的方法之一,大多是启发式的,在延时和完整性直接抉择。
四、状态保存与迁移
流式处理应用无时无刻不在运行,运维上有几个重要考量:
更改应用逻辑、修改bug等,如何将前一执行的状态迁移到新的执行?
如何重新定义运行的平行化程度?
如何升级运算丛集的版本号?
Flink通过Savepoint功能可以满足以上需求。Savepoint 我们可以简单理解为就是手动生成的checkpoint。
Savepoint 跟 Checkpoint 的差别在于Checkpoint 是 Flink 对于一个有状态应用在运行中利用分布式快照持续周期性的产生 Checkpoint,而 Savepoint 则是手动产生的 Checkpoint,Savepoint 记录着流式应用中所有运算元的状态。
无论是变更底层代码逻辑、修 bug 或是升级 Flink 版本,重新定义应用、计算的平行化程度等,最先需要做的事情就是产生 Savepoint。