端到端的實時計算:TiDB + Flink 最佳實踐

PingCAP發表於2021-09-27

端到端的實時計算:TiDB + Flink 最佳實踐

作者簡介

孫曉光,PingCAP Community Development 團隊負責人,原知乎基礎研發團隊架構師,長期從事分散式系統相關研發工作,關注雲原生技術。

本文來自孫曉光在 Apache Flink x TiDB Meetup · 北京站的演講,主要分享了知乎在 TiDB x Flink 批流一體方面的部分工作,並以實際業務為例介紹如何充分利用兩者的特點完成端對端實時計算的閉環交付

背景


上圖是非常典型的實時數倉鏈路上的各個元件和資料,可以看到在很多地方 TiDB 和 Flink 都可以結合在一起去解決我們的業務問題。比如 TiDB 的大本營是線上交易,所以 ODS 是可以利用 TiDB 的,後邊的維表和應用資料儲存等也都可以利用 TiDB。

實時業務場景

場景分析


先看一下知乎這邊一個實際的業務場景,知乎主站上的創作中心為創作者提供了內容互動資料的分析能力。創作者可以在這看到自己創作的內容所獲得的贊同,評論,喜歡,收藏的資料以及過去一段時間內這些資料的變化。

這些資料可以幫助創作者更好地優化自己的創作。比如創作者對內容做了一些調整,然後發現互動資料開始發生顯著的變化,創作者就可以基於這個訊號對內容做相應的調整,去規避不好的或者進一步發揚光大好的策略,因此對創作者具有非常大的價值。另外,這個資料越即時,創作者的策略調整就能越即時。比如說創作者剛追更了一篇回答,希望立刻就可以看到相關的資料變化,如果資料變化是正向的,下次就可以做更多類似的調整。或者抽象出來過去好的調整都是什麼,這樣每次都可以基於之前的經驗做出讀者更喜歡的創作。

可惜對創作者這麼有價值的資料目前仍然是不是實時的,大家可以在右上角看到資料更新的說明。這是我們在實時應用上還沒有覆蓋得足夠好的一個證據,還是用傳統的 T+1 的技術去實現的一個產品。

Flink 是我們把類似創作中心這樣的應用場景實時化必然的選擇,然而同大量使用 MySQL 的公司不同。知乎站上接近 40% 的 MySQL 資料庫已經完成了到 TiDB 的遷移,所以我們必須將 TiDB 和 Flink 的實時計算能力做一個深入的整合。在未來當 TiDB 成為我們絕對主力資料庫的時刻,能夠獲得更好的綜合收益。

接下來我們探討如何將內容互動資料的統計實時化,利用 TiDB 和 Flink 實現回答和文章這兩種內容的喜歡、評論和贊同資料的實時計算。

業務資料模型分析


圖中是對這些資料進行實時計算所需要關注的相關業務,這幾個業務包括問答也就是左邊的 QA,還有右邊的專欄文章,以及評論、使用者互動、視訊回答。我們希望通過整合這些分散在不同業務裡面的資料,得到創作者中心裡的使用者互動的統計資料,而且我們希望它是實時的

首先我們先放大一下問答業務,左邊是 QA 業務裡比較基礎的幾個基本表,實質上我們並不需要為計算互動資訊瞭解到所有表所有的細節,只需要關注右邊這幾張表的部分欄位就可以了。從這些表裡我們只需要知道回答的 id,這個回答創作者的 member_id 還有被點讚的回答 id,就可以完整地計算某一個人的某一個回答有多少點贊。

與此相似的是專欄文章,這邊同樣列出了一些基礎表。如果要去做專欄文章的點贊這件事情的實時計算,我們關注 article 和 article_vote 這兩張表,利用 member_id、id 和 vote 欄位可以非常容易的計算得到文章的點贊數。

除了在業務系統內的點贊互動資料,其它型別的互動資料分散在多個不同的業務系統中。比如評論系統的 comment_relation 表,視訊回答的 vote 表,還有其它互動的 reaction 表。基於這些使用者的行為資料,再加上內容資料就能夠計算得到使用者創作的完整互動資料了。

從業務模型上可以得到互動資料計算的本質是把各種不同型別的內容和各種互動行為的資料作為源表,然後按照對這些資料以內容的 ID 分組進行聚合計算。比如說點贊就是一個 count 計算,因為表裡一行資料就是一個點贊。如果說它是一個分值,那麼這個資料的計算就是 sum。在拿到所有內容和所有互動聚合的結果後,再次同內容表做一個左連線就能拿到最後的計算結果了。

