Flink SQL 在快手的擴充套件和實踐

ApacheFlink發表於2022-02-16

摘要:本文整理自快手實時計算團隊技術專家張靜、張芒在 Flink Forward Asia 2021 的分享。主要內容包括:

  1. Flink SQL 在快手
  2. 功能擴充套件
  3. 效能優化
  4. 穩定性提升
  5. 未來展望

FFA 2021 直播回放 & 演講 PDF 下載

一、Flink SQL 在快手

img

經過一年多的推廣,快手內部使用者對 Flink SQL 的認可度逐漸提高,今年新增的 Flink 作業中,SQL 作業達到了 60%,與去年相比有了一倍的提升,峰值吞吐達到了 6 億條/秒。

二、功能擴充套件

為了支援內部的業務需求,快手做了很多功能擴充套件,本文重點分享其中的兩個圍繞視窗的擴充套件,一個是 Group Window Aggregate 擴充套件,一個是在 Flip-145 裡提出的 Window Table-valued Function 擴充套件。

img

解釋一下以上兩者的區別和聯絡:

  • Group Window Aggregate 是在 Flink 1.12 和更早的版本里用來做視窗聚合的,它有兩個侷限性,第一個是它的語法不符合 SQL 標準,要藉助特殊的視窗函式,還要配合視窗輔助函式來完成作業聚合。另外它還限制了視窗函式只能出現在 group by 的子句裡面,所以只能用於聚合;
  • 因此 Flink 在 Flip-145 裡提出了 Window TVF,它是基於 2017 年的 SQL 標準裡提出的多型表函式的語法,另外它除了可以在視窗上做聚合,還可以做視窗關聯,TopN 和去重等操作。

2.1 Group Window Aggregate 擴充套件

大家可能會問,既然已經有 Window TVF 了,為什麼還要在 Group Window Aggregate 上做功能擴充套件呢?因為快手是在今年下半年才開始進行版本 1.10 到 1.13 的升級,大部分業務還是在使用 1.10 版本。

在 Group Window Aggregate 上快手做了兩個擴充套件,一個是支援多維聚合,一個是引入高階視窗函式。

2.1.1 支援多維分析

img

Flink SQL 很早就支援無限流上的多維聚合,快手在 Group Window Aggregate 上也增加了多維分析的功能,支援標準的 Grouping Sets、Rollup 和 CUBE 子句,另外還支援各種視窗型別,比如滾動、滑動、會話視窗等。

比如上圖例項,需要統計主題維度和總維度下的累計 UV,SQL 的 group by 子句裡包含兩部分:一個是 CUMULATE 視窗函式,一個是 Grouping Sets 子句。括號裡有兩個元素:一個表示總維度,一個表示主題維度。

2.1.2 引入高階視窗函式

img

資料分析的開發者經常會遇到這樣的需求,繪製一條曲線,每個點的含義是當天 0 點到當前時間點的累計指標,橫座標表示時間,縱座標是表示累計的指標。對這樣的需求可以有兩個解決方案:

  • 第一個方案是使用無限流聚合,把時間歸一到分鐘粒度以後作為 group key 的一列,但是業務上要求輸出到螢幕上的曲線不再變化,而無限流聚合的輸出結果是一個更新流,所以不符合要求。
  • 第二個方案是使用一天的滾動視窗函式。為了提前輸出結果,還是要設定提前觸發,時間點選用當前機器的時間或者是歷史輸入資料裡最大的時間戳。這個方案的缺點,首先是每個點的縱座標的值並不是這個時間點上的累計值。這會導致幾個異常現象,比如作業失敗重啟或者是業務主動回溯歷史的時候,不能完全還原當時的歷史曲線。而且各個點上分維度的累計值加起來也不等於總維度的值。還有一個缺點,統計 UV 的時候,我們經常會使用兩階段的聚合來避免 distinct key 的傾斜,但是使用這個方案的時候,原本自身的曲線上可能會出現凹坑。

img

