前言
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等是兩個概念,在這裏終於匯合。