(轉自我的微信公衆號 KAMI說 )
Flink 是當前最流行的分佈式計算框架,其提供的 Table API 和 SQL 特性,使得開發者可以通過成熟,直觀、簡潔、表達力強的標準 SQL 描述計算邏輯,大大減少其學習、開發和維護成本。
Flink SQL 支持面向無邊界輸入流的流處理。然而。聚合統計、窗口統計等計算是有狀態的。在流處理中,若這些狀態數據隨時間不斷堆積、不斷膨脹,會導致因爲OOM頻繁發生導致的作業崩潰、重啓。
從 Flink 1.6 版本開始,社區引入了狀態 TTL(Time-To-Live) 特性。在通過Flink SQL 實現流處理時,開發者可以爲作業 SQL 設置TTL,實現過期狀態的自動清理,從而防止作業狀態無限膨脹。
然而,目前Flink SQL 只支持粗粒度的TTL設置,即一段 SQL 只能設置一個TTL。在一些常見的應用場景中,這不足夠。
一
下面是一段計算DAU指標的 SQL 代碼
SELECT
t_date
, COUNT(DISTINCT user_id) AS cnt_login
, COUNT(DISTINCT CASE WHEN t_date = t_debut THEN user_id END) AS cnt_new
FROM
(
SELECT
t_date
, user_id
, MIN(t_date) OVER (
PARTITION BY user_id
ORDER BY proctime
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS t_debut
FROM Login
) AS t
GROUP BY t_date
這段SQL的業務意義很直觀,就是計算實時每日登陸用戶和新增登陸用戶。
- 第一層的窗口統計,計算每個用戶有史以來最小的登陸日期,即其新增日期
- 第二層的聚合統計,按天進行聚合,計算每天的登陸用戶數和新增用戶數
然而,在TTL的設置上,我們面臨兩難狀況:
- 不設置TTL。那麼在第二層按天進行的聚合統計,COUNT DISTINCT計算帶來的狀態會隨着天數近乎線性增長,狀態會不斷膨脹,帶來OOM等一系列問題
- 設置TTL,例如 n 天未訪問的狀態自動清理。那麼在第一層的窗口統計,n天不活躍的用戶的登陸日期狀態就可能被清除,導致其後續再次登錄時被誤判爲新增
要解決這個矛盾,我們實際上需要 Flink SQL 提供 TTL 的細粒度配置,即爲一段SQL設置多個 TTL :
- 第一層的窗口統計不設置TTL,所有用戶的登陸日期狀態永久保留
- 第二層的聚合統計設置 n 天的 TTL,保證其狀態不會無限增長
下面給大家介紹,如何實現Flink SQL的細粒度 TTL 配置。
二
大家都知道,在 Flink 中,通過 Table API 和 SQL 實現的流處理邏輯,最終會翻譯爲基於 DataStream API 實現的 DataStream 作業,返回這個作業輸出的 DataStream (writeToSink 本質上也是先得到 DataStream 作業,再爲其輸出 DataStream 加上一個 DataStreamSink) 。
從一段 SQL 到 DataStream 作業,其過程簡單描述如下:
- 在 TableEnvironment,即“表環境”,將數據源註冊爲動態表。例如,通過表環境的接口
registerDataStream
, 作爲源的DataStream,即數據流, 在表環境註冊爲動態表 - 通過表環境的接口
sqlQuery
,將 SQL 構造爲 Table 對象 - 通過toAppendStream/toRetractedStream接口,即翻譯接口,將 Table 對象表達的作業邏輯,翻譯爲 DataStream 作業。
在調用翻譯接口,將 Table 對象翻譯爲 DataStream 作業時,通過翻譯接口傳入的 TTL 配置,遞歸傳遞到各個計算節點的翻譯、構造邏輯裏,使得翻譯出來的 DataStream 算子的內部狀態按照該 TTL 配置及時清理。
如果我們將上述計算DAU的SQL拆分成兩段,前者作爲一箇中間結果,提供給後者調用。
SQL1:
SELECT
t_date
, user_id
, MIN(t_date) OVER (
PARTITION BY user_id
ORDER BY proctime
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS t_debut
FROM Login
SQL2:
SELECT
t_date
, COUNT(DISTINCT user_id) AS cnt_login
, COUNT(DISTINCT CASE WHEN t_date = t_debut THEN user_id END) AS cnt_new
FROM t_middle
GROUP BY t_date
從第一段 SQL 構建對應 Table 對象,再調用翻譯接口,翻譯成 DataStream 作業,其輸出數據流爲 s_middle
。其可以使用 Row 作爲流數據類型,各個字段的名稱和類型可以通過 Table 對象的 Schema獲得。顯然,這個 DataStream 作業是原來完整DAU計算 DataStream 作業的一部分,其輸出爲一箇中間結果。
然後,將這個中間結果數據流 s_middle
在表環境重新註冊爲動態表 t_middle
,各個字段的名稱和類型可以通過 Table 對象的 Schema獲得。這是第二段 SQL 需要調用的中間結果動態表。
最後,從第二段 SQL 構建對應 Table 對象,再調用翻譯接口,加上 n 天的 TTL 配置,翻譯成 DataStream 作業。顯然,這個 DataStream 作業是原來完整DAU計算 DataStream 作業的另外一部分,其輸出爲完整的 DAU 計算結果。
顯然,第一段 SQL 對應的計算節點,其狀態 TTL 爲永不過期。第二段 SQL 對應的計算節點,其狀態 TTL 爲 n 天后過期!TTL的細粒度配置實現!
三
歸納一下,如果要給 Flink SQL 設置細粒度TTL配置,我們只需要:
1. 將原來一段 SQL 代碼,按照不同的TTL,改寫爲前後依賴的多個子 SQL。
2. 對於每個子 SQL,若不是最下游的,進行“翻譯-重註冊”:
a. 加上對應的 TTL 配置,翻譯爲 DataStream 作業,得到其輸出數據流,其中,流數據類型使用 Row,各個字段的名稱和類型可以通過 Table 對象的 Schema獲得
b. 將中間結果數據流在表環境重新註冊,表名爲下游子SQL調用的表名,各個字段的名稱和類型可以通過 Table 對象的 Schema獲得
3. 最後一個子 SQL,加上對應的 TTL 配置,翻譯成 DataStream 作業,其輸出數據流即爲完整計算的輸出。
需要注意的是,處理時間(Process-Time)和事件時間(Event-Time)字段,對應的數據類型在Flink Table API & SQL 的包 flink-table
中是私有的,在外部訪問會出錯。
所以,在“翻譯-重註冊”過程中,需要特殊處理時間和事件時間字段:
- 通過 Table 對象的 Schema 找出時間特性字段,然後通過 Table.select 方法,剔除時間特性字段,再翻譯成 DataStream 作業,得到中間結果數據流。
- 爲中間結果數據流重新構造時間特性字段,在重註冊爲動態表時,按照原字段名重新聲明。
總結一下,整個細粒度TTL配置的實現過程實施:
- 按 TTL 的不同,將 SQL 拆解爲多個子 SQL
- 對每個子 SQL 進行“翻譯-重註冊”,包括時間特性字段的處理
- 最後一個子 SQL 完成翻譯,得到的 DataStream 作業的輸出便是完整計算邏輯的輸出
四
細心的讀者會發現,如果中間的計算過程包含聚合計算,翻譯出的 DataStream 作業的輸出數據流只能是帶撤回標誌位的數據流(簡稱撤回流)DataStream<Tuple<Boolean, Row>>
,無法直接重註冊到表環境中。上述的方法無法應用於有多層 TTL 配置不一樣的聚合操作的 Flink SQL 中。
也就是說,要實現所有場景下的 Flink SQL 的細粒度 TTL 配置,我們必須實現撤回流注冊爲動態表這一特性。
本系列文的第二篇《Flink SQL 細粒度TTL配置的實現(二)》將給大家介紹具體的實現方法,需要對Flink Table API & SQL 的包 flink-table
的源碼進行一點修改,盡情期待。
掃描下方二維碼關注我的公衆號“KAMI說”,有更多精彩原創內容哦~