Flink 常見問題總結

xingdianp發表於2020-11-27

Flink 問題總結

目錄
Flink 問題總結
作業執行流程
Watermark
基礎
原理擴充套件
State
基礎
實踐
CheckPoint/SavePoint
基礎
配置
實踐
原理擴充套件
Back Pressure
基礎
處理
原理擴充套件
語義/exactly-once
基礎
使用
原理擴充套件
資源管理
Mechine Learning/AI
基礎

作業執行流程

新增的 operator 會被 transform 封裝,例如 map(udf) -> OneInputTransformation,裡面有序列化的 udf和operator配置(名稱、uid、並行度等),並記錄前一個 transformation 作為輸入。

當 execute 被呼叫,client 先遍歷 transformation 構造 StreamGraph -> JobGraph -> 合併 chain 最後到達 JM。JM 將其翻譯為 ExecutionGraph。ExecutionJobVertex 有一個或多個並行度且可能被排程和執行多次,其中一個並行度的一次執行稱為 Execution,JobManager 的 Scheduler 會為每個 Execution 分配 slot。

Watermark

基礎
Process 和 Event 的選擇:是否需要重現。

watermark代表了 timestamp 數值,表示以後到來的資料已經再也沒有小於或等於這個時間的了。

生成方式:

SourceFunction:collectWithTimestamp 或 emitWatermark
Stream:assignTimestampsAndWatermarks
定期
資料特徵
傳播方式:廣播,單輸入取其大,多輸入取其小(因此多次輸入相同的 watermark,並不會影響當前的 watermark)

缺陷:對於同一個流的不同 partition,我們對他做這種強制的時鐘同步是沒有問題的,因為一開始就把一條流拆散成不同的部分,但每一個部分之間共享相同的時鐘(多輸入取其小)。但是 JOIN 流中,多流強制同步時鐘,對於快慢流關聯就要求快流快取大量資料等待慢流。

watermark處理:operator 先更新 watermark,然後遍歷計時器觸發 trigger,watermark 傳送下游。

Table API 中的時間

processing time 可以從一個 DataStream,把增加一列為時間來轉化成一個 Table,或直接通過 TableSource 定義 DefinedRowtimeAttributes 生成
event time:需要保證 DataStream 中已經存在 Record Timestamp 和 watermark,從 TableSource 生成,也需要已經有 long 欄位。
操作:over window、group by、window join、order(對一個 DataStream 轉化成 Table 進行排序的話,只能是按照時間列進行排序,當然同時也可以指定一些其他的列,但是時間列這個是必須的,並且必須放在第一位)。這些操作在flink底層都是按照時間列掃描計算的,這也是流處理的特點或者相對於批處理的劣勢。掃描過程中積累的狀態不能無限增長是流處理的前提(其實批處理也一樣,但批處理模型在這方面的效能應該好些)。

原理擴充套件
在 event-time 場景下,如果 source 沒有收到資料,那麼 watermark 就有可能停滯,這裡有兩種情況:

source 某個 partition 沒有新資料

此時 source function 可以呼叫 sourceContext.markAsTemporarilyIdle() 來把該 partition 設定為 idle,在這之後的 watermark 生成機制就不會考慮這個停滯了的當前 watermark,進而讓 operator 隨著 active partition 的最小 watermark 繼續推進。

原始碼可參考:

SourceFunction.markAsTemporarilyIdle(), StreamStatus, StreamTaskNetworkInput.processElement, StatusWatermarkValve.inputStreamStatus 和 inputWatermark

整個 source 沒有資料

這種情況就要考慮 AssignerWithPeriodicWatermarks,使用者自己判斷多久的 idle 後,把 event-time 改為某種 process-time 形式的推進。

參考:

Apache Flink 進階(二):時間屬性深度解析

State
基礎

在這裡插入圖片描述

