尚矽谷大資料技術之Flink最佳化_V2

四叶草520發表於2024-03-26

尚矽谷大資料技術之Flink最佳化

(作者:尚矽谷大資料研發部)

版本:V2.0

資源配置調優

Flink效能調優的第一步,就是為任務分配合適的資源,在一定範圍內,增加資源的分配與效能的提升是成正比的,實現了最優的資源配置後,在此基礎上再考慮進行後面論述的效能調優策略。

提交方式主要是yarn-per-job,資源的分配在使用指令碼提交Flink任務時進行指定。

  • 標準的Flink任務提交指令碼(Generic CLI 模式)

從1.11開始,增加了通用客戶端模式,引數使用-D <property=value>指定

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \ 指定並行度

-Dyarn.application.queue=test \ 指定yarn佇列

-Djobmanager.memory.process.size=1024mb \ 指定JM的總程序大小

-Dtaskmanager.memory.process.size=1024mb \ 指定每個TM的總程序大小

-Dtaskmanager.numberOfTaskSlots=2 \ 指定每個TM的slot數

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

引數列表:

https://ci.apache.org/projects/flink/flink-docs-release-1.13/deployment/config.html

記憶體設定

TaskManager記憶體模型

尚矽谷大資料技術之Flink最佳化_V21、記憶體模型詳解

  • JVM 特定記憶體:JVM本身使用的記憶體,包含JVM的metaspace和over-head

1)JVM metaspace:JVM元空間

taskmanager.memory.jvm-metaspace.size,預設256mb

2)JVM over-head執行開銷:JVM執行時自身所需要的內容,包括執行緒堆疊、IO、編譯快取等所使用的記憶體。

taskmanager.memory.jvm-overhead.fraction,預設0.1

taskmanager.memory.jvm-overhead.min,預設192mb

taskmanager.memory.jvm-overhead.max,預設1gb

總程序記憶體*fraction,如果小於配置的min(或大於配置的max)大小,則使用min/max大小

  • 框架記憶體:Flink框架,即TaskManager本身所佔用的記憶體,不計入Slot的資源中。

堆內:taskmanager.memory.framework.heap.size,預設128MB

堆外:taskmanager.memory.framework.off-heap.size,預設128MB

  • Task記憶體:Task執行使用者程式碼時所使用的記憶體

堆內:taskmanager.memory.task.heap.size,預設none,由Flink記憶體扣除掉其他部分的記憶體得到。

堆外:taskmanager.memory.task.off-heap.size,預設0,表示不使用堆外記憶體

  • 網路記憶體:網路資料交換所使用的堆外記憶體大小,如網路資料交換緩衝區

堆外:taskmanager.memory.network.fraction,預設0.1

taskmanager.memory.network.min,預設64mb

taskmanager.memory.network.max,預設1gb

Flink記憶體*fraction,如果小於配置的min(或大於配置的max)大小,則使用min/max大小

  • 託管記憶體用於RocksDB State Backend 的本地記憶體和批的排序、雜湊表、快取中間結果。

堆外:taskmanager.memory.managed.fraction,預設0.4

taskmanager.memory.managed.size,預設none

如果size沒指定,則等於Flink記憶體*fraction

2、案例分析

基於Yarn模式,一般引數指定的是總程序記憶體,taskmanager.memory.process.size,比如指定為4G,每一塊記憶體得到大小如下:

(1)計算Flink記憶體

JVM元空間256m

JVM執行開銷: 4g*0.1=409.6m,在[192m,1g]之間,最終結果409.6m

Flink記憶體=4g-256m-409.6m=3430.4m

(2)網路記憶體=3430.4m*0.1=343.04m,在[64m,1g]之間,最終結果343.04m

(3)託管記憶體=3430.4m*0.4=1372.16m

(4)框架記憶體,堆內和堆外都是128m

(5)Task堆內記憶體=3430.4m-128m-128m-343.04m-1372.16m=1459.2m

尚矽谷大資料技術之Flink最佳化_V2

尚矽谷大資料技術之Flink最佳化_V2

所以程序記憶體給多大,每一部分記憶體需不需要調整,可以看記憶體的使用率來調整。

生產資源配置示例

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \ 指定並行度

-Dyarn.application.queue=test \ 指定yarn佇列

-Djobmanager.memory.process.size=2048mb \ JM2~4G足夠

-Dtaskmanager.memory.process.size=4096mb \ 單個TM2~8G足夠

-Dtaskmanager.numberOfTaskSlots=2 \ 與容器核數1core:1slot或2core:1slot

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

Flink是實時流處理,關鍵在於資源情況能不能抗住高峰時期每秒的資料量,通常用TPS來描述資料吞吐情況。

合理利用cpu資源

Yarn的容量排程器預設情況下是使用“DefaultResourceCalculator”分配策略,只根據記憶體排程資源,所以在Yarn的資源管理頁面上看到每個容器的vcore個數還是1。

可以修改策略為 DominantResourceCalculator,該資源計算器在計算資源的時候會綜合考慮cpu和記憶體的情況。在capacity-scheduler.xml 中修改屬性:

<property>

<name>yarn.scheduler.capacity.resource-calculator</name>

<!-- <value>org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator</value> -->

<value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value>

</property>

使用DefaultResourceCalculator 策略

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

可以看到一個容器只有一個vcore:

尚矽谷大資料技術之Flink最佳化_V2

使用DominantResourceCalculator策略

修改後yarn配置後,分發配置並重啟yarn,再次提交flink作業:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

看到容器的vcore數變了:

尚矽谷大資料技術之Flink最佳化_V2

JobManager1個,佔用1個容器,vcore=1

TaskManager3個,佔用3個容器,每個容器vcore=2,總vcore=2*3=6,因為預設單個容器的vcore數=單TM的slot數

使用DominantResourceCalculator策略並指定容器vcore數

指定yarn容器的vcore數,提交:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Dyarn.containers.vcores=3 \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

尚矽谷大資料技術之Flink最佳化_V2

JobManager1個,佔用1個容器,vcore=1

TaskManager3個,佔用3個容器,每個容器vcore =3,總vcore=3*3=9

並行度設定

全域性並行度計算

開發完成後,先進行壓測。任務並行度給10以下,測試單個並行度的處理上限。然後 總QPS/單並行度的處理能力 = 並行度

開發完Flink作業,壓測的方式很簡單,先在kafka中積壓資料,之後開啟Flink任務,出現反壓,就是處理瓶頸。相當於水庫先積水,一下子洩洪。

不能只從QPS去得出並行度,因為有些欄位少、邏輯簡單的任務,單並行度一秒處理幾萬條資料。而有些資料欄位多,處理邏輯複雜,單並行度一秒只能處理1000條資料。

最好根據高峰期的QPS壓測,並行度*1.2倍,富餘一些資源。

Source 端並行度的配置

資料來源端是 Kafka,Source的並行度設定為Kafka對應Topic的分割槽數。

如果已經等於 Kafka 的分割槽數,消費速度仍跟不上資料生產速度,考慮下Kafka 要擴大分割槽,同時調大並行度等於分割槽數。

Flink 的一個並行度可以處理一至多個分割槽的資料,如果並行度多於 Kafka 的分割槽數,那麼就會造成有的並行度空閒,浪費資源。

Transform端並行度的配置

  • Keyby之前的運算元

一般不會做太重的操作,都是比如map、filter、flatmap等處理較快的運算元,並行度可以和source保持一致。

  • Keyby之後的運算元

如果併發較大,建議設定並行度為 2 的整數次冪,例如:128、256、512;

小併發任務的並行度不一定需要設定成 2 的整數次冪;

大併發任務如果沒有 KeyBy,並行度也無需設定為 2 的整數次冪;

Sink 端並行度的配置

Sink 端是資料流向下遊的地方,可以根據 Sink 端的資料量及下游的服務抗壓能力進行評估。如果Sink端是Kafka,可以設為Kafka對應Topic的分割槽數。

Sink 端的資料量小,比較常見的就是監控告警的場景,並行度可以設定的小一些。

Source 端的資料量是最小的,拿到 Source 端流過來的資料後做了細粒度的拆分,資料量不斷的增加,到 Sink 端的資料量就非常大。那麼在 Sink 到下游的儲存中介軟體的時候就需要提高並行度。

另外 Sink 端要與下游的服務進行互動,並行度還得根據下游的服務抗壓能力來設定,如果在 Flink Sink 這端的資料量過大的話,且 Sink 處並行度也設定的很大,但下游的服務完全撐不住這麼大的併發寫入,可能會造成下游服務直接被寫掛,所以最終還是要在 Sink 處的並行度做一定的權衡。

狀態及Checkpoint調優

RocksDB大狀態調優

RocksDB 是基於 LSM Tree 實現的(類似HBase),寫資料都是先快取到記憶體中,所以RocksDB 的寫請求效率比較高。RocksDB 使用記憶體結合磁碟的方式來儲存資料,每次獲取資料時,先從記憶體中 blockcache 中查詢,如果記憶體中沒有再去磁碟中查詢。使用 RocksDB 時,狀態大小僅受可用磁碟空間量的限制,效能瓶頸主要在於 RocksDB 對磁碟的讀請求,每次讀寫操作都必須對資料進行反序列化或者序列化。當處理效能不夠時,僅需要橫向擴充套件並行度即可提高整個Job 的吞吐量。

null

