Flink on TiDB —— 便捷可靠的實時數據業務支撐

作者介紹: 林佳,網易互娛計費數據中心實時業務負責人,實時開發框架 JFlink-SDK 和實時業務平臺 JFlink 的主程,Flink Code Contributor。

本文由網易互娛計費數據中心實時業務負責人林佳老師分享,主要介紹網易數據中心在處理實時業務時爲什麼選擇 Flink 和 TiDB,以及兩者的結合應用情況。

今天主要從開發的角度來跟大家聊一聊爲什麼網易數據中心在處理實時業務時,選擇 Flink 和 TiDB。

首先,TiDB 是一個混合型的 HTAP 分佈式數據庫,具備一鍵水平伸縮、強一致性的多副本數據安全、分佈式事務、實時 OLAP 等重要特性,同時兼容 MySQL 協議和生態,遷移便捷,運維成本極低。而 Flink 是目前最熱門的開源計算框架,在處理實時數據方面,其高吞吐量、低延遲的優異性能以及對 Exactly Once 語義的保障爲網易遊戲實時業務處理提供了便捷支持。

Flink on TiDB 究竟可以創造怎樣的業務價值? 本文將從一個實時累加值的故事來跟大家分享。

從一個實時累加值的故事說起

接觸過線上業務的同學應該對上述數據非常熟悉,這是一張經典的線上實時業務表,也可以理解爲日誌或某種單調遞增的數據,包含了事實發生的時間戳、賬戶、購買物品、購買數量等。針對這類數據的分析,假設使用 Flink 等實時計算框架,可以通過分桶處理,如 groupby 用戶 ID,groupby 道具,再對時間進行分桶,最終將產生如下的持續數據。

如果將上述持續數據落入 TiDB,與此同時 TiDB 仍保持已有的線上維度表,如賬戶信息、道具信息等,通過對錶做一個 JOIN 操作就能快速從事實的統計數據中分析出時序數據所代表的價值,再對接到可視化應用,能發現很多不一樣的東西。

整個過程看起來非常簡單又完美, Flink 解決計算問題,TiDB 解決海量存儲問題。但,事實真的如此嗎?

實際接觸線上數據的同學可能會遇到類似的問題,如:

  • 多種數據源:各個業務方的外部系統日誌,並且存在有的數據存儲在數據庫,有的需要以日誌的方式調用,還有以 rest 接口調用的方式。

  • 數據格式多樣:各個業務或渠道打的數據格式完全不同,有的是 JSON,有的是 Encoded URL。

  • 亂序到達:數據到達順序被打亂。

基於上述問題,我們引入了 Flink 。在數據中心內部,我們封裝了一套稱之爲 JFlink - SDK 的框架,主要基於 Flink 對 ETL 、亂序處理、分組聚合以及一些常用需求進行模塊化、配置化,然後通過線上數據源的配置,計算得到一些事實的統計或事實數據,最後入到可以容納海量數據的 TiDB 中。

但是, Flink 在處理這批數據時,爲了故障恢復,會通過 CheckPoint 保存數據當前的計算狀態。如果在兩次保存期間,發生了數據計算的 commit,即這部分計算結果已經刷出 TiDB 了,然後發生了故障,那麼 Flink 會自動回退到上一個 CheckPoint 的位置,即回退到上一次正確的狀態。此時,如圖的 4 筆數據就會被重算,重算之後可能會被更新到 TiDB 中。

如果數據是個累加值的話,可以看到其累加值被錯誤地累加了兩遍,這是使用 Flink on TiDB 可能出現的問題之一。

Flink 的準確保證

Flink 的準確保證

Flink 如何提供準確性保證?首先,需要了解 Flink 的 CheckPoint 機制。CheckPoint 類似於 MySQL 的事務保存點,指在做實時數據處理時,對臨時狀態的保存。

CheckPoint 分爲 At least Once 和 Exactly Once,但即使選擇使用 Exactly Once 也無法解決上面累加值重複計算的問題。比如從 Kafka 讀了數據,以上述事實表爲基礎 account 是 1000、購買物品爲 a 、購買數量分別爲 1 件和 2 件,此時 Flink 處理數據就會被分到分桶裏。與此同時,另一種 Key 會被 Keyby,相當於 MySQL 的 groupby 分到另一個桶裏去計算,然後通過聚合函數刷到 TiDB Sink 中。

計算狀態的保存

Flink 通過 CheckPoint 機制來保證數據的 Exactly Once。假設需要進行一個比較簡單的執行計劃 DAG,只有一個 source,然後通過 MAP 刷 TiDB sink。在這個過程中,Flink 是線性的,通過在數據流裏面插入 CheckPoint barrier 機制來完成,相當於 CheckPoint barrier 走到哪裏,哪裏就觸發線性執行計劃中的算子保存點。

