Flink 2.0 狀態存算分離改造實踐

發表於2024-02-11

本文整理自阿里雲智慧 Flink 儲存引擎團隊蘭兆千在 FFA 2023 核心技術(一)中 的分享,內容關於 Flink 2.0 狀態存算分離改造實踐的研究,主要分為以下四部分:

  1. Flink 大狀態管理痛點
  2. 阿里雲自研狀態儲存後端 Gemini 的存算分離實踐
  3. 存算分離的進一步探索
  4. 批次化存算分離適用場景

一、Flink 大狀態管理痛點

1.1 Flink 狀態管理

圖片

狀態管理是有狀態流計算的核心。目前在 Flink 生產環境中使用的最多的狀態後端是基於 RocksDB 的實現,它是一個以本地磁碟為主的狀態管理,將狀態檔案儲存於本地,同時在進行檢查點的時候將檔案週期性寫入 DFS 。這是一種存算一體的架構,它足夠簡單,在小狀態作業下能夠保證穩定高效,可以滿足絕大部分場景的需求。隨著 Flink 的發展,業務場景日益複雜,大狀態作業屢見不鮮,在存算一體的架構下湧現了很多與狀態管理有關的現實問題。

1.2 大狀態作業痛點

圖片

大狀態作業下,基於 RocksDB 本地磁碟存算一體的狀態管理主要會遇到以下四方面的問題:

  • 本地磁碟有可能會出現空間不足的情況,通常解決這類問題的方法就是擴容。在目前叢集部署或是雲原生部署的模式下,單獨進行本地盤的擴容是不方便的,所以使用者一般會增加併發度,也就是涉及到儲存和計算綁在一起進行擴容,加劇了計算資源的浪費。
  • 作業正常狀態訪問時,本地磁碟 I/O 也會遇到一些瓶頸。這導致作業整體效能不足,同樣需要進行擴併發操作。
  • 檢查點的開銷比較大。由於狀態非常大,在檢查點期間對於遠端儲存訪問量呈現一個尖峰態勢。
  • 在作業恢復的時候,需要從遠端讀取全量檔案到本地,這個過程也十分緩慢。

上述前兩點是影響使用者成本的問題,而檢查點的開銷與恢復速度是 Flink 中影響易用性的關鍵問題。

1.3 存算分離的架構

圖片

對於以上問題,我們提出了存算分離的架構來解決。存算分離可以擺脫本地磁碟的限制,將遠端儲存(DFS)作為主儲存,同時將空閒的本地磁碟作為一個 Cache 來進行使用。同時使用者仍可以選擇本地磁碟作為主儲存,還用原來的模式來執行。這樣做的顯著的好處是,一方面由於磁碟空間和 I/O 效能不足的問題不再影響計算資源,另一方面是狀態檢查點與恢復在遠端就可以直接完成,變得更加輕量級。從架構上完美解決了大狀態作業面臨的問題。

二.阿里雲自研狀態儲存後端 Gemini 的存算分離實踐

在進入存算分離架構探討的最開始,我希望先從阿里雲自研的企業級狀態儲存 Gemini 入手,探尋它在存算分離上的一些實踐,主要分為以下三項:

2.1 多種檔案系統分層管理

圖片

Gemini 能夠把遠端作為狀態主儲存的一部分。它首先將狀態檔案儲存於本地磁碟,如果本地磁碟不足,則將檔案移動到遠端儲存。本地磁碟中存留的是訪問機率高的檔案,遠端儲存的是不容易訪問的檔案。兩部分共同構成了主儲存,並在此基礎上進行了冷熱劃分,保證了在給定資源條件下的高效服務。Gemini 的這種檔案分層管理模式擺脫了本地磁碟空間的限制。理論上本地空間可以配置為零,以達到純遠端儲存的效果。

2.2 狀態懶載入

圖片