從Flink1.10開始,Flink預設將RocksDB的記憶體大小配置為每個task slot的託管記憶體。除錯記憶體效能的問題主要是透過調整配置項taskmanager.memory.managed.size 或者 taskmanager.memory.managed.fraction以增加Flink的託管記憶體(即堆外的託管記憶體)。進一步可以調整一些引數進行高階效能調優,這些引數也可以在應用程式中透過RocksDBStateBackend.setRocksDBOptions(RocksDBOptionsFactory)指定。下面介紹提高資源利用率的幾個重要配置:

開啟State訪問效能監控

Flink 1.13 中引入了 State 訪問的效能監控,即 latency trackig state。此功能不侷限於 State Backend 的型別,自定義實現的 State Backend 也可以複用此功能。

尚矽谷大資料技術之Flink最佳化_V2

State 訪問的效能監控會產生一定的效能影響,所以,預設每 100 次做一次取樣 (sample),對不同的 State Backend 效能損失影響不同:

對於 RocksDB State Backend,效能損失大概在 1% 左右

對於 Heap State Backend,效能損失最多可達 10%

state.backend.latency-track.keyed-state-enabled:true #啟用訪問狀態的效能監控

state.backend.latency-track.sample-interval: 100 #取樣間隔

state.backend.latency-track.history-size: 128 #保留的取樣資料個數,越大越精確

state.backend.latency-track.state-name-as-variable: true #將狀態名作為變數

正常開啟第一個引數即可。

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-Dstate.backend.latency-track.keyed-state-enabled=true \

-c com.atguigu.flink.tuning.RocksdbTuning \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

開啟增量檢查點和本地恢復

1)開啟增量檢查點

RocksDB是目前唯一可用於支援有狀態流處理應用程式增量檢查點的狀態後端,可以修改引數開啟增量檢查點:

state.backend.incremental: true #預設false,改為true。

或程式碼中指定

new EmbeddedRocksDBStateBackend(true)

2)開啟本地恢復

當 Flink 任務失敗時,可以基於本地的狀態資訊進行恢復任務,可能不需要從 hdfs 拉取資料。本地恢復目前僅涵蓋鍵控型別的狀態後端(RocksDB),MemoryStateBackend不支援本地恢復並忽略此選項。

state.backend.local-recovery: true

3)設定多目錄

如果有多塊磁碟,也可以考慮指定本地多目錄

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

注意:不要配置單塊磁碟的多個目錄,務必將目錄配置到多塊不同的磁碟上,讓多塊磁碟來分擔壓力。

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-Dstate.backend.incremental=true \

-Dstate.backend.local-recovery=true \

-Dstate.backend.latency-track.keyed-state-enabled=true \

-c com.atguigu.flink.tuning.RocksdbTuning \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

調整預定義選項

Flink針對不同的設定為RocksDB提供了一些預定義的選項集合,其中包含了後續提到的一些引數,如果調整預定義選項後還達不到預期,再去調整後面的block、writebuffer等引數。

當前支援的預定義選項有DEFAULT、SPINNING_DISK_OPTIMIZED、SPINNING_DISK_OPTIMIZED_HIGH_MEM或FLASH_SSD_OPTIMIZED。有條件上SSD的,可以指定為FLASH_SSD_OPTIMIZED

state.backend.rocksdb.predefined-options: SPINNING_DISK_OPTIMIZED_HIGH_MEM

#設定為機械硬碟+記憶體模式

增大block快取

整個 RocksDB 共享一個 block cache,讀資料時記憶體的 cache 大小,該引數越大讀資料時快取命中率越高,預設大小為 8 MB,建議設定到 64 ~ 256 MB。

state.backend.rocksdb.block.cache-size: 64m #預設8m

增大write buffer和 level 閾值大小

RocksDB 中,每個 State 使用一個 Column Family,每個 Column Family 使用獨佔的 write buffer,預設64MB,建議調大。

調整這個引數通常要適當增加 L1 層的大小閾值max-size-level-base,預設256m。該值太小會造成能存放的SST檔案過少,層級變多造成查詢困難,太大會造成檔案過多,合併困難。建議設為 target_file_size_base(預設64MB) 的倍數,且不能太小,例如5~10倍,即320~640MB。

state.backend.rocksdb.writebuffer.size: 128m

state.backend.rocksdb.compaction.level.max-size-level-base: 320m

增大write buffer數量

每個 Column Family 對應的 writebuffer 最大數量,這實際上是記憶體中“只讀記憶體表“的最大數量,預設值是 2。對於機械磁碟來說,如果記憶體足夠大,可以調大到 5 左右

state.backend.rocksdb.writebuffer.count: 5

增大後臺執行緒數和write buffer合併數

1)增大執行緒數

用於後臺 flush 和合並 sst 檔案的執行緒數,預設為 1,建議調大,機械硬碟使用者可以改為 4 等更大的值

state.backend.rocksdb.thread.num: 4

2)增大writebuffer最小合併數

將資料從 writebuffer 中 flush 到磁碟時,需要合併的 writebuffer 最小數量,預設值為 1,可以調成3。

state.backend.rocksdb.writebuffer.number-to-merge: 3

開啟分割槽索引功能

Flink 1.13 中對 RocksDB 增加了分割槽索引功能,複用了 RocksDB 的 partitioned Index & filter 功能,簡單來說就是對 RocksDB 的 partitioned Index 做了多級索引。也就是將記憶體中的最上層常駐,下層根據需要再 load 回來,這樣就大大降低了資料 Swap 競爭。線上測試中,相對於記憶體比較小的場景中,效能提升 10 倍左右。如果在記憶體管控下 Rocksdb 效能不如預期的話,這也能成為一個效能最佳化點。

state.backend.rocksdb.memory.partitioned-index-filters:true #預設false

引數設定案例

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=4096mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-Dstate.backend.incremental=true \

-Dstate.backend.local-recovery=true \

-Dstate.backend.rocksdb.predefined-options=SPINNING_DISK_OPTIMIZED_HIGH_MEM \

-Dstate.backend.rocksdb.block.cache-size=64m \

-Dstate.backend.rocksdb.writebuffer.size=128m \

-Dstate.backend.rocksdb.compaction.level.max-size-level-base=320m \

-Dstate.backend.rocksdb.writebuffer.count=5 \

-Dstate.backend.rocksdb.thread.num=4 \

-Dstate.backend.rocksdb.writebuffer.number-to-merge=3 \

-Dstate.backend.rocksdb.memory.partitioned-index-filters=true \

-Dstate.backend.latency-track.keyed-state-enabled=true \

-c com.atguigu.flink.tuning.RocksdbTuning \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

Checkpoint設定

一般需求,我們的 Checkpoint 時間間隔可以設定為分鐘級別(1 ~5 分鐘)。對於狀態很大的任務每次 Checkpoint 訪問 HDFS 比較耗時,可以設定為 5~10 分鐘一次Checkpoint,並且調大兩次 Checkpoint 之間的暫停間隔,例如設定兩次Checkpoint 之間至少暫停 4或8 分鐘。同時,也需要考慮時效性的要求,需要在時效性和效能之間做一個平衡,如果時效性要求高,結合end- to-end時長,設定秒級或毫秒級。如果 Checkpoint 語義配置為 EXACTLY_ONCE,那麼在 Checkpoint 過程中還會存在 barrier 對齊的過程,可以透過 Flink Web UI 的 Checkpoint 選項卡來檢視 Checkpoint 過程中各階段的耗時情況,從而確定到底是哪個階段導致 Checkpoint 時間過長然後針對性的解決問題。

RocksDB相關引數在前面已說明,可以在flink-conf.yaml指定,也可以在Job的程式碼中呼叫API單獨指定,這裡不再列出。

// 使⽤ RocksDBStateBackend 做為狀態後端,並開啟增量 Checkpoint

RocksDBStateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://hadoop1:8020/flink/checkpoints", true);

env.setStateBackend(rocksDBStateBackend);

// 開啟Checkpoint,間隔為 3 分鐘

env.enableCheckpointing(TimeUnit.MINUTES.toMillis(3));

// 配置 Checkpoint

CheckpointConfig checkpointConf = env.getCheckpointConfig();

checkpointConf.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

// 最小間隔 4分鐘

checkpointConf.setMinPauseBetweenCheckpoints(TimeUnit.MINUTES.toMillis(4))

// 超時時間 10分鐘

checkpointConf.setCheckpointTimeout(TimeUnit.MINUTES.toMillis(10));

// 儲存checkpoint

checkpointConf.enableExternalizedCheckpoints(

CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

反壓處理

概述

Flink網路流控及反壓的介紹:

https://flink-learning.org.cn/article/detail/138316d1556f8f9d34e517d04d670626

反壓的理解

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

反壓(BackPressure)通常產生於這樣的場景:短時間的負載高峰導致系統接收資料的速率遠高於它處理資料的速率。許多日常問題都會導致反壓,例如,垃圾回收停頓可能會導致流入的資料快速堆積,或遇到大促、秒殺活動導致流量陡增。

反壓的危害

反壓如果不能得到正確的處理,可能會影響到checkpoint 時長和 state 大小,甚至可能會導致資源耗盡甚至系統崩潰。

1)影響checkpoint時長:barrier 不會越過普通資料,資料處理被阻塞也會導致 checkpoint barrier 流經整個資料管道的時長變長,導致 checkpoint 總體時間(End to End Duration)變長。

2)影響state大小:barrier對齊時,接受到較快的輸入管道的 barrier 後,它後面資料會被快取起來但不處理,直到較慢的輸入管道的 barrier 也到達,這些被快取的資料會被放到state 裡面,導致 checkpoint 變大。

