分佈式數據庫如何平衡一致性和讀寫延遲?

作者 | 劉繼聰

審校 | 蔡芳芳  

爲了提供高可用能力、避免數據丟失,在分佈式數據庫或存儲系統中需要設立數據副本機制,而副本的引入,可以說是分佈式存儲中的 “萬惡之源”。多副本之間應該滿足強一致嗎?強一致會導致請求延遲增加多少?強一致約束下能提供哪些可用性?諸如此類,種種問題,不一而足。

此外,分佈式系統中的 CAP 原理可以被表述爲:在網絡分區存在的情況下,強一致與可用性是不可兼得的。由此發展出符合 BASE 標準的 NoSQL 數據庫,在這類數據庫中,以最終一致性取代強一致性。

那麼,我們所說的強一致和最終一致究竟是指什麼呢?

強一致意味着多副本數據間的絕對一致嗎?顯然,在分佈式系統中,由於網絡通信延遲的存在,多副本的嚴格一致是不可能的。

那是代表返回寫入請求時多副本已經達到完全一致了嗎?熟悉 Raft 的朋友會立即指出,不一定,Raft 就只需要在 quorum 中(超過半數)副本達成一致即可返回寫入成功。

抑或是隻需要 quorum 的一致即可嗎?這取決於具體的算法,如果我們不限定讀取操作只被 leader 處理,那麼,達成 quorum 一致之後仍然可能讀取到舊數據。

而在實際系統中,一致性問題的解法可能更加複雜,需要在一致性、讀寫延遲中做出權衡。

以時序數據庫 TDengine 爲例,我們爲元數據的讀寫提供強一致性;時序數據在部分場景中則需要降低讀寫延遲、提高吞吐,僅需滿足最終一致即可;而在另一些場景中,時序數據又需要有強一致保證。

爲了更好地探討一致性的相關問題,我們首先需要爲不同的一致性級別下一個定義。

1 從併發模型中的一致性到分佈式多副本一致性

一致性模型(consistency model)最早是定義在併發模型上的 [1]。常用的一致性級別定義包括以下 5 種:

之所以能夠從併發模型直接推廣到分佈式多副本系統,是因爲它們都可以建立在抽象的多讀者、多寫者寄存器(MWMR register, multiple writer multiple reader register)模型之上 [2]。

值得注意的是,這裏的寄存器是抽象的概念,並不是硬件寄存器。它泛指定義了讀、寫操作的一系列值的存儲對象。

在併發系統中,讀操作與寫操作可能是多線程併發地在不同 CPU 上執行;在分佈式系統中,它們可能是多進程被分佈在不同的物理節點上執行。但相同的是,這兩類系統中的讀操作與寫操作都有一定的延遲,不是瞬間完成的。

爲了便於理解,我們不會照搬形式化的定義,而是提供一些更符合直覺,但不失準確的描述。

假設在分佈式系統中存在絕對的物理時鐘(真實時間),進程與進程之間不存在時間的漂移。那麼,我們將讀寫操作的開始與結束的真實時間記錄下來,爲每個操作從開始到結束的持續時間段中選擇一個時刻點,視該操作爲在此時刻瞬間完成。如此,所有操作都可以被等效爲一個進程在真實時間軸上瞬時完成的讀寫操作。若所有的讀操作得到的都是上一次寫操作的結果,那麼,該系統滿足線性一致性。

圖 1

可以看到,進程 p3 的寫操作 R.write(3) 開始和結束都分別晚於進程 p2 的寫操作 R.write(2) 的開始與結束,但由於它們時間有交疊,R.write(3) 的線性化點(操作的等效時刻)可以先於 R.write(2)。因此,圖 1 系統滿足線性一致性。