Gemini 能夠支援遠端檔案儲存,在作業恢復的場景之下,無需將資料從遠端檔案載入回本地就可以開啟服務,使使用者作業進入執行狀態。這一功能稱為狀態懶載入。在實際恢復過程中,Gemini 僅需將後設資料以及少量記憶體中的資料從遠端載入回,就可以重建整個儲存並啟動。

雖然作業已經從遠端檔案啟動了,但讀取遠端檔案涉及到更長的 I/O 延遲,效能仍舊不理想,此時需要使用記憶體和本地磁碟進行加速。Gemini 會使用後臺執行緒進行非同步下載,將未下載的資料檔案逐漸轉移至本地磁碟。下載過程分為多種策略,比如按照 LSM-tree 層次的順序,或者按照實際訪問的順序來下載。這些策略可以在不同場景進一步縮短從懶載入恢復到全速執行效能的時間。

2.3 Gemini 延遲剪裁

圖片

在改併發的場景中,比如將兩個併發的狀態資料合併成一個併發時,目前 RocksDB 是把這兩份資料都下載完成之後再做一個合併,涉及到將多餘的資料剪裁掉,重建資料檔案,其速度是比較慢的。社群針對這個過程進行了很多的針對性最佳化,但仍然避免不了資料檔案的下載。Gemini 只需要把這兩部分資料的後設資料進行載入,並且把它們合成一個特殊的 LSM-tree 結構,就可以啟動服務,這一過程稱為延遲剪裁。

重建後 LSM-tree 的層數相比正常情況下會比較多。比如針對圖中的例子,有兩個 L0 層,兩個 L1 層和兩個 L2 層。由於 Flink 有 KeyGroup 資料劃分的機制存在,層數變多並不會對讀鏈路長度造成影響。由於並未對資料進行實際的裁剪,會存在一些多餘的資料,這些資料會在之後的整理 (Compaction) 過程逐步清理掉。延遲剪裁的過程無需對資料本身進行下載和實際合併操作,它可以極大地縮短狀態恢復的時間。

2.4 Gemini 恢復效果

圖片

有了非同步剪裁狀態+狀態懶載入,對於 Gemini 來說,恢復時間即作業從 INITIALIZING 到 RUNNING 的狀態可以變得非常之短,相比於本地狀態儲存的方案是一個極大的提升。

圖片

我們針對 Gemini 與 RocksDB 的改併發時間進行了評測。評測的指標為從作業啟動一直到恢復原有效能的時間,這包含了 Gemini 非同步下載檔案的時間。從上述實驗結果中可以看到 Gemini 相比於RocksDB 在縮容、擴容的場景下都有明顯的提升。

三.存算分離的進一步探索

圖片

Gemini 做存算分離相關的最佳化部分解決了前述大作業場景的問題。本地空間不足的問題可以透過遠端空間來解決。針對檢查點開銷大的問題,因為已經有一部分檔案遠端儲存上了,無需再次上傳,這部分的開銷也得以減少。針對作業恢復慢的問題,狀態懶載入+延遲剪裁功能,使得作業能夠快速的恢復執行狀態。

這裡還有一個功能是對 Memtable 的快照。Gemini 在做檢查點的時候,是將 Memtable 的原樣上傳到遠端儲存上,不會影響 Memtable flush 的過程,也不會影響內部的 Compaction。它的效果和通用增量快照的 changelog 的效果是類似的,都會緩解檢查點時的 CPU 開銷和 DFS I/O 量的尖峰。

3.1 Gemini 存算分離的問題

圖片

Gemini 在存算分離方面做了不錯的實踐,在阿里內部與雲上客戶的大狀態作業場景下均取得了不錯的效果。但它仍存在著一些問題:

第一個問題,Gemini 還是把本地磁碟作為主存的一部分,狀態檔案是優先寫到本地磁碟的,這並非最徹底的一個存算分離。這樣會導致檢查點時上傳檔案數量還是會比較多,持續時間較長,做不到非常輕量級的檢查點。

