1. StreamOperator接口概述
这个就是那些算子的一个最顶层的接口,虽然它是一个最顶层接口,那也是站在算子这个体系的角度来看。其实这个接口本身也继承了一些其他的接口。
1.1 接口定义
java
@PublicEvolving
public interface StreamOperator<OUT> extends CheckpointListener, KeyContext, Serializable
1.2 继承的接口分析
CheckpointListener接口
checkpoint listener是干嘛的呢?
- 谁去继承它,那么就意味着你这个东西就能够得到那个checkpoint完成的通知和这个checkpoint失败的或者放弃的通知
- 你继承了这个checkpoint listener,那你就是一个checkpoint的监听者
- Flink的内核里面的各种功能,它就会去看你如果是一个checkpoint listener,那么在checkpoint完成的时候它就会通知到你
为什么StreamOperator要继承这个?
- 我们的算子确实是要去监听checkpoint的
- 如果checkpoint发生的时候,或者checkpoint完成的时候,我要做些什么事?
- 如果checkpoint放弃的时候我要做些什么事?确实有些算子是很关心的
- 尤其是那种带状态的算子,比如说两阶段提交的sink
- 两阶段提交sink要去把这个数据真实地提交到外部系统里面去,那就得等整个系统的一个checkpoint完成了,才能去做这个事,或者说才能真正地提交事务
KeyContext接口
key context是什么?
- 这是一个相当于便利化的组件
- 里面就是一个
setCurrentKey
这么个方法 - 谁去继承这个key context,那么你就必须去实现这个
setCurrentKey
为什么需要这个?
- 因为我们StreamOperator里面有那种,比如说有一些算子,它可能会去用到那个key的state
- key的state它是和key绑定的那种状态
Serializable接口
- 这个就不用解释,这个是JDK里面的一个接口,一个可序列化的标记接口
- 我们算子是要被序列化的,因为你在我们的客户端生成这个graph,生成这个stream graph,还有job graph,最后都是要发到那个远程的集群里面去运行的
- 所以这个里面是一定要被序列化的,所以它实现这个序列化的接口
2. key状态切换的详细机制
2.1 实际应用场景举例
比如说你现在累加各个品类的成交额,而你这边进来的数据,里面就有各种品类的:
- 比如说有C1品类的
- 有C2品类的
- 又有C1品类的
在你的代码里面你会发现:
- 你去累加的时候从来没关心过这条数据是哪个品类的
- 你只管从状态里面取出一个值出来,然后加上新的值
- 然后再赋给那个状态,去更新那个状态
state.update()
2.2 状态切换的疑问
你有没有考虑过:
- 假设进来的是个C1品类的,一个金额100
- 你要去取出状态里面C1品类原来的值
- 但你这边有区分这个是C1品类还是C2品类的状态吗?你并没有区分,对不对?
- 那这个C1品类会加到C2品类的那个累加值里面去吗?也不会
2.3 Flink的自动切换机制
原因就是:
- 我们这个里面的状态,这一个key的状态就是你在这边操作这个所谓的状态,其实是跟某个key绑定的
- 如果说我们用户要去区分这个状态,比如说来了一条C1的100,我得去找那个C1的那个状态
- 然后来了一个C2的200,我又得去找那个C2的状态
如果没有自动切换会怎样?
- 如果我这个并行度接受了1万种key,那岂不是你这里面状态会有1万多个对象?
- 你要持有,你要自己去管理这1万多个state对象
- 然后来了不同的品类数据的时候,你还要去找对应的那个状态来去取值或者去更新值,那不要我的命嘛?
Flink的解决方案:
- 这些跟key绑定的状态在实际运行中的切换,这个切换是Flink体系内部去帮你做的
- 就是那个状态管理器,或者说那个状态管理体系,它会去自动给你切换
- 你代码这边你说
state.value()
,它就会拿到你当前进来的数据所属的key是谁? - 比如说你进的是C1,100,那么你的key是那个品类C1
- 然后根据这个C1去拿到C1所绑定的状态,所以你在取value的时候,其实它底层已经帮你切换过了
2.4 切换的实现机制
其实这条数据一进到这个算子之后:
- 它这边就有一个set,就刚才讲的那个key context里面有个
setCurrentKey
- 它已经做了这个事情了,
setCurrentKey
就是把算子这个,或者说这个状态管理器里面当前的key已经给你切换成了你进来的这条数据的key了 - 所以你后面再去调
state.value()
的时候,就已经就一定能拿到你这个数据所属的key所绑定的那个状态 - 你就不会去加混了,不会去把比如说C1品类的金额加到C2里去,C2加到C3里面去
所以这个里面它有这么一套机制:
- 就是去自动切换这个key所绑定的状态
- 而这个里面有一个机制就很重要,就是你的算子里面要能够提供这个一个方法,叫做
setCurrentKey
- 不然的话,我们那个task内部的那个工作机制就没办法去进行切换了
- 所以说我们这个StreamOperator里面就实现了一下,继承一下这个key context就是去实现这个
setCurrentKey
3. StreamOperator自身的方法
3.1 生命周期方法
open()方法
java
void open() throws Exception;
这个方法的作用:
- 一般一个组件,在正式工作之前可能要做一些初始化的工作,那么它就给你安排这么个open方法
- 所以在这个StreamOperator的一些具体的实现里面,可以根据需求在这个open里面安排一些初始化的逻辑
- 英文注释也说明了这一点:
this method is called immediately before any element process
- 就是在任何元素被处理之前,任何的实际数据处理发生之前,这个方法会被立刻调用
it should contains the Operators initialization Logic
,它里面应该包含的是你这个具体算子里面的那些初始化的逻辑- 所以它就是一个生命周期方法,一个open
finish()方法
java
void finish() throws Exception;
这个方法的作用:
- 就是说在数据处理完成之后,它会去调用这个东西
- 数据处理完成之后,这显然应该是面向那种有界流的任务,或者说批处理那种任务
- 咱们在那种无界流的这种正儿八经的流式处理里面,其实这个东西很难finish,一般也不会finish
After this, just this method is expected to flush all remaining buffer data
- 这个finish是干嘛的?就是我的数据要处理完了,就是这个任务的数据要处理完了,那么可能就会调你这个finish
- 它希望你在里面做些什么呢?比如说你把你缓存的一些数据赶紧地刷到下游去
- 因为它很快就要做这个close了,close就要关闭了,所以你赶紧刷一刷,把你缓存的一些数据刷出去
close()方法
java
void close() throws Exception;
这个方法的作用:
- 这个close就是正式要结束了,这个operator(算子)的生命要结束了
- 只有当算子的生命要结束了才会调用close方法
- 在这个方法中,算子应该释放所有占用的资源,进行最终的清理工作
3.1.4 三个生命周期方法总结
StreamOperator顶层接口准备了三个核心的生命周期方法:
-
open() - 算子初始化阶段
- 在任何数据处理之前调用
- 用于执行算子的初始化逻辑
-
finish() - 数据处理完成阶段
- 当数据即将要处理完成时调用
- 主要用于有界流/批处理任务
- 没有新数据进来了,算子应该刷新缓存的数据到下游
-
close() - 算子生命结束阶段
- 算子生命周期的最后阶段
- 用于资源清理和最终的收尾工作
这三个方法构成了算子完整的生命周期管理机制。
3.2 状态快照和恢复相关方法
StreamOperator接口还定义了三个与状态checkpoint和状态恢复相关的重要方法:
prepareSnapshotPreBarrier()
- checkpoint前的准备工作snapshotState()
- 执行状态快照initializeState()
- 状态初始化和恢复
这三个方法构成了算子状态管理的完整生命周期。
prepareSnapshotPreBarrier()方法
java
void prepareSnapshotPreBarrier(long checkpointId) throws Exception;
方法作用:
- 在执行状态快照之前的准备工作
- 算子在做checkpoint时需要将所有状态进行持久化
- 如果算子需要在快照前做特殊的准备工作(如刷新缓存、同步数据等),可以在此方法中实现
- 参数
checkpointId
标识当前checkpoint的唯一ID
snapshotState()方法
java
OperatorSnapshotFutures snapshotState(...)
方法作用:
- 正式执行状态快照操作
- 对算子内部使用的所有状态进行持久化
- 返回
OperatorSnapshotFutures
对象,包含异步快照操作的Future - 这是checkpoint过程中的核心方法,确保算子状态能够被正确保存
initializeState()方法
java
void initializeState(StreamTaskStateInitializer streamTaskStateManager)
方法作用:
- 用于算子状态的初始化和恢复
- 在以下场景中会被调用:
- 任务从故障中恢复
- 任务重启
- 从checkpoint恢复状态
- 参数
streamTaskStateManager
是状态初始化器,提供状态恢复的能力 - 算子可以利用这个初始化器来恢复之前保存的状态数据
3.3 Key上下文相关方法
setKeyContextElement方法
java
void setKeyContextElement1(StreamRecord<?> record) throws Exception;
void setKeyContextElement2(StreamRecord<?> record) throws Exception;
- 下面还有一个
setKeyContextElement
,setKeyContextElement
这个就是用来做那个叫做key切换的当前key切换的 - 但是它里面写了两个这个context是
setKeyContext
。因为有一些算子,它可能会有两个输入接收两个输入,所以它这边会有两个
设计上的考虑:
- StreamOperator接口既继承了KeyContext接口,又定义了自己的
setKeyContextElement
方法 - 这种设计看起来有些冗余,但有其原因:
- KeyContext提供了通用的key切换能力
setKeyContextElement1/2
是专门为多输入算子设计的
- 有些算子可能接收两个输入流,因此需要两个不同的key上下文切换方法
- 虽然设计上可以优化,但目前的架构已经稳定,保持现状
3.4 其他方法
getMetricGroup()方法
java
OperatorMetricGroup getMetricGroup();
- 再下面这个是跟它内部的那种叫做统计度量有关的,它所属的那个统计度量的组
- 这个你要去实现一下,这个跟核心逻辑无关
getOperatorID()方法
java
OperatorID getOperatorID();
方法作用:
- 所有具体的算子实现都必须实现这个方法
- 返回算子的唯一标识符(OperatorID)
- 算子ID在以下场景中非常重要:
- checkpoint状态的标识和恢复
- 算子链的构建和优化
- 监控和调试时的算子识别
- 这个ID在前面的实战案例中也有涉及
4. 总结
OK,这里就是整个StreamOperator这个顶层接口的一些相关的这种设计
这个接口虽然是算子体系的顶层接口,但它继承了其他重要接口,提供了:
- CheckpointListener - 让算子能监听checkpoint的完成和失败
- KeyContext - 提供key状态的自动切换机制
- Serializable - 支持算子的序列化传输
同时定义了算子的核心方法:
- 生命周期方法:open()、finish()、close()
- 状态管理方法:prepareSnapshotPreBarrier()、snapshotState()、initializeState()
- Key切换方法:setKeyContextElement1()、setKeyContextElement2()
- 其他方法:getOperatorID()、getMetricGroup()