偏移出来的数据不准_独家解读!京东高可用分布式流数据存储的架构设计
京東于 2018 年對其自研的消息隊列中間件 JMQ 進行了一次徹底的重構,升級為 JournalQ。相比上一代產(chǎn)品,JournalQ 大幅提升了性能,功能上增加了 Kafka、MQTT 等協(xié)議的支持,提供更加完善的事務機制;設計上采用了存儲與計算分離的模式,數(shù)據(jù)存儲層從 JournalQ 中分離出來作為一個獨立的中間件產(chǎn)品,高可用分布式的流數(shù)據(jù)存儲:JournalKeeper。基于這種存儲計算分離的設計,JournalQ 在產(chǎn)品的定位上從單純的消息數(shù)據(jù)管道升級為流數(shù)據(jù)的存儲分發(fā)平臺。
筆者作為架構師,全程參與了 JournalQ 和 JournalKeeper 的設計和開發(fā)。這篇文章中,我將跟大家分享在開發(fā)這兩款產(chǎn)品過程中的一些技術心得和實踐經(jīng)驗。
為什么需要流數(shù)據(jù)存儲?流數(shù)據(jù)存儲并不是當下技術圈火熱的話題之一,甚至很少人會聽到過這個話題,更少的人會在實際業(yè)務中使用一款流數(shù)據(jù)存儲的產(chǎn)品。那京東為什么要開發(fā)這樣一款流數(shù)據(jù)存儲呢?
一切還需要從數(shù)據(jù)治理說起。隨著微服務架構的普及,服務治理的理念已經(jīng)深入每個開發(fā)者的心中。我們先回顧一下服務架構的演進過程:從最原始的單體應用,發(fā)展為煙筒式架構,然后是 SOA 模式,直到現(xiàn)在流行的微服務架構,服務的粒度被拆分的更細,服務的復用能力更強,服務間耦合度更低,直接帶來的益處是降低了總體擁有成本。
和服務治理一樣,當企業(yè)擁有的數(shù)據(jù)規(guī)模發(fā)展到一定階段,數(shù)據(jù)也需要被治理。同樣回顧一下數(shù)據(jù)存儲架構的發(fā)展過程:早期業(yè)務規(guī)模不大時,單體服務配合單個數(shù)據(jù)庫就可以滿足需求;隨著業(yè)務規(guī)模逐步擴大,數(shù)據(jù)規(guī)模也越來大,單體數(shù)據(jù)庫已經(jīng)無法滿足性能和容量的需求,普遍的解決辦法是對數(shù)據(jù)庫進行分庫分表,并且為了提高性能和可靠性,采用讀寫分離的架構。具備一定規(guī)模的互聯(lián)網(wǎng)公司,往往業(yè)務分工更加細致,對數(shù)據(jù)的使用方式也更加多樣化,分庫分表已經(jīng)不能滿足其業(yè)務需求。例如,對于同樣一份數(shù)據(jù),搜索團隊需要把數(shù)據(jù)存儲在 ElasticSearch 中以便于提升搜索性能;大數(shù)據(jù)團隊希望把實時數(shù)據(jù)接入到 Kafka 中,離線數(shù)據(jù)存放到 HDFS 中,以便于其計算和分析;負責在線業(yè)務的團隊,需要將數(shù)據(jù)存放到 Redis 中用于緩存,獲得更好的在線訪問體驗,等等。
為了滿足不同的業(yè)務需求,同一份數(shù)據(jù)被轉(zhuǎn)換成各種特定的數(shù)據(jù)格式,存放在各種各樣數(shù)據(jù)庫中。這種多副本的數(shù)據(jù)結構的優(yōu)點是顯而易見的:每個副本的數(shù)據(jù)結構都是基于特定業(yè)務的查詢方式進行優(yōu)化,并且選用最適合的數(shù)據(jù)庫進行存儲,可以達到最佳的查詢性能。
為此付出的代價是耗費了大量存儲和計算資源。為了維護數(shù)據(jù)新鮮,每一份數(shù)據(jù)副本都要實時或者定期從上游數(shù)據(jù)源進行數(shù)據(jù)同步,當數(shù)據(jù)量很大的時候,這種 ETL 操作需要大量的計算資源;每一份數(shù)據(jù)為了保證查詢性能和可靠性,需要存放多個數(shù)據(jù)副本,為了確保數(shù)據(jù)可靠性,還需要定期備份數(shù)據(jù)快照,這些副本和快照都需要占用大量的存儲資源。另外一個問題是數(shù)據(jù)耦合,當業(yè)務需要對某個數(shù)據(jù)庫的數(shù)據(jù)結構變更時,還需要考慮是否能滿足下游數(shù)據(jù)的需求,這種在不同的數(shù)據(jù)庫之間直接進行數(shù)據(jù)同步的方式,造成了事實上的數(shù)據(jù)依賴。
為了治理這種數(shù)據(jù)亂象,在不降低各種業(yè)務性能的前提下,減少對存儲和計算資源的使用,解決數(shù)據(jù)耦合問題,我們提出了如下這種數(shù)據(jù)架構:
我們這里面提到的“流數(shù)據(jù)”相比大家熟知的流計算中對應的概念更加寬泛一些,幾乎所有的數(shù)據(jù)在產(chǎn)生的源頭都可以認為是“流數(shù)據(jù)”,例如: ?
- Nginx 收到的 Http 請求; 
- 微服務計算后生成的更新數(shù)據(jù)的 SQL; 
- 從頁面和 APP 采集到的埋點數(shù)據(jù); 
- 各種應用程序的日志等。 
將流數(shù)據(jù)從產(chǎn)生的源頭就實時存入流數(shù)據(jù)平臺,各業(yè)務系統(tǒng)統(tǒng)一從流數(shù)據(jù)平臺獲取數(shù)據(jù)經(jīng)過必要的計算和轉(zhuǎn)換后,存入對應的業(yè)務數(shù)據(jù)庫中。數(shù)據(jù)使用方可以像使用消息隊列一樣從數(shù)據(jù)流平臺獲取訂閱數(shù)據(jù)的實時推送,也可以按照指定的位置或者時間來進行數(shù)據(jù)定期的數(shù)據(jù)同步,實現(xiàn)了批流一體的模式。統(tǒng)一數(shù)據(jù)訂閱避免了數(shù)據(jù)多次 ETL 浪費的計算資源。并且由于數(shù)據(jù)流的可回溯性,不需要對數(shù)據(jù)流本身備份數(shù)據(jù)快照,數(shù)據(jù)的使用方可以也可以減少數(shù)據(jù)快照的密度,節(jié)省了存儲資源。使用統(tǒng)一的數(shù)據(jù)流平臺,隔離了數(shù)據(jù)的生產(chǎn)者和數(shù)據(jù)使用方,有效的解決了數(shù)據(jù)耦合的問題。
當然,數(shù)據(jù)流存儲也不是萬能的,這種存儲形式只支持按照時間和位置進行查詢,并不適合業(yè)務系統(tǒng)直接使用,所以其定位還是一個數(shù)據(jù)存儲、交換和分發(fā)的平臺。
我們需要什么樣的流數(shù)據(jù)存儲?數(shù)據(jù)庫和中間件這類 PaaS 層的基礎設施類軟件,近些年的發(fā)展趨勢是越來越專業(yè)化、精細化。只在一個很窄的領域內(nèi)解決一兩個特定的問題,但是在這個領域內(nèi),具備極致的性能和體驗,可以以極高的性能的處理海量的數(shù)據(jù)。我們的流數(shù)據(jù)存儲也是這樣一種設計思路,它的功能非常的簡單,就是存儲流數(shù)據(jù),但需要具備存儲海量數(shù)據(jù)的能力,并且具備非常高的性能。
我們在設計這款產(chǎn)品的時候,給它定義了如下這些特性: ?
- 有序:數(shù)據(jù)必須是嚴格有序的,不同順序有可能導致完全不一樣的結果。 
- Append Only: 數(shù)據(jù)只能追加寫入,并且寫入成功的數(shù)據(jù)具有不可變的特性。 
此外,它還需要具備其它數(shù)據(jù)存儲集群相同的一些通用特性,包括: ?
- 分布式: 支持集群模式,可以水平擴展; 
- 高性能:具有遠超一般結構化數(shù)據(jù)庫的至少一個數(shù)量級超高的性讀寫能,這樣整個系統(tǒng)才不會因為引入這個流數(shù)據(jù)存儲而顯著的降低總體性能; 
- 可靠性:單節(jié)點損壞不會丟失數(shù)據(jù); 
- 順序一致性:集群中所有節(jié)點按照一致的順序更新數(shù)據(jù),簡單的說,剛剛寫入的數(shù)據(jù)不要求立刻在所有節(jié)點都能讀到,經(jīng)過一個短暫的時延后數(shù)據(jù)陸續(xù)更新至所有節(jié)點是可以接受的。 
- 近乎無限的容量。 
我們請專門的測試團隊對 JournalQ 進行了極限性能的壓測,測試結果顯示,單節(jié)點的極限寫入性能為:32,961,776 條每秒,并且在極限情況下具有非常好的穩(wěn)定性,響應時延的 tp99 不超過 1ms。數(shù)據(jù)同步讀取的性能與寫入性能相當,可以滿足同步讀寫的要求,做到“寫入多快就讀取多快”。測試環(huán)境如下: ?
- 測試服務器:32C/256G/4TB SSD/ 萬兆以太網(wǎng) 
- 測試每條消息大小為:1KB 
- 壓縮方式:LZ4 壓縮 
接下來分享一下我們在實現(xiàn)過程中性能優(yōu)化的一些經(jīng)驗。
存儲結構設計對于數(shù)據(jù)存儲類的系統(tǒng),決定其讀寫性能的根本因素是存儲結構的設計。JournalKeeper 采用了一種非常簡單高效的存儲結構,如下圖所示:
數(shù)據(jù)按照順序依次寫入 Journal 文件中,然后將每條數(shù)據(jù)的全局偏移量作為索引值,按照同樣的順序記錄在 Index 文件中。考慮到單個文件的大小限制,把 Journal 和 Index 都拆分成多個連續(xù)的文件,每個文件的文件名就是文件內(nèi)第一條數(shù)據(jù)的全局偏移量。
數(shù)據(jù)寫入時,由于流數(shù)據(jù)尾部追加寫入的特性,只要一直保存索引和數(shù)據(jù)尾部的所在的文件和偏移量,就可以直接進行寫數(shù)據(jù)操作,因此寫入的時間復雜度為 O(1)。
讀取的查找過程稍微復雜一些: ?
首先需要根據(jù)給定的索引序號找到對應的索引文件。由于每個索引的長度固定為 16 個字節(jié),索引序號 x16 即可以計算出索引的全局偏移量。
JournalKeeper 把每個分區(qū)的索引文件的文件名(即這個文件第一條索引的全局偏移量)都存放在一個跳表中,找到索引所在文件的過程相當于在跳表中進行一次搜索,其時間復雜度為:O(logn),其中 n 為 Index 文件的個數(shù);
找到文件用,用索引全局偏移量減去文件名就可以找到索引在文件中的位置,通過讀取索引獲得數(shù)據(jù)在 Journal 中的全局偏移量 ;
根據(jù)數(shù)據(jù)的全局偏移量查找數(shù)據(jù)的過程和查找索引類似,其時間復雜度為:O(logm),其中 m 為 Journal 文件的個數(shù);
總體的讀取時間復雜度為:O(logn)+O(logm)
其中 n 和 m 分別為 Index 文件和 Jouranl 文件的數(shù)量,考慮到 n 和 m 遠遠小于數(shù)據(jù)的總數(shù),可以近似的認為:O(logn)+O(logm)≈O(1)
緩存設計在 JournalKeeper 中,流數(shù)據(jù)是存儲在磁盤中的,為了提高讀寫的性能,我們?yōu)槠湓O計了一套定制的內(nèi)存緩存系統(tǒng)。經(jīng)測試,在正常讀寫的情況下,這套緩存的命中率約為 99.96%,幾乎全部的讀請求都可以命中緩存,提升了讀性能的同時,還可以將幾乎全部的磁盤 IO 用于數(shù)據(jù)寫入,進一步提升了數(shù)據(jù)寫入的性能。
在緩存頁粒度的選擇時,JournalKeeper 使用了最簡單的策略:將整個文件緩存在內(nèi)存中。無論是 Journal 文件還是 Index 文件,每個緩存頁面對應一個文件。這種設計的優(yōu)勢在于,不需要再為緩存頁編寫單獨的查找算法,只需要復用文件的查找算法即可,并且緩存頁和文件的對應關系也變得非常簡單。
不足之處是,如果只是為了讀取文件中的一小部分數(shù)據(jù),不得不加載整個文件,這種設計顯然是不太經(jīng)濟的。但是考慮到流數(shù)據(jù)的順序連續(xù)讀寫特性,隨機的讀寫非常少,更多的讀寫方式從某個位置開始連續(xù)的向后讀寫,這種場景下,較大的緩存粒度不僅很少會出現(xiàn)“數(shù)據(jù)讀到內(nèi)存中卻最終沒有被使用”的情況,反而可以避免頻繁的換頁帶來的性能抖動。
另外一個問題是,緩存頁比較大,從磁盤加載整個文件到內(nèi)存中的耗費的時間相對較長。我們針對這個問題做了二方面的優(yōu)化。
大多數(shù)應用對流數(shù)據(jù)的訪問有一個特性:越新的數(shù)據(jù)訪問概率越高。比如像消息隊列,正常情況下生產(chǎn)的數(shù)據(jù)馬上就會被消費掉。數(shù)據(jù)在寫入磁盤前一定會經(jīng)過內(nèi)存,那我們就沒必要在讀的時候再從磁盤上重新加載一次,直接從內(nèi)存中讀出來更快,而且節(jié)省了寶貴又特別慢的磁盤 IO,這個我們稱為 讀寫共頁,這是第一項優(yōu)化。
第二項優(yōu)化叫 異步預加載,原理非常簡單但是效果很好。既然是連續(xù)讀寫,那上一個文件讀寫完成后,有非常大的概率會繼續(xù)讀寫下一個文件。基于這個特性,當讀寫到接近文件的尾部時,JournalKeeper 會開啟一個異步線程,把下一個文件先加載好,這樣不僅能解決大文件加載慢的問題,還能避免同步加載文件導致的卡頓和性能抖動。
在內(nèi)存管理方面,為了避免 JVM 頻繁的垃圾回收造成的卡頓,JournalKeeper 選擇使用堆外內(nèi)存作為緩存。使用堆外內(nèi)存的好處是性能更好,多數(shù)情況下可以減少一次內(nèi)存拷貝。JournalKeeper 自己進行內(nèi)存管理,避免了不可預期的 FullGC。
最后說一下緩存的淘汰策略,內(nèi)存空間是有限的,不斷有新的頁需要緩存必然要淘汰一些緩存頁。JournalKeeper 采用一種改進的 LRU 策略 PLRU。LRU 淘汰最近最少使用的頁,JournalKeeper 根據(jù)流數(shù)據(jù)存儲的特點,在淘汰時增加了一個考量維度:頁面位置(即文件名)與尾部的距離。因為越是靠近尾部的數(shù)據(jù),被訪問的概率越大。這樣綜合考慮下的淘汰算法,不僅命中率更高,還能有效的避免“挖墳”問題:例如某個客戶端正在從很舊的位置開始的向后讀取一批歷史數(shù)據(jù),內(nèi)存中的緩存很快都會被替換成這些歷史數(shù)據(jù),相當于大部分緩存資源都被消耗掉了,這樣會導致其他客戶端的訪問命中率下降。加入位置權重后,比較舊的頁面會很快被淘汰掉,減少挖墳對系統(tǒng)的影響。
線程模型說完了存儲接下來聊一聊代碼本身的優(yōu)化。
首先更正一個在很多開發(fā)者的觀念里都存在的誤區(qū):高并發(fā)并不等于高性能。在很多開發(fā)者的認知里,應用增加并發(fā)后性能確實得到了成倍的提升。其實根本的原因是單個并發(fā)的性能沒有很好的優(yōu)化,沒有做到充分的利用計算資源,大部分時間都浪費在等待上了。
對于計算密集型的應用,瓶頸資源是 CPU,理想情況下,最高效的方式 CPU 有幾個核就起幾個線程,這樣才是最充分的利用 CPU 資源。啟動了過多的線程,反而會有一部分 CPU 時間在 CPU 上下文切換被浪費掉了。但如果代碼優(yōu)化的不夠好,比如說每次計算出一批結果后把計算結果寫到磁盤里,在寫磁盤等待 IO 的這段時間內(nèi),這個線程對應的 CPU 核心是處于閑置狀態(tài)的。這種情況下啟動更多的線程,操作系統(tǒng)會自動把 CPU 調(diào)度給其它線程,這樣看起來提高并發(fā)確實帶來了性能提升。但我們要知道,只不過是因為我們的代碼優(yōu)化的不夠充分,操作系統(tǒng)替我們的程序做了一些調(diào)度優(yōu)化而已,總體的性能并沒有達到最優(yōu)的狀態(tài)。
所以,做極致的性能優(yōu)化,最先要解決的是減少等待。
實際開發(fā)過程中,可用的方法有很多,這里面分享幾個比較簡單實用方法: ?
- 異步化:將你的線程模型都改成異步化,比如使用 CompletableFuture、RxJava 等異步框架,避免等待那些可能耗時的操作結果。 
- 拆分流程:把一個很長的流程拆分成幾個短的流程。 
- 減少鎖:設計時盡量少的使用共享資源,減少鎖的使用。 
- 減少鎖等待:實在需要使用鎖的的地方,盡量減少鎖的粒度或者用讀寫鎖,減少鎖的等待時間; 
一般來說消息隊列都是生產(chǎn)的時候需要處理的業(yè)務邏輯相對比較多,我們看下 JournalQ 是如何優(yōu)化它這部分設計的。
寫入數(shù)據(jù)的流程如下: ?
Producer 發(fā)消息給 Leader Broker;
Leader Broker 解析處理消息;
Leader Broker 將想消息復制給所有的 Follower Broker,同時異步將消息寫入磁盤;
Leader Broker 收到大多數(shù) Follower Broker 的復制成功確認后,給 Producer 回響應告知消息發(fā)送成功。
對于這個流程,我們設計的線程模型是這樣的:
圖中白色的細箭頭是數(shù)據(jù)流,藍色的箭頭是控制流,白色的粗箭頭代表遠程調(diào)用。
這里我們設計了 6 組線程,將一個大的流程拆成了 6 個小流程。并且整個過程完全是異步化的。除了 JournalCache 的加載和卸載需要對文件加鎖以外,沒有用到其它的鎖。每個小流程都不會等待其它流程的共享資源(沒有數(shù)據(jù)需要處理時等待上游流程提供數(shù)據(jù)的情況除外),并且只要有數(shù)據(jù)就能第一時間處理。
高可用架構說完了單節(jié)點的性能優(yōu)化,我們來談整個集群的架構。
從實用角度出發(fā),我們在設計一個集群或者一個系統(tǒng)的總體架構時,需要在 CAPC 這幾個方面進行取舍: ?
- 一致性 (Consistency) 
- 可用性 (Availability) 
- 性能 (Performance) 
- 復雜度 (Complexity) 
舉個例子,現(xiàn)在很多微服務的應用都是用 MySQL 存儲在線業(yè)務數(shù)據(jù),為了加快業(yè)務訪問會使用 Redis 緩存部分 MySQL 中的數(shù)據(jù)。這種設計提升了系統(tǒng)整體的性能,付出的代價是犧牲了數(shù)據(jù)的一致性:從 Redis 中讀出的數(shù)據(jù)有可能并不是最新的,在某些特定應用的場景下,這種暫時的數(shù)據(jù)不一致是可以接受的。
系統(tǒng)的復雜度是容易被忽略的考量指標。過于復雜的設計更難于實現(xiàn)和維護,會大幅提高系統(tǒng)的總體擁有成本,因此在其它三個考量因素都可以接受的范圍內(nèi),盡量采用簡單的設計總是一個不錯的選擇。
如果可能的話,可以將服務設計成無狀態(tài)的。無狀態(tài)服務的設計讓集群的結構更加簡單,天然支持水平擴容。對于有狀態(tài)的服務,可以嘗試將存儲和計算邏輯分離為無狀態(tài)的計算服務和有狀態(tài)的存儲服務,然后用一致性的存儲來保存狀態(tài)數(shù)據(jù)。
Raft 一致性算法很多分布式系統(tǒng)選擇 Apache ZooKeeper(以下簡稱 ZK)用于存儲狀態(tài)數(shù)據(jù),ZK 一主多從的架構和其自動選舉機制很好的平衡了數(shù)據(jù)可靠性、一致性和可用性,并且具有相對不錯的性能。JouralQ 的上一代產(chǎn)品 JMQ 也使用 ZK 存儲元數(shù)據(jù),但我們在運維 JMQ 的過程中也遇到了一些 ZK 的問題: ?
- 可維護性問題: 運維人員部署和運維 JMQ 集群時,不得不一并維護 ZK 集群,并且 ZK 集群故障會影響到 JMQ 集群。 
- 多機房部署的問題:京東的 JMQ 集群包含超過 2000 個節(jié)點部署在全球多個機房中,當機房間的鏈路出現(xiàn)問題時,在擁有少數(shù)節(jié)點的機房中 ZK 集群將處于不可用狀態(tài),不可避免的會對使用 ZK 的 JMQ 集群產(chǎn)生影響。 
- 數(shù)據(jù)容量的問題:ZK 本身的容量是有上限的(我們的經(jīng)驗數(shù)據(jù)是 500MB 左右),否則很容易導致選舉失敗,陷入反復選舉集群不可用的狀態(tài)。 
- 選舉速度慢:ZK 選舉完成后,還需要完成超過半數(shù)以上節(jié)點的數(shù)據(jù)同步過程才能提供服務,當數(shù)據(jù)量比較大時數(shù)據(jù)同步的耗時也比較長,導致不可用時間也會相應變長。 
考慮到上述問題,在設計 JournalKeeper 時,我們決定基于 Raft 協(xié)議自行實現(xiàn)分布式協(xié)調(diào)相關的服務,并把這部分功能直接集成到 JournalKeeper 的服務進程中,避免運維不必要的協(xié)調(diào)服務集群。
JournalKeeper 不僅使用 Raft 來維護其元數(shù)據(jù),Raft 協(xié)議也被用來維護存儲的流數(shù)據(jù)的一致性。我們?yōu)閷τ诿總€數(shù)據(jù)流(可以理解為一個 Topic)都創(chuàng)建一個 Raft 集群,集群的每個節(jié)點為一個虛擬進程,Leader 節(jié)點提供流數(shù)據(jù)寫入服務,所有節(jié)點都可以提供流數(shù)據(jù)的讀服務。
關于 Raft 一致性算法本身,大家可以參考作者在 GitHub 上的 主頁:https://raft.github.io 和 論文:https://raft.github.io/raft.pdf。
Raft 的優(yōu)點在于: ?
- 強一致:嚴格按照 Raft 協(xié)議實現(xiàn)的集群可以提供最高等級的一致性保證。 
- 快速選舉:Raft 的選舉算法非常簡單高效,大多數(shù)情況向通過一輪投票即可選出新的 Leader,并且選舉完成后 Leader 立刻就可以提供服務,不需要等待數(shù)據(jù)同步。 
- 易于理解:Raft 相比于其它的一致性算法,更易于理解和實現(xiàn)。 
Raft 協(xié)議也存在一些不足之處:
首先,Raft 的大多數(shù)原則限制了集群的規(guī)模,一般來說,集群的節(jié)點數(shù)設置為 3、5 或 7 個,更多的節(jié)點數(shù)量會顯著拖慢選舉和復制的過程。受限于一致性的要求,Leader 只能順序處理寫入請求,處理寫入請求過程中需要等待數(shù)據(jù)安全復制到大多數(shù)節(jié)點上。集群節(jié)點越多,Leader 的出流量更高,復制的時延更大,將導致集群的寫入的性能下降。類似的,集群節(jié)點越多,選舉的過程越慢,由于選舉過程中集群是處于不可用狀態(tài)的,過多的節(jié)點數(shù)量會降低集群的可用率。
改進版的 Raft原生的 Raft 協(xié)議并不能直接滿足 JournalKeeper 的需求,我們在實現(xiàn)過程中對協(xié)議的算法做了一些適應性的調(diào)整,犧牲了部分一致性,用以換取性能的極大提升。
讀請求分流對于流數(shù)據(jù)存儲來說,并不需要強一致,順序一致已經(jīng)可以滿足需求。剛剛寫入的日志在通過短暫的復制后才能讀到是可以接受的。
JournalKeeper 在支持強一致的同時,提供另外一種比更寬松的高性能一致性實現(xiàn):順序一致性,來緩解性能和可用性的問題。順序一致不要求在同一時刻所有節(jié)點的狀態(tài)都保證完全相同,只要保證集群各節(jié)點按照一致的順序保存同一份日志即可。Raft 協(xié)議中,已經(jīng)提交的日志具有不變性,也就是說在集群任何一個節(jié)點上同一個位置,只要這個位置已經(jīng)提交,讀到的日志就是一樣的。基于這個保證,對于流數(shù)據(jù)(也就是 Raft 的日志),可以把讀請求分流到 Follower 節(jié)點上。
將一致性約束放寬至順序一致的前提下,JournalKeeper 的所有的節(jié)點都可以提供讀服務,實現(xiàn)了讀寫分離,大幅提高了集群整體的讀性能。并且,可以通過增加 Follower 的數(shù)量來水平擴容,集群的節(jié)點數(shù)量越多,總體的讀性能越好。通過將讀請求的壓力從 Leader 分流到 Followers 上去,相對的提高了寫入性能。
我們將兩種一致性混合使用,在一致性、性能和可用性三方面達到一個相對最優(yōu)的平衡: ?
- 對于元數(shù)據(jù)的訪問,通過 Leader 讀寫確保強一致; 
- 對于流數(shù)據(jù)的寫請求,通過 Leader 寫入保證流數(shù)據(jù)的順序和一致性; 
- 對于流數(shù)據(jù)的讀請求,不需要嚴格一致,通過 Follower 讀取; 
為了提高集群的吞吐量,需要用更多的節(jié)點數(shù)量分攤壓力,但增加節(jié)點數(shù)量又會導致集群的寫性能和可用率下降。JournalKeeper 提出了一種新的角色 觀察者 (OBSERVER) 來解決這一矛盾。集群中的節(jié)點被劃分為如下 2 種角色:
- 選民(VOTER) 擁有選舉權和被選舉權的節(jié)點,可以成為 Leader、Follower 或 Candidate 三種狀態(tài)。 
- 觀察者(OBSERVER) 沒有選舉權和被選舉權的節(jié)點,提供只讀服務,只從集群的其它節(jié)點上復制已提交的日志。 
選民節(jié)點即 Raft 中的節(jié)點,可以成為 Leader、Follower 或 Candidate,參與選舉和復制過程。觀察者從集群的其它節(jié)點拉取已提交的日志,更新自己的日志和提交位置。觀察者節(jié)點提供和選民節(jié)點完全相同的讀服務。
觀察者既可以從選民節(jié)點拉取日志,也可以從其它觀察者節(jié)點拉取日志。為觀察者節(jié)點提供日志的節(jié)點無需維護觀察者節(jié)點的狀態(tài),觀察者節(jié)點也無需固定從某一個節(jié)點上拉取數(shù)據(jù)。觀察者對于選民來說是透明的,選民無需感知觀察者,這樣確保 Raft 中定義的選舉和復制的算法無需做任何變更,不破壞原有的安全性。觀察者可以提供和所有選民一樣的讀服務,因此可以通過增加觀察者的數(shù)量來提升集群的吞吐量。觀察者不參與選舉和復制的過程,增加觀察者的數(shù)量不會拖慢選舉和復制的性能。
集群節(jié)點超過一定數(shù)量時,大量的觀察者節(jié)都從少量的選民節(jié)點拉取數(shù)據(jù),可能會導致網(wǎng)絡擁塞。這種情況下,可以使用多級復制的結構來分散日志復制的流量。需要注意的是,復制的層級越多,處于邊緣的節(jié)點更新到最新狀態(tài)的所需的時間越長。
并行復制針對 Raft 線性復制的性能較差的問題,JournalKeeper 在保證一致性的前提下,給出了一種并行復制的實現(xiàn),能顯著降低日志復制的平均時延,提升總體吞吐量。
在 Raft 中,串行復制的流程是: ?
讀取:Leader 讀取數(shù)據(jù),構建復制請求;
網(wǎng)絡傳輸:Leader 將復制請求發(fā)送給 Follower;
寫入:Follower 收到日志后寫入內(nèi)存或磁盤,構建響應;
網(wǎng)絡傳輸:Follower 將響應發(fā)送給 Leader;
提交:Leader 收到響應,如果滿足條件則提交已完成復制的日志。
并行復制的思路是,Leader 并行發(fā)送復制請求,Follower 中維護一個按照日志位置排序請求列表,按照日志位置串行處理這些復制請求,Leader 按照位置順序處理響應。也就是說整個復制流程拆分成上面的 5 個小流程,其中 1、2、4 三個小流程可以并發(fā),3、5 為了保證數(shù)據(jù)一致性不能并發(fā),依然串行執(zhí)行。對于并發(fā)后可能出現(xiàn)的亂序和數(shù)據(jù)空洞問題,可以通過對請求按照數(shù)據(jù)的位置進行排序和少量數(shù)據(jù)重傳解決,具體的實現(xiàn)細節(jié)大家可以參照 JournalKeeper 的源碼或文檔。
結? 語如果說單節(jié)點的性能優(yōu)化更多的是一些小的方法和技巧,這個在中國傳統(tǒng)文化里面稱之為“術”。而集群層面的架構設計更多的是一些大方向的選擇和取舍,這個稱之為“道”,也就是道理的“道”。沒有最好的架構,只有最適合的架構,所謂有一得必有一失,一個優(yōu)秀的架構師,不僅要有具備足夠的技術能力,更要有足夠的高度和大局觀,懂得在宏觀層面做好把握和取舍,方能成就優(yōu)秀產(chǎn)品。
JournalQ 和 JournalKeeper 這兩款中間件產(chǎn)品將會在近期開源,也請大家多關注,謝謝!
福利推薦QCon 全球軟件開發(fā)大會(北京站)2019 已經(jīng)圓滿結束,QCon 上海 2019 即將起航,點擊
你也「在看」嗎??
總結
以上是生活随笔為你收集整理的偏移出来的数据不准_独家解读!京东高可用分布式流数据存储的架构设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: apache poi 修改docx表格_
- 下一篇: 8s 使用本地打包镜像_在Docker环