這兩個影響對於生產環境的作業來說是十分危險的,因為 checkpoint 是保證資料一致性的關鍵,checkpoint 時間變長有可能導致 checkpoint 超時失敗,而 state 大小同樣可能拖慢 checkpoint 甚至導致 OOM (使用 Heap-based StateBackend)或者實體記憶體使用超出容器資源(使用 RocksDBStateBackend)的穩定性問題。

因此,我們在生產中要儘量避免出現反壓的情況。

定位反壓節點

解決反壓首先要做的是定位到造成反壓的節點,排查的時候,先把operator chain禁用,方便定位到具體運算元。

提交UvDemo:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

利用 Flink Web UI 定位

Flink Web UI 的反壓監控提供了 SubTask 級別的反壓監控,1.13版本以前是透過週期性對 Task 執行緒的棧資訊取樣,得到執行緒被阻塞在請求 Buffer(意味著被下游佇列阻塞)的頻率來判斷該節點是否處於反壓狀態。預設配置下,這個頻率在 0.1 以下則為 OK,0.1 至 0.5 為 LOW,而超過 0.5 則為 HIGH。

Flink 1.13 最佳化了反壓檢測的邏輯(使用基於任務 Mailbox 計時,而不在再於堆疊取樣),並且重新實現了作業圖的 UI 展示:Flink 現在在 UI 上透過顏色和數值來展示繁忙和反壓的程度。

null

1)透過WebUI看到Map運算元處於反壓:

尚矽谷大資料技術之Flink最佳化_V2

3)分析瓶頸運算元

如果處於反壓狀態,那麼有兩種可能性:

(1)該節點的傳送速率跟不上它的產生資料速率。這一般會發生在一條輸入多條輸出的 Operator(比如 flatmap)。這種情況,該節點是反壓的根源節點,它是從 Source Task 到 Sink Task 的第一個出現反壓的節點。

(2)下游的節點接受速率較慢,透過反壓機制限制了該節點的傳送速率。這種情況,需要繼續排查下游節點,一直找到第一個為OK的一般就是根源節點。

總體來看,如果我們找到第一個出現反壓的節點,反壓根源要麼是就這個節點,要麼是它緊接著的下游節點。

通常來講,第二種情況更常見。如果無法確定,還需要結合 Metrics進一步判斷。

利用Metrics定位

監控反壓時會用到的 Metrics 主要和 Channel 接受端的 Buffer 使用率有關,最為有用的是以下幾個 Metrics:

Metris

描述

outPoolUsage

傳送端 Buffer 的使用率

inPoolUsage

接收端 Buffer 的使用率

floatingBuffersUsage(1.9 以上)

接收端 Floating Buffer 的使用率

exclusiveBuffersUsage(1.9 以上)

接收端 Exclusive Buffer 的使用率

其中 inPoolUsage = floatingBuffersUsage + exclusiveBuffersUsage。

1)根據指標分析反壓

分析反壓的大致思路是:如果一個 Subtask 的傳送端 Buffer 佔用率很高,則表明它被下游反壓限速了;如果一個 Subtask 的接受端 Buffer 佔用很高,則表明它將反壓傳導至上游。反壓情況可以根據以下表格進行對號入座(1.9以上):

outPoolUsage低

outPoolUsage高

inPoolUsage低

正常

被下游反壓,處於臨時情況

(還沒傳遞到上游)

可能是反壓的根源,一條輸入多條輸出的場景

inPoolUsage高

如果上游所有outPoolUsage都是低,有可能最終可能導致反壓(還沒傳遞到上游)

被下游反壓

如果上游的outPoolUsage是高,則為反壓根源

2)可以進一步分析資料傳輸

Flink 1.9及以上版本,還可以根據 floatingBuffersUsage/exclusiveBuffersUsage 以及其上游 Task 的 outPoolUsage 來進行進一步的分析一個 Subtask 和其上游 Subtask 的資料傳輸。

在流量較大時,Channel 的 Exclusive Buffer 可能會被寫滿,此時 Flink 會向 Buffer Pool 申請剩餘的 Floating Buffer。這些 Floating Buffer 屬於備用 Buffer。

exclusiveBuffersUsage低

exclusiveBuffersUsage高

floatingBuffersUsage 低

所有上游outPoolUsage 低

正常

floatingBuffersUsage 低

上游某個outPoolUsage 高

潛在的網路瓶頸

floatingBuffersUsage 高

所有上游outPoolUsage 低

最終對部分inputChannel反壓(正在傳遞)

最終對大多數或所有inputChannel反壓(正在傳遞)

floatingBuffersUsage 高

上游某個outPoolUsage 高

只對部分inputChannel反壓

對大多數或所有inputChannel反壓

總結:

1)floatingBuffersUsage 為高,則表明反壓正在傳導至上游

2)同時exclusiveBuffersUsage為低,則表明可能有傾斜

比如,floatingBuffersUsage 高、exclusiveBuffersUsage 低為有傾斜,因為少數 channel 佔用了大部分的 Floating Buffer。

反壓的原因及處理

注意:反壓可能是暫時的,可能是由於負載高峰、CheckPoint 或作業重啟引起的資料積壓而導致反壓。如果反壓是暫時的,應該忽略它。另外,請記住,斷斷續續的反壓會影響我們分析和解決問題。

定位到反壓節點後,分析造成原因的辦法主要是觀察 Task Thread。按照下面的順序,一步一步去排查。

檢視是否資料傾斜

在實踐中,很多情況下的反壓是由於資料傾斜造成的,這點我們可以透過 Web UI 各個 SubTask 的 Records Sent 和 Record Received 來確認,另外 Checkpoint detail 裡不同 SubTask 的 State size 也是一個分析資料傾斜的有用指標。

尚矽谷大資料技術之Flink最佳化_V2

(關於資料傾斜的詳細解決方案,會在下一章節詳細討論)

使用火焰圖分析

如果不是資料傾斜,最常見的問題可能是使用者程式碼的執行效率問題(頻繁被阻塞或者效能問題),需要找到瓶頸運算元中的哪部分計算邏輯消耗巨大。

最有用的辦法就是對 TaskManager 進行 CPU profile,從中我們可以分析到 Task Thread 是否跑滿一個 CPU 核:如果是的話要分析 CPU 主要花費在哪些函式里面;如果不是的話要看 Task Thread 阻塞在哪裡,可能是使用者函式本身有些同步的呼叫,可能是 checkpoint 或者 GC 等系統活動導致的暫時系統暫停。

1)開啟火焰圖功能

Flink 1.13直接在 WebUI 提供 JVM 的 CPU 火焰圖,這將大大簡化效能瓶頸的分析,預設是不開啟的,需要修改引數:

rest.flamegraph.enabled: true #預設false

也可以在提交時指定:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Drest.flamegraph.enabled=true \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

2)WebUI檢視火焰圖

尚矽谷大資料技術之Flink最佳化_V2

火焰圖是透過對堆疊跟蹤進行多次取樣來構建的。每個方法呼叫都由一個條形表示,其中條形的長度與其在樣本中出現的次數成正比。

  • On-CPU: 處於 [RUNNABLE, NEW]狀態的執行緒
  • Off-CPU: 處於 [TIMED_WAITING, WAITING, BLOCKED]的執行緒,用於檢視在樣本中發現的阻塞呼叫。

3)分析火焰圖

顏色沒有特殊含義,具體檢視:

      • 縱向是呼叫鏈,從下往上,頂部就是正在執行的函式
      • 橫向是樣本出現次數,可以理解為執行時長。

看頂層的哪個函式佔據的寬度最大。只要有"平頂"(plateaus),就表示該函式可能存在效能問題。

如果是Flink 1.13以前的版本,可以手動做火焰圖:

如何生成火焰圖:http://www.54tianzhisheng.cn/2020/10/05/flink-jvm-profiler/

分析GC情況

TaskManager 的記憶體以及 GC 問題也可能會導致反壓,包括 TaskManager JVM 各區記憶體不合理導致的頻繁 Full GC 甚至失聯。通常建議使用預設的G1垃圾回收器。

可以透過列印GC日誌(-XX:+PrintGCDetails),使用GC 分析器(GCViewer工具)來驗證是否處於這種情況。

  • 在Flink提交指令碼中,設定JVM引數,列印GC日誌:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Denv.java.opts="-XX:+PrintGCDetails -XX:+PrintGCDateStamps" \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

  • 下載GC日誌的方式:

因為是on yarn模式,執行的節點一個一個找比較麻煩。可以開啟WebUI,選擇JobManager或者TaskManager,點選Stdout,即可看到GC日誌,點選下載按鈕即可將GC日誌透過HTTP的方式下載下來。

尚矽谷大資料技術之Flink最佳化_V2

  • 分析GC日誌:

透過 GC 日誌分析出單個 Flink Taskmanager 堆總大小、年輕代、老年代分配的記憶體空間、Full GC 後老年代剩餘大小等,相關指標定義可以去 Github 具體檢視。

GCViewer地址:https://github.com/chewiebug/GCViewer

Linux下分析:

java -jar gcviewer_1.3.4.jar gc.log

Windows下分析:

直接雙擊gcviewer_1.3.4.jar,開啟GUI介面,選擇gc的log開啟

擴充套件:最重要的指標是Full GC 後,老年代剩餘大小這個指標,按照《Java 效能最佳化權威指南》這本書 Java 堆大小計演算法則,設 Full GC 後老年代剩餘大小空間為 M,那麼堆的大小建議 3 ~ 4倍 M,新生代為 1 ~ 1.5 倍 M,老年代應為 2 ~ 3 倍 M。

外部元件互動

