一個作業,多個TTL-Flink SQL 細粒度TTL配置的實現(一)

(轉自我的微信公衆號 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 作業,其過程簡單描述如下:

  1. 在 TableEnvironment,即“表環境”,將數據源註冊爲動態表。例如,通過表環境的接口registerDataStream, 作爲源的DataStream,即數據流, 在表環境註冊爲動態表
  2. 通過表環境的接口 sqlQuery,將 SQL 構造爲 Table 對象
  3. 通過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 中是私有的,在外部訪問會出錯。
所以,在“翻譯-重註冊”過程中,需要特殊處理時間和事件時間字段:

  1. 通過 Table 對象的 Schema 找出時間特性字段,然後通過 Table.select 方法,剔除時間特性字段,再翻譯成 DataStream 作業,得到中間結果數據流。
  2. 爲中間結果數據流重新構造時間特性字段,在重註冊爲動態表時,按照原字段名重新聲明。

總結一下,整個細粒度TTL配置的實現過程實施:

  1. 按 TTL 的不同,將 SQL 拆解爲多個子 SQL
  2. 對每個子 SQL 進行“翻譯-重註冊”,包括時間特性字段的處理
  3. 最後一個子 SQL 完成翻譯,得到的 DataStream 作業的輸出便是完整計算邏輯的輸出

細心的讀者會發現,如果中間的計算過程包含聚合計算,翻譯出的 DataStream 作業的輸出數據流只能是帶撤回標誌位的數據流(簡稱撤回流)DataStream<Tuple<Boolean, Row>>,無法直接重註冊到表環境中。上述的方法無法應用於有多層 TTL 配置不一樣的聚合操作的 Flink SQL 中。

也就是說,要實現所有場景下的 Flink SQL 的細粒度 TTL 配置,我們必須實現撤回流注冊爲動態表這一特性。

本系列文的第二篇《Flink SQL 細粒度TTL配置的實現(二)》將給大家介紹具體的實現方法,需要對Flink Table API & SQL 的包 flink-table 的源碼進行一點修改,盡情期待。

掃描下方二維碼關注我的公衆號“KAMI說”,有更多精彩原創內容哦~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章