寫在前面:我是「雲祁」,一枚熱愛技術、會寫詩的大數據開發猿。暱稱來源於王安石詩中一句
[ 雲之祁祁,或雨於淵 ]
,甚是喜歡。
寫博客一方面是對自己學習的一點點總結及記錄,另一方面則是希望能夠幫助更多對大數據感興趣的朋友。如果你也對數據中臺、數據建模、數據分析以及Flink/Spark/Hadoop/數倉開發
感興趣,可以關注我的動態 https://blog.csdn.net/BeiisBei ,讓我們一起挖掘數據的價值~
每天都要進步一點點,生命不是要超越別人,而是要超越自己! (ง •_•)ง
文章目錄
一、前言
流式計算分爲無狀態和有狀態兩種情況。無狀態的計算觀察每個獨立事件,並根據最後一個事件輸出結果。例如,流處理應用程序從傳感器接收溫度讀數,並在溫度超過 90 度時發出警告。有狀態的計算則會基於多個事件輸出結果。以下是一些例子。
- 所有類型的窗口。例如,計算過去一小時的平均溫度,就是有狀態的計算。
- 所有用於複雜事件處理的狀態機。例如,若在一分鐘內收到兩個相差 20 度以上的溫度讀數,則發出警告,這是有狀態的計算。
- 流與流之間的所有關聯操作,以及流與靜態表或動態表之間的關聯操作,都是有狀態的計算。
下圖展示了無狀態流處理和有狀態流處理的主要區別。無狀態流處理分別接收每條數據記錄(圖中的黑條),然後根據最新輸入的數據生成輸出數據(白條)。有狀態流處理會維護狀態(根據每條輸入記錄進行更新),並基於最新輸入的記錄和當前的狀態值生成輸出記錄(灰條)。
上圖中輸入數據由黑條表示。無狀態流處理每次只轉換一條輸入記錄,並且僅根據最新的輸入記錄輸出結果(白條)。有狀態流處理維護所有已處理記錄的狀態值,並根據每條新輸入的記錄更新狀態,因此輸出記錄(灰條)反映的是綜合考慮多個事件之後的結果。
儘管無狀態的計算很重要,但是流處理對有狀態的計算更感興趣。事實上,正確地實現有狀態的計算比實現無狀態的計算難得多。舊的流處理系統並不支持有狀態的計算,而新一代的流處理系統則將狀態及其正確性視爲重中之重。
二、有狀態的算子和應用程序
Flink 內置的很多算子,數據源 source,數據存儲 sink 都是有狀態的,流中的數據都是 buffer records,會保存一定的元素或者元數據。例如: ProcessWindowFunction會緩存輸入流的數據,ProcessFunction 會保存設置的定時器信息等等。
在 Flink 中,狀態始終與特定算子相關聯,爲了使運行時的Flink瞭解算子的狀態,算子需要預先註冊其狀態。總的來說,有兩種類型的狀態:
- 算子狀態(operator state)
- 鍵控狀態(keyed state)
2.1 算子狀態(operator state)
算子狀態的作用範圍限定爲算子任務。這意味着由同一並行任務所處理的所有數據都可以訪問到相同的狀態,狀態對於同一任務而言是共享的。算子狀態不能由相同或不同算子的另一個任務訪問。
Flink 爲算子狀態提供三種基本數據結構:
- 列表狀態(List state)
將狀態表示爲一組數據的列表。 - 聯合列表狀態(Union list state)
也將狀態表示爲數據的列表。它與常規列表狀態的區別在於,在發生故障時,或者從保存點(savepoint)啓動應用程序時如何恢復。 - 廣播狀態(Broadcast state)
如果一個算子有多項任務,而它的每項任務狀態又都相同,那麼這種特殊情況最適合應用廣播狀態。
2.2 鍵控狀態(keyed state)
鍵控狀態是根據輸入數據流中定義的鍵(key)來維護和訪問的。Flink 爲每個鍵值維護一個狀態實例,並將具有相同鍵的所有數據,都分區到同一個算子任務中,這個任務會維護和處理這個 key 對應的狀態。當任務處理一條數據時,它會自動將狀態的訪問範圍限定爲當前數據的 key。因此,具有相同 key 的所有數據都會訪問相同的狀態。Keyed State 很類似於一個分佈式的 key-value map 數據結構,只能用於KeyedStream(keyBy 算子處理之後)。
Flink 的 Keyed State 支持以下數據類型:
-
值狀態 ValueState[T] 保存單個的值,值的類型爲 T。
- get 操作: ValueState.value()
- set 操作: ValueState.update(value: T)
-
列表狀態 ListState[T] 保存一個列表,列表裏的元素的數據類型爲 T。
- ListState.add(value: T)
- ListState.addAll(values: java.util.List[T])
- ListState.get()返回 Iterable[T]
- ListState.update(values: java.util.List[T])
-
映射狀態 MapState[K, V] 保存 Key-Value 對。
- MapState.get(key: K)
- MapState.put(key: K, value: V)
- MapState.contains(key: K)
- MapState.remove(key: K)
-
聚合狀態 講狀態表示爲一個用於聚合操作的列表。
- ReducingState[T]
- AggregatingState[I, O]
State.clear()是清空操作。
2.3 鍵控狀態的使用
2.4 狀態後端(State Backends)
- 每傳入一條數據,有狀態的算子任務都會讀取和更新狀態
- 由於有效的狀態訪問對於處理數據的低延遲至關重要,因此每個並行任務都會在本地維護其狀態,以確保快速的狀態訪問。
- 狀態的存儲、訪問以及維護,由一個可插入的組件決定,這個組件就叫做狀態後端(state backend)
- 狀態後端主要負責兩件事:本地的狀態管理,以及將檢查點(checkpoint)狀態寫入遠程存儲
2.5 選擇一個狀態後端
-
MemoryStateBackend
- 內存級的狀態後端,會將鍵控狀態作爲內存中的對象進行管理,將它們存儲在TaskManager 的 JVM 堆上,而將 checkpoint 存儲在 JobManager 的內存中
- 特點:快速、低延遲,但是不穩定 。所以實際生產中不會使用。
-
FsStateBackend
- 將 checkpoint 存儲到遠程的持久化文件系統 (FileSystem)上,而對於本地狀態,跟 MemoryStateBackend 一樣,也會存在 TaskManager 的 JVM 堆上
- 同時擁有內存級的本地訪問速度,和更好的容錯保證
-
RocksDBStateBackend
- 將所有狀態序列化後,存入本地的 RocksDB 中存儲。
注意:RocksDB 的支持並不直接包含在 flink 中,需要引入依賴:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.7.2</version>
</dependency>
設置狀態後端爲 FsStateBackend:
val env = StreamExecutionEnvironment.getExecutionEnvironment
val checkpointPath: String = ???
val backend = new RocksDBStateBackend(checkpointPath)
env.setStateBackend(backend)
env.setStateBackend(new FsStateBackend("file:///tmp/checkpoints"))
env.enableCheckpointing(1000)
// 配置重啓策略
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(60, Time.of(10,
TimeUnit.SECONDS)))
三、狀態編程
val sensorData: DataStream[SensorReading] = ...
val keyedData: KeyedStream[SensorReading, String] = sensorData.keyBy(_.id)
val alerts: DataStream[(String, Double, Double)] = keyedData
.flatMap(new TemperatureAlertFunction(1.7))
class TemperatureAlertFunction(val threshold: Double) extends
RichFlatMapFunction[SensorReading, (String, Double, Double)] {
private var lastTempState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
val lastTempDescriptor = new ValueStateDescriptor[Double]("lastTemp",classOf[Double])
lastTempState = getRuntimeContext.getState[Double](lastTempDescriptor)
}
override def flatMap(reading: SensorReading,out: Collector[(String, Double, Double)]): Unit = {
val lastTemp = lastTempState.value()
val tempDiff = (reading.temperature - lastTemp).abs
if (tempDiff > threshold) {
out.collect((reading.id, reading.temperature, tempDiff))
}
this.lastTempState.update(reading.temperature)
}
}
通過 RuntimeContext 註冊 StateDescriptor。StateDescriptor 以狀態 state 的名字和存儲的數據類型爲參數。
在 open()方法中創建 state 變量。注意複習之前的 RichFunction 相關知識。
接下來我們使用了 FlatMap with keyed ValueState 的快捷方式 flatMapWithState
實現以上需求。
val alerts: DataStream[(String, Double, Double)] = keyedSensorData
.flatMapWithState[(String, Double, Double), Double] {
// 如果沒有狀態的話,也就是沒有數據來過,那麼就將當前的數據溫度存入狀態
case (in: SensorReading, None) => (List.empty, Some(in.temperature))
// 如果有狀態,就應該與上次的溫度值比較差值,如果大於閾值就輸出報警
case (r: SensorReading, lastTemp: Some[Double]) =>
val tempDiff = (r.temperature - lastTemp.get).abs
if (tempDiff > 1.7) {
(List((r.id, r.temperature, tempDiff)),Some(r.temperature))
} else {
(List.empty, Some(r.temperature))
}
}