backend分類(https://img-blog.csdnimg.cn/20201127232622890.png)

預設使用 memory backend,不管執行state還是 checkpoint 資料都儲存在 JM heap(如果JM掛了,連 cp 都不存在了),生產環境正常情況只考慮 file 和 rocksdb backend。其中 file 執行時 state 儲存在 heap,效能更好,但有 OOM 風險,且不支援增量 checkpoint;rocksdb 不管執行還是最終 checkpoint 資料都在資料庫中,無需擔心 OOM。

HeapKeyedStateBackend 有兩種實現:
支援非同步 Checkpoint(預設):儲存格式 CopyOnWriteStateMap
僅支援同步 Checkpoint:儲存格式 NestedStateMap
RocksDBKeyedStateBackend,每個 state 都儲存在一個單獨的 column family 內,其中 keyGroup,Key 和 Namespace 進行序列化儲存在 DB 作為 key。
RocksDB StateBackend 概覽和相關配置討論

所有儲存的 key,value 均被序列化成 bytes 進行儲存。

在這裡插入圖片描述

在 RocksDB 中,每個 state 獨享一個 Column Family,而每個 Column family 使用各自獨享的 write buffer 和 block cache,上圖中的 window state 和 value state實際上分屬不同的 column family。

對效能比較有影響的引數配置
在這裡插入圖片描述

實踐
小心儲存大量元素到 operator state

operator state 的結構是一個 list,由於沒有 key group,為了實現改併發恢復的功能,需要對 operator state 中的每一個序列化後的元素儲存一個位置偏移 offset。這個 offset 是一個 long 陣列,但數量一大,這個 offset 資料就會很大。在 checkpoint 的時候,JM 需要接收這個 offset 陣列作為原資料,進而引起 JM 的記憶體超用問題。

UnionListState的使用

從檢查點恢復之後每個併發 task 內拿到的是原先所有operator 上的 state。切記恢復的 task 只取其中的一部分進行處理和用於下一次 snapshot,否則有可能隨著作業不斷的重啟而導致 state 規模不斷增長。

keyed state 的清空

state.clear() 實際上只能清理當前 key 對應的 value 值,如果想要清空整個 state,需要藉助於 applyToAllKeys 方法。如果需求中只是對 state 有過期需求,藉助於 state TTL 功能來清理會是一個效能更好的方案。

RocksDB執行

預設使用 Flink managed memory 方式的情況下,state.backend.rocksdb.metrics.block-cache-usage ,state.backend.rocksdb.metrics.mem-table-flush-pending,state.backend.rocksdb.metrics.num-running-compactions 以及 state.backend.rocksdb.metrics.num-running-flushes 是比較重要的相關 metrics。

Flink-1.10 之後,由於引入了 RocksDB 的記憶體託管機制,在絕大部分情況下, RocksDB 的這一部分 native 記憶體是可控的,不過受限於 RocksDB 的相關 cache 實現限制,在某些場景下,無法做到完美控制,這時候建議開啟上文提到的 native metrics,觀察相關 block cache 記憶體使用是否存在超用情況,可以將相關記憶體新增到 taskmanager.memory.task.off-heap.size 中,使得 Flink 有更多的空間給 native 記憶體使用。

大狀態

大狀態基本只考慮 RocksDB。

將state進行拆分,使用 MapState 來替代 ListState 或者 ValueState,因為RocksDB 的 map state 並不是將整個 map 作為 value 進行儲存,而是將 map 中的一個條目作為鍵值對進行儲存。

SSD磁碟

多硬碟分擔壓力(單塊磁碟的多個目錄無意義):在 flink-conf.yaml 中配置 state.backend.rocksdb.localdir 引數來指定 RocksDB 在磁碟中的儲存目錄。當一個 TaskManager 包含 3 個 slot 時,那麼單個伺服器上的三個並行度都對磁碟造成頻繁讀寫,從而導致三個並行度的之間相互爭搶同一個磁碟 io,這樣務必導致三個並行度的吞吐量都會下降。Flink 的 state.backend.rocksdb.localdir 引數可以指定多個目錄,一般大資料伺服器都會掛載很多塊硬碟,我們期望同一個 TaskManager 的三個 slot 使用不同的硬碟從而減少資源競爭。

state.backend.rocksdb.localdir: /data1/flink/rocksdb,/data2/flink/rocksdb

對於硬碟較少的情況,flink 預設的隨機策略容易碰撞,考慮採用自定義磁碟選擇策略,比如輪訓。具體參考下面的 flink 大狀態優化。

參考:

Flink State 最佳實踐

Flink大狀態的優化

CheckPoint/SavePoint
基礎
flink 的一致性快照,實際是當前 state 的資料快照。

flink 會週期性地進行 cp,且過程是非同步的,所以 cp 期間仍可以處理資料。 cp 資料根據 statebackend 的不同會儲存到不同的地方。

原理:失敗/暫停重啟時,flink 會根據最新成功 cp 的資料來初始化重啟的 state,這個 state 包括 source 中記錄的消費位移,從而讓整個 flink 狀態回到該 cp 完成的那一刻。

分散式快照

JM 的 Checkpoint Coordinator 向所有 source 節點 trigger Checkpoint
source 接收到 cp barrier 後,觸發本地 state 的 cp,將資料儲存到持久儲存,將備份資料的地址(state handle)通知給 Checkpoint coordinator,然後廣播 cp barrier 到下游。當下遊獲得其中一個CB時,就會暫停處理這個CB對應的資料,並將這些資料存到緩衝區,直到其他相同ID的CB都到齊(checkpoint barrier 對齊),就會觸發本地 state 的 cp,並廣播 cp barrier,然後處理快取的資料。最後,當所有 task 的 state handle 都被 Checkpoint Coordinator 收集,本次 cp 就算是完成了,最後向持久化儲存中再備份一個 Checkpoint meta 檔案。

在這裡插入圖片描述

優化:

RocksDB的非同步checkpoint:首先 RocksDB 會全量刷資料到磁碟上,然後 Flink 框架會從中選擇沒有上傳的檔案進行持久化備份。後臺執行緒非同步傳送快照到遠端storage。
如果是 at-least-once,就不會進行 checkpoint barrier 對齊。
checkpoint 和 savepoint

在這裡插入圖片描述

配置
間隔不宜太短,預設情況,如果一個 cp 時間超過 cp 觸發間隔,那麼這個 cp 一旦完成,就會馬上出發下一次 cp。可以考慮設定env.getCheckpointConfig().setMinPauseBetweenCheckpoints(milliseconds)
大 state 要適當增加超時時間(預設10min)
實踐
cp 失敗排查

cp webui 介面

Acknowledged 一列表示有多少個 subtask 對這個 Checkpoint 進行了 ack

Latest Acknowledgement 表示該 operator 的所有 subtask 最後 ack 的時間;

End to End Duration 表示整個 operator 的所有 subtask 中完成 snapshot 的最長時間;

State Size 表示當前 Checkpoint 的 state 大小 – 主要這裡如果是增量 checkpoint 的話,則表示增量大小;

Buffered During Alignment 表示在 barrier 對齊階段積攢了多少資料,如果這個資料過大也間接表示對齊比較慢);

