00:00
了解了flink当中状态管理的基本思路和它的机制,那么接下来我们就来具体看一看flink当中状态是怎么样去分类进行管理的。那首先我们要介绍一个概念,就是在flink里边,其实所有的状态是可以分成两大类的啊,就是一类叫做托管状态,另外一类叫做原始状态,顾名思义,托管状态manage state,它由谁来管理呢?那是。有。Flink统一进行管理的,也就是说我们作为一个代码的控制者,我们作为一个代码的开发者,其实是把所有定义出来的状态交由flink托管,给flink统一进行管理,这就包括了状态的存储和访问,发生故障之后怎么样去进行恢复,以及进行并行度调整的时候状态不同的。并情子任务之间状态怎么样去进行重组,这一系列问题全部由flink底层帮我们实现啊,这其实就是我们想要的这个过程嘛,这样的话我们可以集中精力放在业务逻辑上面,只要调接口去使用状态就可以了。
01:10
而与之对应另外一类原始状态,那就是所谓的RO state,非常原始,没有经过任何的加工,没有任何的管理操作,全部都是自定义的,那就相当于flink只给我们开辟一块内存,接下来呢,那就是状态,诶,怎么样做序列化,反序列化诶,不同的并行子任务之间啊,状态怎么样去合并管理,怎么样去分配,怎么样去重组,发生故障的时候怎么样把持久化之后的状态读取出来进行恢复,这些通通都不管,都要我们自己去定义。那具体来说的话,当然是托管状态比较复杂了,托管状态呢,它是由flink的运行时,也就是所谓的装态来进行托管。如果我们配置了容错机制,那么所有我们定义出来的状态都会自动进行持久化的保存,并且发生故障的时候呢自动进行恢复,所以我们就就会发现啊,只要我们使用了托管状态,接下来我们其实就是直接把它拿来用就可以了。
02:11
不需要考虑它的这个故障,哎,怎么样去恢复了,都是自动的。那另外就是当应用发生横向扩展的时候,所谓横向扩展就是并行度发生调整,增大或者缩小的时候,我们说状态需要重组,诶那这个也不需要我们考虑,状态也是自动重组,分配到所有的此任务实力上。这里边呢,再介绍一下,对于具具体的使用而言,Flink的托管状态给我们提供了。具体。多种不同的形态,这里面最主要的就是这样四种。直状态value。列表状态list state,这两种状态呢,其实我们在之前的代码里都已经使用过了,看起来也非常的简单,State就是一个嘛,列表状态的话,就相当于是保存到了一个列表里面。另外除此之外还有映射状态map it啊,很简单,容易想到,就是把状态保存成了一个map,还有聚合状态aggregate state。
03:14
在flink自己给我们提供的这些底层的状态类型里边,它是支持各种各样的数据类型的,就比如说如果我们定义了一个list set,那具体这个列表里边啊,每一个具体的状态到底是什么数据类型呢?啊,我们定义什么样的数据类型都行,之前我们所说的flink定义支持的flink底层所支持的所有数据类型都可以用在这里。那对于聚合窗口等等算子,它里边其实也都内置的状态,就比如说我们那个agate function里边的ACC。其实也是所谓的托管状态,我们都不需要,就是需要去管理的东西就更少了,都是底层帮我们把它都搞定的。另外我们说也可以在函数类function里边,只要获取到这个上下文runtime contact啊,那接下来就可以直接去定义我们的自定义状态,这些通通都是托管状态。
04:17
啊,那当然,相对应之下,那原始状态就全部需要自定义了,Flink不会进行任何的自动操作,他只知道。状态的具体数据类型,也就是说抓va底层的那些数据类型,我们代码里面他他只管这些对吧,然后把它当做最原始的字节数组,Bit类型的数组来进行存存储,那这个时候如果说我们还想让这个状态拥有故障的时候能够自动恢复,然后发生扩展的时候还可以进行自动的重组调整,想要实现这些功能,那肯定就要花费大量的精力来处理它的管理和维护了。那当然就是说。正常情况,显然我们只有在遇到托管状态没有办法实现的非常特殊的需求的时候,我们才去考虑用这种最底层的原始状态。一般情况下,当然就是直接用托管状态帮我们把这些都搞定就可以了。
05:14
这是基本的一个使用的思路,所以我们的重点当然也是会放在托管状态的学习上,对于托管状态而言呢,那在弗林克里边,整体来讲又可以分成两大类,一类叫做算子状态,另外一类叫做按键分区状态,啊,这只是一个翻译的名称,那具体来讲的话,其实就是在底层结构里边啊,一个叫做operator state。啊,它是跟operator,就是我们所说的这个算子任务相关的,还有另外一个呢,叫做kid state kid state,那顾名思义了,这就是跟K相关了,很显然它是要经过KBY之后啊,指定了K之后,然后再去定义状态,当然每一个状态应该都跟K有关。
06:02
这里面主要就是要强调一个概念,我们知道在flink里边,每一个算子任务按照并行度,它可以分成多个并行子任务,不同的子任务呢,又会占据不同的任务槽,就是我们所说的task slot,由于不同的slot,我们知道它在资源上,计算资源上主要就是内存资源,它是物理隔离的,每一个并行的子任务都会占据独立的一块内存空间,那所以弗林格里边我们这个。整个某一个任务,它的并行子任务之间,它是没有办法互相访问对方的状态啊,就是说假如说我们这里有一个map任务,我们知道map尽管它是一个本身是一个无状态的算子,我们也可以通过函数类去给它定义,单独的算定义它的状态啊。那我们知道,假如说当前这个并行度是三的话,有三个并行的map子任务,每一个里边我们都定义了这个状态的话。
07:02
他们其实是彼此不能共享的,也就是说,第二个并行子任务,他只能访问自己当前保存的这个状态,他没有办法知道第一个和第三个。并行子任务,Map子任务里边这个状态到底是几啊?他只能处理自己。所以我们会发现状态它的作用范围啊,正常情况下就应该只针对当前的并行子任务实例是有效的。而我们知道在有些情况下呢啊,有些情况下,在这个当前的并行子任务这个实例里边还要做细详细的划分,因为当前这个子任务里边有可能还分了K啊。我们知道K根据它K的哈coded去进行分区,分区之后,那我们知道同一个K一定会在同一个分区,但是同一个分区里边可能有多个K,那这个时候我们如果让他们共享一个状态,显然就是不合理的。
08:05
所以我们就会想到,那你应该根据这个K的不同,再把这个状态进行一个划分。所以基于这样的一个思路啊,我们把托管状态整体分成了算子状态和案件分区状态两大类。首先我们来看算子状态,那所谓的算子状态非常简单,Operator state,它所谓的算子状态就是当前状态的作用范围,就是当前的算子任务实力,也就是我们说的啊,并行子任务之间,他们彼此之间因为内存上是互相隔离的嘛,所以状态也不能互相访问。当前的某一个并行子任务,就只能访问自己的这块内存空间里面的状态啊。那那互相之间他不能通信,也不能访问,相当于就是完全独立,各干各的。这就意味着,对于每一个并行子任务,它占据了一个分区,那么他所处理的所有数据访问到的就都是同一个状态,而这个状态呢?别的分区的数据是无论如何访问不到。
09:14
那对于同一个分区里边,同一个并行子任务里边啊,所有数据。状态对于他们来讲是共享的。所以我们看到算子状态,它整体来讲就是对于当前的分区的子任务有效,算子状态呢,可以应用在所有的算子上,使用的时候,那我们就知道这就跟一个本地变量没什么区别吗?因为本地变量的作用域不也是当前的任务实例吗?哎,我们知道当前这个。设置了并行度之后,那其实对于这个job manager而言啊,在分配在创建这个执行图的时候,那。根据并行度,会把当前的一个算子任务拆成好多个并行子任务,然后分别分发给执行它的task manager,那我们知道task manager在给它对应的分配了对应的这个物理执行的计算资源有它的单独一块内存,那接下来确实就跟本地变量没什么区别了,对吧?你要执行的想要获取的所有状态就在当前这这块内存空间里边,这不就是一个Java变量吗?
10:23
那在使用的过程当中呢,区别就在于。本地的内存变量,它只依赖于gbm去进行管理,然后进行垃圾回收GC就可以了,而我们现在呢,不能那么简单,因为我还得保证掉电它不能丢啊,我还需要去进行一个持久化的保存啊,那所以它做了持久化保存的这个过程啊,我们是把它叫做制作一个检查点的过程叫做。这里可以先提一句,后边具体的这个过程呢,我们会在后边的第十章进行详细的介绍。所以在使用算子状态的过程中,我们在代码里边还需要进一步实现一个接口,叫做checkpoint方式。
11:08
就是要进行这个checkpoint的保存,检查点的保存主要是为它的故障恢复容错机制考虑。那另外还有一大类,这一大类呢?啊,这其实是最为复杂,也是我们学习的重点,也是在实际项目应用当中的重点,这一类就是所谓的案件分区状态state。所谓的k state,它就是根据输入流里边定义的K来维护和访问当前的状态,所以k state呢,只能定义在kid stream里面,也就是说只有我们KBY之后。才能够定义案件分区状态。哎,那对应的我们看,其实在这个状态里边,它就变成了什么呢?每一个分区子任务,我们这不是有并行的两个TASK1TASK22个并行子任务吗?每一个子任务里边这就不是一个状态了,它有很多个。
12:08
也就是说我们前面假如说按照当前这个节点啊,这里边颜色就代表它一个K的数据,那么按照这个颜色作为K进行了分区划分,我们看到每一个分区里边,那有可能有各种不同的K啊。所以接下来呢,针对每一个不同的K都应该保存一份它的。对应的状态实力啊,那接下来我们要访问的过程当中呢,也是按照当前的K,当前的K是什么样的,我就直接去访问它里边对应的这个状态就可以了,那至于其他的状态跟它无关啊。那所以这样的好处在于,首先我们不需要再去做区分了,只要把这个K经过KBY处理之后,接下来所做的所有操作,我们定义的状态都是只针对当前K有效,而在代码当中呢,我们不需要去进去做特殊的处理,只要一份代码,一段同样的代码把它实现,那么自动flink就会给我们去进行K的分配和管理。
13:13
啊,我们不需要去考虑当前K到底是什么,只要把对应的逻辑处理起来就可以。按键分区状态的应用是非常广泛的,那之前我们讲到啊,所有的聚合算子必须在K败之后才能够使用,对吧?哎,那为什么我们说这个为什么直接一个非常简单data stream不能直接使用这个聚合算子呢?其实本质上就是因为聚合的结果是以。K state形式保存的,它是一个按键分区状态,按键分区状态那当然就必须要K之后才能用。那除了之前我们讲到的聚合算子或者是窗口算子之外。我们也可以自定义,那就是同样通过复函数类reach function来自定义一个k set,所以只要是提供了复函数类接口的算子,几乎是所有的算子都提供了负函数类的接口啊,哎,那么他们都可以去使用KC。
14:16
即使是像map filter这样的无状态的基本转换算子,哎,那我们就可以通过复函数类给他们追加按键分区状态,那或者呢,你你单独去实现这个checkpoint的function接口,也可以去把给它们去追加定义一个算子状态operator啊,那所以就是。弗link里边所有的算子任务都可以是有状态的,弗link不愧是有状态的,有处理。这里面需要强调的一点就是,不管是kid state还是operator state,他们都是在本地实力上去进行维护的,也就是说每个并行子任务都会维护对应的这个状态。他们的区别只是在于k state是相当于每一个子任务要维护一组状态,按照K维护一组状态。
15:09
或者我们可以本质上可以认为这就相当于是有一个key value对应的一个。一个map的存储吧,啊,按照不同的K把它对应的状态存在这个value里边,这个相当于是每一个并行子任务存了一个map作为状态保存起来,而对于这个operator state呢,就是简单的把数据作为状态保存起来就可以了。那它的共同点都是,只有当前这个并行子任务的所有数据才能够访问到当前子任务的状态。那Kate还只有当前K才能访问对应的状态。这就是关于。Flink里边所谓的算子状态和按键分区状态的基本定义。
16:00
那么关于状态的具体使用,在代码当中的应用,我们会在后续的章节继续讲解。
我来说两句