上圖是方案二導致的一些異常曲線:

  • 第一個曲線是進行歷史回溯, lag 消除以後曲線才開始正常,在沒有完全消除 lag 的時候,曲線是不平滑的,而且不能還原歷史曲線。
  • 第二個曲線是自增曲線上出現凹坑。

img

因為第一級聚合的輸出流是一個更新流,Flink 目前的更新機制是傳送撤回和更新兩條獨立的訊息,而不是一個原子訊息,所以第二個聚合可能會先收到上游多個併發上發下來的撤回訊息,這就會導致累計值先下降再上升,所以形成了凹坑。

我們引入 CUMULATE 視窗來解決這些問題。

img

這個視窗函式和 Flip-145 裡提出的 CUMULATE 視窗是不謀而合的,只是語法上在 Group Window Aggregate 上引入這個視窗型別。它有三個必選引數:時間屬性列、視窗的步長和 max size,還有一個可選引數,用來指定視窗開始的偏移量。

img

關於 CUMULATE 視窗的劃分邏輯,假設 CUMULATE 視窗的步長是一分鐘,max size 是三分鐘,window1 的區間是 0~1 分,window2 是 0~2 分,window3 是 0~3 分,window4 開始是 3~4 分,window5 是 3~5 分,以此類推,而一條時間戳是 0 分 30 秒的資料,會被劃分到 window1、window2 和 window3 三個視窗裡。

img

比如需要繪製一條資料曲線,一分鐘打一個點,每個點表示各個子頁面當天累計 UV。查詢語句採用事件時間,CUMULATE 視窗函式的步長是一分鐘,max size 是一天,業務的 group key 是子頁面的 ID,時間戳是視窗的結束時間。

img

上圖可以看到,使用 CUMULATE 方案繪製出來的曲線不管是正常消費還是歷史回溯都很平滑。

CUMULATE 視窗的優點

img

  • 第一個優點是使用視窗的結束時間作為每個點的橫座標,曲線上每個點的縱座標就是在橫座標對應時間點上的累計值,所以無論在回溯歷史或者是作業發生 failover 的情況下,曲線都可以完全還原,而且各個時間點上分維度的值加起來總是等於總維度的值。
  • 第二個優點是使用兩階段聚合,能夠防止 distinct key 傾斜。由於資料是在視窗結束的時候才傳送,所以不存在撤回,輸出的是 append 流,因此自增曲線上也不會有凹坑。

Dynamic cumulate window

img

Dynamic cumulate window 也是為了解決曲線類的需求,比如計算直播間自開播以來的累計指標,與前面需求的不同點是每個直播間的開播關播能持續多久都是不確定的,但它也是一種計算累計指標。它有兩個必選引數:時間屬性列和視窗的步長,還有一個可選引數視窗的 gap,用來定義視窗多久沒有輸入資料就認為它已經結束了。這裡需要注意,一個視窗結束會觸發視窗的結果輸出,而且會清理掉狀態,如果又來了一條相同 key 的資料,遲到的資料會被丟棄,沒有遲到的資料會被劃分到新的視窗去,累計值也會從零開始。

img

如上圖案例,需要繪製一個曲線,每個點表示每個直播間開播以來的累計 UV,如果一個直播間連續一個小時沒有資料流入,則認為關播。視窗函式使用 Dynamic cumulate window,步長是 1 分鐘,gap 是 60 分鐘,Group key 是直播間 ID,時間戳依然使用視窗的結束時間。

2.2 Window Table-valued Function 擴充套件

2.2.1 豐富 Window TVF 運算元

img

社群在 Flip-145 中提出的 Window Table-valued Function (window tvf) 語法,並且實現了視窗聚合。在這個基礎上我們豐富了視窗運算元,包括 TopN、關聯和去重,還支援了一個單獨的 window Table-valued Function 的查詢語句,這些功能都已經陸續推到社群的各個版本里。有了這些視窗的運算元,使用者就可以用 Flink SQL 實現更復雜的業務邏輯。