失敗原因

Checkpoint Decline

// JM日誌
Decline checkpoint 10423 by task 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178
// 可以從 JM日誌 查詢 0b60f08bf8984085b59f8d9bc74ce2e1 被分到哪個 TM
org.apache.flink.runtime.executiongraph.ExecutionGraph        - XXXXXXXXXXX (100/289) (87b751b1fd90e32af55f02bb2f9a9892) switched from SCHEDULED to DEPLOYING.
org.apache.flink.runtime.executiongraph.ExecutionGraph        - Deploying XXXXXXXXXXX (100/289) (attempt #0) to slot container_e24_1566836790522_8088_04_013155_1 on hostnameABCDE
// 從上面日誌可以確定被排程到 hostnameABCDE 的 container_e24_1566836790522_8088_04_013155_1 slot,當相應 TM 檢視日誌。
Checkpoint cancel

當前 Flink 中如果較小的 Checkpoint 還沒有對齊的情況下,收到了更大的 Checkpoint,則會把較小的 Checkpoint 給取消掉。

Checkpoint Expire

// 由下面日誌可知,參考上面 Checkpoint Decline 的方法找到對應的 TM 日誌。
Received late message for now expired checkpoint attempt 1 from 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178.
開啟debug後,可以通過日誌分析出慢在哪個階段。

// barrier 對齊後,準備 cp
Starting checkpoint (6751) CHECKPOINT on task taskNameWithSubtasks
// 同步階段
org.apache.flink.runtime.state.AbstractSnapshotStrategy       - DefaultOperatorStateBackend snapshot (FsCheckpointStorageLocation {fileSystem=org.apache.flink.core.fs.SafetyNetWrapperFileSystem@70442baf, checkpointDirectory=xxxxxxxx, sharedStateDirectory=xxxxxxxx, taskOwnedStateDirectory=xxxxxx, metadataFilePath=xxxxxx, reference=(default), fileStateSizeThreshold=1024}, synchronous part) in thread Thread[Async calls on Source: xxxxxx
_source -> Filter (27/70),5,Flink Task Threads] took 0 ms.
// 非同步階段
... asynchronous part) in thread Thread[pool-48-thread-14,5,Flink Task Threads] took 369 ms

分析原因:

source trigger 慢

搶不到鎖:一般不會,在舊版可能因為搶不到鎖,如果對應 TM 沒有 準備 cp 日誌,則可以考慮這種情況,並用 jstack 分析鎖情況。在新版已經使用 mailBox 優化。
反壓或資料傾斜
主執行緒cpu消耗太高:在 task 端,所有的處理都是單執行緒的,資料處理和 barrier 處理都由主執行緒處理,如果主執行緒在處理太慢(比如使用 RocksDBBackend,state 操作慢導致整體處理慢),導致 barrier 處理的慢,也會影響整體 Checkpoint 的進度,在這一步我們需要能夠檢視某個 PID 對應 hotmethod。使用工具 AsyncProfile dump 一份火焰圖,檢視佔用 CPU 最多的棧
同步階段慢

同步階段一般不會太慢,但是如果我們通過日誌發現同步階段比較慢的話,對於非 RocksDBBackend 我們可以考慮檢視是否開啟了非同步 snapshot,如果開啟了非同步 snapshot 還是慢,需要看整個 JVM 在幹嘛,也可以使用前一節中的工具。對於 RocksDBBackend 來說,我們可以用 iostate 檢視磁碟的壓力如何,另外可以檢視 tm 端 RocksDB 的 log 的日誌如何,檢視其中 SNAPSHOT 的時間總共開銷多少。

非同步慢

對於非同步階段來說,tm 端主要將 state 備份到持久化儲存上,對於非 RocksDBBackend 來說,主要瓶頸來自於網路,這個階段可以考慮觀察網路的 metric,或者對應機器上能夠觀察到網路流量的情況(比如 iftop)。

對於 RocksDB 來說,則需要從本地讀取檔案,寫入到遠端的持久化儲存上,所以不僅需要考慮網路的瓶頸,還需要考慮本地磁碟的效能。另外對於 RocksDBBackend 來說,如果覺得網路流量不是瓶頸,但是上傳比較慢的話,還可以嘗試考慮開啟多執行緒上傳功能