第二個問題,是所有存算分離方案都會遇到的一個問題,就是與本地方案的效能差距。目前的方案中 Gemini 已經利用了本地磁碟,但本地磁碟的利用效率並不是最高的。如果更多的請求可以落到記憶體或者本地磁碟,對應的遠端 I/O 的請求數降低,作業整體效能會有提升。另外,非同步 I/O 是很多儲存系統都會採用的最佳化。它使用提高 I/O 並行度的方式來解決提高作業的吞吐,是值得嘗試的下一步最佳化方向。

針對這幾個問題我們進行了簡單的探索,首先是做了一個非常徹底的存算分離,直接寫入遠端儲存並且把本地磁碟直接作為 Cache 來使用,在此基礎上實踐了不同形式的 Cache。第二方面,我們實現了一個簡單的非同步 I/O PoC,驗證其在存算分離場景上的效能提升。

3.2 直接寫入遠端與本地磁碟 Cache 的探索

3.2.1 原始方案:基於檔案的本地磁碟 Cache

圖片

直接使用遠端儲存作為主存的改動我們不作詳述,在這裡主要探討 Cache 的形態與最佳化問題。最簡單的架構是基於檔案的 Cache 。如果遠端的檔案被訪問到,它會被載入到本地磁碟 Cache。與此同時記憶體 Cache 仍然存在,並且仍舊採用 BlockCache 的形式。這個形式是非常簡單高效的架構。但是記憶體 BlockCache 和本地磁碟的檔案 Cache 有很大的一個資料重複,這相當於浪費了很多空間。另一方面,由於檔案的粒度相對較粗,對於同一個檔案的不同 block ,其訪問的機率並不一樣,所以會有一些冷的 block 維持在磁碟中,降低了本地磁碟的命中率。針對這兩個問題,我們設計了全新的本地磁碟 Cache 的形態,對上述問題進行最佳化。

3.2.2 最佳化方案:基於 Block 的本地磁碟 Cache

圖片

我們提出將本地磁碟與記憶體結合起來,組成一個以 block 為粒度的混合式 Cache。它使用一個整體的 LRU 進行統一的管理,不同 block 只有介質上的不同。記憶體上相對冷的 block 會非同步地刷到本地磁碟上,磁碟的 block 是按照順序以追加寫的形式來寫在底層檔案中。如果由於 LRU 策略淘汰了某些磁碟的 block,必然會對映到某個檔案上形成空洞。為了維持 Cache 空間有效性,我們採取了空間回收來進行最佳化。空間回收的過程是一個空間和 CPU 開銷的權衡。

圖片

不同層的檔案如 L0 file 、L1 file 以及 L2 file,它們的生命週期是不一樣的。對於 L0 file 來講,它的生命週期比較短一些,但是熱度相對高。對於 L2 file 來講,檔案本身更容易存活,但是熱度是相對低的。根據這些不同的特點,我們可以採取不同的策略來進行空間回收。來自不同層檔案 block 會被 Cache 在不同的底層檔案中。針對不同的底層檔案可以執行不同的空間回收閾值與頻率,這樣可以保證最大的空間回收效率。

另外我們針對 block 淘汰策略也提出了最佳化方案。最原始的 LRU 是根據命中頻率來進行管理的,某個 block 一段時間內不命中則會被淘汰。這種策略並沒有考慮到在快取某一個block 的空間開銷。也就是說可能為了快取某個 block,卻有更多的 block 沒有辦法進行快取。在這裡引入了一個新的評判體系叫做快取效率,用一段時間內命中次數除以 block 大小,來更好的評判每一個快取的 block 是否應該被快取。這種評判方式的缺點是開銷會比較大。最基本的 LRU 針對於查詢都是 O(1) 的,但快取效率的評分需要實現一個優先佇列,其執行效率會有較大下降。所以在這裡的思路還是在保持 LRU 主體管理的情況下,針對 block 的快取效率異常的情況進行特殊化處理。