如果發現我們的 Source 端資料讀取效能比較低或者 Sink 端寫入效能較差,需要檢查第三方元件是否遇到瓶頸,還有就是做維表join時的效能問題。

例如:

Kafka 叢集是否需要擴容,Kafka 聯結器是否並行度較低

HBase 的 rowkey 是否遇到熱點問題,是否請求處理不過來

ClickHouse併發能力較弱,是否達到瓶頸

……

關於第三方元件的效能問題,需要結合具體的元件來分析,最常用的思路:

1)非同步io+熱快取來最佳化讀寫效能

2)先攢批再讀寫

維表join參考:

https://flink-learning.org.cn/article/detail/b8df32fbc6542257a5b449114e137cc3

https://www.jianshu.com/p/a62fa483ff54

資料傾斜

判斷是否存在資料傾斜

相同 Task 的多個 Subtask 中,個別Subtask 接收到的資料量明顯大於其他 Subtask 接收到的資料量,透過 Flink Web UI 可以精確地看到每個 Subtask 處理了多少資料,即可判斷出 Flink 任務是否存在資料傾斜。通常,資料傾斜也會引起反壓。

尚矽谷大資料技術之Flink最佳化_V2

另外, 有時Checkpoint detail 裡不同 SubTask 的 State size 也是一個分析資料傾斜的有用指標。

資料傾斜的解決

keyBy 後的聚合操作存在資料傾斜

提交案例:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SkewDemo1 \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--local-keyby false

檢視webui:

尚矽谷大資料技術之Flink最佳化_V2

1)為什麼不能直接用二次聚合來處理

Flink是實時流處理,如果keyby之後的聚合操作存在資料傾斜,且沒有開視窗(沒攢批)的情況下,簡單的認為使用兩階段聚合,是不能解決問題的。因為這個時候Flink是來一條處理一條,且向下遊傳送一條結果,對於原來keyby的維度(第二階段聚合)來講,資料量並沒有減少,且結果重複計算(非FlinkSQL,未使用回撤流),如下圖所示:

尚矽谷大資料技術之Flink最佳化_V2

2)使用LocalKeyBy的思想

在 keyBy 上游運算元資料傳送之前,首先在上游運算元的本地對資料進行聚合後,再傳送到下游,使下游接收到的資料量大大減少,從而使得 keyBy 之後的聚合操作不再是任務的瓶頸。類似MapReduce 中 Combiner 的思想,但是這要求聚合操作必須是多條資料或者一批資料才能聚合,單條資料沒有辦法透過聚合來減少資料量。從Flink LocalKeyBy 實現原理來講,必然會存在一個積攢批次的過程,在上游運算元中必須攢夠一定的資料量,對這些資料聚合後再傳送到下游。

實現方式:

  • DataStreamAPI需要自己寫程式碼實現
  • SQL可以指定引數,開啟miniBatch和LocalGlobal功能(推薦,後續介紹)

3)DataStream API自定義實現的案例

以計算每個mid出現的次數為例,keyby之前,使用flatMap實現LocalKeyby功能

import org.apache.flink.api.common.functions.RichFlatMapFunction;

import org.apache.flink.api.common.state.ListState;

import org.apache.flink.api.common.state.ListStateDescriptor;

import org.apache.flink.api.common.typeinfo.Types;

import org.apache.flink.api.java.tuple.Tuple2;

import org.apache.flink.runtime.state.FunctionInitializationContext;

import org.apache.flink.runtime.state.FunctionSnapshotContext;

import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;

import org.apache.flink.util.Collector;

import java.util.HashMap;

import java.util.Map;

import java.util.concurrent.atomic.AtomicInteger;

public class LocalKeyByFlatMapFunc extends RichFlatMapFunction<Tuple2<String, Long>, Tuple2<String, Long>> implements CheckpointedFunction {

//Checkpoint 時為了保證 Exactly Once,將 buffer 中的資料儲存到該 ListState 中

private ListState<Tuple2<String, Long>> listState;

//本地 buffer,存放 local 端快取的 mid 的 count 資訊

private HashMap<String, Long> localBuffer;

//快取的資料量大小,即:快取多少資料再向下游傳送

private int batchSize;

//計數器,獲取當前批次接收的資料量

private AtomicInteger currentSize;

//構造器,批次大小傳參

public LocalKeyByFlatMapFunc(int batchSize) {

this.batchSize = batchSize;

}

@Override

public void flatMap(Tuple2<String, Long> value, Collector<Tuple2<String, Long>> out) throws Exception {

// 1、將新來的資料新增到 buffer 中

Long count = localBuffer.getOrDefault(value, 0L);

localBuffer.put(value.f0, count + 1);

// 2、如果到達設定的批次,則將 buffer 中的資料傳送到下游

if (currentSize.incrementAndGet() >= batchSize) {

// 2.1 遍歷 Buffer 中資料,傳送到下游

for (Map.Entry<String, Long> midAndCount : localBuffer.entrySet()) {

out.collect(Tuple2.of(midAndCount.getKey(), midAndCount.getValue()));

}

// 2.2 Buffer 清空,計數器清零

localBuffer.clear();

currentSize.set(0);

}

}

@Override

public void snapshotState(FunctionSnapshotContext context) throws Exception {

// 將 buffer 中的資料儲存到狀態中,來保證 Exactly Once

listState.clear();

for (Map.Entry<String, Long> midAndCount : localBuffer.entrySet()) {

listState.add(Tuple2.of(midAndCount.getKey(), midAndCount.getValue()));

}

}

@Override

public void initializeState(FunctionInitializationContext context) throws Exception {

// 從狀態中恢復 buffer 中的資料

listState = context.getOperatorStateStore().getListState(

new ListStateDescriptor<Tuple2<String, Long>>(

"localBufferState",

Types.TUPLE(Types.STRING, Types.LONG)

)

);

localBuffer = new HashMap();

if (context.isRestored()) {

// 從狀態中恢復資料到 buffer 中

for (Tuple2<String, Long> midAndCount : listState.get()) {

// 如果出現 pv != 0,說明改變了並行度,ListState 中的資料會被均勻分發到新的 subtask中

// 單個 subtask 恢復的狀態中可能包含多個相同的 mid 的 count資料

// 所以每次先取一下buffer的值,累加再put

long count = localBuffer.getOrDefault(midAndCount.f0, 0L);

localBuffer.put(midAndCount.f0, count + midAndCount.f1);

}

// 從狀態恢復時,預設認為 buffer 中資料量達到了 batchSize,需要向下遊發

currentSize = new AtomicInteger(batchSize);

} else {

currentSize = new AtomicInteger(0);

}

}

}

提交localkeyby案例:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SkewDemo1 \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--local-keyby true

檢視webui:

尚矽谷大資料技術之Flink最佳化_V2

可以看到每個subtask處理的資料量基本均衡,另外處理的資料量相比原先少了很多。

keyBy 之前發生資料傾斜

如果 keyBy 之前就存在資料傾斜,上游運算元的某些例項可能處理的資料較多,某些例項可能處理的資料較少,產生該情況可能是因為資料來源的資料本身就不均勻,例如由於某些原因 Kafka 的 topic 中某些 partition 的資料量較大,某些 partition 的資料量較少。對於不存在 keyBy 的 Flink 任務也會出現該情況。

這種情況,需要讓 Flink 任務強制進行shuffle。使用shuffle、rebalance 或 rescale運算元即可將資料均勻分配,從而解決資料傾斜的問題。

keyBy 後的視窗聚合操作存在資料傾斜

因為使用了視窗,變成了有界資料(攢批)的處理,視窗預設是觸發時才會輸出一條結果發往下游,所以可以使用兩階段聚合的方式:

1)實現思路:

  • 第一階段聚合:key拼接隨機數字首或字尾,進行keyby、開窗、聚合

注意:聚合完不再是WindowedStream,要獲取WindowEnd作為視窗標記作為第二階段分組依據,避免不同視窗的結果聚合到一起)

  • 第二階段聚合:按照原來的key及windowEnd作keyby、聚合

2)提交原始案例

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SkewDemo2 \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--two-phase false

檢視WebUI:

尚矽谷大資料技術之Flink最佳化_V2

3)提交兩階段聚合的案例

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SkewDemo2 \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--two-phase true \

--random-num 16

檢視WebUI:可以看到第一次打散的視窗聚合,比較均勻

尚矽谷大資料技術之Flink最佳化_V2

第二次聚合,也比較均勻:

尚矽谷大資料技術之Flink最佳化_V2

隨機數範圍,需要自己去測,因為keyby的分割槽器是(兩次hash*下游並行度/最大並行度)

SQL寫法參考:https://zhuanlan.zhihu.com/p/197299746

Job最佳化

使用DataGen造資料

開發完Flink作業,壓測的方式很簡單,先在kafka中積壓資料,之後開啟Flink任務,出現反壓,就是處理瓶頸。相當於水庫先積水,一下子洩洪。

資料可以是自己造的模擬資料,也可以是生產中的部分資料。造測試資料的工具:DataFactory、datafaker 、DBMonster、Data-Processer 、Nexmark、Jmeter等。

Flink從1.11開始提供了一個內建的DataGen 聯結器,主要是用於生成一些隨機數,用於在沒有資料來源的時候,進行流任務的測試以及效能測試等。

DataStream的DataGenerator

import com.atguigu.flink.tuning.bean.OrderInfo;
import com.atguigu.flink.tuning.bean.UserInfo;
import org.apache.commons.math3.random.RandomDataGenerator;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.RestOptions;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.datagen.DataGeneratorSource;
import org.apache.flink.streaming.api.functions.source.datagen.RandomGenerator;
import org.apache.flink.streaming.api.functions.source.datagen.SequenceGenerator;


