Flink状态管理(一) 原理和数据持久化

前言

Flink documentation 中 “work with state” 中提到了Flink的状态管理机制。实现思想来源于Chandy-Lamport的分布式快照算法。分别对理论和源码了解后,发现Flink其实是算法的一个极简实现。具体来说一下怎么来简化实现的。

Chandy-Lamport 分布式快照算法熟肉版

文章中通过Token传递的两个典型场景来分析分布式快照应该遵循的法则:

  1. Sender Record状态时,已发送的数量和Channel Record时已经接受数量一致;
  2. Receiver Record状态时,已经接受的数量和Channel Record时已经发送的数量一致;

关于模型本身不多说,适用于典型的数据有向传输场景。基于以上的规则,C-L给出了解决办法:

  1. Sender record状态前,发送一个Marker;
  2. Receiver 除了record自己的状态,还record Channel的状态,cause Channel没有实体承载计算逻辑。 具体细分两种场景:
    1. 收到marker时,还没有record,则记录channel为空,触发receiver record;
    2. 收到marker时,已经record过了,则记录channel的状态为从Record后到marker的所有记录,本质上是补充了一份record时刻Channel的状态;

论文本身后面都是在证明结果时有效的,不赘述,赶紧结合Flink来看看。

Flink的简化实现

首先Flink肯定保留了marker的思想,具体对应于Flink中的barrier。其次,我们可以从C-L算法中可以figure out Receiver是可以自主触发record的,Flink中各Operator是不能自主触发Record的。这是关键。看带来的效果:

  1. 在非主动触发的情况下,我们可以不用记录Channel的状态,按照C-L的算法,Channel中必然是空的。
  2. Operator到底需要记录什么?这个答案开始明确,需要记录的是Operator中持有的State,如果不带State完,其实啥都不需要记录。
  3. Source的State略有不同。因为Source更关注的是整流Restart后从哪个点读取数据,例如记录kafka的offset。

Flink State数据持久化

First of all,区分两个概念:State和 StateBackend:

  1. State,表示我们记录的状态数据;
  2. 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,根据存储类型不同会有不同的实现。贴个调用栈吧:
创建State的调用栈
可以看到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等是两个概念,在这里终于汇合。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章