00:00
我们已经了解了弗link当中的状态编程,那这章的最后呢,我们介绍了状态的持久化和状态后端的概念,这部分内容主要是在处理发生故障之后怎么样去进行重启和恢复啊,因为我们知道在实际应用的过程当中,我们往往是希望这个服务是要七乘24小时不间断运行的,诶,那假如说遇上一些异常的情况,发生故障了,宕机了怎么办呢?诶,那正常情况下我们希望耽误的时间越少越好,尽快能够续上,能够恢复之前的状态,而且呢,之前我们处理的这个计算的结果也不要断掉,不要从头开始再重新计算,而是直接基于在发生故障之前的状态继续进行计算,诶,这就是我们所说的发生故障之后进行重启恢复。对于弗link而言,当然它是有一整套的机制来保证这个过程的,这就是我们所说的容错机制。
01:00
那在容错机制当中呢?其实最重要的概念,它的核心就是前面我们已经提到的检查点checkpoint。所以接下来的第十章,我们就专门去展开讲解一下flink当中checkpoint的原理,讨论一下它的容错机制到底是怎么样保证发生故障之后能够完整的恢复的,那首先呢,我们要来详细的了解一下检查点的概念啊,其实这个之前我们都已经介绍过了啊,什么叫做检查点呢?简单来讲,它其实就是我们直接做了一个状态的存盘啊,我们说做了一个快照嘛,Snap short,这就相当于是做了一个存盘的机制。那这种存盘的方式基于的思想也非常的简单啊,就是我们想到发生故障之后怎么办呢?诶,重启不就完了吗?直接重启,还有一个问题就是我们之前的计算结果就相当于断了续不上,那怎么办呢?哎,那非常简单的一种方式,就是把之前我们现在不是有状态的流处理吧,那就让每一个任务节点把之前已经保存的状态先持久化,把它记录下来,进行一个存盘,那这样一个进行持久化存盘保存的过程啊,就是叫制作检查点,最后得到的这个状态的持久化的快照就叫做checkpoint检查点。
02:21
如果我们类比生活当中的一些经验的话,其实就非常容易理解了啊,比如说诶,我们在编写一个word文档,或者说是在这个,呃,打游戏的时候啊,往往在一些关键的节点,我们都要做一个存盘保存诶,防止一旦出现状况,比方说打游戏的时候啊,后边碰到一个boss直接没打过,诶,那大侠重新来过了,那这个时候如果前面没有存档的话,那我们就只能从头开始再打一遍,如果说在打bos之前做了一个存盘保存啊,那接下来的话,如果没打过,那重新读档不就完了吗?所以类似的检查点的功能跟我们所谓的这个打游戏时候的存档是一个概念。
03:03
所以我们就想到了检查点的保存的原理就非常非常的简单,哎,那就是我们。为了能够在发生故障的时候把状态恢复出来,所以就应该随时的把当前的状态做一个保存,那这里说的随时其实是一个非常理想的状态,也就是说我们稍微有一点变化的时候,就像我们编辑一个文档的时候啊,稍微改了一个字,我们就马上做一个存盘保存,对应在这个流处理里边呢,那就是每处理完一个数据就立刻保存一下当前的状态,那如果说处理某一条数据的时候发生了故障,那相当于我们就只要读取出它的上一条数据处理完成之后的那个保存的状态,然后重新处理这条数据就可以。那这种方式当然是延迟非常的小啊,也没有做重复计算,所以整体来讲是非常完美的情况,但是如果这样去保存的话,那很显然保存的就太过于频繁了啊,那所以出于效率上的一个考虑,因为我们发生故障很显然不是随时随刻都会发生的嘛,呃,正常情况下我们只要每隔一段时间去做一个周期性的存盘保存,其实就可以了。
04:17
那如果说真的发生故障的话,稍微的有一点延迟啊,就相当于在保存存盘之后到发生故障这段时间内所处理的一些数据呢,相当于我们就丢掉了,需要重新的处理一遍,但是这样一个延迟我们应该是可以接受的,既然已经发生故障了嘛,啊,那整整体来讲是不影响我们整个流处理的实时性的。这就是我们基本的一个思路,检查点的保存,自动存盘,也是做一个周期性的处罚。那这里呢,我们还有一个非常重要的问题需要单独的去讨论一下,那就是保存的时间点到底是什么时候,诶这个问题主要是因为什么呢?诶主要就是考虑到我们当前想要做这样一个存盘的话,如果在word文档里面的话,我们会想到啊,那就周期性的直接把我们当前这个word的状态做一个快照存下来不就完了吗?但是想现在的flink集群,它是一个分布式的集群。
05:15
我们同时可能有很多正在执行的并行子任务,而且这个前后,这还是一个流水作业线。前后还有很多同时正在执行的任务,所以这个时候我们要保存的话,难道是说诶就直接告诉所有的任务好,现在我们要做快照保存了,所以大家现在全部暂停,排好队,然后咔嚓照一张照片,是这样去做保存吗?这样倒是很符合我们平常在生活当中啊,拍集体照的这样一个思路,但是呢,这样做是有问题的,因为我们想到啊,如果要拍集体照,我们就是要先通知所有人,把所有人都叫到一起,然后排好队,然后123茄子拍出这张照片,那其实这个过程啊,拍照并不花费太长时间,但是等人的过程。
06:04
这个非常的麻烦,所以如果说我们想让分布式的集群里边啊,每一个子任务全部。按照同一个时刻来做一个快照保存的话,诶,那相当于我们这里要停止所有的任务,等他们全保存完成之后,接下来再进一步的进行处理,这个过程延迟会非常的高,哎,那所以我们正常情况下啊,就快速的流处理,然后你周期性的呢,每隔一会儿就要做一个保存,这个保存如果影响到了我们正常的处理数据的话,那显然这种方案我们就不应该去选择。那另外除此之外呢,这种方式其实还有一个非常重要的问题,也就是在我们每一个算子进行状态的处理的时候,那其实这中间可能还有很多个不同的步骤,比如比如说啊,我们在某一步操作里边自定义了一个值状态value state,之前我们说啊,这个value state里边,它就是只保存一个单独的值。
07:03
那我们里边的处理逻辑呢,诶可能比较麻烦一点啊,比较绕,比方说遇到来了一个数据之后,哎,这个状态就先加一,比方说它就是一个整形的数字数据啊呃,Int类型或者是long类型,那来了一个数据之后,它先加一,然后呢,经过一系列的转换计算操作之后。在某种条件下,它再加一。哎,那所以在这种情况下,这个value state呢,有可能来了一个数据,它会被加二,在我们中断当前任务进行处理的时候,那就会有一个问题,当前这个数据它到底处理完了没有呢?处理完了之后可能它是加了二,那如果处理到中间有可能就只加了一。所以之后如果我们发生故障恢复的时候,那这个数据到底是。全部重新来做一个计算,还是说完全不重新计算,还是说从一半的位置开始重新计算呢?哎,那所以说如果说我们是按下暂停键,然后让所有的任务保存当前的状态的话,就会有这样一个问题,你需要把它的运行的上下文到底运行到哪一步了,全部保存下来。
08:12
这个就太麻烦了。啊,那为了解决这个问题呢?我们就不能想到啊,要让所有的任务都同时停下来,然后立刻保存他们当前的状态,而是要怎么样呢?至少让他把当前的数据先处理计算完,弄完这个数据之后,接下来的状态就可以进行一个保存了。然后这里还有另外一个问题。就是我们现在做这个流式处理的时候啊,前后上下游正在处理的数据,当然不可能是同一个,那那如果说比方说啊,我们在上游和下游之间,它有可能还有很多数据。是在网络传输的路上,那如果说第一个流程,第一个算子,我们这里处理的已经处理完十个数了。
09:01
而后边的第二步操作,第二个算子只处理到第七个数据,那中间的三个数据有可能还在路上啊,哎,当然发送的过程当中可能是在他们的网络缓冲区里边,那所有的这些缓冲区的数据是不是也应该要作为状态保存下来呢?要不然的话,接下来我们就没有办法知道在两步操作任务之间在路上传输的这几个数据,诶,到底他们去了哪里了,我们必须把它要暂存下来。那所以我们会发现这个过程也非常的麻烦,那一个简单的想法就是我也不要这样去做,而是怎么办呢?我们保存的时间点是某一个数据。来了之后,他从头到尾完整的流程都走了一遍,所有任务都处理完这个数据的时候,那把自己的状态做一个保存就可以了。哎,所以我们会发现啊,这个保存的时间点,它并不是我们所说的这一个同一时刻,而是什么呢?而是处理完同一个数据。
10:09
而且这样在处理的过程当中也会非常的具有可操作性,哎,那就是你不用保存各种各样的上下文,当前它处理到哪里了,到底操作到哪一步,我们都是把这个数据处理完了之后再去做保存啊,那每一个操作任务,每一个并行的子任务,也不用去管别的事情,只要把当前这个数据处理完了,我就可以单独的保存自己的状态,所以我们整个这个分布式的系统里边,分布式的集群里边,每个任务都可以单独的去做一个异步的状态保存。这样就完美的解决了之前我们所说的这些问题啊,那如果发生故障之后怎么办呢?诶,那发生故障也简单嘛,我们保存的这个状态都是处理完某一个数据之后的状态,那就把它恢复出来之后,然后从这个数据之后开始在重放后边的数据不就完了吗?所以我们看到这个过程。
11:05
相当于就是把数据的读取和后边整个数据的处理流程都打包在一起,构建出了一个事物,一个transaction,啊,这就是我们所说的这个事物处理的一个思路,那如果发生故障的话,就相当于要让事物回滚,从之前的状态开始重新进行处理。这关于事物处理,这涉及到了状态一致性的保证,这个我们会在后边详细的展开去进行讲解啊,那现在呢,我们就知道了整个检查点保存的原理,接下来我们可以完整的把这个过程再梳理一遍啊,那比如说我们回忆一下啊,最初的一个例子workout,那workout里边呢,那其实就是把一个文本文件里边啊,所有的单词都拆出来,统计每个单词出现了频次,那这里为了方便呢,我们直接认为当前输入的数据已经是一个一个的单词了,比如我们现在输的数据就只有hello word和hello flink。
12:04
只有这几个词,哎,所以我们就是一个hello word,然后一个fli,后面又是hello word后面又是hello flink一个一个交错开,然后接下来我们对于它的处理流程呢?啊,那既然已经是一个一个单词了,那就不用再去Fla map去做拆分了,我们就直接把这个数据源读进来之后,把它map成一个二元组,哎,就是每一个单词作为K,后边再来一个一啊,那接下来呢,当然就是KBY当前的单词,然后去做一个some,直接统计它的个数,这就是我们所谓的workout。那对于这样一个处理流程呢,我们可以用这张图来表示一下,哎,那首先前边这里是数据源啊,那一般在实际应用的时候,当然一般就是卡夫卡了,我们创建一个SS任务,一个原任务来从外部数据源里边读取数据,那当前这个源任务呢?为了后边我们构建事物去进行回滚,显然就应该保存一下当前已经读到的数据的偏移量啊,因为接下来如果说发生故障的话,在之前已经保存的那个数据之后的数据我们是要全部重放的话,那所以你如果不知道之前已经读到哪个数的话,那显然就没有办法重放啊,那对于卡夫卡这样的数据源,它是可以重置偏移量,重新去进行读取的,哎,那所以我们这里的原任务SS算子也应该记录一个当前读取的偏移量,把它作为算子的状态保存下来,哎,这就是我们SS任务所要保存的东西。
13:39
比如说现在保存的是三,那就说明已经读取了三个数,那就是hello word hello,啊,这三个单词已经读完了,后边再跟着的就是flink。然后接下来的第二个任务呢,哎,那我们这里看到是map map这个操作它是转换成二元组。本身这个操作其实是没有状态的啊,所以这里我们不用考虑啊,那当然了,这里我们也没有去考虑,就是前后任务之间去合并算子链啊,Operator ch这种情况我们也没有考虑,就完全把它们都拆开了,就相当于每一个方法调用,我们都认为是一步操作一个算子,然后再接下来呢,诶做K败,K败我们知道并不是一个真正意义上的算子,它只是定义了数据要按照K去进行一个分组传输,做一个分区,然后接下来呢,做萨做一个聚合求和的操作。
14:32
很显然,这里对应的每一个sum做计算的时候啊,针对每一个K做计算的时候,都会保存一个当前它的count值,这其实对应的就是一个kid state,就是当前的一个状态反,那比如说我们现在前面已经读了三个数是hello word hello,那很显然现在的哈就应该已经统计了两个,那word呢,就应该统计了一个啊,当然了,最后如果说我们还想打印输出的话啊,那看到的就应该是统计过了HELLO1 hello2,另外word这里统计到了一,这就是我们当前整个的处理流程。
15:11
如果说这三条数据都已经处理完毕啊,这三个数据啊,从头到尾的这个流程都已经处理完了,那现在所有任务他们的这个状态就可以保存下来,作为一个快照,作为一个检查点保存起来,保存到外部存储空间里面去,所以我们看到现在这个检查点里边的保存的东西到底是什么呢?哎,那就是SS任务这里面保存了一个。三当前的偏移量读了三个数,然后map这个任务没有状态,后边的sum这一步操作对应的就相当于,因为它是kid state嘛,我们说底层就相当于是一个哈希map,一个K,一个值啊,那对应的保存数据是哈,有两个word有一个,这就是当前保存的东西。所以通过这样一个简单的例子,我们也就搞清楚了在flink当中,检查点的保存到底是怎么样的一个过程。
我来说两句