public class DataStreamDataGenDemo {
public static void main(String[] args) throws Exception {

Configuration conf = new Configuration();
conf.set(RestOptions.ENABLE_FLAMEGRAPH, true);
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf);

env.setParallelism(1);
env.disableOperatorChaining();


SingleOutputStreamOperator<OrderInfo> orderInfoDS = env
.addSource(new DataGeneratorSource<>(new RandomGenerator<OrderInfo>() {
@Override
public OrderInfo next() {
return new OrderInfo(
random.nextInt(1, 100000),
random.nextLong(1, 1000000),
random.nextUniform(1, 1000),
System.currentTimeMillis());
}
}))
.returns(Types.POJO(OrderInfo.class));


SingleOutputStreamOperator<UserInfo> userInfoDS = env
.addSource(new DataGeneratorSource<UserInfo>(
new SequenceGenerator<UserInfo>(1, 1000000) {
RandomDataGenerator random = new RandomDataGenerator();
@Override
public UserInfo next() {
return new UserInfo(
valuesToEmit.peek().intValue(),
valuesToEmit.poll().longValue(),
random.nextInt(1, 100),
random.nextInt(0, 1));
}
}
))
.returns(Types.POJO(UserInfo.class));

orderInfoDS.print("order>>");
userInfoDS.print("user>>");


env.execute();
}
}

SQL的DataGenerator

import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.RestOptions;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;


public class SQLDataGenDemo {
public static void main(String[] args) throws Exception {

Configuration conf = new Configuration();
conf.set(RestOptions.ENABLE_FLAMEGRAPH, true);
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf);

env.setParallelism(1);
env.disableOperatorChaining();

StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

String orderSql="CREATE TABLE order_info (\n" +
" id INT,\n" +
" user_id BIGINT,\n" +
" total_amount DOUBLE,\n" +
" create_time AS localtimestamp,\n" +
" WATERMARK FOR create_time AS create_time\n" +
") WITH (\n" +
" 'connector' = 'datagen',\n" +
" 'rows-per-second'='20000',\n" +
" 'fields.id.kind'='sequence',\n" +
" 'fields.id.start'='1',\n" +
" 'fields.id.end'='100000000',\n" +
" 'fields.user_id.kind'='random',\n" +
" 'fields.user_id.min'='1',\n" +
" 'fields.user_id.max'='1000000',\n" +
" 'fields.total_amount.kind'='random',\n" +
" 'fields.total_amount.min'='1',\n" +
" 'fields.total_amount.max'='1000'\n" +
")";

String userSql="CREATE TABLE user_info (\n" +
" id INT,\n" +
" user_id BIGINT,\n" +
" age INT,\n" +
" sex INT\n" +
") WITH (\n" +
" 'connector' = 'datagen',\n" +
" 'rows-per-second'='20000',\n" +
" 'fields.id.kind'='sequence',\n" +
" 'fields.id.start'='1',\n" +
" 'fields.id.end'='100000000',\n" +
" 'fields.user_id.kind'='sequence',\n" +
" 'fields.user_id.start'='1',\n" +
" 'fields.user_id.end'='1000000',\n" +
" 'fields.age.kind'='random',\n" +
" 'fields.age.min'='1',\n" +
" 'fields.age.max'='100',\n" +
" 'fields.sex.kind'='random',\n" +
" 'fields.sex.min'='0',\n" +
" 'fields.sex.max'='1'\n" +
")";


tableEnv.executeSql(orderSql);
tableEnv.executeSql(userSql);

tableEnv.executeSql("select * from order_info").print();
// tableEnv.executeSql("select * from user_info").print();

}
}

運算元指定UUID

對於有狀態的 Flink 應用,推薦給每個運算元都指定唯一使用者ID(UUID)。 嚴格地說,僅需要給有狀態的運算元設定就足夠了。但是因為 Flink 的某些內建運算元(如 window)是有狀態的,而有些是無狀態的,可能使用者不是很清楚哪些內建運算元是有狀態的,哪些不是。所以從實踐經驗上來說,我們建議每個運算元都指定上 UUID。

預設情況下,運算元UID是根據JobGraph自動生成的,JobGraph的更改可能會導致UUID改變。手動指定運算元 UUID ,可以讓 Flink 有效地將運算元的狀態從 savepoint 對映到作業修改後(拓撲圖可能也有改變)的正確的運算元上。比如替換原來的Operator實現、增加新的Operator、刪除Operator等等,至少我們有可能與Savepoint中儲存的Operator狀態對應上。這是 savepoint 在 Flink 應用中正常工作的一個基本要素。

Flink 運算元的 UUID 可以透過 uid(String uid) 方法指定,通常也建議指定name。

#運算元.uid("指定uid")

.reduce((value1, value2) -> Tuple3.of("uv", value2.f1, value1.f2 + value2.f2))

.uid("uv-reduce").name("uv-reduce")

1)提交案例:未指定uid

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

觸發儲存點:

//直接觸發

flink savepoint <jobId> [targetDirectory] [-yid yarnAppId] #on yarn模式需要指定-yid引數

//cancel觸發

flink cancel -s [targetDirectory] <jobId> [-yid yarnAppId] #on yarn模式需要指定-yid引數

bin/flink cancel -s hdfs://hadoop1:8020/flink-tuning/sp 98acff568e8f0827a67ff37648a29d7f -yid application_1640503677810_0017

修改程式碼,從savepoint恢復:

bin/flink run \

-t yarn-per-job \

-s hdfs://hadoop1:8020/flink-tuning/sp/savepoint-066c90-6edf948686f6 \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UvDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

報錯如下:

Caused by: java.lang.IllegalStateException: Failed to rollback to checkpoint/savepoint hdfs://hadoop1:8020/flink-tuning/sp/savepo

int-066c90-6edf948686f6. Cannot map checkpoint/savepoint state for operator ddb598ad156ed281023ba4eebbe487e3 to the new program,

because the operator is not available in the new program. If you want to allow to skip this, you can set the --allowNonRestoredSt

ate option on the CLI.

臨時處理:在提交命令中新增--allowNonRestoredState (short: -n)跳過無法恢復的運算元。

2)提交案例:指定uid

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UidDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

觸發儲存點:

//cancel觸發savepoint

bin/flink cancel -s hdfs://hadoop1:8020/flink-tuning/sp 272e5d3321c5c1481cc327f6abe8cf9c -yid application_1640268344567_0033

修改程式碼,從儲存點恢復:

bin/flink run \

-t yarn-per-job \

-s hdfs://hadoop1:8020/flink-tuning/sp/savepoint-272e5d-d0c1097d23e0 \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.UidDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

鏈路延遲測量

對於實時的流式處理系統來說,我們需要關注資料輸入、計算和輸出的及時性,所以處理延遲是一個比較重要的監控指標,特別是在資料量大或者軟硬體條件不佳的環境下。Flink提供了開箱即用的LatencyMarker機制來測量鏈路延遲。開啟如下引數:

metrics.latency.interval: 30000 #預設0,表示禁用,單位毫秒

監控的粒度,分為以下3檔:

  • single:每個運算元單獨統計延遲;
  • operator(預設值):每個下游運算元都統計自己與Source運算元之間的延遲;
  • subtask:每個下游運算元的sub-task都統計自己與Source運算元的sub-task之間的延遲。

metrics.latency.granularity: operator #預設operator

一般情況下采用預設的operator粒度即可,這樣在Sink端觀察到的latency metric就是我們最想要的全鏈路(端到端)延遲。subtask粒度太細,會增大所有並行度的負擔,不建議使用。

LatencyMarker不會參與到資料流的使用者邏輯中的,而是直接被各運算元轉發並統計。為了讓它儘量精確,有兩點特別需要注意:

  • 保證Flink叢集內所有節點的時區、時間是同步的:ProcessingTimeService產生時間戳最終是靠System.currentTimeMillis()方法,可以用ntp等工具來配置。
  • metrics.latency.interval的時間間隔宜大不宜小:一般配置成30000(30秒)左右。一是因為延遲監控的頻率可以不用太頻繁,二是因為LatencyMarker的處理也要消耗一定效能。

提交案例:

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-Dmetrics.latency.interval=30000 \

-c com.atguigu.flink.tuning.UidDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

可以透過下面的metric檢視結果:

flink_taskmanager_job_latency_source_id_operator_id_operator_subtask_index_latency

端到端延遲的tag只有murmur hash過的運算元ID(用uid()方法設定的),並沒有運算元名稱,(https://issues.apache.org/jira/browse/FLINK-8592)並且官方暫時不打算解決這個問題,所以我們要麼用最大值來表示,要麼將作業中Sink運算元的ID統一化。比如使用了Prometheus和Grafana來監控,效果如下:

尚矽谷大資料技術之Flink最佳化_V2

開啟物件重用

尚矽谷大資料技術之Flink最佳化_V2

當呼叫了enableObjectReuse方法後,Flink會把中間深複製的步驟都省略掉,SourceFunction產生的資料直接作為MapFunction的輸入,可以減少gc壓力。但需要特別注意的是,這個方法不能隨便呼叫,必須要確保下游Function只有一種,或者下游的Function均不會改變物件內部的值。否則可能會有執行緒安全的問題。

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-Dpipeline.object-reuse=true \

-Dmetrics.latency.interval=30000 \

-c com.atguigu.flink.tuning.UidDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar

細粒度滑動視窗最佳化

1)細粒度滑動的影響

