DataPipeline CTO陳肅:構建批流一體數據融合平臺的一致性語義保證

批流一體.png


首先,本文將從數據融合角度,談一下DataPipeline對批流一體架構的看法,以及如何設計和使用一個基礎框架。其次,數據的一致性是進行數據融合時最基礎的問題。如果數據無法實現一致,即使同步再快,支持的功能再豐富,都沒有意義。



另外,DataPipeline目前使用的基礎框架爲Kafka Connect。爲實現一致性的語義保證,我們做了一些額外工作,希望對大家有一定的參考意義。

最後,會提一些我們在應用Kafka Connect框架時,遇到的一些現實的工程問題,以及應對方法。儘管大家的場景、環境和數據量級不同,但也有可能會遇到這些問題。希望對大家的工作有所幫助。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


一、批流一體架構


批和流是數據融合的兩種應用形態

下圖來自Flink官網。傳統的數據融合通常基於批模式。在批的模式下,我們會通過一些週期性運行的ETL JOB,將數據從關係型數據庫、文件存儲向下遊的目標數據庫進行同步,中間可能有各種類型的轉換。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


另一種是Data Pipeline模式。與批模式相比相比, 其最核心的區別是將批量變爲實時:輸入的數據不再是週期性的去獲取,而是源源不斷的來自於數據庫的日誌、消息隊列的消息。進而通過一個實時計算引擎,進行各種聚合運算,產生輸出結果,並且寫入下游。
 現代的一些處理框架,包括Flink、Kafka Streams、Spark,或多或少都能夠支持批和流兩種概念。只不過像Kafka,其原生就是爲流而生,所以如果基於Kafka Connect做批流一體,你可能需要對批量的數據處理做一些額外工作,這是我今天重點要介紹的。


數據融合的基本問題


如果問題簡化到你只有一張表,可能是一張MySQL的表,裏面只有幾百萬行數據,你可能想將其同步到一張Hive表中。基於這種情況,大部分問題都不會遇到。因爲結構是確定的,數據量很小,且沒有所謂的並行化問題。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1



但在一個實際的企業場景下,如果做一個數據融合系統,就不可避免要面臨幾方面的挑戰:

第一,“動態性”
數據源會不斷地發生變化,主要歸因於:表結構的變化,表的增減。針對這些情況,你需要有一些相應的策略進行處理。

第二,“可伸縮性”
任何一個分佈式系統,必須要提供可伸縮性。因爲你不是隻同步一張表,通常會有大量數據同步任務在進行着。如何在一個集羣或多個集羣中進行統一的調度,保證任務並行執行的效率,這是一個要解決的基本問題。

第三,“容錯性”
在任何環境裏你都不能假定服務器是永遠在正常運行的,網絡、磁盤、內存都有可能發生故障。這種情況下一個Job可能會失敗,之後如何進行恢復?狀態能否延續?是否會產生數據的丟失和重複?這都是要考慮的問題。

第四,“異構性”
當我們做一個數據融合項目時,由於源和目的地是不一樣的,比如,源是MySQL,目的地是Oracle,可能它們對於一個字段類型定義的標準是有差別的。在同步時,如果忽略這些差異,就會造成一系列的問題。

第五,“一致性”
一致性是數據融合中最基本的問題,即使不考慮數據同步的速度,也要保證數據一致。數據一致性的底線爲:數據先不丟,如果丟了一部分,通常會導致業務無法使用;在此基礎上更好的情況是:源和目的地的數據要完全一致,即所謂的端到端一致性,如何做到呢? 

Lambda架構是批流一體化的必然要求

目前在做這樣的平臺時,業界比較公認的有兩種架構:一種是Lambda架構,Lambda架構的核心是按需使用批量和流式的處理框架,分別針對批式和流式數據提供相應的處理邏輯。最終通過一個服務層進行對外服務的輸出。

爲什麼我們認爲Lambda架構是批流一體化的必然要求?這好像看起來是矛盾的(與之相對,還有一種架構叫Kappa架構,即用一個流式處理引擎解決所有問題)。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


