攜程Dynamo風格存儲的落地實踐
作者|根泰,攜程高級后端開發工程師,關注數據存儲和數據庫領域;遐齡,攜程研發總監,關注大數據存儲、性能調優。
Dynamo風格數據庫來源于亞馬遜的Dynamo: Amazon’s Highly Available Key-value Store 論文,在該論文中論述了一種無主復制的數據庫,受此啟發,攜程酒店開發了多存儲介質預定庫Hare和高可用性高性能的動態信息存儲服務InfoKeeper。本文將介紹Dynamo風格的無主復制數據庫,及其在攜程酒店的實踐。
一、Dynamo風格數據庫
在分布式系統中,為了提高數據的可用性和性能,通常會將同樣的數據復制多份,分擔讀寫請求和主備切換,在復制形式上,主要有單主復制、多主復制、無主復制。
1.1 單主復制
在單主復制中,只有一個主節點可以寫入,數據從主節點復制到從節點,從節點可以承擔讀請求,單主復制的結構簡單,易于實現,沒有數據沖突。但是寫入依賴主節點,寫入性能由主節點的性能決定,主從節點之間存在復制延遲(在從節點上讀取到的數據不一定是最新的數據),在主節點發生故障進行主從切換的時候存在數據丟失或者寫入的不可用。
1.2 多主復制
在多主復制中,有多個主節點承擔寫入的請求,相比于單主復制,數據的寫入請求被多個主節點分擔,但主從節點之間的復制延遲問題依然存在。除此之外,兩個主節點對同一份數據的并發寫入需要沖突解決機制決定以哪次寫入為準。
1.3 無主復制
Dynamo風格的數據庫就是無主復制,寫入的請求不會經過特定的主節點復制到從節點,所有的節點都可以承擔讀取和寫入,容忍寫入時的不一致,在讀取時解決不一致。
假設一個數據庫中有三個節點,存儲的鍵值對X=1。在下面的示意圖中,三個節點都收到了同一個寫入的請求,C節點寫入失敗。
此時,三個節點內鍵值X對應的value是不一樣的,收到讀請求后自然會返回不同的值。
從上帝視角看,此時此刻,鍵值X對應的value應該是100,但對于一個運行的系統,需要一個機制解決下面兩個問題,這個機制稱為仲裁。
對于讀取到的不同的值,哪個值為正確的值?
讀取多少個節點才能保證讀取到正確的值?顯然,如果只從C節點上讀取,那不管問題1的答案是什么,都得不到正確的值。
1.4 嚴格仲裁
使用時間戳或者版本號判定哪個值為正確的值:時間戳最大的或者版本號最大的,代表數據是最新的,最新的數據就是正確的數據。
R+W>N,N:?總的節點個數,W: 判斷寫入成功所需的節點個數,R:讀取時至少需要讀取成功的節點個數,W+R>N時總會讀到最新的數據。如下圖所示,藍色的節點表示寫入成功的節點,即W=3,當R=3時,讀取成功的節點和寫入成功的節點一定會有交集。W越小,寫入的可用性更高,寫性能越好,R越小,讀的可用性更高,讀性能越好。
假設單個節點的可用性P=99.9%,以此來計算無主復制時的讀和寫的可用性,不同的R、W的可用性情況如下表所示,以N=3舉例,R=1時讀的可用性等于。
節點的數量 | R、W | 讀可用性 | 寫可用性 |
2 | R=2 W=1 | 99.8% | 99.9999% |
R=1 W=2 | 99.9999% | 99.8% | |
3 | R=2 W=2 | 99.999% | 99.999% |
R=3 W=1 | 99.7% | 99.9999999% | |
R=1 W=3 | 99.9999999% | 99.7% |
根據表中所示,在N=3,R=W=2時,讀和寫的可用性都比單個節點的讀寫可用性高,這也是Dynamo風格數據庫使用的推薦配置。
1.5 寬松仲裁
在嚴格仲裁時,如果達不到嚴格仲裁的R+W>N時會返回調用端錯誤碼,假設N=5,W=R=3,讀取的時候讀了5個節點,但是三個節點讀失敗了,只有兩個節點讀成功了,此時如果以兩個節點的結果比較版本號或者時間戳,得到的數據有可能是錯誤的,也有可能是正確的。
如果我們的系統能夠忍受返回不新鮮的數據的可能性,那么使用寬松仲裁是提高系統可用性的一種辦法。我們來定義寬松仲裁:在系統達不到嚴格仲裁的條件時,利用僅有的條件返回調用端結果,注意,必須是先嘗試滿足嚴格仲裁,達不到嚴格仲裁時使用僅有的條件返回調用端結果,比如,N=5,R=W=3,在讀取數據時先讀取三個節點,兩個節點讀取失敗,為了滿足嚴格仲裁,再讀取剩余的兩個節點,但是一個成功,一個失敗,此時一共有兩個節點讀取成功,使用兩個節點的數據寬松仲裁,得出結果,而不是一開始就只讀兩個節點,這兩種方式讀取到錯誤數據的概率差別很大。
使用寬松仲裁時得到正確數據的概率如下表所示,假設單個節點的可用性P=99.9%,N=1,R=W=1時,讀和寫的可用性是,N=3,R=1,W=1時讀到錯誤數據的概率
。
?
節點的數量 | R、W | 讀可用性 | 寫可用性 | 讀到正確數據的概率 |
2 | R=1 W=1 | 99.9999% | 99.9999% | 99.9998% |
3 | R=1 W=2 | 99.9999999% | 99.999% | 99.9999997% |
R=2 W=1 | 99.999% | 99.9999999% | 99.9999997% | |
R=1 W=1 | 99.9999999% | 99.9999999% | 99.9999994% |
無主復制的數據庫在寫入的時候容忍了部分節點的不一致,但是我們希望每個節點上的數據盡可能的完整,這就需要節點版本補齊。
1.6 節點間的版本補齊
1)寫修復,節點寫失敗在寫入的時候已經是被感知到的,可以通過消息隊列等方式異步的在寫入失敗之后補償修復。
2)讀修復,在讀取數據的時候,已經知道了節點間的數據不一致,此時可以根據仲裁得出的數據來修復版本滯后的節點上的數據。
3)巡檢,主動的掃描介質間的數據,根據仲裁的結果修復數據。
二、由無主復制向多介質存儲擴展
前面介紹無主復制數據庫的時候一直在使用“節點”這個概念,這里對節點做一個定義:運行同一套代碼的、擁有完全相同功能的進程,比如Redis的master和slave節點。
在攜程酒店的預定訂單和價態信息存儲中,選擇合適的存儲介質一直是一個核心的技術問題,我們希望數據不僅在介質內有互備(Redis的master和slave),還能有介質間的互備(比如Redis和Trocks),因為同一個存儲介質總是擁有相似的運作機制,同時出問題的概率更高。
在多介質數據存儲中,我們對前面理論部分用存儲介質代替“節點”后的語義就是:數據同時寫到多個存儲介質中,容忍部分存儲介質的寫入失敗,在讀出數據時,仲裁決定整個系統中數據最終的值,整個系統能夠容忍單一存儲介質級別不可用的情況,系統的穩定性從容忍單個節點故障提升到了存儲介質級別。
三、Hare:多存儲介質的預定庫
Hare的名稱來源于成語“狡兔三窟”,數據存儲在多個介質中,以保證數據的安全。Hare承擔攜程酒店預定庫的功能,主要用于存儲在用戶下單的各個環節(創單、支付、提交)中產生的訂單相關數據。在訂單完成提交后從Hare同步到訂單庫,進入訂單處理環節。Hare的架構圖如下圖所示,應用層代碼管理底層的Redis、Trocks、Hbase的寫入和讀取,以及仲裁返回給調用端的數據。介質間版本補齊使用寫修復。
Hare內部采用寬松仲裁,N=3,W=1,R=1,使用版本號判斷最新版本。需要特別指出的是,W=1并非任何一個介質寫入成功就算成功,Hare內部“期望”的寫入成功個數為2,但是當所有介質寫入完成后,寫入成功的介質個數依然沒有達到2,就會優先考慮可用性,寫入成功的個數等于1也算寫入成功。
當W=1時,嚴格仲裁的R應該等于3,Hare內部會讀所有的3個介質并比較版本號,返回版本號最大的數據。但如果讀完所有數據,依然只有一個介質讀成功,還是會以成功的這個介質的數據返回給調用方。所以寬松仲裁的含義是,在使用嚴格仲裁但達不到嚴格仲裁的條件時,優先保證可用性。寫入和讀取時的流程圖如下所示。
四、InfoKeeper:高可用高性能的動態信息存儲
InfoKeeper是對Hare架構在酒店價態量存儲場景下的改進,Hare作為下單場景用,對性能要求較低,但對數據的可靠性要求更高。但在酒店的價態量存儲中,對性能要求更高,數據可靠性要求較下單場景低,所以InfoKeeper中存儲介質的個數較Hare更少,選擇了Redis和Trocks兩個存儲介質,仲裁的N=2,W=1,R=1。
我們將InfoKeeper中參與仲裁的介質稱為主介質(圖中綠色),將只會寫入但是不參與仲裁的介質稱為從介質(圖中淡藍色),從介質的寫入是否成功都不會影響對客戶端的響應。介質間的版本補齊使用寫修復。在酒店價態量存儲中架構圖如下。
InfoKeeper寫入的流程圖如下。
InfoKeeper現在支持的存儲介質有redis、trocks、mysql、es、hbase、oceanbase、Tikv、qmq、kafka、soa。qmq通常作為推送增量的方式,kafka用于推送離線數據,soa用于通過soa接口調用的方式更新服務端的緩存。因為接口較消息隊列延時更低,所以soa面向對緩存新鮮程度要求很高的使用方,比如酒店查詢服務,在InfoKeeper中將消息隊列和soa接口當作一種存儲介質看待,只是這種存儲介質不能提供讀功能。
InfoKeeper中存儲的數據目前在百億級別,InfoKeeper完成了這些數據的存儲、承擔了40萬QPS的讀能力,以及數據從存儲方到各個使用方的高效流轉。得益于強大的讀能力(強大的讀寫能力主要是因為選擇了性能更好的KV型存儲介質為主介質,可以根據數據讀取方對性能和數據新鮮度的要求,選擇對應的存儲介質和仲裁的方式),一些散落在各個使用方的緩存廢棄,改為直接從InfoKeeper讀。根據統計,InfoKeeper節省了20%的硬件成本,數據的流轉效率較以往使用關系型數據庫存儲,使用方從關系型數據庫拉取的方式大大提高,還消除了關系型數據庫的單點性能限制。
建立緩存的一種新模式
在InfoKeeper前面的架構圖中,如果將主介質改為關系型數據庫,從介質改為redis,就實現了為DB建緩存的目的,只是把從DB拉數據改為了主動往redis寫數據,減輕了DB的壓力。如果需要建多份緩存,只需要多掛幾個從介質就可以實現。目前酒店的房型通用緩存就是使用這種方式。
五、設計目標的驗證
怎么確認多介質存儲系統符合設計預期,能夠容忍存儲介質級別的故障?Hare上線6個季度,InfoKeeper上線4個季度以來,我們在每個季度都會對Hare和InfoKeeper做單個介質注入故障的演練,在演練期間應用和上下游正常,在注入故障恢復之后,寫修復最終追趕成功,可以確認系統符合設計預期。
六、展望
現在InfoKeeper和Hare還在應用代碼層面,沒有形成通用的組件,新的業務的加入需要在現有代碼的基礎上增加業務邏輯,開發者對底層的多介質存儲的代碼是有感的,也可能需要修改多介質存儲層的代碼以更好的貼合新的業務。
我們計劃對Infokeeper和Hare的代碼進行合并,形成一個通用的組件,讓新的使用方能對多介質存儲層無感,做到開箱即用,降低多介質存儲的使用門檻,使得使用方能更專注于業務代碼。