當使用細粒度的滑動視窗(視窗長度遠遠大於滑動步長)時,重疊的視窗過多,一個資料會屬於多個視窗,效能會急劇下降。

尚矽谷大資料技術之Flink最佳化_V2

我們經常會碰到這種需求:以3分鐘的頻率實時計算App內各個子模組近24小時的PV和UV。我們需要用粒度為1440 / 3 = 480的滑動視窗來實現它,但是細粒度的滑動視窗會帶來效能問題,有兩點:

  • 狀態

對於一個元素,會將其寫入對應的(key, window)二元組所圈定的windowState狀態中。如果粒度為480,那麼每個元素到來,更新windowState時都要遍歷480個視窗並寫入,開銷是非常大的。在採用RocksDB作為狀態後端時,checkpoint的瓶頸也尤其明顯。

  • 定時器

每一個(key, window)二元組都需要註冊兩個定時器:一是觸發器註冊的定時器,用於決定視窗資料何時輸出;二是registerCleanupTimer()方法註冊的清理定時器,用於在視窗徹底過期(如allowedLateness過期)之後及時清理掉視窗的內部狀態。細粒度滑動視窗會造成維護的定時器增多,記憶體負擔加重。

2)解決思路

DataStreamAPI中,自己解決(https://issues.apache.org/jira/browse/FLINK-7001)。

我們一般使用滾動視窗+線上儲存+讀時聚合的思路作為解決方案:

(1)從業務的視角來看,往往視窗的長度是可以被步長所整除的,可以找到視窗長度和視窗步長的最小公約數作為時間分片(一個滾動視窗的長度);

(2)每個滾動視窗將其週期內的資料做聚合,存到下游狀態或打入外部線上儲存(記憶體資料庫如Redis,LSM-based NoSQL儲存如HBase);

(3)掃描線上儲存中對應時間區間(可以靈活指定)的所有行,並將計算結果返回給前端展示。

3)細粒度的滑動視窗案例

提交案例:統計最近1小時的uv,1秒更新一次(滑動視窗)

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SlideWindowDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--sliding-split false

尚矽谷大資料技術之Flink最佳化_V2

4)時間分片案例

提交案例:統計最近1小時的uv,1秒更新一次(滾動視窗+狀態儲存)

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SlideWindowDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--sliding-split true

尚矽谷大資料技術之Flink最佳化_V2

Flink 1.13對SQL模組的 Window TVF 進行了一系列的效能最佳化,可以自動對滑動視窗進行切片解決細粒度滑動問題。

尚矽谷大資料技術之Flink最佳化_V2

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/sql/queries/window-tvf

FlinkSQL調優

FlinkSQL官網配置引數:

https://ci.apache.org/projects/flink/flink-docs-release-1.13/dev/table/config.html

設定空閒狀態保留時間

Flink SQL新手有可能犯的錯誤,其中之一就是忘記設定空閒狀態保留時間導致狀態爆炸。列舉兩個場景:

  • FlinkSQL的regular join(inner、left、right),左右表的資料都會一直儲存在狀態裡,不會清理!要麼設定TTL,要麼使用FlinkSQL的interval join。
  • 使用Top-N語法進行去重,重複資料的出現一般都位於特定區間內(例如一小時或一天內),過了這段時間之後,對應的狀態就不再需要了。

Flink SQL可以指定空閒狀態(即未更新的狀態)被保留的最小時間,當狀態中某個key對應的狀態未更新的時間達到閾值時,該條狀態被自動清理:

#API指定
tableEnv.getConfig().setIdleStateRetention(Duration.ofHours(1));

#引數指定

Configuration configuration = tableEnv.getConfig().getConfiguration();

configuration.setString("table.exec.state.ttl", "1 h");

開啟MiniBatch

MiniBatch是微批處理,原理是快取一定的資料後再觸發處理,以減少對State的訪問,從而提升吞吐並減少資料的輸出量。MiniBatch主要依靠在每個Task上註冊的Timer執行緒來觸發微批,需要消耗一定的執行緒排程效能。

  • MiniBatch預設關閉,開啟方式如下:

// 初始化table environment

TableEnvironment tEnv = ...

// 獲取 tableEnv的配置物件

Configuration configuration = tEnv.getConfig().getConfiguration();

// 設定引數:

// 開啟miniBatch

configuration.setString("table.exec.mini-batch.enabled", "true");

// 批次輸出的間隔時間

configuration.setString("table.exec.mini-batch.allow-latency", "5 s");

// 防止OOM設定每個批次最多快取資料的條數,可以設為2萬條

configuration.setString("table.exec.mini-batch.size", "20000");

  • 適用場景

微批處理透過增加延遲換取高吞吐,如果有超低延遲的要求,不建議開啟微批處理。通常對於聚合的場景,微批處理可以顯著的提升系統效能,建議開啟。

尚矽谷大資料技術之Flink最佳化_V2

  • 注意事項:

1)目前,key-value 配置項僅被 Blink planner 支援。

2)1.12之前的版本有bug,開啟miniBatch,不會清理過期狀態,也就是說如果設定狀態的TTL,無法清理過期狀態。1.12版本才修復這個問題。

參考ISSUE:https://issues.apache.org/jira/browse/FLINK-17096

開啟LocalGlobal

原理概述

LocalGlobal最佳化將原先的Aggregate分成Local+Global兩階段聚合,即MapReduce模型中的Combine+Reduce處理模式。第一階段在上游節點本地攢一批資料進行聚合(localAgg),並輸出這次微批的增量值(Accumulator)。第二階段再將收到的Accumulator合併(Merge),得到最終的結果(GlobalAgg)。

LocalGlobal本質上能夠靠LocalAgg的聚合篩除部分傾斜資料,從而降低GlobalAgg的熱點,提升效能。結合下圖理解LocalGlobal如何解決資料傾斜的問題。

尚矽谷大資料技術之Flink最佳化_V2

由上圖可知:

  • 未開啟LocalGlobal最佳化,由於流中的資料傾斜,Key為紅色的聚合運算元例項需要處理更多的記錄,這就導致了熱點問題。
  • 開啟LocalGlobal最佳化後,先進行本地聚合,再進行全域性聚合。可大大減少GlobalAgg的熱點,提高效能。
  • LocalGlobal開啟方式:

1)LocalGlobal最佳化需要先開啟MiniBatch,依賴於MiniBatch的引數。

2)table.optimizer.agg-phase-strategy: 聚合策略。預設AUTO,支援引數AUTO、TWO_PHASE(使用LocalGlobal兩階段聚合)、ONE_PHASE(僅使用Global一階段聚合)。

// 初始化table environment

TableEnvironment tEnv = ...

// 獲取 tableEnv的配置物件

Configuration configuration = tEnv.getConfig().getConfiguration();

// 設定引數:

// 開啟miniBatch

configuration.setString("table.exec.mini-batch.enabled", "true");

// 批次輸出的間隔時間

configuration.setString("table.exec.mini-batch.allow-latency", "5 s");

// 防止OOM設定每個批次最多快取資料的條數,可以設為2萬條

configuration.setString("table.exec.mini-batch.size", "20000");

// 開啟LocalGlobal

configuration.setString("table.optimizer.agg-phase-strategy", "TWO_PHASE");

  • 注意事項:

1)需要先開啟MiniBatch

2)開啟LocalGlobal需要UDAF實現Merge方法。

提交案例:統計每天每個mid出現次數

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo count

尚矽谷大資料技術之Flink最佳化_V2

可以看到存在資料傾斜。

提交案例:開啟miniBatch和LocalGlobal

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo count \

--minibatch true \

--local-global true

尚矽谷大資料技術之Flink最佳化_V2

從WebUI可以看到分組聚合變成了Local和Global兩部分,資料相對均勻,且沒有資料傾斜。

開啟Split Distinct

LocalGlobal最佳化針對普通聚合(例如SUM、COUNT、MAX、MIN和AVG)有較好的效果,對於DISTINCT的聚合(如COUNT DISTINCT)收效不明顯,因為COUNT DISTINCT在Local聚合時,對於DISTINCT KEY的去重率不高,導致在Global節點仍然存在熱點。

原理概述

之前,為了解決COUNT DISTINCT的熱點問題,通常需要手動改寫為兩層聚合(增加按Distinct Key取模的打散層)。

從Flink1.9.0版本開始,提供了COUNT DISTINCT自動打散功能,透過HASH_CODE(distinct_key) % BUCKET_NUM打散,不需要手動重寫。Split Distinct和LocalGlobal的原理對比參見下圖。

尚矽谷大資料技術之Flink最佳化_V2

Distinct舉例:

SELECT a, COUNT(DISTINCT b)

FROM T

GROUP BY a

手動打散舉例:

SELECT a, SUM(cnt)

FROM (

SELECT a, COUNT(DISTINCT b) as cnt

FROM T

GROUP BY a, MOD(HASH_CODE(b), 1024)

)

GROUP BY a

  • Split Distinct開啟方式

預設不開啟,使用引數顯式開啟:

  • table.optimizer.distinct-agg.split.enabled: true,預設false。
  • table.optimizer.distinct-agg.split.bucket-num: Split Distinct最佳化在第一層聚合中,被打散的bucket數目。預設1024。

// 初始化table environment

TableEnvironment tEnv = ...

// 獲取 tableEnv的配置物件

Configuration configuration = tEnv.getConfig().getConfiguration();

// 設定引數:(要結合minibatch一起使用)

// 開啟Split Distinct

configuration.setString("table.optimizer.distinct-agg.split.enabled", "true");