實際上,這在很大程度來自於現實中用戶的需求。DataPipeline在剛剛成立時只有一種模式,只支持實時流同步,在我們看來這是未來的一種趨勢。

但後來發現,很多客戶實際上有批量同步的需求。比如,銀行在每天晚上可能會有一些月結、日結,證券公司也有類似的結算服務。基於一些歷史原因,或出於對性能、數據庫配置的考慮,可能有的數據庫本身不能開change log。所以實際上並不是所有情況下都能從源端獲取實時的流數據。

考慮到上述問題,我們認爲一個產品在支撐數據融合過程中,必須能同時支撐批量和流式兩種處理模式,且在產品裏面出於性能和穩定性考慮提供不同的處理策略,這纔是一個相對來說比較合理的基礎架構。

數據融合的Ad-Hoc模式

具體到做這件事,還可以有兩種基礎的應用模式。假如我需要將數據從MySQL同步到 Hive,可以直接建立一個ETL的JOB(例如基於Flink),其中封裝所有的處理邏輯,包括從源端讀取數據,然後進行變換寫入目的地。在將代碼編譯好以後,就可以放到Flink集羣上運行,得到想要的結果。這個集羣環境可以提供所需要的基礎能力,剛纔提到的包括分佈式,容錯等。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

 
數據融合的MQ模式

另一種模式是ETL JOB本身輸入輸出實際上都是面對消息隊列的,實際上這是現在最常使用的一種模式。在這種模式下,需要通過一些獨立的數據源和目的地連接器,來完成數據到消息隊列的輸入和輸出。ETL JOB可以用多種框架實現,包括Flink、Kafka Streams等,ETL JOB只和消息隊列發生數據交換。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

 DataPipeline選擇MQ模式的理由


DataPipeline選擇MQ模式,主要有幾點考慮:

第一,在我們產品應用中有一個非常常見的場景:要做數據的一對多分發。數據要進行一次讀取,然後分發到各種不同的目的地,這是一個非常適合消息隊列使用的分發模型。詳情見:數據融合重磅功能丨一對多實時分發、批量讀取模式

第二,有時會對一次讀取的數據加不同的處理邏輯,我們希望這種處理不要重新對源端產生一次讀取。所以在多數情況下,都需將數據先讀到消息隊列,然後再配置相應的處理邏輯。

第三,Kafka Connect就是基於MQ模式的,它有大量的開源連接器。基於Kafka Connect框架,我們可以重用這些連接器,節省研發的投入。

第四,當你把數據抽取跟寫入目的地,從處理邏輯中獨立出來之後,便可以提供更強大的集成能力。因爲你可以在消息隊列上集成更多的處理邏輯,而無需考慮重新寫整個Job。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1



相應而言,如果你選擇將MQ作爲所有JOB的傳輸通道,就必須要克服幾個缺點:

第一,所有數據的吞吐都經過MQ,所以MQ會成爲一個吞吐瓶頸。

第二,因爲是一個完全的流式架構,所以針對批量同步,你需要引入一些邊界消息來實現一些批量控制。

第三,Kafka是一個有持久化能力的消息隊列,這意味着數據留存是有極限的。比如,你將源端的讀到Kafka Topic裏面,Topic不會無限的大,有可能會造成數據容量超限,導致一些數據丟失。

第四,當批量同步在中間因爲某種原因被打斷,無法做續傳時,你需要進行重傳。在重傳過程中,首先要將數據進行清理,如果基於消息隊列模式,清理過程就會帶來額外的工作。你會面臨兩個困境:要麼清空原有的消息隊列,要麼你創造新的消息隊列。這肯定不如像直接使用一些批量同步框架那樣來的直接。

二、一致性語義保證

 來自DataPipeline客戶的需求


先簡單介紹一下我們客戶對於數據同步方面的一些基本要求:
 第一種需求,批量同步需要以一種事務性的方式完成同步