cp 超時的排查(可以結合下面反壓的排查思路)

Barrier對齊,由於某些
檢視 JM 日誌,看是哪些 task 的問題,有可能是資料傾斜等原因。
狀態大,非同步狀態遍歷和寫hdfs耗時:考慮使用 RocksDB 的增量 cp,考慮 state 是否可以用 mapstate 優化。
cp 執行情況

通過 ui 可以計算公式 end_to_end_duration - synchronous_duration - asynchronous_duration = checkpoint_start_delay。如果計算結果通常比較大,那說明 checkpoint barrier 不能暢通地流經所有的 operator,有可能有反壓存在。
對於 exactly-once,如果快取佇列在 cp 時很高,那說明 operator 處理資料的效率不均,可能資料傾斜。
上面兩個數值如果一直高,那很可能是 cp 本身的問題。

Tuning Checkpoints and Large State(未完)

原理擴充套件
增量 checkpoint

Flink 的增量 checkpoint 以 RocksDB 的 checkpoint 為基礎。RocksDB 把所有的修改儲存在記憶體的可變快取中(稱為 memtable),所有對 memtable 中 key 的修改,會覆蓋之前的 value,當前 memtable 滿了之後,RocksDB 會將所有資料以有序的寫到磁碟。當 RocksDB 將 memtable 寫到磁碟後,整個檔案就不再可變,稱為有序字串表(sstable)。RocksDB 的後臺壓縮執行緒會將 sstable 進行合併,就重複的鍵進行合併,合併後的 sstable 包含所有的鍵值對,RocksDB 會刪除合併前的 sstable。

在這個基礎上,Flink 會記錄上次 checkpoint 之後所有新生成和刪除的 sstable,另外因為 sstable 是不可變的,Flink 用 sstable 來記錄狀態的變化。為此,Flink 呼叫 RocksDB 的 flush,強制將 memtable 的資料全部寫到 sstable,並硬鏈到一個臨時目錄中。這個步驟是在同步階段完成,其他剩下的部分都在非同步階段完成,不會阻塞正常的資料處理。

Flink 將所有新生成的 sstable 備份到持久化儲存(比如 HDFS,S3),並在新的 checkpoint 中引用。Flink 並不備份前一個 checkpoint 中已經存在的 sstable,而是引用他們。Flink 還能夠保證所有的 checkpoint 都不會引用已經刪除的檔案。

在這裡插入圖片描述

增量 checkpoint 可以減少 checkpoint 的總時間,但是也可能導致恢復的時候需要更長的時間。(從上面的流程可知,被持久化的sstable會包含未被刪除等多餘資料,所以在恢復時,TM 下載的state資料量更大,再要經過一次全體的merge才能做到去重,才能最終用於state的初始化)

checkpoint原始碼

JM CheckpointCoordinator trigger checkpoint
Source 收到 trigger checkpoint 的 PRC,並往下游傳送 barrier,自己開始做 snapshot。
下游接收 barrier(需要 barrier 都到齊才會開始做 checkpoint)
Task 開始同步階段 snapshot
Task 開始非同步階段 snapshot
Task snapshot 完成,彙報給 JM

ExecutionGraphBuilder.buildGraph() {
	if config checkpoint {
		executionGraph.enableCheckpointing() {
			checkpointCoordinator.createActivatorDeactivator
		}
	}
}

// ActivatorDeactivator 這個物件在 JobStatus 變為 RUNNING 時會呼叫 
coordinator.startCheckpointScheduler(){
	scheduleTriggerWithDelay(){
		return timer.scheduleAtFixedRate(new ScheduledTrigger()...) // 返回一個 ScheduledFuture
	}
}

// 上面的 ScheduledTrigger 會在設定的時候執行 run 方法,這個方法就是 triggerCheckpoint
// CheckpointCoordinator 在例項化時就被傳入下面三個陣列(buildGraph時生成)
/** Tasks who need to be sent a message when a checkpoint is started. */
private final ExecutionVertex[] tasksToTrigger;
/** Tasks who need to acknowledge a checkpoint before it succeeds. */
private final ExecutionVertex[] tasksToWaitFor;
/** Tasks who need to be sent a message when a checkpoint is confirmed. */
private final ExecutionVertex[] tasksToCommitTo;
{
	檢查 tasksToTrigger、tasksToWaitFor 陣列,看 task 是否都符合 checkpoint 條件
	for execution {
		triggerCheckpoint(checkpointID, timestamp, checkpointOptions) {
			taskManagerGateway.triggerCheckpoint(){
				taskExecutorGateway.triggerCheckpoint(){ // rpc呼叫,tm 封裝了 te
					task.triggerCheckpointBarrier(){
						invokable.triggerCheckpointAsync
					}
				}
			}
		}
	}
}