// 第一層打散的bucket數目

configuration.setString("table.optimizer.distinct-agg.split.bucket-num", "1024");

  • 注意事項:

(1)目前不能在包含UDAF的Flink SQL中使用Split Distinct最佳化方法。

(2)拆分出來的兩個GROUP聚合還可參與LocalGlobal最佳化。

(3)該功能在Flink1.9.0版本及以上版本才支援。

提交案例:count(distinct)存在熱點問題

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo distinct

尚矽谷大資料技術之Flink最佳化_V2

可以看到存在熱點問題。

提交案例:開啟split distinct

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo distinct \

--minibatch true \

--split-distinct true

尚矽谷大資料技術之Flink最佳化_V2

從WebUI可以看到有兩次聚合,而且有partialFinal字樣,第二次聚合時已經均勻。

多維DISTINCT使用Filter

原理概述

在某些場景下,可能需要從不同維度來統計count(distinct)的結果(比如統計uv、app端的uv、web端的uv),可能會使用如下CASE WHEN語法。

SELECT

a,

COUNT(DISTINCT b) AS total_b,

COUNT(DISTINCT CASE WHEN c IN ('A', 'B') THEN b ELSE NULL END) AS AB_b,

COUNT(DISTINCT CASE WHEN c IN ('C', 'D') THEN b ELSE NULL END) AS CD_b

FROM T

GROUP BY a

在這種情況下,建議使用FILTER語法, 目前的Flink SQL最佳化器可以識別同一唯一鍵上的不同FILTER引數。如,在上面的示例中,三個COUNT DISTINCT都作用在b列上。此時,經過最佳化器識別後,Flink可以只使用一個共享狀態例項,而不是三個狀態例項,可減少狀態的大小和對狀態的訪問。

將上邊的CASE WHEN替換成FILTER後,如下所示:

SELECT

a,

COUNT(DISTINCT b) AS total_b,

COUNT(DISTINCT b) FILTER (WHERE c IN ('A', 'B')) AS AB_b,

COUNT(DISTINCT b) FILTER (WHERE c IN ('C', 'D')) AS CD_b

FROM T

GROUP BY a

提交案例:多維Distinct

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo dim-difcount

尚矽谷大資料技術之Flink最佳化_V2

提交案例:使用Filter

bin/flink run \

-t yarn-per-job \

-d \

-p 5 \

-Drest.flamegraph.enabled=true \

-Dyarn.application.queue=test \

-Djobmanager.memory.process.size=1024mb \

-Dtaskmanager.memory.process.size=2048mb \

-Dtaskmanager.numberOfTaskSlots=2 \

-c com.atguigu.flink.tuning.SqlDemo \

/opt/module/flink-1.13.1/myjar/flink-tuning-1.0-SNAPSHOT.jar \

--demo dim-difcount-filter

尚矽谷大資料技術之Flink最佳化_V2

透過WebUI對比前10次Checkpoint的大小,可以看到狀態有所減小。

設定引數總結

總結以上的調優引數,程式碼如下:

// 初始化table environment

TableEnvironment tEnv = ...

// 獲取 tableEnv的配置物件

Configuration configuration = tEnv.getConfig().getConfiguration();

// 設定引數:

// 開啟miniBatch

configuration.setString("table.exec.mini-batch.enabled", "true");

// 批次輸出的間隔時間

configuration.setString("table.exec.mini-batch.allow-latency", "5 s");

// 防止OOM設定每個批次最多快取資料的條數,可以設為2萬條

configuration.setString("table.exec.mini-batch.size", "20000");

// 開啟LocalGlobal

configuration.setString("table.optimizer.agg-phase-strategy", "TWO_PHASE");

// 開啟Split Distinct

configuration.setString("table.optimizer.distinct-agg.split.enabled", "true");

// 第一層打散的bucket數目

configuration.setString("table.optimizer.distinct-agg.split.bucket-num", "1024");

// 指定時區

configuration.setString("table.local-time-zone", "Asia/Shanghai");

常見故障排除

非法配置異常

如果您看到從 TaskExecutorProcessUtils 或 JobManagerProcessUtils丟擲的 IllegalConfigurationException,通常表明存在無效的配置值(例如負記憶體大小、大於 1 的分數等)或配置衝突。請重新配置記憶體引數。

Java 堆空間異常

如果報 OutOfMemoryError: Java heap space 異常,通常表示 JVM Heap 太小。可以嘗試透過增加總記憶體來增加 JVM 堆大小。也可以直接為 TaskManager 增加任務堆記憶體或為 JobManager 增加 JVM 堆記憶體。

還可以為 TaskManagers 增加框架堆記憶體,但只有在確定 Flink 框架本身需要更多記憶體時才應該更改此選項。

直接緩衝儲存器異常

如果報 OutOfMemoryError: Direct buffer memory 異常,通常表示 JVM直接記憶體限制太小或存在直接記憶體洩漏。檢查使用者程式碼或其他外部依賴項是否使用了 JVM 直接記憶體,以及它是否被正確考慮。可以嘗試透過調整直接堆外記憶體來增加其限制。可以參考如何為 TaskManagers、 JobManagers 和 Flink 設定的JVM 引數配置堆外記憶體。

元空間異常

如果報 OutOfMemoryError: Metaspace 異常,通常表示 JVM 元空間限制配置得太小。您可以嘗試加大 JVM 元空間 TaskManagers 或JobManagers 選項。

網路緩衝區數量不足

如果報 IOException: Insufficient number of network buffers 異常,這僅與 TaskManager 相關。通常表示配置的網路記憶體大小不夠大。您可以嘗試增加網路記憶體。

超出容器記憶體異常

如果 Flink 容器嘗試分配超出其請求大小(Yarn 或 Kubernetes)的記憶體,這通常表明 Flink 沒有預留足夠的本機記憶體。當容器被部署環境殺死時,可以透過使用外部監控系統或從錯誤訊息中觀察到這一點。

如果在 JobManager 程序中遇到這個問題,還可以透過設定排除可能的 JVM Direct Memory 洩漏的選項來開啟 JVM Direct Memory 的限制:

jobmanager.memory.enable-jvm-direct-memory-limit: true

如果想手動多分一部分記憶體給 RocksDB 來防止超用,預防在雲原生的環境因 OOM 被 K8S kill,可將 JVM OverHead 記憶體調大。

之所以不調大 Task Off-Heap,是由於目前 Task Off-Heap 是和 Direct Memeory 混在一起的,即使調大整體,也並不一定會分給 RocksDB 來做 Buffer,所以我們推薦透過調整 JVM OverHead 來解決記憶體超用的問題。

Checkpoint 失敗

Checkpoint 失敗大致分為兩種情況:Checkpoint Decline 和 Checkpoint Expire。

Checkpoint Decline

我們能從 jobmanager.log 中看到類似下面的日誌:

Decline checkpoint 10423 by task 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178.

我們可以在 jobmanager.log 中查詢 execution id,找到被排程到哪個 taskmanager 上,類似如下所示:

2019-09-02 16:26:20,972 INFO [jobmanager-future-thread-61] org.apache.flink.runtime.executiongraph.ExecutionGraph - XXXXXXXXXXX (100/289) (87b751b1fd90e32af55f02bb2f9a9892) switched from SCHEDULED to DEPLOYING.

2019-09-02 16:26:20,972 INFO [jobmanager-future-thread-61] org.apache.flink.runtime.executiongraph.ExecutionGraph - Deploying XXXXXXXXXXX (100/289) (attempt #0) to slot container_e24_1566836790522_8088_04_013155_1 on hostnameABCDE

從上面的日誌我們知道該 execution 被排程到 hostnameABCDE 的 container_e24_1566836790522_8088_04_013155_1 slot 上,接下來我們就可以到 container container_e24_1566836790522_8088_04_013155 的 taskmanager.log 中查詢 Checkpoint 失敗的具體原因了。

另外對於 Checkpoint Decline 的情況,有一種情況在這裡單獨抽取出來進行介紹:Checkpoint Cancel。

當前 Flink 中如果較小的 Checkpoint 還沒有對齊的情況下,收到了更大的 Checkpoint,則會把較小的 Checkpoint 給取消掉。我們可以看到類似下面的日誌:

$taskNameWithSubTaskAndID: Received checkpoint barrier for checkpoint 20 before completing current checkpoint 19. Skipping current checkpoint.

這個日誌表示,當前 Checkpoint 19 還在對齊階段,我們收到了 Checkpoint 20 的 barrier。然後會逐級通知到下游的 task checkpoint 19 被取消了,同時也會通知 JM 當前 Checkpoint 被 decline 掉了。

在下游 task 收到被 cancelBarrier 的時候,會列印類似如下的日誌:

DEBUG

$taskNameWithSubTaskAndID: Checkpoint 19 canceled, aborting alignment.

或者

DEBUG

$taskNameWithSubTaskAndID: Checkpoint 19 canceled, skipping alignment.

或者

WARN

$taskNameWithSubTaskAndID: Received cancellation barrier for checkpoint 20 before completing current checkpoint 19. Skipping current checkpoint.

上面三種日誌都表示當前 task 接收到上游傳送過來的 barrierCancel 訊息,從而取消了對應的 Checkpoint。

Checkpoint Expire

如果 Checkpoint 做的非常慢,超過了 timeout 還沒有完成,則整個 Checkpoint 也會失敗。當一個 Checkpoint 由於超時而失敗是,會在 jobmanager.log 中看到如下的日誌:

Checkpoint 1 of job 85d268e6fbc19411185f7e4868a44178 expired before completing.

表示 Chekpoint 1 由於超時而失敗,這個時候可以可以看這個日誌後面是否有類似下面的日誌:

Received late message for now expired checkpoint attempt 1 from 0b60f08bf8984085b59f8d9bc74ce2e1 of job 85d268e6fbc19411185f7e4868a44178.

可以按照7.7.1中的方法找到對應的 taskmanager.log 檢視具體資訊。

我們按照下面的日誌把 TM 端的 snapshot 分為三個階段:開始做 snapshot 前,同步階段,非同步階段,需要開啟DEBUG才能看到:

DEBUG

Starting checkpoint (6751) CHECKPOINT on task taskNameWithSubtasks (4/4)

上面的日誌表示 TM 端 barrier 對齊後,準備開始做 Checkpoint。

DEBUG

2019-08-06 13:43:02,613 DEBUG 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.

上面的日誌表示當前這個 backend 的同步階段完成,共使用了 0 ms。

DEBUG

DefaultOperatorStateBackend snapshot (FsCheckpointStorageLocation {fileSystem=org.apache.flink.core.fs.SafetyNetWrapperFileSystem@7908affe, checkpointDirectory=xxxxxx, sharedStateDirectory=xxxxx, taskOwnedStateDirectory=xxxxx, metadataFilePath=xxxxxx, reference=(default), fileStateSizeThreshold=1024}, asynchronous part) in thread Thread[pool-48-thread-14,5,Flink Task Threads] took 369 ms

上面的日誌表示非同步階段完成,非同步階段使用了 369 ms

在現有的日誌情況下,我們透過上面三個日誌,定位 snapshot 是開始晚,同步階段做的慢,還是非同步階段做的慢。然後再按照情況繼續進一步排查問題。

Checkpoint 慢

Checkpoint 慢的情況如下:比如 Checkpoint interval 1 分鐘,超時 10 分鐘,Checkpoint 經常需要做 9 分鐘(我們希望 1 分鐘左右就能夠做完),而且我們預期 state size 不是非常大。

對於 Checkpoint 慢的情況,我們可以按照下面的順序逐一檢查。

1)Source Trigger Checkpoint 慢