無論是同步一整塊的歷史數據,還是同步某一天的增量,該部分數據到目的地,必須是以事務性的方式出現的。而不是在同步一半時,數據就已經在目的地出現了,這可能會影響下游的一些計算邏輯。

第二種需求,流式數據儘可能快的完成同步

大家都希望越快越好,但相應的,同步的越快,吞吐量有可能因爲你的參數設置出現相應的下降,這可能需要有一個權衡。

第三種需求,批量和流式可能共存於一個JOB

作爲一個數據融合產品,當用戶在使用DataPipeline時,通常需要將存量數據同步完,後面緊接着去接增量。然後存量與增量之間需要進行一個無縫切換,中間的數據不要丟、也不要多。

第四種需求,按需靈活選擇一致性語義保證

DataPipeline作爲一個產品,在客戶的環境中,我們無法對客戶數據本身的特性提出強制要求。我們不能要求客戶數據一定要有主鍵或者有唯一性的索引。所以在不同場景下,對於一致性語義保證,用戶的要求也不一樣的:

比如在有主鍵的場景下,一般我們做到至少有一次就夠了,因爲在下游如果對方也是一個類似於關係型數據庫這樣的目的地,其本身就有去重能力,不需要在過程中間做一個強一致的保證。但是,如果其本身沒有主鍵,或者其下游是一個文件系統,如果不在過程中間做額外的一致性保證,就有可能在目的地產生多餘的數據,這部分數據對於下游可能會造成非常嚴重的影響。

數據一致性的鏈路視角

如果要解決端到端的數據一致性,我們要處理好幾個基本環節:

第一,在源端做一個一致性抽取

一致性抽取是什麼含義?即當數據從通過數據連接器寫入到MQ時,和與其對應的offset必須是以事務方式進入MQ的。

第二,一致性處理

如果大家用過Flink,Flink提供了一個端到端一致性處理的能力,它是內部通過checkpoint機制,並結合Sink端的二階段提交協議,實現從數據讀取處理到寫入的一個端到端事務一致性。其它框架,例如Spark Streaming和Kafka Streams也有各自的機制來實現一致性處理。

第三,一致性寫入

在MQ模式下,一致性寫入,即consumer offset 跟實際的數據寫入目的時,必須是同時持久化的,要麼全都成功,要麼全部失敗。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


第四,一致性銜接

在DataPipeline的產品應用中,歷史數據與實時數據的傳輸有時需要在一個任務中共同完成。所以產品本身需要有這種一致性銜接的能力,即歷史數據和流式數據,必須能夠在一個任務中,由程序自動完成它們之間的切換。

Kafka Connect的一致性保證

Kafka Connect如何保證數據同步的一致性?就目前版本,Kafka Connect只能支持端到端的at least once,核心原因在於,在Kafka Connect裏面,其offset 的持久化與數據發送本身是異步完成的。這在很大程度上是爲了提高其吞吐量考慮,但相應產生的問題是,如果使用Kafka Connect,框架本身只能爲你提供at least once的語義保證。


在該模式下,如果沒有通過主鍵或下游應用進行額外地去重,同步過程當中的數據會在極端情況下出現重複,比如源端發送出一批數據已經成功,但offset持久化失敗了,這樣在任務恢復之後,之前已經發送成功的數據會再次重新發送一批,而下游對這種現象完全是不知情的。目的端也是如此,因爲consumer的offset也是異步持久化,就會到導致有可能數據已經持久化到Sink,但實際上consumer offset還沒有推進。這是我們在應用原生的Kafka Connect框架裏遇到最大的兩個問題。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


三、DataPipeline的解決之道


二階段提交協議
DataPipeline如何解決上述問題?首先,需要用協議的方式保證每一步都做成事務。一旦做成事務,由於每個環節都是解耦的,其最終數據就可以保證一致性。下圖爲二階段提交協議的最基礎版本,接下來爲大家簡單介紹一下。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


