前言
Flink documentation 中 “work with state” 中提到了Flink的状态管理机制。实现思想来源于Chandy-Lamport的分布式快照算法。分别对理论和源码了解后,发现Flink其实是算法的一个极简实现。具体来说一下怎么来简化实现的。
Chandy-Lamport 分布式快照算法熟肉版
文章中通过Token传递的两个典型场景来分析分布式快照应该遵循的法则:
- Sender Record状态时,已发送的数量和Channel Record时已经接受数量一致;
- Receiver Record状态时,已经接受的数量和Channel Record时已经发送的数量一致;
关于模型本身不多说,适用于典型的数据有向传输场景。基于以上的规则,C-L给出了解决办法:
- Sender record状态前,发送一个Marker;
- Receiver 除了record自己的状态,还record Channel的状态,cause Channel没有实体承载计算逻辑。 具体细分两种场景:
- 收到marker时,还没有record,则记录channel为空,触发receiver record;
- 收到marker时,已经record过了,则记录channel的状态为从Record后到marker的所有记录,本质上是补充了一份record时刻Channel的状态;
论文本身后面都是在证明结果时有效的,不赘述,赶紧结合Flink来看看。
Flink的简化实现
首先Flink肯定保留了marker的思想,具体对应于Flink中的barrier。其次,我们可以从C-L算法中可以figure out Receiver是可以自主触发record的,Flink中各Operator是不能自主触发Record的。这是关键。看带来的效果:
- 在非主动触发的情况下,我们可以不用记录Channel的状态,按照C-L的算法,Channel中必然是空的。
- Operator到底需要记录什么?这个答案开始明确,需要记录的是Operator中持有的State,如果不带State完,其实啥都不需要记录。
- Source的State略有不同。因为Source更关注的是整流Restart后从哪个点读取数据,例如记录kafka的offset。
Flink State数据持久化
First of all,区分两个概念:State和 StateBackend:
- State,表示我们记录的状态数据;
- StateBackend,表示State具体的存储方式;
明白记录的是什么,怎么记录之后,就可以很happy的去撸源码了。日常State有哪些类型?ValueState、ListState、MapState、ReduceState、OperatorState。其实可以直接去找State的写入,也就是在update是怎么做的。
HeapValueState的存储
拿最简单的Heap方式记录来看,对应的是HeapValueState
// From HeapValueState.java
@Override
public void update(V value) {
if (value == null) {
clear();
return;
}
stateTable.put(currentNamespace, value);
}
stateTable一般是在RichOperator open的时候注册上的,看这个类。KeyedStateFactory.createInternalState,根据存储类型不同会有不同的实现。贴个调用栈吧:
可以看到HeapValue的存储,就简单的用了内存的数据结构,关于StateTable的实现,先挖坑(已填坑,Flink状态管理(二)状态数据结构和注册流程)。继续看看RocksDB的玩法
RocksDBValueState 数据持久化
HeapValueState将数据放在内存中,HeapRocksDB则是通过RocksDB的接口将数据写入到RocksDB中。
// From RocksDBValueState
@Override
public void update(V value) {
if (value == null) {
clear();
return;
}
DataOutputViewStreamWrapper out = new DataOutputViewStreamWrapper(keySerializationStream);
try {
writeCurrentKeyWithGroupAndNamespace();
byte[] key = keySerializationStream.toByteArray();
keySerializationStream.reset();
valueSerializer.serialize(value, out);
backend.db.put(columnFamily, writeOptions, key, keySerializationStream.toByteArray());
} catch (Exception e) {
throw new FlinkRuntimeException("Error while adding data to RocksDB", e);
}
}
和HeapValueState的区别在于,当State做Update操作的时候,需要将数据序列化之后存储到RocksDB中去。
雷点
采用RocksDB的方式,如果更新很频繁,会带来大量的序列化动作,吃CPU的叻。后面也看到了RocksDB的增量更新等玩法。同时,也心知肚明要想存储到外部系统,序列化的动作少不了。怎么办?首先你要指定好序列化器,给TypeHint,再者,用Tuples代替POJO是序列化相关包治百病的处理方式。再者,我其实没有用过RocksDB存储状态啦,欢迎测试过的小伙伴给我点结果。
Last
本文基于最简单的ValueState迁出了State的实现。于自己于他人,都是抛砖引玉。知道ValueState怎么玩的了,其他的都是一个玩法。曾今以为Flink大名鼎鼎的“有状态”和ValueState等是两个概念,在这里终于汇合。