假設從 source 開始,那麼會保存 source,如果是 Kafka,需要存一下 Kafka 的當前消費位置。在節點保存完畢之後,需要做下一個算子的狀態保存,此處的 MAP 假設是分桶計算,那麼它其實就已經存了桶裏的累積數據。

在此之後,CheckPoint barrier 就到達了 sink,此時 sink 也去做相應的狀態儲存。當相應的狀態存儲分別做完之後,總的 Job Manager (相當於 Master) 彙報狀態存儲的 CheckPoint 已經完成了。

而當 Master 確認了所有的子任務都已經完成了分佈式任務的 CheckPoint 之後,會分發一個 Complete 的信息。如上圖模型所示,可以聯想到它其實就是 2PC,分佈式二階段提交協議,每個分佈式子任務分別提交自己的事務,然後再整體提交整個事務。被存下來的狀態將存儲在 RocksDB 中,當出現故障時,可以從 RocksDB 恢復數據,然後從斷點重新計算整個流程。

Exactly Once 語義支持

回看 Exactly Once,上述方式真的能實現 Exactly Once 嗎?其實不能,但爲何 Flink 官方稱這是 Exactly Once 呢?以下將詳述其中緣由。

從上圖的代碼可以看出,Exactly Once CheckPoint 是無法保證端到端的,只能保證 Flink 內部算子的 Exactly Once。因此,將計算數據去寫入 TiDB 時,如果 TiDB 無法與 Flink 聯動,就無法保證端到端的 Exactly Once 了。

類比一下什麼是端到端,其實 Kafka 就支持這種語義,因爲 Kafka 對外暴露了 2PC 的接口,允許用戶手動調整接口來控制 Kafka 事務的 2PC 過程,也因此可以利用 CheckPoint 機制來避免算錯的情況。

但如果不能手動控制,那會怎麼樣呢?

我們來看看如下實例,假設仍然將用戶設置爲 1000,購買道具爲 A 的數據寫入到 TiDB 的累加表,會生成如下 SQL:INSERT VALUES ON DUPLICATE UPDATE。當 CheckPoint 發生時,能否保證該語句被執行到 TiDB?

如果不加特殊處理,簡單執行這條 SQL 的話,其實不能保證這條 SQL 究竟有沒有被執行,如未執行,則會報錯,退回到上一個 CheckPoint,皆大歡喜。因爲它實際上沒有計算,沒有累加,也不會重複計算一遍,所以是對的。但如果已經寫出,再去重複的退回上一個 CheckPoint,那麼將會出現重複累加 3 的情況。

Flink 爲了解決這個問題,提供了一種接口,可以手動實現 SinkFunction,控制事務的開始,Pre Commit、Commit、Rollback。

而 CheckPoint 機制本質是一種 2PC,當分佈式算子在執行內部事務時,其實算子關聯到 Pre Commit。同理,假設在 Kafka 中,可以通過 Pre Commit 事務將 Kafka 事務預提交。當算子收到 Job Manager(即 Master)同步的所有算子 CheckPoint 的狀態保存都已完成時,此時 Commit,事務是必定成功的。

如果其他算子失敗了,則需要進行 Rollback,確保事務沒有被成功地提交到遠端。這裏如果有 2PC SinkFunction 加上 XA 全 section 語義的話,其實就可以做到嚴格意義的 Exactly Once。

但不是所有的 sink 都支持二階段提交協議,比如 TiDB 內部是二階段提交來管理協調其事務,但是目前來說,並沒有把二階段提交協議提供給用戶手動控制。

冪等計算

那麼,如何做到保證業務的 Exactly Once 結果落到 TiDB?其實也很簡單,採用 At Least Once 語義加上一個 Unique Key,即冪等計算。

如何選擇 Unique Key? 如果一份數據有一個唯一標誌,我們自然會選擇其唯一標誌。比如一份數據有唯一 ID,當一張表通過 Flink 同步到另一張表的時候,這就是很經典的利用其 Primary key 做 insert ignore 或者 replace into 的語義去重。如果是日誌,可以選擇日誌文件特有的屬性。而如果通過 Flink 去計算聚合結果,則可以用聚合的 Key 加上窗口邊界值,或者其他的冪等方式來計算出數值,作爲最終計算的唯一鍵。

如此,就可以實現結果是可重入的。既然可重入,再加上 CheckPoint 的可回退特性,就可以把 Flink 跟 TiDB 結合起來,做到精準的 Exactly Once 結果寫入。

Flink on TiDB

在 Flink on TiDB 部分,我們內部的 JFlink 框架對 Flink 進行封裝,然後在與 TiDB 聯動上又做了什麼?以下將詳述。