傳統解決方案


在開始講 Flink 的計算之前,我們可以先看看沒有 Flink,同樣的實時應用是什麼樣的開發模式。知乎內部有一套自己過去積累的技術框架去做這樣的事件驅動計算,如果用這樣的技術做實時計算,開發的方式是上圖這個樣子。

業務工程師需要用自己熟悉的語言和框架來開發中間紅色的這些基於訊息系統的 worker,對拿到的實時資料變化事件進行補數和聚合操作,再將計算得到的結果以預先約定好的格式傳送到訊息系統。最後用一個最終的 worker,將內容源表和多個上游 worker 的實時計算結果拼接在一起得到最終的計算結果並儲存到下游。這樣我們就可以基於比較傳統的技術來實現實時應用。在這種開發方式下,業務工程師需要關注多個 worker 的實現,和不同系統之間資料傳遞的格式。資料庫和訊息系統由平臺團隊來維護,對於工程師來說沒有額外的學習成本,學習成本低和易於理解是這種傳統開發方式的優點。

這種開發方式存在著一些問題。比如上面圖裡有 5 個 worker,worker 程式首先是一個訊息系統的 consumer,它需要根據業務需求對接收到的實時資料進行聚合計算,並填充必要的維度資料。在保證這些計算邏輯的正確性之後,還要把這些計算的結果正確的傳送到訊息系統的下游 topic 中。不誇張地講這樣的一個程式至少需要 1000 行的工作量,5 個這樣的 worker 不論從管理還是開發甚至是維護方面的成本都是非常高的。另外,這些業務團隊自行開發的 worker 程式需要由開發者自行解決規模擴充套件性問題,還需要獨立地預留資源應對突發流量造成全域性的資源浪費。難以在合理的成本下平衡彈性不足帶來的系統規模問題。

Flink 解決方案


作為對比,如果用 Flink 去開發整個應用的結構會變得非常簡單。當我們使用 SQL 來開發應用時,得益於更高的可維護性和可理解性,我們能夠在不損失可維護性的情況下將這個應用的全部邏輯放在一個 job 裡統一維護。不論從業務團隊的開發成本還是是維護成本角度看都是更優的選擇。

如上圖所示,這是回答使用者互動資料的實時計算邏輯用 Flink SQL 來開發,最後得到的 SQL 。利用 SQL 這種宣告式的方式開發業務邏輯,非常容易地理解和驗證它的正確性。

接下來看一下這種方式的優勢
首先,單一 SQL 開發可維護性高,元件數少,維護成本低。

其次,Flink 統一處理系統級問題,業務層無需關心擴充套件性、高可用、效能優化和正確性的問題,極大地降低了處理這些問題的負擔。

最後,SQL 開發幾乎沒有額外的學習成本。為什麼說 “幾乎”,這個業務是典型的線上工程師的工作領域,而線上工程師一定很熟悉 SQL。但他們日常工作中使用到的 SQL 範圍和大資料工程師使用的 SQL 範圍還存在著些許的不同。所以不能說 Flink SQL 沒有學習成本,但這個成本非常低,學習曲線也非常平緩

任何事情都有兩面性,基於 Flink 開發實時應用也需要解決下面的這些問題:
首先,SQL 的表達力不是無限的,一定會有一些業務邏輯和業務場景很難拿目前的 Flink SQL 完全覆蓋。如果我們用 28 法則來看這個問題,SQL 加上一些 UDF 就能夠解決其中 80% 標準 Flink SQL 無法覆蓋的問題,最後還剩下無法解決的 20% 問題有 DataStream API 進行兜底,確保整個業務問題能夠在一個 Flink 技術棧上全部解決。

另外,Flink SQL 開發簡單,但 Flink 系統本身的複雜度並不低。這些複雜度對許多業務工程師來說是一個非常重的負擔,他們並不希望理解 Flink 如何工作如何維護。他們更希望在一個可自助操作的平臺上編寫 SQL 解決自己的領域問題,避免關注運維 Flink 這樣一個複雜的問題。對此我們需要以平臺化的方式降低業務接入系統的成本,利用技術手段和規模效應把單個業務的成本降到合理水平。

所以問題雖然存在,但都有合適的辦法解決。

POC Demo