首先,在二階段提交協議中,對於分佈式事務的參與方,在DataPipeline的場景下爲數據寫入與offset寫入,這是兩個獨立組件。兩者之間的寫入操作由Coordinator進行協調。第一步是一個prepare階段,每一個參與方會將數據寫入到自己的目的地,具體持久化的位置取決於具體應用的實現。

第二步,當prepare階段完成之後,Coordinator會向所有參與者發出commit指令,所有參與者在完成commit之後,會發出一個ack,Coordinator收到ack之後,事務就完成了。如果出現失敗,再進行相應的回滾操作。其實在分佈式數據庫的設計領域中,單純應用一個二階段提交協議會出現非常多的問題,例如Coordinator本身如果不是高可用的,在過程當中就有可能出現事務不一致的問題。

所以應用二階段提交協議,最核心的問題是如何保證Coordinator高可用。所幸在大家耳熟能詳的各種框架裏,包括Kafka和Flink,都能夠通過分佈式一致協議實現Coordinator高可用,這也是爲什麼我們能夠使用二階段提交來保證事務性。

Kafka事務消息原理

關於Kafka事務消息原理,網上有很多資料,在此簡單說一下能夠達到的效果。Kafka通過二階段提交協議,最終實現了兩個最核心的功能。

第一,一致性抽取

上文提到數據要被髮送進Kafka,同時offset要被持久化到Kafka,這是對兩個不同Topic的寫入。通過利用Kafka事務性消息,我們能夠保證offset的寫入和數據的發送是一個事務。如果offset沒有持久化成功,下游是看不到這批數據的,這批數據實際上最終會被丟棄掉。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


所以對於源端的發送,我們對Kafka Connect的Source Worker做了一些改造,讓其能夠提供兩種模式,如果用戶的數據本身是具備主鍵去重能力的,就可以繼續使用Kafka Connect原生的模式。

如果用戶需要強一致時,首先要開啓一個源端的事務發送功能,這就實現了源端的一致性抽取。其可以保證數據進Kafka一端不會出現數據重複。這裏有一個限制,即一旦要開啓一致性抽取,根據Kafka必須要將ack設置成all,這意味着一批數據有多少個副本,其必須能夠在所有的副本所在的broker都已經應答的情況下,纔可以開始下一批數據的寫入。儘管會造成一些性能上的損失,但爲了實現強一致,你必須要接受這一事實。

第二,一致性處理

事務性消息最早就是爲Kafka Streams設計和準備的。可以寫一段Kafka Streams應用,從Kafka裏讀取數據,然後完成轉化邏輯,進而將結果再輸出回Kafka。Sink端再從Kafka中消費數據,寫入目的地。

數據一致性寫入

之前簡要談了一下二階段提交協議的原理,DataPipeline實現的方式不算很深奧,基本是業界的一種統一方式。其中最核心的點是,我們將consumer offset管理從Kafka Connect框架中獨立出來,實現事務一致性提交。另外,在Sink端封裝了一個類似於Flink的TwoPhaseCommitSinkFunction方式,其定義了Sink若要實現一個二階段提交所必須要實現的一些功能。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


DataPipeline將Sink Connector分爲兩類,一類是Connector本身具備了事務能力,比如絕大部分的關係型數據庫,只需將offset跟數據同時持久化到目的地即可。額外的可能需要有一張offset表來記錄提交的offset。還有一類Sink不具備事務性能力,類似像FTP、OSS這些對象存儲,我們需要去實現一個二階段提交協議,最終才能保證Sink端的數據能夠達到一致性寫入。

數據一致性銜接
關於批量數據與實時數據如何銜接的問題,主要有兩個關鍵點:

第一,當開始進行一個批量數據同步時,以關係型數據庫爲例,你應該拿到當時一個整體數據的Snapshot,並在一個事務中同時記錄當時對應的日誌起始值。以MySQL爲例,當要獲取一個Binlog起始偏移量時,需要開啓一個START TRANSACTION WITH CONSISTENT SNAPSHOT,這樣才能保證完成全量之後,後期的讀取增量日誌同步不會產生重複數據。