數據連接器的設計

首先,是數據連接器的設計。因爲 Flink 對於 TiDB 的支持或者說對關係型數據庫的支持都比較慢,Flink Conector JDBC 在 Flink 1.11 版本纔出現,時間還不太長。

目前,我們將 TiDB 作爲數據源,把數據放在 Flink 處理,主要是通過 TiDB 官方提供的 CDC 工具,相當於通過監聽 TiDB 的變更,將數據落到 Kafka。而 Kafka 又是非常經典的流式數據管道,所以通過 Kafka 將數據進行消費處理,然後再通過 Flink 進行處理。

但不是所有業務都可以用 CDC 模式,比如落數據時要增加一些比較複雜的過濾條件,或者落數據時需要定期讀取某些配置表,亦或者先需要了解外部的一些配置項才能知道切分情況時,可能就需要手動的自定義 source。

而 JFlink 在封裝時,其實是封裝了業務字段的單調錶來進行切片讀取。單調是指某張表一定會有某個字段,單調變化的,或者是 append only。

在實現上,TiDB 和 Flink 之間,封裝了 JFlink TiDB Connect,通過一個連接詞去創建跟 TiDB 的鏈接。然後通過異步線程來撈數據,再通過阻塞隊列進行阻塞。阻塞隊列的作用主要是爲了流控。

對於 Flink 的主線程,主要通過監聽阻塞隊列上的有非空信號。當收到非空信號時,就把數據拉出來,通過反序列化器作爲整個實時處理框架的流轉對象,然後可以對接後面各種模塊化了的 UDF。在實現 source 的 At Least Once 語義時,如果藉助 Flink 的 CheckPoint 機制,就變得非常簡單了。

因爲我們已經有個大前提,即這張表是一張由某個字段組成的單調錶,在單調錶上進行數據切分時,就可以記下當前的切分位置。如果發生故障,讓整條流回退到上一個 CheckPoint,source 也會回退到上一個保存的切片位置,此時就能夠保證不漏數據的消費,即實現了 source 的 At Least Once。

對於 sink,其實 Flink 官方是提供了 JDBC sink,當然 source 也提供了JDBC sink,但是 Flink 官方提供的 JDBC sink 實現比較樸素,使用同步批量插入的語義。

其實同步批量插入是比較保守的,當數據量比較大時,且沒有嚴格的先來先提交的語義,此時使用同步提交相對來說性能不是很高,如果使用異步提交的話,性能就會提升很多,相當於充分利用了 TiDB 分佈式數據庫的特性,支持小事務高併發,有助於提升 QPS。

當我們實現 sink 時,實際上原理也非常簡單。我們這裏先講講 Flink 官方是怎麼實現。Flink 官方是通過將 Flink 的主線程寫到一張 buffer 中,當 buffer 寫滿時進行換頁,同時拉起一條線程將數據同步到 TiDB。

而我們的改進是通過一個阻塞隊列來進行流控,然後把數據寫到某個 buffer 頁,當 buffer 頁寫滿時,馬上拉起一條異步線程去刷出,這樣就可以保證在非 FIFO 語義下提升 QPS 的性能。實踐證明,通過這種方式,我們可以把官方寫出的 QPS 從大概 3 萬多提升到接近 10 萬。

不過在實現 sink 的 At Least Once 語義的時候就相對來說複雜一點。回想 CheckPoint 機制,如果我們要實現 sink 的 At Least Once,就必須保證 CheckPoint 完成時,sink 是乾淨的,即所有數據都刷出了,這樣才能保證其 At Least Once。在這種情況下,可能就需要將 CheckPoint 的線程、普通刷出的主線程以及其他的換頁線程等都加上。當觸發 CheckPoint 的時候,同步把所有數據都保證刷乾淨之後,纔去完成 CheckPoint。如此,一旦 CheckPoint 完成,sink 必然是乾淨的,也意味着前面流過來的所有數據都正確更新到 TiDB 了。

在我們優化完畢之後,實現了 100k 左右的 OPS。在我們測試環境上,大概是三臺物理機混布 PD、TiKV、TiDB 這些節點。

業務場景

我們目前技術中心計費數據中心使用 TiDB 跟 Flink 結合的應用場景非常多。如:

  • 海量業務日誌數據的實時格式化入庫;

  • 基於海量數據的分析統計;

  • 實時 TiDB / Kafka 雙流連接的支付鏈路分析;

  • 對通數據地圖;

  • 時序數據。

所以,可以看到其實 Flink on TiDB 在網易數據中心業務層的應用是遍地開花的,此處引用一句,“桃李不言,下自成蹊”,既然能用到這麼廣泛,也就證明了這條路其實是非常有價值的。

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