剛剛講到的創作中心實時應用還處於 POC 過程,POC 使用知乎站上實際的表結構,大家可以從 POC Demo 感受業務工程師能夠基於 Flink 實現什麼,實現的效果,以及正確性是否有保障。

前面看到的部分只包括了線上業務的技術棧範圍,也就是說源資料在 TiDB 上,經過 Flink 處理後的計算結果也儲存在 TiDB,端到端的解決實時計算問題。如果需要在計算中引入離線產生的資料怎麼辦?比如我們想要在計算結果中包含每個內容的實時 PV,我們可以把大資料系統中的 PV history 的表和 PV 實時流進行一個 union 操作,再按照內容 ID sum 在一起,就可以得到實時的內容維度 PV 資料。傳統方式的實現可能要寫 1-2 個 worker,現在只需要在 Flink job 中加幾行 SQL 程式碼就可以實現。

可能的疑問


如果不熟悉 Flink 不熟悉大資料的同學現在可能會有一些疑問,接下來我們 一一 看下這幾大類疑問。

第一個就是計算到底怎麼做的,在 TP 系統裡面都是客戶端請求觸發計算,Flink 的計算是如何觸發的呢?

答案是在事件觸發時進行計算,每產生一個 event 就會觸發一次計算。對資料庫裡任何一行的變化都觸發一次計算,觸發的顆粒度可能太細導致成本過高。所以 Flink 裡邊有 mini batch 的優化,可以攢一批變化事件以批的模式驅動計算。如果是關於時間段內資料的計算,還可以用 window 機制,使用 Watermark/Trigger 來觸發計算並獲得結果。如果計算的過程中需要維護狀態,那麼 Flink runtime 會負責管理狀態資料。

第二個問題是 window 在哪裡

並不是所有業務都必須要用 window,當計算和觸發邏輯跟時間段沒有關係的時候,就不需要使用 window。比如這裡的 demo 場景計算邏輯由資料變更觸發狀態永久有效,整個邏輯中不需要使用 window。

如果需要用 window 的時候怎麼處理遲到的事件?這裡有 discard 和 retract 兩個主要的策略處理遲到事件,當遇到遲到事件時開發者可以選擇扔掉遲到的資料,也可以用 retract 機制去處理。除此以外我們還可以用自定義的邏輯來處理遲到事件。總之 window 的作用是協助使用者以預置的視窗策略,將落在某一時間段內的資料攢在一起觸發計算,在有超出視窗的延遲資料到達時,按照應用期望的方式進行處理。

第三是開發上手難度如何?

Streaming SQL 在標準 SQL 的基礎上建立,它的學習過程是漸進性的、平緩的。再配合上易擴充套件的 UDF 能力,能夠解決大多數單純使用 Flink SQL 無法解決的問題,少數只適合用編寫程式碼方式解決的問題仍然有 Flink 的 DataStream API 可以解決。

最後 TiDB 和 Flink 如何保證計算結果的正確性

TiDB 是一個預設快照隔離級別的資料庫,我們能夠直接拿到某個時間點的靜止全域性快照狀態。在 SI 隔離級別下保證整個資料流的正確性非常容易。我們只需要拿到一個時間戳,並讀取這個時間戳時刻全部資料的靜止快照,處理完快照資料後對接上 CDC 裡所有時間戳之後發生的 CDC event。在 Flink 的角度這就是一個流批一體的動態表,Flink 自身的機制能夠保證流入到系統中事件計算結果的正確性。

TiDB x Flink 批流一體

下面來了解在做 POC 過程中,我們在 TiDB 和 Flink 整合方面開展了哪些工作,以及這些整合工作帶來的能力處於什麼樣的狀態。

TiDB as MySQL


作為一款和 MySQL 相容的分散式資料庫,即便我們不做 TiDB 到 Flink 的原生整合,我們仍然能夠以圖示的方式把 TiDB 當作一個大號的 MySQL 和 Flink 配合在一起使用。這個架構下所有批任務流量都需要先過 LB ,然後再經過 TiDB 最後根據讀取的資料範圍訪問相應的 TiKV 節點。而流任務流量是利用 TiCDC 從 TiKV 抓取資料變更事件,經由訊息系統交付給 Flink 進行處理。

這種非原生對接的使用方式雖然能工作,但是在許多場景下無法充分利用 TiDB 架構的特點做更極致的成本優化和價值放大。比如在流量波動大的應用場景,由於所有的流量要在整個路徑上,從 LB 到 TiDB 到 TiKV 的每一層走一遍。而流量會對每一個程式產生全量的衝擊,為了保證在峰值流量衝擊下的業務表現,我們不得不按照峰值流量去預備所有的資源,造成了極大的資源浪費。