img

如上圖,需要統計當天最熱銷的 100 件商品的銷售額和買家數,以及這些爆品所屬主播的銷售情況。首先做一個視窗聚合,得到 0 點以來每個商品的銷售額和買家數,再做一個視窗聚合,得到每個主播所有寶貝的買家數,兩個結果做視窗關聯,最後做視窗上的 TopN,得到前 100 名爆品以及它的各項累計指標。

2.2.2 支援 Window Offset

img

window offset 主要用來調整視窗的劃分邏輯,它是個可選引數,預設值是 0,表示 unix 時間的零點作為視窗劃分的起始時間,它的值可以是一個正數,表示向右偏移,也可以是一個負數,表示向左偏移。但是它只會影響如何劃分視窗,不會影響 watermark。另外,相同的視窗,不同的 offset 可能會產生相同的偏移效果,比如對一個 10 分鐘的滾動視窗,把起點向左偏移 4 分鐘或者向右偏移 6 分鐘,對視窗劃分產生的影響是一樣的。

img

如上圖例項,需要繪製一個資料曲線,每分鐘打一個點表示每個頁面本週以來的累積 UV。可以使用 CUMULATE 視窗函式,採用事件時間,步長是 1 分鐘,max size 是 7 天。因為 unix time 零點那天是在週四,假設用預設的 offset,視窗劃分就是從本週四到下週四,所以要設定 offset 為 4 天,表示向右偏移 4 天,這樣就是從本週一到下週一。

2.2.3 支援 Batch Mode

img

另外我們還增加了對批模式的支援。原理是引入一個 windows 運算元,給輸入資料附上所屬的視窗屬性後發給下游,而下游的運算元複用批上已經存在的運算元,比如說聚合上是用 HashAgg 或者 SortAgg,關聯上是 HashJoin 或者 SortMergeJoin,這些批上的運算元和流模式下的運算元相比,不需要狀態,所以吞吐上也有更好的表現。

三、效能優化

img

本文主要介紹兩個方面的優化,一個是聚合上的狀態優化,一個是維表關聯上的攢批優化。

3.1 聚合上的狀態優化

img

先通過一個例子來了解一下聚合場景下 distinct states 的狀態複用。需要統計應用下每個子頻道的 UV,該用例有兩個特點,頻道是可列舉的以及每個頻道訪客的重合度很高的。

img

最原始的查詢語句如上圖,group key 是一個頻道,用一個 count distinct 來計算各個頻道的 UV。裝置集合在狀態中首先是存在一個 map state,假設頻道的列舉只有三個,A、B 和 other,group key 是頻道 ID, map state 的 key 裝置 ID, value 是一個 64 bit 的 long 型別的值,每個 bit 表示這個頻道下該裝置是否出現,在簡單的場景下這個 Value 值就是 1。

上圖 A 頻道下有兩個裝置,ID 分別是 1 和 2,ID 為 1 的裝置同時訪問了 B 頻道,id 為 2 的裝置同時訪問了 other 頻道。可以發現,不同頻道的 map 可以有大量的重合,想要複用這些 key,可以用社群提供的方法來手動改寫 SQL。

img

首先做個行轉列的操作,把三個頻道值拍到三個 count distinct 聚合函式的 filter 條件,在輸出之前再用一個自定義的表函式來做列轉行。

img

改寫後的查詢語句、裝置集合的狀態和儲存如上圖。Group key 是 empty,map state key 是裝置 ID,map state value 是一個 64bit 的 long 型別,每個 bit 表示各頻道下此裝置是否出現,比如 ID 為 1 的裝置 value 是 110,表示這個裝置訪問了頻道 A 和 B,ID 為 3 的裝置訪問了頻道 B 和 other。

這個方案大大減少了狀態,但也存在兩個缺點。第一是需要手動改寫 SQL,如果一個維度有多個值或有多個可列舉的維度,那麼手動改寫的 SQL 會很長,另外一個缺點是需要用自定義的表函式進行列轉行轉換。