但是在實際的分佈式系統中,並不存在絕對的真實時間 [3]。因此,在外部的觀察者看來,任意單個進程的操作順序是確定的,但考慮所有操作的某種全序關係時,一個進程的讀寫操作可以被插入到其他進程的任意兩個操作之間。不同的插入方式,會生成不同的全序關係,只要能保證存在一個合法的全序關係,則滿足順序一致性。

所謂的合法是指:1. 全序化後讀操作必須讀到上一個寫操作的值;2. 單個進程內的操作先後順序與在全序關係中的操作先後順序一致。

圖 2

圖 3

由於不存在絕對時間,我們不再要求畫出讀寫操作的起止時刻,而將其視爲瞬間完成的操作。在圖 2 中,全序 1、2、3 中,只有 3 是一個合法的全序,因此圖 2 中的讀寫滿足順序一致性。而在圖 3 中,不存在一個合法的全序,因此,圖 3 中的讀寫不滿足順序一致性。

由此可見,線性一致性可以被看作順序一致性在存在絕對時間的系統模型下的特例。

由於進程 p2 是讀取了進程 p1 寫入的結果之後寫入,進程 p2 的寫入值和進程 p1 的寫入值可能存在因果關係。那麼,所有在同一個進程內的連續讀操作都不可以先讀到進程 p2 的寫入值,再讀到進程 p1 的寫入值。

圖 4

圖 5

圖 4 中,進程 p1 和 p2,p2 先讀到了 p1 寫 x:=1 的結果,然後寫 x:=3。寫 x:=3 可能是由讀 x=1 計算而來,因此存在因果關係。p3 滿足了因果一致性,p4 則違背了因果一致性。

圖 5 中,p2 不再讀 x=1,因此不存在因果關係。該系統滿足因果一致性,但不滿足順序一致性。

FIFO 一致性不會考慮多個進程之間的操作排序。對任意一個進程的寫操作 1 與寫操作 2,若寫操作 1 先於寫操作 2 完成,那麼任何進程不可以先讀到寫操作 2 的值,再讀到寫操作 1 的值。

圖 3 就是一個違背 FIFO 一致性的例子。圖 4 與圖 5 中,由於不存在同一個進程中的多個寫操作,因此都滿足 FIFO 一致性。

最終一致性,可以這樣定義,若系統中不再發生寫操作,經過一段時間後,所有的讀操作都會得到同樣的值。

圖 6

以圖 6 爲例,最終一致的結果,既可能是 1,又可能是 2,還可能是 3(在不存在絕對時間的系統,甚至無法定義最後寫入的值是 1、2 還是 3),但所有進程再足夠長的時間後的讀結果都必須保持一致。

多副本的一致性級別是由併發模型中的一致性級別直接應用到分佈式系統中的結果,但有少許差別。細心的朋友可能注意到,我們並沒有提及嚴格一致性,那是因爲嚴格一致性要求數據被立即同步(比如在 CPU 的下一個時鐘週期可見),這隻能存在於單機系統中,並不存在於存在消息延遲的分佈式系統中 [4]。

我們通常所說的強一致,通常是指線性一致性或順序一致性 [5][6]。線性一致性與順序一致性之間的區別,也可以被理解爲系統模型的區別,即系統中是否存在絕對時間。弱於順序一致性的一致性級別都可被稱爲弱一致,而最終一致性是弱一致性的一種形式。

探討一致性級別時,一個常犯的錯誤是與事務的隔離級別混淆,事實上,雖然它們之間存在一定的聯繫,但並不是同一回事。當一個數據庫在隔離級別上滿足可串行化(Serializable),在一致性上滿足線性一致性,則稱爲嚴格可串行化(Strict Serializable),這是一個統一了一致性級別與隔離級別的定義。

另一個常犯的錯誤是將一致性(consistency)與共識(consensus)混淆。共識問題是在分佈式系統中有明確定義的一類問題,解決共識問題的經典算法是 paxos;強一致讀寫可以通過一系列的全序共識實現,Raft 便是這樣一種算法。