// source function
SourceStreamTask.triggerCheckpointAsync(){
	mailboxProcessor.getMainMailboxExecutor().submit(() -> triggerCheckpoint(){
		StreamTask.performCheckpoint(){
			prepareSnapshotPreBarrier()
			broadcastCheckpointBarrier()
			checkpointState(){
				storage = checkpointStorage.resolveCheckpointStorageLocation
				new CheckpointingOperation.executeCheckpointing {
					// synchronous checkpoints
					for (StreamOperator<?> op : allOperators) {
						checkpointStreamOperator(op);
					}
					// asynchronous part
					StreamTask.asyncOperationsThreadPool.execute(new AsyncCheckpointRunnable);
				}
			}
		}
	});
}

// non-source function. The CheckpointedInputGate uses CheckpointBarrierHandler to handle incoming CheckpointBarrier from the InputGate.
pollNext(){
	if (bufferOrEvent.getEvent().getClass() == CheckpointBarrier.class) {
		CheckpointBarrierAligner.processBarrier() {
			// regular case
			onBarrier(channelIndex) {
				blockedChannels[channelIndex] = true;
			}
			// 判斷是否已經全部對齊
			notifyCheckpoint() {
				toNotifyOnCheckpoint.triggerCheckpointOnBarrier() {
					performCheckpoint() // 
				}
			}
		}
	}
}

參考:

Apache Flink 進階(三):Checkpoint 原理剖析與應用實踐

《Stream Processing with Apache Flink》

Apache Flink 管理大型狀態之增量 Checkpoint 詳解

Tuning Checkpoints and Large State

Flink Checkpoint 問題排查實用指南

Back Pressure

基礎
概念:資料管道中某個節點處理速率跟不上上游傳送資料的速率,而對上游,直到資料來源,進行限速。

原理:Flink 拓撲中每個節點(Task)間的資料都以阻塞佇列的方式傳輸,下游來不及消費導致佇列被佔滿後,上游的生產也會被阻塞,最終導致資料來源的攝入被阻塞。

在這裡插入圖片描述

影響:

潛在的效能瓶頸,可能導致更大的資料延遲。
增加 checkpoint 時長,因為 checkpoint barrier 不會超過普通資料,而資料的阻塞也導致 barrier 的阻塞。
在 exactly-once 下,state 變大,因為 checkpoint barrier 需要對齊,導致快的節點要等慢的節點,此時快的節點可能已經處理了很多資料,這些資料在慢節點完成 checkpoint 前都要被快取加到 state 中。(對於 heap-base statebackend 影響更大,可能 oom)
處理
定位:

基於網路的反壓 metrics 並不能定位到具體的 Operator,只能定位到 Task。

在這裡插入圖片描述

TaskManager 傳輸資料時,不同的 TaskManager 上的兩個 Subtask 間通常根據 key 的數量有多個 Channel,這些 Channel 會複用同一個 TaskManager 級別的 TCP 連結,並且共享接收端 Subtask 級別的 Buffer Pool。在接收端,每個 Channl 在初始階段會被分配固定數量的 Exclusive Buffer,這些 Buffer 會被用於儲存接受到的資料,交給 Operator 使用後再次被釋放。Channel 接收端空閒的 Buffer 數量稱為 Credit,Credit 會被定時同步給傳送端被後者用於決定傳送多少個 Buffer 的資料。在流量較大時,Channel 的 Exclusive Buffer 可能會被寫滿,此時 Flink 會向 Buffer Pool 申請剩餘的 Floating Buffer。這些 Floating Buffer 屬於備用 Buffer,哪個 Channel 需要就去哪裡。而在 Channel 傳送端,一個 Subtask 所有的 Channel 會共享同一個 Buffer Pool,這邊就沒有區分 Exclusive Buffer 和 Floating Buffer。

Metris描述
outPoolUsage傳送端 Buffer 的使用率
inPoolUsage接收端 Buffer 的使用率
floatingBuffersUsage(1.9 以上)接收端 Floating Buffer 的使用率
exclusiveBuffersUsage (1.9 以上)接收端 Exclusive Buffer 的使用率

inPoolUsage = floatingBuffersUsage + exclusiveBuffersUsage

當 outPoolUsage 和 inPoolUsage 使用率同低正常,同高被下游反壓,不同時,要麼是反壓傳遞階段,要麼就是反壓根源。如果一個 Subtask 的 outPoolUsage 是高,通常是被下游 Task 所影響,所以可以排查它本身是反壓根源的可能性。如果一個 Subtask 的 outPoolUsage 是低,但其 inPoolUsage 是高,則表明它有可能是反壓的根源。反壓有時是短暫的且影響不大,比如來自某個 Channel 的短暫網路延遲或者 TaskManager 的正常 GC。

通常來說,floatingBuffersUsage 為高則表明反壓正在傳導至上游,而 exclusiveBuffersUsage 則表明了反壓是否存在傾斜(floatingBuffersUsage 高、exclusiveBuffersUsage 低為有傾斜,因為少數 channel 佔用了大部分的 Floating Buffer)。
Web UI:提供了 SubTask 級別的反壓監控。