還有大資料場景經常遇到的資料傾斜問題。在沒有業務知識的前提下,面對業務各種各樣的表結構設計和業務資料分佈特徵,我們很難以統一的方式自動化地解決所有的資料傾斜問題。實際上在目前版本的 Flink JDBC connector 上,如果表主鍵不是整數型別且不存在分割槽表,那麼 Flink 的 source 就只能以 1 個並行度去處理全部資料。這在面對 TiDB 上海量儲存業務資料的場景是非常困難的。

最後,我們無法直接利用為 MySQL 設計的 flink-cdc-connector 專案為 TiDB 提供流和批一體的 connector。那麼在許多需要這個能力的應用場景中,業務方就需要自己去關注批和流資料統一處理的問題。

TiDB 適配

為了解決在 Flink 中使用非原生 TiDB 支援遇到的這些缺陷,我們充分利用了 TiDB 架構的特點,為 TiDB 開發了原生的 Flink Connector,更好地服務於 Flink 的廣泛計算場景。

首先是針對大流量衝擊場景的資源優化。在 TiDB 中有系統表可以得知整個叢集所有 TiDB 伺服器的地址和埠。我們實現了一個非常薄的代理到原生 MySQL JDBC driver 的 JDBC driver,利用系統表中的叢集拓撲資訊直接在客戶端實現了負載均衡。通過直連 TiDB-server 的方式我們實現了負載均衡器的流量繞行,只有初次和後續定期更新叢集資訊的小資料量請求會經過負載均衡器,真正的大流量資料讀寫請求都通過到 TiDB 的直接連線來承載。

接下來是避免 TiDB-server 的流量衝擊。在對 TiDB 上的資料進行讀取操作時,我們能夠讓客戶端從 PD 上獲取到需要讀取資料範圍內的所有 region 資訊。通過直接連線 region 背後 TiKV 節點的方式,我們能夠將所有讀的流量繞行 TiDB,極大地降低 TiDB 層負載,節約硬體資源成本。在實現 TiDB 繞行方案時,我們實現了同 TiDB 一致的 predicate 下推和 projection 下推能力,TiDB connector 對 TiKV 產生的壓力同真正的 TiDB 非常接近,不會對 TiKV 產生額外的負擔。

下一個是利用 placement rules 讓一批物理隔離的 TiKV 節點只承載 follower 角色的資料副本,再配合 follower read 能力我們能夠在沒有付出額外伺服器成本的情況下將實時計算的大流量負載,同線上的業務負載物理隔離開。讓大家能夠放心的在一個 TiDB 叢集上同時支撐線上業務和大資料業務。

接下來是業務無關的資料均衡能力。如前面所講,在沒有業務層領域知識和資料分佈資訊的情況下,JDBC 方式只能對整數主鍵的資料進行近似均衡的拆分,而對於非整數其它型別主鍵的非分割槽表就只能序列化的處理所有的資料。在 TiDB 這種海量資料儲存的情況下,不論是單併發還是不均衡都會導致任務執行效率低的問題。而前面介紹 TiDB 繞行的時候大家也看到了,TiDB connector 的任務拆分粒度是 region 級別。而 region 尺寸是由 TiKV 按照一個最優的尺寸去自動保持的,所以對於任意一個表結構,我們都能夠做到任務單元的均衡性,在無任何業務知識的情況下完全避免資料傾斜問題

接下來是 TiDB connector 原生實現的批流一體能力。它的原理是利用 TiDB 的快照隔離級別拿到一個資料的全域性快照,在處理完這個快照資料後,再接入所有 commit 版本號大於快照版本號的 CDC 事件。通過這個內嵌的流批一體能力,在資料處理工作得到極大簡化的同時,還能確保整個實時計算流水線的絕對正確性。

最後為了進一步優化 TiDB 大流量寫能力帶來的 CDC 流量衝擊。我們還對 TiCDC 的資料編解碼格式做了二進位制編碼優化。大家經常在 TiCDC 中使用的 canel json 和 open protocol 都是 JSON 的格式,然而這些以 JSON 為物理格式的協議都傾向於尺寸更大和編解碼 CPU 消耗過大的問題。而全新設計的 binary protocol 充分利用了 CDC 資料的一些特點,在典型場景下能夠將資料尺寸壓縮到 open protocol 的 42%,同時提升 encode 速度到接近原來的 6 倍,decode 速度到接近原來的 10 倍。