這個一般發生較少,但是也有可能,因為 source 做 snapshot 並往下游傳送 barrier 的時候,需要搶鎖(Flink1.10開始,用 mailBox 的方式替代當前搶鎖的方式,詳情參考https://issues.apache.org/jira/browse/FLINK-12477)。如果一直搶不到鎖的話,則可能導致 Checkpoint 一直得不到機會進行。如果在 Source 所在的 taskmanager.log 中找不到開始做 Checkpoint 的 log,則可以考慮是否屬於這種情況,可以透過 jstack 進行進一步確認鎖的持有情況。

2)使用增量 Checkpoint

現在 Flink 中 Checkpoint 有兩種模式,全量 Checkpoint 和 增量 Checkpoint,其中全量 Checkpoint 會把當前的 state 全部備份一次到持久化儲存,而增量 Checkpoint,則只備份上一次 Checkpoint 中不存在的 state,因此增量 Checkpoint 每次上傳的內容會相對更好,在速度上會有更大的優勢。

現在 Flink 中僅在 RocksDBStateBackend 中支援增量 Checkpoint,如果你已經使用 RocksDBStateBackend,可以透過開啟增量 Checkpoint 來加速。

3)作業存在反壓或者資料傾斜

task 僅在接受到所有的 barrier 之後才會進行 snapshot,如果作業存在反壓,或者有資料傾斜,則會導致全部的 channel 或者某些 channel 的 barrier 傳送慢,從而整體影響 Checkpoint 的時間。

4)Barrier 對齊慢

從前面我們知道 Checkpoint 在 task 端分為 barrier 對齊(收齊所有上游傳送過來的 barrier),然後開始同步階段,再做非同步階段。如果 barrier 一直對不齊的話,就不會開始做 snapshot。

barrier 對齊之後會有如下日誌列印:

DEBUG

Starting checkpoint (6751) CHECKPOINT on task taskNameWithSubtasks (4/4)

如果 taskmanager.log 中沒有這個日誌,則表示 barrier 一直沒有對齊,接下來我們需要了解哪些上游的 barrier 沒有傳送下來,如果你使用 At Least Once 的話,可以觀察下面的日誌:

DEBUG

Received barrier for checkpoint 96508 from channel 5

表示該 task 收到了 channel 5 來的 barrier,然後看對應 Checkpoint,再檢視還剩哪些上游的 barrier 沒有接受到。

5)主執行緒太忙,導致沒機會做 snapshot

在 task 端,所有的處理都是單執行緒的,資料處理和 barrier 處理都由主執行緒處理,如果主執行緒在處理太慢(比如使用 RocksDBBackend,state 操作慢導致整體處理慢),導致 barrier 處理的慢,也會影響整體 Checkpoint 的進度,可以透過火焰圖分析。

6)同步階段做的慢

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

RocksDB 開始 snapshot 的日誌如下:

2019/09/10-14:22:55.734684 7fef66ffd700 [utilities/checkpoint/checkpoint_impl.cc:83] Started the snapshot process -- creating snapshot in directory /tmp/flink-io-87c360ce-0b98-48f4-9629-2cf0528d5d53/XXXXXXXXXXX/chk-92729

snapshot 結束的日誌如下:

2019/09/10-14:22:56.001275 7fef66ffd700 [utilities/checkpoint/checkpoint_impl.cc:145] Snapshot DONE. All is good

6)非同步階段做的慢

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

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

Kafka動態發現分割槽

當 FlinkKafkaConsumer 初始化時,每個 subtask 會訂閱一批 partition,但是當 Flink 任務執行過程中,如果被訂閱的 topic 建立了新的 partition,FlinkKafkaConsumer 如何實現動態發現新建立的 partition 並消費呢?

在使用 FlinkKafkaConsumer 時,可以開啟 partition 的動態發現。透過 Properties指定引數開啟(單位是毫秒):

FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS

該參數列示間隔多久檢測一次是否有新建立的 partition。預設值是Long的最小值,表示不開啟,大於0表示開啟。開啟時會啟動一個執行緒根據傳入的interval定期獲取Kafka最新的後設資料,新 partition 對應的那一個 subtask 會自動發現並從earliest 位置開始消費,新建立的 partition 對其他 subtask 並不會產生影響。

程式碼如下所示:

properties.setProperty(FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS, 30 * 1000 + "");

Watermark不更新

如果資料來源中的某一個分割槽/分片在一段時間內未傳送事件資料,則意味著 WatermarkGenerator 也不會獲得任何新資料去生成 watermark。我們稱這類資料來源為空閒輸入或空閒源。在這種情況下,當某些其他分割槽仍然傳送事件資料的時候就會出現問題。比如Kafka的Topic中,由於某些原因,造成個別Partition一直沒有新的資料。由於下游運算元 watermark 的計算方式是取所有不同的上游並行資料來源 watermark 的最小值,則其 watermark 將不會發生變化,導致視窗、定時器等不會被觸發。

為了解決這個問題,你可以使用 WatermarkStrategy 來檢測空閒輸入並將其標記為空閒狀態。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

Properties properties = new Properties();

properties.setProperty("bootstrap.servers", "hadoop1:9092,hadoop2:9092,hadoop3:9092");

properties.setProperty("group.id", "fffffffffff");

FlinkKafkaConsumer<String> kafkaSourceFunction = new FlinkKafkaConsumer<>(

"flinktest",

new SimpleStringSchema(),

properties

);

kafkaSourceFunction.assignTimestampsAndWatermarks(

WatermarkStrategy

.forBoundedOutOfOrderness(Duration.ofMinutes(2))

.withIdleness(Duration.ofMinutes(5))

);

env.addSource(kafkaSourceFunction)

……

依賴衝突

ClassNotFoundException/NoSuchMethodError/IncompatibleClassChangeError/...

一般都是因為使用者依賴第三方包的版本與Flink框架依賴的版本有衝突導致。根據報錯資訊中的類名,定位到衝突的jar包,idea可以藉助maven helper外掛查詢衝突的有哪些。打包外掛建議使用maven-shade-plugin。

超出檔案描述符限制

java.io.IOException: Too many open files

首先檢查Linux系統ulimit -n的檔案描述符限制,再注意檢查程式內是否有資源(如各種連線池的連線)未及時釋放。值得注意的是,低版本Flink使用RocksDB狀態後端也有可能會丟擲這個異常,此時需修改flink-conf.yaml中的state.backend.rocksdb.files.open引數,如果不限制,可以改為-1(1.13預設就是-1)。

髒資料導致資料轉發失敗

org.apache.flink.streaming.runtime.tasks.ExceptionInChainedOperatorException: Could not forward element to next operator

該異常幾乎都是由於程式業務邏輯有誤,或者資料流裡存在未處理好的髒資料導致的,繼續向下追溯異常棧一般就可以看到具體的出錯原因,比較常見的如POJO內有空欄位,或者抽取事件時間的時間戳為null等。

通訊超時

akka.pattern.AskTimeoutException: Ask timed out on [Actor[akka://...]] after [10000 ms]

Akka超時導致,一般有兩種原因:一是叢集負載比較大或者網路比較擁塞,二是業務邏輯同步呼叫耗時的外部服務。如果負載或網路問題無法徹底緩解,需考慮調大akka.ask.timeout引數的值(預設只有10秒);另外,呼叫外部服務時儘量非同步操作(Async I/O)。

Flink on Yarn其他常見錯誤

https://developer.aliyun.com/article/719703

相關文章