摘要:本文整理自 ASF Member、Apache Flink & HBase PMC、阿里巴巴資深技術專家李鈺 (絕頂),Apache Flink Committer、阿里巴巴技術專家唐雲 (茶幹) 在 Flink Forward Asia 2021 核心技術專場的演講。主要內容包括:
- State Backend Improvement
- Snapshot Improvement
- Future Work
一、State Backend improvement
在過去一年中,Flink 社群 state-backend 模組有了很大的發展。在 1.13 版本之前,使用者對於狀態相關運算元的效能缺乏監控手段,也沒有很好的辦法可以獲悉狀態讀寫操作的延遲。
我們引入了狀態訪問的延時監控,原理就是在每次狀態訪問前後,使用 system.nowTime 去統計訪問延時,然後將其儲存在一個 histgram 型別的指標中,監控功能開啟對其效能的影響,尤其是 state 訪問的效能影響是比較小的。
關於狀態訪問延遲監控相關的配置,需要特別強調的是取樣間隔和保留歷史資料這兩個配置,取樣間隔越小,資料結果就越準確,但是對日常訪問的效能影響也稍大一些;歷史資料保留的個數越多,資料結果越精確,但是記憶體佔用也會稍大一些。
在 Flink 1.14 版本中,我們終於將 RocksDB 從 5.17 升級到 6.20 版本,除了 RocksDB 自身若干 bug 的修復,新版 RocksDB 也增加了一些特性,可以用於 Flink 1.14 和 1.15 中。首先它支援了 ARM 平臺,可以保證 Flink 作業能夠在 arm 基礎上執行,其次提供了更細粒度的 WriteBuffer 記憶體管控,提升記憶體管控的穩定性。此外,還提供了 deleteRange 介面,為之後在擴容場景下的效能提升帶來非常大的幫助。
隨著雲原生的愈發流行,通過 K8s 調入在容器環境中執行 Flink 作業已經成為越來越多廠商的選擇,這其中不可避免需要考慮受限資源如何穩定執行,尤其是記憶體使用方面的管控。而誕生在 2010 年度的 RocksDB 在這方面的能力先天有些不足,Flink 1.10 才首次引入了記憶體管控。在過去的一年中, RocksDB 在記憶體管控方面又有了一些進步和改善。
首先回顧一下 RocksDB 記憶體方面的問題,談這個問題之前要了解 Flink 是如何使用 state 和 RocksDB 的。
- Flink 每宣告一個 state,都會對應 RocksDB 中的一個 column family,column family 是 RocksDB 中獨立的記憶體分配,它們之間通過物理資源來隔離;
- 其次,Flink 並不限制使用者在一個 operator 內宣告的 state 數目,所以它也沒有限制 column family 數目;
- 最後,Flink 在 slot-sharing 機制下一個 slot 內可以存在多個包含 keyed state 的 operator。
基於以上三個原因,即使不考慮 RocksDB 自身在記憶體管理上的限制,理論上來說 Flink 的使用方式就有可能導致不受限的記憶體使用。
上圖定義了一個 SQL 類的多個 RocksDB 的例項,共享了一個 writeBuffer manager 及其對應的 block cache,其中管理多個 writeBuffer 的 manager 將它所申請的記憶體在 block cache 中進行記賬,資料相關的 block 在 block cache 中進行快取,快取包括資料相關的 data block,索引相關的 index block 和過濾用的 filter block,可以簡單地理解成寫快取和讀快取。
由此可見,writeBuffer manager 與 block cache 協同工作的方式就是 manager 在 block cache 中進行記賬。
manager 在 buffer 申請流程後,會以 io blocks 為基本單位在 block cache 中進行記憶體升級。預設 io block 是單個 writeBuffer 的 1/8,writeBuffer 的配置是 64Mb,所以 io block 的 size 是 8Mb,而這 8Mb 記憶體申請會再次拆成若干 dummy entry,分配到 Block 若干的 shard 中。需要特別說明的一點是,Flink 升級 RocksDB 之後,dummy entry 的最小單元降到了 256KB,降低了記憶體申請超額的概率。
因為 RocksDB 本身的設計是為多執行緒考慮的,所以在一個 cache 中會存在多個 shard,所以它的記憶體申請就會比較複雜。
WriteBuffer manager 內部實現產生的記憶體中,可變的 WriteBuffer 何時轉化為不可變的 WriteBuffer。Immutable table 刷到磁碟上的過程中,預設 mutable writebuffer 的使用量是有上限的,達到上限之後就會提前 flush 這些 WriteBuffer。這會導致一個問題,即使寫入的資料量不大,一旦申請 arena block,尤其是 arena block 比較多的情況下,就會提前觸發 member table flush 的問題。從使用者角度來說,會發現本地存在大量很小的 SST 檔案,整體的讀寫效能也很差,因此 Flink 社群專門對此做了 arena block size 的配置校驗功能。
目前 RocksDB 自身存在記憶體管控的不足和限制,所以需要在特定場景下預留一部分對外記憶體給 RocksDB 超額使用。對照上圖 Flink process 記憶體模型,可以看到需要在 jvm-overhead 上對記憶體進行適當的保留,防止 RocksDB 超用。左邊的表格展示了 jvm-overhead 相關的預設配置值,如果想要將 jvm-overhead 配置成 512Mb,只要將 mini 和 max 都配置成 512Mb 即可。
在記憶體有限的場景下,data block,index Block 以及 fliter blocks 是存在競爭問題的。上圖的 Block 例項是按照實際大小進行繪製的,以 256Mb 檔案的 SST 舉例,其中 index block 大約是 0.5Mb,fliter block 大概是 5Mb,data block 一般是 4KB-64KB,可以看到 block 的競爭會導致大量的換入換出,極大影響讀效能。
為了上述問題,我們將 RocksDB 的 partition-index 和 partition-filter 功能進行封裝,優化了記憶體受限情況下的效能。就是將索引 index 和過濾 filter 進行分層儲存,從而可以在有限記憶體中儘可能儲存資料 block,減少磁碟的讀取概率,從而提升整體效能。
除了關於穩定性相關的改善,Flink 還著重重構了 state 相關的 API,對新手的理解會更友好。
以前的 API 是混合了狀態讀寫的 statebackend 和負責容錯備份的 checkpoint 兩者的概念。以 MemoryStatebackend 和 FsStateBackend 為例,二者在狀態讀寫、訪問物件方面是完全相同的,區別僅在於容錯備份,所以初學者很容易混淆其中的區別。
上圖展示了更新之後的 Flink 狀態讀寫和容錯點查 API 與更新前的區別。
新版裡我們將狀態訪問與容錯備份進行了分別的設定,上圖是新版 API 與舊版 API 的對照表格。可以看到,MemoryStatebackend 和 FsStateBackend 負責狀態讀寫的都是 HashMaoStateBackend 的狀態儲存。
二者的最大區別就是在 checkpoint 容錯方面,一個是對應全記憶體的 ManagercCheckpointStorage,而另一個對應的是基於檔案的 FileSystemSCheckpointStorage。相信通過對 API 的重構,能夠給開發者以更深刻的理解。
二、Snashot Improvement
SavePoint 本身是與 state-backend 結耦的,並不侷限於是通過什麼樣的 state-backend 實現。然而以前的 Flink 版本中,不同的 state-backend 的 SavePoint 格式是不同的,但是在新版 Flink 中,社群統一了相關的 SavePoint 格式,對於同樣的作業可以在不丟失狀態的情況下無縫切換 state-backend。
此外,社群還進一步增強了 unaligned checkpoint 的穩定性。將 channel 中的 buffer 作為 in-flight 資料,看作 operator state 的一部分進行提前持久化,避免 barrier 對齊的時間。
此外,在新版 Flink 中,社群支援了傳統的 aligned 與 unaligned 之間的自動切換,只要設定一個全域性的超時時間,Flink 的 checkpoint 達到閾值之後,就會自動的進行切換,相信這個功能的引入也可以進一步幫助開發者獲得更好的 checkpoint 效能。
三、Future Work
未來,我們會進一步提高 RocksDB backend 的生產易用性。一方面我們會將 RocksDB 內部一些關鍵的效能指標,例如 block cache 命中率等新增到標準監控指標中,從而可以更加方便地對 RocksDB 的效能進行調優。另一方面,我們計劃將 RocksDB 的日誌檔案重定向到 TM 日誌目錄下或者 TM 日誌中,能夠更方便地檢視 RocksDB 日誌資訊,來定位問題和調優。
其次我們會進一步梳理明確 Flink 的快照語義,目前在 Flink 中有三種形態的快照,分別是 checkpoint,savepoint 和 retained checkpoint 。
- 其中 checkpoint 是系統快照,其資料生命週期完全由 Flink 框架控制,用於在異常發生時進行 fail over,一旦作業停止,將被自動刪除;
- savepoint 負責統一格式的資料備份,其生命週期與 Flink 作業解耦,完全由使用者控制,可以用來實現 Flink 作業的版本升級、跨叢集遷移、state-backend 的切換等需求;
- 而 retained checkpoint 的語義和生命週期目前都比較模糊,它可以獨立於 Flink 作業生命週期之外存在,但當基於它恢復並且開啟增量快照的時候,新作業的 checkpoint 會依賴其中的資料,從而導致使用者很難判斷何時可以安全地將其刪除。
為了解決這一問題,社群提出了 FLIP-193,要求使用者基於 retained checkpoint 啟動作業的時候,宣告是採用 claim 還是 no-claim 模式。
- 如果採用 claim 模式,則該 retained checkpoint 的資料生命週期完全由新作業掌控,即隨著資料 Compaction 的發生,當新快照不再依賴於 retained checkpoint 當中的資料時,新作業可以將其安全刪除;
- 而如果採用 no-claim 模式,則新作業不能修改 retained checkpoint 的資料,這意味著新作業在第一次快照時需要做物理拷貝,不能引用 retained checkpoint 當中的資料。這樣,在需要的時候可以隨時手動將 retained checkpoint 的刪除,而不需要擔心影響基於其恢復的作業。
此外,後續我們計劃對使用者控制的快照賦予更清晰的語義,引入 native format 的 savepoint 的概念來替代 retained checkpoint。
最後介紹一下正在進行中的 FLIP-158 的工作。它引入了 Changelog based state backend 來實現更快速平穩的增量快照,相當於引入了一種基於 log 打點方式的快照。相比於目前已有的 snapshot 穩健的增量快照機制,它有更短的快照間隔,但同時會犧牲一些狀態資料處理延時。這其實就是在延遲和容錯之間的取捨和均衡。
更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~