目前發現有兩部分異常,第一部分是記憶體中的 data block 。它的命中率是記憶體中相對低的,但是它的佔比能達到 50%。目前對於它的策略就是進行壓縮,其代價是每次訪問涉及到解壓,但這個開銷要比進行一個 I/O 的開銷要小得多。第二部分是磁碟中的 filter block 。雖然它有命中,但它的大小是比較大的,快取效率並不高。在這裡實現了一個傾向於把磁碟中的 filter block 優先踢出的策略,使得相對上層的資料可以快取進來。在測試作業場景中,這兩條特殊規則與 LRU 相結合,相比於沒有這兩條規則的時候,整體 TPS 會上升 22%,效果比較顯著。

圖片

但直接寫入遠端使系統出現了遠端檔案冷讀問題,即檔案第一次生成後的讀取仍然需要涉及到遠端 I/O。為了解決這個問題,我們在這裡也做了一個小的最佳化,在本地磁碟上提供一個上傳遠端的佇列,並且讓其中的檔案多快取一段時間。這個時間並不會很長,大概是二三十秒的一個級別,在此期間佇列檔案的遠端 I/O 會變為本地 I/O。這樣的做法能夠讓遠端冷讀的問題大大的緩解。

圖片

到目前為止我們有兩種存算分離的 Cache 方案,第一種是基於檔案的本地磁碟 Cache 方案,它的優點是非常簡單和有效,在磁碟充足的場景下有與本地方案類似的效能,因為本地磁碟可以快取所有檔案。第二種是混合式 block cache 的最佳化,在本地磁碟不足的情況下是一個非常好的方案,因為它提升了 Cache 的命中率。但是它也帶來了比較大的管理開銷。如果我們想要有一個通用的方案來適配所有場景,應該怎麼做呢?

3.2.3 混合方案:自適應變化

圖片

將上述兩種方案結合,我們設計了一個自適應變化的的混合方案。在磁碟充足的情況下使用的是基於檔案的 Cache 方案,在磁碟不足的情況下,會把本地磁碟自動的和記憶體結合在一起組成混合式 block cache 方案。兩種方案的結合會讓它們兩個的優點結合在一起,在所有的場景下都能夠最大化的滿足效能效率的需求。

3.2.4 混合方案:評測

圖片

圖片

我們針對上述提出的混合方案使用測試作業進行評測。可以看到在 TPS 上,新方案相比於檔案為粒度的原始快取方案有 80% 的提升。同時它也伴隨著一些 CPU 的開銷,用 CPU 效率(TPS/CPU)作為評判標準,新方案也有 40% 的提升。Cache 命中率的提升是 TPS 提升的一個主要來源。

3.3 非同步 I/O 的探索

3.3.1 同步單條處理模式

圖片

第二項探索是對 Flink 進行的非同步 I/O 改造與測試。如圖展示了目前 Flink 的單執行緒處理模型,在 Task 執行緒上面,所有的資料是按順序來進行處理的。對於每一條資料處理,會分為運算元(operator)的 CPU 開銷,狀態(State)訪問的 CPU 開銷,以及狀態訪問所需的 I/O 時間,其中 I/O 是最大的一塊開銷。由於存算分離需要訪問遠端儲存,其 I/O 延遲會比本地方案大,最終會導致整體 TPS 有明顯下降。

3.3.2 批次處理+內部非同步模式

圖片

我們對這一模式進行更改,使得 State 操作可以同時進行。在 Task 執行緒的角度來講,State 被並行化之後整體的時間被縮小,所以 TPS 會有一個提升。同時,Task 執行緒需要預先攢批,這和 micro-batch 做的事情是非常類似的,同理也可以借用預聚合的功能,降低 state 訪問的數目,TPS 得以進一步提升。

3.3.3 運算元非同步+批次處理模式

圖片