通過週期性對 Task 執行緒的棧資訊取樣,得到執行緒被阻塞在請求 Buffer(意味著被下游佇列阻塞)的頻率來判斷該節點是否處於反壓狀態。預設配置下,這個頻率在 0.1 以下則為 OK,0.1 至 0.5 為 LOW,而超過 0.5 則為 HIGH。問題有兩種可能:

節點的傳送速率跟不上它的產生資料速率(如 flatmap 中一條輸入產生多條輸出)
下游節點反壓導致。反壓皮膚監控的是傳送端,如果某個節點是效能瓶頸並不會導致它本身出現高反壓,而是導致它的上游出現高反壓。
如果我們找到第一個出現反壓的節點,那麼反壓根源要麼是就這個節點,要麼是它緊接著的下游節點。區分這兩種狀態需要結合上面的 metrics。

如果作業的節點數很多或者並行度很大,由於要採集所有 Task 的棧資訊,反壓皮膚的壓力也會很大甚至不可用。

分析

Web UI 各個 SubTask 的 Records Sent 和 Record Received
Checkpoint detail 裡不同 SubTask 的 State size
對 TaskManager 進行 CPU profile,從中我們可以分析到 Task Thread 是否跑滿一個 CPU 核,是的話,是哪個函式效率低;不是的話,哪裡阻塞。
未來的版本 Flink 將會直接在 WebUI 提供 JVM 的 CPU 火焰圖。

如果是記憶體或GC相關,可以啟動G1優化,加上 -XX:+PrintGCDetails 來觀察日誌。
原理擴充套件
首先 Producer Operator 從自己的上游或者外部資料來源讀取到資料後,對一條條的資料進行處理,處理完的資料首先輸出到 Producer Operator 對應的 NetWork Buffer 中。Buffer 寫滿或者超時或者特殊事件(如 checkpoint barrier)後,就會觸發將 NetWork Buffer 中的資料拷貝到 Producer 端 Netty 的 ChannelOutbound Buffer(嚴格來講,Output flusher 不提供任何保證——它只向 Netty 傳送通知,而 Netty 執行緒會按照能力與意願進行處理,所以即便觸發了 flush,也不一定傳送資料),之後又把資料拷貝到 Socket 的 Send Buffer 中,這裡有一個從使用者態拷貝到核心態的過程,最後通過 Socket 傳送網路請求,把 Send Buffer 中的資料傳送到 Consumer 端的 Receive Buffer。資料到達 Consumer 端後,再依次從 Socket 的 Receive Buffer 拷貝到 Netty 的 ChannelInbound Buffer,再拷貝到 Consumer Operator 的 NetWork Buffer,最後 Consumer Operator 就可以讀到資料進行處理了。這就是兩個 TaskManager 之間的資料傳輸過程,我們可以看到傳送方和接收方各有三層的 Buffer。

在這裡插入圖片描述

每個 Operator 計算資料時,輸出和輸入都有對應的 NetWork Buffer,這個 NetWork Buffer 對應到 Flink 就是圖中所示的 ResultSubPartition 和 InputChannel。ResultSubPartition 和 InputChannel 都是向 LocalBufferPool 申請 Buffer 空間,然後 LocalBufferPool 再向 NetWork BufferPool 申請記憶體空間。這裡,NetWork BufferPool 是 TaskManager 內所有 Task 共享的 BufferPool,TaskManager 初始化時就會向堆外記憶體申請 NetWork BufferPool。LocalBufferPool 是每個 Task 自己的 BufferPool,假如一個 TaskManager 內執行著 5 個 Task,那麼就會有 5 個 LocalBufferPool,但 TaskManager 內永遠只有一個 NetWork BufferPool。Netty 的 Buffer 也是初始化時直接向堆外記憶體申請記憶體空間。雖然可以申請,但是必須明白記憶體申請肯定是有限制的,不可能無限制的申請,我們在啟動任務時可以指定該任務最多可能申請多大的記憶體空間用於 NetWork Buffer。

Flink 1.5 後才用 credit 反壓機制,例如,上游 SubTask A.2 傳送完資料後,還有 5 個 Buffer 被積壓,那麼會把傳送資料和 Backlog size = 5 一塊傳送給下游 SubTask B.4,下游接受到資料後,知道上游積壓了 5 個Buffer,於是向 Buffer Pool 申請 Buffer,由於容量有限,下游 InputChannel 目前僅有 2 個 Buffer 空間,所以,SubTask B.4 會向上遊 SubTask A.2 反饋 Channel Credit = 2。然後上游下一次最多隻給下游傳送 2 個 Buffer 的資料,這樣每次上游傳送的資料都是下游 InputChannel 的 Buffer 可以承受的資料量。當可發資料為0時,上游會定期地僅傳送 backlog size 給下游,直到下游反饋大於0的 credit。

當然,有了這個機制也並不代表 Flink 能解決外部的反壓問題。比如 Flink 寫入 ES,而 ES 沒有反饋機制,那麼就會導致 ES 的 socket 被塞滿,甚至響應 timeout,結果任務就失敗了。Kafka 有反饋功能。