img

我們提出一種簡化的 SQL 表達方式,既能達到狀態上的收益,又能減輕資料開發人員的負擔。使用者只需要在查詢語句裡,通過一個方式告訴優化器 group key 的列舉值,優化器就會自動改寫,進行轉列和列轉行,改寫後就可以複用 distinct map state。改寫後等價下的查詢語句,只需要在過濾條件裡指定列舉值就可以,用 in 或 or 的表達方式都可以。

img

上述效能優化可以用在無限流聚合和視窗聚合,並且一個可列舉維度或多個可列舉維度都是可以的,可以用在簡單的聚合查詢,也可以用在多維聚合。

但它的限制條件是 group key 裡面至少有一個 key 是可列舉的,而且列舉值必須是靜態的,能夠明確寫在過濾條件裡。另外每個維度下的 distinct key 得有重合才能達到節約狀態的效果。如果需要統計每個省份的 UV,基本上可以認為不同省份的訪客是沒有交集的,這個時候複用 distinct key 是沒有收益的。另外在視窗聚合的時候,視窗函式必須具有行語義,不可以是集合語義。對於行語義的視窗,當前這個資料屬於哪個視窗取決於資料本身;但是對於集合語義的視窗,當前這條資料屬於哪個視窗,不僅取決於資料本身,還取決於這個視窗收到過的歷史資料集合。這個優化調整聚合運算元的 group key,會影響每個視窗收到的資料集合,所以不適用於集合語義的視窗。

最後可能有使用者會問,為什麼語法上不採用 Calcite 提供 pivot/unpivot 來顯式地表達行轉列和列轉行。首先是條件不具備,因為 calcite 的 1.26 版本才引入 pivot,1.27 才引入 unpivot,而 Flink 從 1.12 版本至今都是依賴 Calcite 1.26。第二個原因是如果用 pivot/unpivot 的語法,SQL 會比現在表達方式長很多。

3.2 維表關聯的攢批優化

img

維表關聯的攢批優化是為了減少 RPC 的呼叫次數。原理是攢一批資料以後,呼叫維表的批量查詢介面,語法上我們引入通用的 Mini-Batch hint,它有兩個引數:一個表示多長時間攢一批,一個表示多少條資料攢一批。一個合法的 Mini-Batch hint 的至少包含一個引數。我們將 hint 設計得很通用,希望它不僅可以用於維表關聯,還可以用於聚合的攢批優化。

img

再看一個例子,需要打寬訂單表,關聯訂單的客戶資訊。查詢語句在 customers 維表後面跟一個 hint 表示 5 秒攢一批或 1 萬條資料攢一批,這個優化在底層運算元和設計的實現上,遠比 SQL 語法的表達要複雜得多。

四、穩定性提升

img

穩定性方面主要介紹對 Group Window Aggregate 解決資料傾斜和 Flink SQL 聚合指標調整之後的狀態相容這兩部分快手做的一些優化和改進。

4.1 Group Window Aggregate 的資料傾斜

img

window 計算在快手內部的應用非常廣泛,快手的業務場景比較容易遇到資料傾斜,比如大主播的直播、一些重大活動等。實時計算如果遇到資料傾斜,輕則指標延遲,重則資料事故,所以我們在 Tumble window 上支援了 Mini-Batch、Local-Global、Split Distinct 的優化,其他常用的 window 上也支援了類似的優化。這些優化上線之後,不僅能夠幫助業務規避資料傾斜,同時還可以帶來不錯的效能收益。

4.2 Aggregate State 相容

img

首先來看 Flink SQL 上 Aggregate state 相容的業務場景。隨著業務的發展,日常執行的任務可能要新增指標或者刪除不需要的指標。重大活動過程中,業務要新增天級累計的指標或者活動週期持續累積的指標。

img