以上定義的一致性級別可以被認爲是以數據爲中心(Data-centric)的一致性級別,另一種定義方式是以客戶端爲中心(Client-centric),還可以定義出寫後讀、讀後寫、單調寫、單調讀的一致性,此處不再贅述 [4]。

2 一致性級別分析:以 Raft 爲例

下面我們以 Raft 爲例分析其一致性級別。在正常情況下,Raft 是在 leader 上完成讀與寫操作的,可以被看作單讀單寫的系統,若這個系統中將 leader 的時間視爲絕對時間,則可認爲提供了線性一致性。

但是,我們還需要考慮異常情況,當消息延遲時,Raft 可能出現短暫的雙主;若出現網絡分區,可能持續處於多主狀態。

一種實現方式是,假如每個 leader 在提供讀服務時都不做額外操作,那麼,如果多數派分區的 leader 已經完成了新的寫入,少數派分區的 leader 仍然提供讀服務,就可能讀到舊數據。

這個問題的一種解決方法是,讓少數派分區的 leader 直接拒絕讀服務。這如何實現呢?讓 leader 在與客戶端交互,完成讀操作前發送一個 no-op 並至少得到半數迴應,由於少數派分區的 leader 無法得到半數迴應,因此無法提供讀服務。

關於如何在 Raft 中獲得線性一致性的詳細探討可詳見 Raft 論文 [7] 中第 8 節 Client Interaction。

3 TDengine 提供的一致性級別

在上述的分析中可以看到,Raft 中實現線性一致性會爲讀操作和寫操作都帶來至少 2 個 RTT 的延遲(client 視角,從 client 到 leader,再由 leader 到 follower);即使僅實現順序一致性,也會在寫時帶來至少 2 個 RTT 的延遲。

在 TDengine 中,爲了降低寫入數據的延遲、提高吞吐量,我們爲元數據(表數據、表的標籤數據)提供強一致性,爲時序數據提供最終一致性與強一致性兩種可選的一致性級別。當用戶選擇最終一致同步,寫入的延遲可以被降低到 1 個 RTT(從 client 到 leader),這大大優於 Raft 這類強一致複製協議提供的性能。

隨着 TDengine 集羣版的開源,用戶數量與日俱增,TDengine 被應用到了多種多樣複雜的環境中。當集羣中存在網絡分區、或節點連續宕機等異常情況下,TDengine 中可能無法保證嚴格的強一致性,因此,在即將到來的 3.0 版本中,我們將以 Raft 算法爲基礎重構選主、強一致複製等一系列流程,同時,仍然爲時序數據提供最終一致與強一致兩種同步模式,給用戶提供靈活的選擇,幫助用戶適應最複雜的業務場景需求。

參考文獻:

[1]Lamport, Leslie (Sep 1979). "How to make a multiprocessor computer that correctly executes multiprocess programs". IEEE Transactions on Computers. C-28 (9): 690–691. doi:10.1109/TC.1979.1675439.

[2]RAYNAL, M. (2018). FAULT-TOLERANT MESSAGE-PASSING DISTRIBUTED SYSTEMS: an algorithmic approach.

[3]Leslie Lamport. 1978. Time, clocks, and the ordering of events in a distributed system. Commun. ACM 21, 7 (July 1978), 558–565. doi:10.1145/359545.359563

[4]Sukumar Ghosh. 2014. Distributed Systems: An Algorithmic Approach, Second Edition (2nd. ed.). Chapman & Hall/CRC.

[5]Kemme, B., Schiper, A., Ramalingam, G., & Shapiro, M. (2014). Dagstuhl seminar review: Consistency in distributed systems. ACM SIGACT News, 45(1), 67-89.

[6]Kleppmann, M. (2015). Designing data-intensive applications.

[7]Ongaro, D., & Ousterhout, J. (2013). In search of an understandable consensus algorithm (extended version).

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JiElBRmvkiIWL7ayF9zWZA