以上就是我們在 TiDB 和 Flink 原生整合方面所做的工作,這些工作很好地解決了利用 TiDB 和 Flink 實現端到端實時計算時所遇到的一些問題。

在 TiDB connector 的幫助下,TiDB 和 Flink 配合的方式變成圖上的這個樣子。讀的流量繞行負載均衡和 tidb-server,直接請求 TiKV 的 follower 節點上。寫的流量目前是藉助 JDBC 實現,但在客戶端負載均衡能力的幫助下,我們仍然能夠繞行負載均衡器,降低負載均衡器的成本。

當前 Flink 已經在知乎擁有許多落地的應用場景了。我們基於 Flink 建設了資料整合平臺,並利用 TiDB connector 提供了 TiDB to Hive 和 Hive to TiDB 的能力,解決了 ODS 層資料同步以及離線計算的資料線上提供服務的同步問題。在資料整合平臺之外還有許其他的實時應用,比如商業團隊的點選資料處理程式。再比如搜尋裡的時效性分析,還有關鍵指標的實時數倉。最後還有一些業務利用 Flink 將實時行為資料落到 TiDB 供線上查詢。

展望

除了以上提到的這些進展,我們還有許多可以改善的方面,為 TiDB 和 Flink 的使用者創造更多的價值。接下來就讓我們看下未來還有哪些可以繼續挖掘價值的方向。

TiDB x Flink 核心能力增強


首先是全域性事務支援。目前基於 JDBC 實現的 Flink sink 存在同 JDBC connector 一樣的侷限,無法實現分散式的全域性事務。此外使用 JDBC 連線 TiDB 的同時也帶來了 TiDB 最大事務尺寸的限制,無法支援超大事務的寫入。當我們遇到有全域性可見性要求或類似銀行跑批任務的需求時,目前的 TiDB connector 仍然無法提供理想的能力。我們希望接下來實現原生的寫入能力,直接以分散式的方式向 TiKV 上進行兩步提交,從而實現全域性大事務寫入能力。全域性事務不僅僅能帶來事務隔離和大事務的收益,我們還可以通過將所有大流量的請求繞行 TiDB 的方式,徹底釋放 tidb-server 的壓力,徹底杜絕沒必要的資源浪費。

還有一個改進方向是原生 lookup table 的支援,目前這一塊兒也是基於 JDBC connector 實現的。雖然維表查詢的吞吐通常不會特別大,但 bypass TiDB 仍然能夠獲得 latency 上的額外收益。而這個提升能夠為流計算系統計算吞吐的提升和避免事件積壓起到非常關鍵的正面作用。

最後還有一個尚未明確收益的改進方向是基於 TiKV 的 state backend,可能會解決一些場景下 checkpoint 慢的問題

更多應用場景


在擁有了 TiDB 原生支援具備了許多新的能力之後,我們可以暢想未來 TiDB x Flink 能夠支撐更多的應用場景。

比如當前的資料整合平臺只支援批模式的資料抽取任務,在 TiDB 流批一體能力的幫助下,我們能夠配合 Hudi 或 Iceberg 以非常低的成本完成所有 ODS 層資料的實時化。如果所有 ODS 層資料具備了實時的能力,數倉同學在考慮實時數倉的建設路徑時就沒有太多的前置依賴了。配合常見的實時埋點資料和實時 ODS 資料,完全按照業務價值的高低去安排數倉的實時化建設。

在實時數倉之外,隨著技術的成熟還會有更多的實時應用場景誕生。比如我們能夠以極低的成本從站上現有內容產出實時的內容池。再比如搜尋引擎的實時索引更新,當然還有 demo 的內容互動資料實時統計等等。相信在知乎的 Flink SQL 平臺建設完成後,一定會產生越來越多基於 TiDB x Flink 端到端的技術體系覆蓋的應用場景。

最後如果大家對 TiDB x Flink 的生態整合或者 TiDB 在整個大資料生態的能力,可以在 GitHub 上關注 TiBigData 專案。首先歡迎大家在實際場景中嘗試使用這個專案,如果在使用中遇到問題或有意見建議可以隨時給專案提 issue。最後也希望有更多的開發者參與到這個專案的開發,我們一起讓它為 TiDB 在大資料領域提供成熟完善的一站式解決方案。

相關文章