如果是天級指標的變更,開發者只能丟棄狀態,在 0 點之後升級任務,然後再指定任務從零點的資料開始消費,從而保證指標的連續。如果是活動持續累積的指標,為了避免對原有指標的影響,只能選擇新增一個任務來單獨計算新增指標,但是這樣會導致資源的冗餘。

img

之所以需要這麼複雜的操作,是因為 Flink SQL 判斷 state 是否相容的策略比較簡單,只看引擎需要的 state 和 Savepoint 裡儲存的 state 的資料型別是否完全一致,完全一致就是相容,否則不相容。這種判斷方式存在一個漏洞,State 的型別沒變但是 SQL 中的聚合函式變了,這種情況 Flink 也會認為狀態是相容的。

img

基於這個背景,我們提出了 aggregate state 的相容,目標是使使用者學習使用 state 相容方案的成本極低 (或 0 成本),使用者可以隨時升級任務,不需要再卡零點操作,支援對聚合函式的新增和刪除操作。

img

aggregate state 相容的操作場景只能在聚合函式尾部新增聚合函式,允許刪除任意位置的聚合函式,不允許修改聚合函式的順序,也不允許一次升級同時有新增和刪除兩種操作,需要分為兩次升級完成。

上圖右表格是指標標識和 state 型別的對映關係。為了方便判斷 state 是否相容,我們把指標標識和 state 型別的對映關係儲存到 state 的 meta 中,因為有的聚合函式可能有不止一個 state,比如 avg 函式,它就需要通過 sum 和 count 兩個 state 來輔助計算,所以這個對映關係很重要。

img

在新增聚合函式的時候,需要對新增的 state 做初始值填充,不同函式對應的初始值是不一樣的,比如 count 的初始值是 0,但是 sum 的初始值必須是 null。

img

window 的 early-fire 和 late-fire 場景會引入 Retract 訊息,這樣就多一個 state 來記錄已經下發給下游的資料。它比 window 原有的 state 多了時間欄位,在做判斷和狀態遷移的時候需要做一下處理。

img

前面提到了我們把指標標識和 state 型別的對映關係儲存到了 state 的 meta 資訊裡,這會帶來 state 向前相容的問題,新版本的程式不能正確讀取之前版本的 Savepoint。為了解決這個問題,需要修改 meta 的 version 資訊,利用 version 資訊來區分新老版本的 state,從而做到 state 的向前相容。

img

在 aggregate 的場景下,使用者可能會通過設定 state TTL 來控制無效 state 的清理,aggregate state 相容也要對這個場景做處理,保證遷移之後的狀態,TTL 的時間戳要和原來的資料保持一致,不能做任何改變。

img

Aggregate state 相容的方案,優點是使用者學習使用的成本很低,幾乎無感知、不依賴任何外部的服務架構,不足是對 Flink 原始碼有侵入,增加了未來升級 Flink 版本的成本,而且目前只能支援聚合類計算場景。

img

最後介紹一下快手正在做的狀態相容的終極方案。使用者可以在 Savepoint 的任意位置增加、刪除 state、甚至是修改 state 中的內容;還可以自定義一份完整的 state,比如 Flink on hudi 任務的 state 初始化。

img

終極方案的優點是不侵入 Flink 的原始碼,方便 Flink 版本升級,使用者可以在平臺介面操作,不需要開發程式碼以及支援全場景的 state 相容,不再侷限於具體的場景。不足是對於使用者來說學習成本比較高,需要了解 Operator State 和 keyedState 這些比較專業的知識點,而且還要知道 Operator 裡面是否包含 state。

五、未來展望

img

未來,快手會在 Stream SQL 方向持續擴充套件功能,提升效能,達到降本增效的目的,以及探索更多場景下的狀態相容;流批一提方面,快手將會完善 Flink Batch SQL 的能力,增加推測執行、自適應查詢等優化,提升 Batch SQL 的穩定性和效能,繼續拓寬業務應用場景;在資料湖和實時數倉方面,會繼續推動它們在更多業務場景下的落地。


FFA 2021 直播回放 & 演講 PDF 下載

更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~

image.png

相關文章