參考

一文搞懂 Flink 網路流控與反壓機制

如何分析及處理 Flink 反壓

語義/exactly-once
基礎
AT-MOST-ONCE:do nothing,資料丟失,適合準確度要求不高的。

AT-LEAST-ONCE:保證沒有資料丟失,即便對資料進行重複處理。適合計算最值的情況。

EXACTLY-ONCE:沒有資料丟失、事件只會產生一次最終結果。本質還是處理多次,但之前的處理被抹去。

end-to-end exactly-once 原理:

前提:end-to-end exactly-once 需要外部元件提供 commit 和 roll back 功能。二階段提交是相容這兩個功能的常用方案。下面以 kafka - flink - kafka 為例。資料的輸出必須全部在一個 transaction 裡,commit 包含兩個 cp 間的所有資料,這樣來保證資料輸出能夠 roll back。在分散式的場景下,commit 和 rollback 需要整體的 agree,這裡就要使用 2pc 了。

過程:開始 cp 表示 pre-commit,JM 傳送 checkpoint barrier 來對資料進行分割,barrier 前面為本次 cp 資料,後面為下次 cp 資料。barrier 經過 operator 時會觸發該 operator 的 state backend 快照它的 state。當所有 operator 的快照完成,包括 pre-committed external state,這時 cp 就完成了。下一步 JM 通知所有 operators cp 完成,但實際上只有 sink 需要響應,即進行最終 commit。
這個過程的 pre-commit 如果有失敗,整個 cp 都是失敗,馬上進行回滾。另外,在 commit 階段,必須在 kafka transaction timeout 內正常完成(期間可能出現網路異常、flink重啟等),否則會丟失該批 commit 資料的結果。

消費端注意:isolation.level 為 read_committed

使用

TwoPhaseCommitSinkFunction
1. beginTransaction - to begin the transaction, we create a temporary file in a temporary directory on our destination file system. Subsequently, we can write data to this file as we process it.
2. preCommit - on pre-commit, we flush the file, close it, and never write to it again. We’ll also start a new transaction for any subsequent writes that belong to the next checkpoint.
3. commit - on commit, we atomically move the pre-committed file to the actual destination directory. Please note that this increases the latency in the visibility of the output data.
4. abort - on abort, we delete the temporary file.

原理擴充套件

public abstract class TwoPhaseCommitSinkFunction<IN, TXN, CONTEXT>
		extends RichSinkFunction<IN>
		implements CheckpointedFunction, CheckpointListener {
  // 首先在 initializeState 方法中開啟事務,對於 Flink sink 的兩階段提交.
  // 第一階段就是執行 CheckpointedFunction#snapshotState 當所有 task 的 checkpoint 都完成之後,每個 task 會執行 CheckpointedFunction#notifyCheckpointComplete 也就是所謂的第二階段。
  
  @Override
	public void snapshotState(FunctionSnapshotContext context) throws Exception {
		// this is like the pre-commit of a 2-phase-commit transaction
		// we are ready to commit and remember the transaction

		long checkpointId = context.getCheckpointId();

    // 第一次呼叫的事務都在 initializeState 方法中
		preCommit(currentTransactionHolder.handle); 
    // 儲存了每個 checkpoint 對應的事務
		pendingCommitTransactions.put(checkpointId, currentTransactionHolder);
		// 下一次的事務處理者
		currentTransactionHolder = beginTransactionInternal();

		state.clear();
		state.add(new State<>(
			this.currentTransactionHolder,
			new ArrayList<>(pendingCommitTransactions.values()),
			userContext));
	}
  
  @Override
	public final void notifyCheckpointComplete(long checkpointId) throws Exception {
		// the following scenarios are possible here
		//
		//  (1) there is exactly one transaction from the latest checkpoint that
		//      was triggered and completed. That should be the common case.
		//      Simply commit that transaction in that case.
		//
		//  (2) there are multiple pending transactions because one previous
		//      checkpoint was skipped. That is a rare case, but can happen
		//      for example when:
		//
		//        - the master cannot persist the metadata of the last
		//          checkpoint (temporary outage in the storage system) but
		//          could persist a successive checkpoint (the one notified here)
		//
		//        - other tasks could not persist their status during
		//          the previous checkpoint, but did not trigger a failure because they
		//          could hold onto their state and could successfully persist it in
		//          a successive checkpoint (the one notified here)
		//
		//      In both cases, the prior checkpoint never reach a committed state, but
		//      this checkpoint is always expected to subsume the prior one and cover all
		//      changes since the last successful one. As a consequence, we need to commit
		//      all pending transactions.
		//
		//  (3) Multiple transactions are pending, but the checkpoint complete notification
		//      relates not to the latest. That is possible, because notification messages
		//      can be delayed (in an extreme case till arrive after a succeeding checkpoint
		//      was triggered) and because there can be concurrent overlapping checkpoints
		//      (a new one is started before the previous fully finished).
		//
		// ==> There should never be a case where we have no pending transaction here
		//

		Iterator<Map.Entry<Long, TransactionHolder<TXN>>> pendingTransactionIterator = pendingCommitTransactions.entrySet().iterator();
		Throwable firstError = null;

    // 全部事務提交
		while (pendingTransactionIterator.hasNext()) {
			Map.Entry<Long, TransactionHolder<TXN>> entry = pendingTransactionIterator.next();
			Long pendingTransactionCheckpointId = entry.getKey();
			TransactionHolder<TXN> pendingTransaction = entry.getValue();
			if (pendingTransactionCheckpointId > checkpointId) {
				continue;
			}

			logWarningIfTimeoutAlmostReached(pendingTransaction);
			try {
				commit(pendingTransaction.handle);
			} catch (Throwable t) {
				if (firstError == null) {
					firstError = t;
				}
			}

			pendingTransactionIterator.remove();
		}

		if (firstError != null) {
			throw ...
		}
	}
}