更進一步,在加上狀態訪問非同步的基礎之上,可以繼續探索從運算元的角度上進行非同步化的過程。這意味著狀態訪問已經開始了非同步執行後,讓 Task 執行緒得以繼續進行其他資料的 CPU 操作。但這樣做有一個問題:狀態訪問 I/O 一般都是時間比較長的,雖然在 Task 執行緒閒的時候可以做一些其他的資料的處理工作,但是最終會一個速率不匹配的問題,瓶頸最終還會落到狀態訪問上,會退化到沒有做此最佳化的情況。

經過權衡,我們認為僅採用攢批,再加上批內的狀態訪問使用非同步 I/O 這種方式,是一個比較平衡的方案。

3.3.4 存算分離+批次化:評測

圖片

我們做了一個簡單的支援批次非同步的介面的狀態後端,並在社群 Microbenchmark 上面做了一個簡單的測試,目前僅涉及到 value get 的場景。從對比結果上可以看到,批次執行加上非同步 I/O 是對存算分離場景有很大的提升。

四.批次化非同步 I/O 存算分離適用場景

圖片

上述探索的批次化執行的存算分離狀態訪問有獨特的應用場景。對於大狀態作業來講,存算分離在功能上解決了最開始所述的幾個問題,在效能上,用批次介面的方式來彌補它的低的問題。

4.1 效能分析

圖片

此種方案的效能來源是 State 訪問在批次內並行化,減少了狀態訪問的時間,提升了計算節點的 CPU 利用率。這種方案對於大狀態作業效能提升是很有用的。

4.2 定性效能分析

圖片

在小狀態作業的場景下,狀態訪問可以做到非常的快,將狀態訪問從 Task 執行緒抽離出來的提升量很小,且引入了執行緒之間互動的開銷。所以在小狀態的場景,這種批次非同步狀態訪問的方案或許還不如原始本地狀態管理方案。

隨著狀態大小逐漸增大,狀態 I/O 開銷逐漸增大併成為了瓶頸,非同步 I/O 的執行當於攤薄了每個 I/O 所耗的時間。這導致了圖中紅色線的下降是較慢的,而本地狀態管理(藍色線)降低會比較快。在達到某個狀態大小後,非同步 I/O 的方案效能會顯著的好。這種方案需要消耗 I/O 頻寬,如果狀態訪問已經達到了 I/O 上限,非同步 I/O 不能減少 I/O 的總時間,故此時它的斜率跟本地狀態管理差不多。

圖片

如果狀態很小的時候就達到 I/O 上限,並行化執行並不會產生效果,上圖所示的便是這個場景。

總結一下,批次並非同步執行狀態訪問在滿足以下條件時會有優勢:

  • 大狀態作業場景且狀態訪問是作業的瓶頸
  • I/O 並沒有達到瓶頸(未打滿)
  • 業務對於攢批的延遲(亞秒到秒級別)可以接受

絕大部分存算分離場景下,由於 I/O 效能是儲存叢集提供,可以支撐比較大的 I/O 量且可以靈活伸縮,一般不會過早達到 I/O 瓶頸狀態,非同步 I/O 可以很好的最佳化存算分離場景。

五. 結語

以上介紹了我們在存算分離方面做的一些探索。這些工作我們希望藉著 Flink 2.0 的機會貢獻給社群,一方面是支援純遠端的存算分離方案+混合式快取的儲存後端,另一方面是希望能夠引入非同步化 I/O 保證存算分離模式下的高效能資料處理。

Flink Forward Asia

2023本屆 Flink Forward Asia 更多精彩內容,可微信掃描圖片二維碼觀看全部議題的影片回放及 FFA 2023 峰會資料!

圖片

更多內容

圖片

活動推薦阿里雲基於 Apache Flink 構建的企業級產品-實時計算 Flink 版現開啟活動:0 元試用 實時計算 Flink 版(5000CU*小時,3 個月內)瞭解活動詳情:https://free.aliyun.com/?pipCode=sc

圖片