第二,如果採用增量同步模式,則必須根據實際的數據業務領域,採用一種比較靈活的增量表達式,才能避免讀到寫到一半的數據。比如在你的數據中,其ID是一個完全自增,沒有任何重複的可能,此時只需每次單純的大於上一次同步的最後一條記錄即可。

但如果是一個時間戳,無論精度多高,都有可能在數據庫產生相同的時間戳,所以安全的做法是每次迭代時,取比當前時間稍微少一點,保證留出一個安全時間,比如五秒甚至一分鐘,這樣你永遠不會讀到一些時間戳可能會產生衝突的這部分數據,避免遺漏數據。這是一個小技巧,但如果沒有注意,在使用過程中就會產生各種各樣的問題。

還有一點是上面提及的,如何能夠在一個流式框架實現批量同步的一致性,對於所有的流式框架,需要引入一些邊界條件來標誌着一次批量同步的開始和結束。DataPipeline在每次批量發送開始和結束後,會引入一些控制量信號,然後在Sink端進行相應處理。同樣爲了保證事務一致性,在Sink端處理這種批量同步時,依然要做一些類似於二階段提交這樣的方式,避免在一些極端情況下出現數據不一致的問題。

四、問題和思考


上文介紹的是DataPipeline如何基於Kafka Connect做事務同步一致性的方案。



DataPipeline在使用Kafka Connect過程中遇到過一些問題,目前大部分已經有一些解決方案,還有少量問題,可能需要未來採用新的方法/框架才能夠更好的解決。

第一,反壓的問題
Kafka Connect設計的邏輯是希望實現源端和目的端完全解耦,這種解偶本身是一個很好的特性。但也帶來一些問題,源和目的地的task完全不知道彼此的存在。剛纔我提到Kafka有容量限制,不能假定在一個客戶環境裏面,會給你無限的磁盤來做緩衝。通常我們在客戶那邊默認Topic爲100G的容量。如果源端讀的過快,大量數據會在Kafka裏堆積,目的端沒有及時消費,就有可能出現數據丟失,這是一個非常容易出現的問題。

怎麼解決?DataPipeline作爲一個產品,在Kafka Connect之上,做了控制層,控制層中有像Manager這樣的邏輯組件,會監控每一個Topic消費的lag,當達到一定閾值時,會對源端進行限速,保證源和目的地儘可能匹配。

第二,資源隔離

Connect Worker集羣無法對task進行資源預留,多個task並行運行會相互影響。Worker的rest接口是隊列式的,單個集羣任務過多會導致啓停緩慢。

我們正在考慮利用外部的資源調度框架,例如K8s進行worker節點管理;以及通過路由規則將不同優先級任務運行在不同的worker集羣上,實現預分配和共享資源池的靈活配置。 
第三,Rebalance

在2.3版本以前,Kafka Connect的task rebalance採用stop-the-world模式,牽一髮動全身。在2.3版本之後,已經做了非常大優化,改爲了具有粘性的rebalance。所以如果使用Kafka Connect,強烈推薦一定要升級到2.3以上的版本,也就是目前的最新版本。

五、未來演進路線


基於MQ模式的架構,針對大批量數據的同步,實際上還是容易出現性能瓶頸。主要瓶頸是在MQ的集羣,我們並不能在客戶環境裏無限優化Kafka集羣的性能,因爲客戶提供的硬件資源有限。所以一旦客戶給定了硬件資源,Kafka吞吐的上限就變爲一個固定值。所以針對批量數據的同步,可能未來會考慮用內存隊列替代MQ。

同時,會採用更加靈活的Runtime,主要是爲了解決剛纔提到的預分配資源池和共享資源池的統一管理問題。

另外,關於數據質量管理,實際上金融類客戶對數據質量的一致性要求非常高。所以對於一些對數據質量要求非常高的客戶,我們考慮提供一些後校驗功能,尤其是針對批量同步。


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