參考:

An Overview of End-to-End Exactly-Once Processing in Apache Flink

資源管理
Flink 的記憶體管理也主要指 TaskManager 的記憶體管理。TM 的資源(主要是記憶體)分為三個層級,分別是最粗粒度的程式級(TaskManager 程式本身),執行緒級(TaskManager 的 slot)和 SubTask 級(多個 SubTask 共用一個 slot)。

程式:

Heap Memory: 由 JVM 直接管理的 heap 記憶體,留給使用者程式碼以及沒有顯式記憶體管理的 Flink 系統活動使用(比如 StateBackend、ResourceManager 的後設資料管理等)。
Network Memory: 用於網路傳輸(比如 shuffle、broadcast)的記憶體 Buffer 池,屬於 Direct Memory 並由 Flink 管理。
Cutoff Memory: 在容器化環境下程式使用的實體記憶體有上限,需要預留一部分記憶體給 JVM 本身,比如執行緒棧記憶體、class 等後設資料記憶體、GC 記憶體等。
Managed Memory: 由 Flink Memory Manager 直接管理的記憶體,是資料在 Operator 內部的物理表示。Managed Memory 可以被配置為 on-heap 或者 off-heap (direct memory)的,off-heap 的 Managed Memory 將有效減小 JVM heap 的大小並減輕 GC 負擔。目前 Managed Memory 只用於 Batch 型別的作業,需要快取資料的操作比如 hash join、sort 等都依賴於它。
執行緒:

TaskManager 會將其資源均分為若干個 slot (在 YARN/Mesos/K8s 環境通常是每個 TaskManager 只包含 1 個 slot),沒有 slot sharing 的情況下每個 slot 可以執行一個 SubTask 執行緒。除了 Managed Memory,屬於同一 TaskManager 的 slot 之間基本是沒有資源隔離的,包括 Heap Memory、Network Buffer、Cutoff Memory 都是共享的。所以目前 slot 主要的用處是限制一個 TaskManager 的 SubTask 數。預設情況下, Flink 允許多個 SubTask 共用一個 slot 的資源,前提是這些 SubTask 屬於同一個 Job 的不同 Task。這樣能夠節省 slot(執行緒數),且有效利用資源(比如在同一個 slot 的 source 和 map,source 主要使用網路 IO,而 map 可能主要需要 cpu)

目前 Flink 的記憶體管理是比較粗粒度的,資源隔離並不是很完整,而且在不同部署模式下(Standalone/YARN/Mesos/K8s)或不同計算模式下(Streaming/Batch)的記憶體分配也不太一致,為深度平臺化及大規模應用增添了難度。

目前 Flink 的資源是預先靜態分配的,也就是說 TaskManager 程式啟動後 slot 的數目和每個 slot 的資源數都是固定的而且不能改變,這些 slot 的生命週期和 TaskManager 是相同的。Flink Job 後續只能向 TaskManager 申請和釋放這些 slot,而沒有對 slot 資源數的話語權。

Flink 1.10 的改進

統一記憶體配置
在這裡插入圖片描述

動態 slot:目前涉及到 Managed Memory 資源,TaskManager 的其他資源比如 JVM heap 還是多個 slot 共享的。

細粒度的運算元資源管理:

戶使用 API 構建的 Operator(以 Transformation 表示)會附帶 ResourceSpecs,描述該 Operator 需要的資源,預設為 unknown。
當生成 JobGraph 的時候,StreamingJobGraphGenerator 根據 ResourceSpecs 計算出每個 Operator 佔的資源比例(主要是 Managed Memory 的比例)。
進行排程的時候,Operator 的資源將被加總成為 Task 的 ResourceProfiles (包括 Managed Memory 和根據 Task 總資源算出的 Network Memory)。這些 Task 會被劃分為 SubTask 例項被部署到 TaskManager 上。
當 TaskManager 啟動 SubTask 的時候,會根據各 Operator 的資源佔比劃分 Slot Managed Memory。劃分的方式可以是使用者指定每個 Operator 的資源佔比,或者預設均等分。
參考:Flink 細粒度資源管理解析

Mechine Learning/AI
基礎
流批統一框架
在這裡插入圖片描述

相關文章