淺談分布式一致性(DDIA讀書筆記)


副本一致性

現代的數據庫系統來說,幾乎都具備了復制機制replication,這種設計方式至少能對系統帶來兩個好處:

  • 多副本容災:只要存在一個可用數據副本,數據就能夠恢復
  • 讀性能水平擴展:通過分布到不同的機器上,同一份數據能夠在多個節點上同時供外部訪問

保證多副本的數據一致性consistency是一個難題。

最簡單的實現方式是使用同步復制機制sync-replication:保證寫操作在所有副本上執行成功后,再響應客戶端。不過這一方式通常意味着糟糕的寫性能,因此較少使用。

與之相對的則是異步復制機制async-replication:寫操作在某些副本上成功后即可響應客戶端,數據庫會異步將修改同步到剩余副本上。其優勢在於較高的寫入性能,但是數據副本的一致性無法保證。

以最基礎的主從復制架構為例,雖然主庫與從庫最終都會到達一個一致的狀態,但是主從狀態同步會存在時間延遲,這類延遲被稱為復制滯后replication lag現象。 期間可能同時存在兩份相互沖突的數據副本,依賴這些數據的應用如果沒有做好預防處理,最終會引發系統行為異常。

在分布式數據庫中,維護副本狀態的進程分為以下兩類:

  • leader / master: 能夠同時處理讀寫請求的進程
  • follower / slave: 只能處理讀請求的進程

基於以上定義,常見的復制架構又可以分為下圖中的 3 類:

一主一從 master-slave
潛在問題 寫后讀一致性 read-after-write consistency
客戶端修改數據,變更未同步到從庫,此時從從庫讀取數據,得到的是未修改的結果
解決方案 寫操作對之后的讀操作立即可見
  • 根據應用特點,強制部分功能只訪問主庫,保證讀寫順序一致
  • 維護用戶修改時間戳與從庫修改的時間戳,根據時間戳選擇是否讀取從庫

一主多從 single-leader
潛在問題 單調讀一致性 monotonic read
客戶端多次讀取同一條記錄,但請求路由到不同的從庫上面,可能讀取到舊版本的數據
解決方案 之后讀取到的數據一定要比之前讀取到的數據要新
  • 保證同個用戶的請求只會路由到同個從庫,保證讀命令順序一致

多主多從 multi-leader / leaderless
潛在問題 因果一致性 consistent read prefix
客戶端多次修改數據路由到不同的主庫上,且數據之間有因果關系(例如:問答記錄)
可能讀取到次序混亂的數據,也可能修改一條尚未存在的數據(例如:leader 間的網絡延遲)
解決方案 寫操作的結果必須按照其執行的順序被讀取到
  • 具有因果關系的寫操作在同個主庫上執行,保證寫命令順序一致

沖突解決

在大規模互聯網應用中,多數據中心正變得越來越流行,其優點如下:

  • 地理位置近,訪問速度快
  • 高可用,單個數據中心宕機或者網絡出現問題不會導致不可用的情況

當系統需要部署到多個數據中心的時候,不可避免地會使用到 multi-leader 架構,這帶來以下問題:

  • 同一份數據可能被兩個數據中心並發修改,導致寫沖突
  • 數據庫的某些特性不能很好地支持多 leader 架構,例如:自增主鍵、觸發器

寫入沖突

single-leader 架構下的寫是順序的,對同一份數據的修改,在每個副本上都能得到最終一致的結果。
multi-leader 架構下每個 leader 中的寫也是有序的,但是不同 leader 之間的寫操作是無序的,因此對同一份數據的修改也是無序的,最終導致副本的狀態可能不一致。

如何保證所有庫收斂到同一個狀態?—— 解決沖突

  • 每個寫操作關聯一個唯一 ID,選取優先級最高的操作結果
  • 每個副本關聯一個唯一ID,選取優先級最高的副本對應的結果
  • 將兩個沖突的數據合並成同一條數據
  • 保存所有沖突數據,由后續的操作來進行解決

解決沖突的時機又可以分為:

  • 寫時解決:向數據庫注入解決沖突的邏輯代碼,當發生沖突時由數據庫進行調用MySQL MGR 中通過維護全局一致的 Binlog 實現一致性
  • 讀時解決:當存在沖突數據時,應用會獲取到這些沖突數據,並自動或手動解決這些沖突Dynamo NRW 通過調整讀寫副本數量來保證讀取到最新的數據

一致性模型

作為一個開發者,我們關心的一個重要問題是:數據庫本身究竟為我們提供了哪種等級的一致性保證?

為了支持並發操作,單機數據庫引入了事務的概念,從而避免數據不一致導致行為異常。數據庫的事務模型有一個重要概念:事務隔離級別

隔離級別 讀未提交 Read Uncommitted 讀已提交 Read Committed 可重復讀 Repeatable Read 序列化 Serializable
臟讀 Dirty Read
不可重復讀 Unrepeatable Read
幻讀 Phantom Read
寫偏斜 Write Skew

在不同的事務級別下,開發者能夠得到數據庫不同程度的一致性保證。越高的隔離級別,提供的一致性更強,也同時意味着更大的性能開銷。

這一模型存在的好處,是的我們能夠在一致性與性能之間進行權衡,作出適合自己應用場景的選擇。在分布式場景中,我們要面對更為復雜的一致性問題。為了方便接下來的討論,首先簡單介紹幾種一致性模型。

最終一致性

某個特點時間點下,數據庫系統中的各個副本間的狀態很可能是不一致的。
上面我們看到的幾個一致性問題,都是由應用層解決的,數據庫本身只提供以下保證:

經過一段任意長的時間后,數據庫中的所有副本最終都能收斂convergence到相同的狀態。

這種極弱的一致性保證,就是我們常說的 最終一致性Eventual Consistency

  • 優點:這種弱一致性保證使得系統設計較為靈活,從而能夠達到較高的性能。
        比如:副本間使用異步復制策略、設計專用的對賬系統來離線解決數據沖突……

  • 缺點:當系統設計中涉及到最終一致性時,應用層需要十分關注復制滯后對系統的影響。
        並且需要根據業務所需的一致性保證來設計系統,變相增加了應用開發者的工作量。
        此外,某些問題在網絡錯誤或者高並發時才會暴露出來,難以測試。

線性一致性

數據庫的事務機制本身就是一個容錯協議,能夠為基於事務運行的應用提供數據安全的保障。

為了向應用層隱藏復雜度,事務為應用提供了以下的抽象保障:

  • 原子性:數據庫中的數據是完整的,事務執行是完整的(無需擔心執行過程中進程崩潰)
  • 隔離性:數據庫不會被並發修改,事務之間是不會相互影響(無需擔心競態條件影響執行結果)
  • 持久性:數據庫的存儲是可靠的,事務的變更不會丟失(無需擔心存儲器故障導致數據丟失)

事務機制提供的抽象保證,將應用開發者從繁雜的錯誤處理中解放出去,使其只需要專注於業務。不但提高了開發效率,也減少了 bug 出現的概率,系統更穩定且易於測試。

理想情況下,我們希望能夠分布式數據庫能夠像事務一樣,為我們提供一個更強的一致性保證:

  • 全局寫后讀一致性:
    • 系統只對外暴露一份數據,不存在同時存在多個版本的數據的問題
    • 所有的修改操作都是原子性的,且每次讀取到的數據都是最新的
  • 全局單調讀一致性:一旦寫入操作成功,結果對所有之后的讀取操作均可見,並且不會讀取到舊版本的數據

這種跨進程的全局強一致性保證,被稱為 線性一致性Linearizability

下面通過一些具體場景來具體介紹這一模型。

首先,我們假定 x 是數據庫中的某個條目:

  • 在鍵值數據庫中,x 就是一個 key
  • 在關系數據庫中,x 就是一行
  • 在文檔數據庫中,x 就是一個文檔

線性一致性模型定義了 3 種基本操作:

  • read(x) => v:客戶端從數據庫中讀取 x 對應的值 v
  • write(x, v) => r:客戶端向數據庫中寫入 x 對應的值 v,返回操作結果 r
  • cas(x, v1, v2) => r:客戶端使用 compare-and-set 操作將 x 對應的值從 v1 修改為 v2 並返回操作結果 r

線性一致性是跨進程Cross-Process的,可作為實現以下分布式應用場景的基礎:

  • 分布式鎖與選主:用 CAS 操作實現鎖,獲得鎖的節點就是 leader
  • 唯一性約束:使用 CAS 操作來獲取某個值對應的鎖,如果獲取成功,則這個值是唯一的,否則這個值就不是唯一的
  • 多信道間的時序依賴:一個進程A修改數據成功后通知進程B,此時線程B一定能夠獲取到進程A的修改結果

一個滿足線性一致性的場景


圖中有 3 個客戶端,其中客戶端 A、B 讀 x,客戶端 C 寫 x 。

  • 當 B 第一次讀 x 時,C 正在進行寫操作,此時 B 讀到的值是 0(此時 C 寫操作未提交)
  • 當 A 第二次讀 x 時,C 正在進行寫操作,此時 A 讀到的值是 1(此時 C 寫操作已提交)
  • 當 B 第二次讀 x 時,由於此前 A 讀 x 結果為 1,此時 B 讀取到的值必然也是 1(全局單調讀一致性)

一個違反線性一致性的場景


圖中有 4 個客戶端 A、B、C、D 並發進行讀寫操作,圖中的連線表明了事務提交與讀操作實際發生的時間點。圖中存在一個違反線性一致性的行為: 在 A 讀取到 4 之后 B 讀取到了 2

雖然單從客戶端 B 本身來說並沒有違反單調讀一致性,但是在全局上來說違反了單調讀一致性:后發的 B 讀請求的結果,滯后於先發的 A 讀請求的結果

一個實際應用場景【生成相冊縮略圖】


圖中的 FileStorage 是個多副本的分布式文件存儲,用於存儲用戶的照片數據,后台需要生成縮略圖加快 web 預覽:

  1. 當用戶上傳或修改照片時候,WebServer 將原始尺寸的用戶圖片儲於 FileStorage
  2. 通過 MQ 將圖片 id 異步通知 ImageResizer
  3. ImageResizer 根據 MQ 給出的圖片 id 從 FileStorage 讀取數據並生成縮略圖

在步驟 B 中,MQ 消息傳遞的同時, FileStorage 內部進行副本復制。如果 FileStorage 不滿足線性一致性,ImageResizer 可能讀取不到圖片(違反全局寫后讀)或者讀取到舊(違反全局單調讀)的照片。從而導致處理失敗,甚至會生成錯誤的縮略圖,整個系統最終會處於一個不一致的狀態。

實現方式

實現線性一致性語義的最簡單方式就是只使用一個數據副本,但這種做法會使得系統不具有容錯性。
為了提高系統的容錯能力,多副本的架構是唯一的選擇,下面我們按照不同的多副本架構分情況討論:

復制架構 實現方式
Single-leader 在不使用快照隔離(例如:mysql 的 MVCC)的前提下,同時使用以下兩種策略可以滿足線性一致性:
  • 從 leader 讀寫取數據(只訪問 leader 的數據副本,避免受其他 follower 不一致的數據副本影響)
  • 使用同步復制策略(異步復制無法保證 follower 數據副本最終與 leader 一致)
實現的風險點:
  • 腦裂split-brain時可能出現多個 leader (同時對外暴露多個可寫的副本數據,並且最終會導致數據不一致),違反線性一致性
  • 自動災備failover對新 leader 副本的選擇(如果選擇一個數據不完整的副本作為新 leader,則相當於數據丟失),違反線性一致性
Multi-leader 允許多節點同時寫,並且需要支持異步復制,可能引起寫沖突,因此需要暴露多個副本來解決沖突,因此肯定無法滿足線性一致性。
Consensus algorithms 共識算法涵蓋了 single-leader 的功能,並且同時具備防止腦裂和過期副本的機制,因此天然滿足線性一致性。

權衡

盡管線性一致性是個強有力的一致性保證,這類強一致模型在實踐中的應用並不廣泛。

舉個例子:現代計算機的內存模型並不能保證線性一致性。

在多核 CPU 中,每個內核都有自己的 cache,因此會盡可能的使用數據局部性來提高訪問性能。

當 CPU 需要訪問修改 RAM 中的數據,首先會修改 cache,然后異步將修改刷新到實際的 RAM 中(多副本 + 異步復制機制)。

為了保證多線程訪問的一致性,需要向代碼中插入 memory barrier 指令,強制 CPU 將修改同步刷新到 RAM 中。

犧牲一致性以換取更好的性能,這類權衡在數據庫系統中更為常見。為了保證強一致性,線性一致性模型會帶來糟糕的性能,這是不可改變的事實。

因果一致性

事件的發生先后順序包含了因果關系Causality

一個違反因果一致性的場景【醫生排班表】

每個醫院都有一個排班表 (on-call shift),以 保證至少有一個或者以上的值班醫生 (on-call docotor) 在場,以應付急診之類的突發情況。如果值班當天醫生身體不適,可以在排班系統申請提前下班,值班系統會檢測當前值班醫生人數,判斷是否允許下班。

某天,醫院只有兩個值班醫生 Alice 和 Bob,但是恰巧兩人身體都出現了不適,並且在同一時刻申請了提前下班,此時可能出現以下情況:

  1. 系統首先開啟了兩個並發的事務,分別由 Alice 和 Bob 兩人發起
  2. 兩個事務同時查詢了值班人數,並且得到值班人數為 2 (currently_on_call = 2)
  3. 兩個事務同時更新了值班記錄,分別將 Alice 和 Bob 改為非值班狀態,並提交事務成功
  4. 最終醫院的值班人數為 0,急診病人 R.I.P

上面這個例子違反了因果一致性:事務中的 write 操作依賴於 read 操作

Alice 的 write 事務先提交,從而導致 Bob 事務中 read 的結果失效。

但是 Bob 的事務沒有檢測出 read 失效的情況,而是直接提交了事務,最終導致系統違反了排班約束

這種由並發 read-write 事務導致的不一致現象被稱為寫偏斜Write Skew。值得注意的是,這種事務並發的情況不一定是人為引起的,網絡延遲拉長事務周期而也可能間接引發這一問題。

模型對比

我們先回顧一下兩個順序相關的定義:

  • 全序total order/linear order集合中,任意兩個元素之間都是可以比較
  • 偏序partial order集合中,部分元素之間是可以比較的

這兩種順序分別代表了兩種一致性模型:

  • 線性一致性:系統對外只暴露一份數據,所有操作在唯一一份數據上串行執行(沒有並發操作),因此任意操作之間肯定是有先后順序的
  • 因果一致性:有因果關系的操作之間是有序的,但並發操作之間沒有因果關系,因此也沒有先后順序

線性一致性模型更簡單,容易理解,並且能夠處理多信道時序依賴的因果問題。
然而實現線性一致性需要付出較高的性能代價,操作之間需要相互等待,並且在網絡延遲較高的環境下,系統不可用的概率會增大。

因果一致性模型比較抽象,難以理解,但是足以應付多數應用場景。
因果一致性模型最終可以達到接近最終一致性模型的標准,並且對網絡延遲不敏感,在面臨網絡故障的情況下仍能保證可用性。

相比線性一致性,因果一致性的一個重大差異是:允許不相關數據的並發

  • 線性一致性只有一個單一的時間線
  • 因果一致性則是一棵具有多個分叉的樹(可以參考 Git 的分支模型)

序列號生成

因果關系本身就是一個先后順序的問題,因此只要知道了順序,就能夠據此推導出因果關系。

在討論因果一致性模型前,我們需要找到一種合適的方法來表示因果順序,從而使得我們能夠分析因果依賴。在實際應用中,我們無法記錄所有依賴關系,否則將會造成巨大的開銷。

一個可行方法是:為每個操作分配一個表示順序的序列號,序列號本身占用空間少,並且本身具有全序關系。

常見的大規模序列號生成手段有:

  • 時間戳:使用高精度的時間戳作為序列號
  • 提前規划:使用取模的方式,按照生成器個數來划分可用序列號,部署多個序列號生成服務(例如:兩個節點可以分別使用奇偶序列號的生成器)
  • 批量生成:生成器以批量分配的方式,每次向節點分配一個連續的區間

問題在於,這些序列號不能保證全局有序:

  • 系統時鍾會有偏差,多個節點間時鍾之間不一定同步,時間戳不一定能表示操作的先后順序
  • 如果服務節點的負載不均,則舊的序列號可能會被應用到新的操作上面,先后順序也無法保證

為此,我們需要一個能夠保證全局順序的序列號生成機制。

Lamport timestamps

先介紹一種通過邏輯時鍾生成具有因果關系的序列號的方法:Lamport timestamps

每個進程要維護以下兩個信息:

  • ID:全局唯一的不可變進程標識
  • Counter:初值為 0 的單調遞增的整型計數器

系統的所有交互會被封裝為一系列的事件,每個事件都會關聯一個全局唯一序列號 (C,ID),其生成規則如下:

  • 進程生成一個事件時,首先遞增計數器得到一個局部唯一的序列號 Clatest = ++Counter,然后將其與進程標識組合成 (Clatest,ID) 用於表示這個事件的發生順序。
  • 進程接收一個來自其他進程的事件時,會更新本地計數器Counter = max(Cother, Counter)+1

該序列號滿足以下全序關系:C 越大優先級越高;C相同時,ID 越大優先級越高。通過為每個操作關聯上這樣一個序列號,間接為所有操作建立起一個全序的順序關系,此時任意兩個操作都是有序的。

不過這一方案也存在缺陷:操作順序只能在操作發起之后才能得知,無法立刻對數據沖突作出反應。比如:

兩個客戶端分別在兩個節點上同時發起了一次有沖突的操作(例如:添加同名賬戶)。 當發生沖突時,系統會自動用序列號更大的值來解決沖突,從而導致序列號較小的操作失效,但是在客戶端看來自己操作已經成功了。

這類不能實時解決的沖突可能會導致一致性的問題,出現這種問題的主要原因是:

雖然最終的序列號是全序,但是某個時刻的實時的序列並不完整,后面可能有未知的序列號插入其中。 例如:進程 A 已生成最大序列號 (A,1),進程 B 已生成最大序列號是 (B,5),若 A 此時接收到一個來自進程 C 的事件 (C,2),則后續可能生成一個序列號為 (A,4) 的事件。

為了保證操作的安全性(不可逆),我們需要保證當前已知的序列是不變的。

全序廣播

全序廣播total order broadcast/atomic broadcast通常被描述為一個在節點間交換消息的協議,該協議具有兩個特征:

  • 可靠傳遞:一旦一個節點接收到某條消息,其他節點也一定會收到這個消息。
          不會出現消息丟失的情況,並且每條消息只會出現一次。
  • 全序傳遞:所有節點都以相同的順序接收到消息。

一旦消息發出,此時順序就確定下來了。節點不允許往已經存在的消息序列中插入消息,只能往后追加,因此可以將全序廣播看作是一個記錄日志的過程:所有節點都在異步記錄一個全局順序一致的日志
當出現故障時,需要有一個重試機制來保證上面兩個約束。

很多一致性服務都實現了全序廣播協議,全序廣播可以用於實現以下功能:

  • 一致的復制機制:把對數據庫的寫操作當作是消息,只要能夠保證所有節點以相同的順序接收到這些消息,則能夠保證所有副本的一致性
  • 序列化事務:把事務操作當作是消息,每個節點以相同的順序處理事務,那么每個節點都保證具有一致的狀態
  • 分布式鎖:每個獲取鎖的請求被記錄在一個有序日志中,可以根據請求的順序來決定鎖的獲取順序
  • CAS 操作:CAS(username, A, B)
    • 發出一條 assert(username = A) 消息
    • 監聽 username 的相關日志,在接收到第一條日志之后:
      • 如果是自己發出的 assert 日志,則通過追加一條 commit(username = B) 消息來完成修改,
      • 如果是其他節點的 assert 或 commit 日志,則修改失敗
  • 全局一致性讀:
    • 利用消息在日志中的位置確定讀發生的時間,發出一條消息,監聽這條消息返回后,再進行讀操作(etcd)
    • 如果日志系統能夠獲取到最新的日志所在的位置,則可以等待日志追加到這一位置之后,再進行讀操作(zk)
    • 從寫操作同步更新的副本上讀去數據

將最后兩個功能合並起來,則相當於實現了線性一致性。

實現方式

在 single-leader 架構中,只有一個 leader 能接受寫操作,從而能夠保證所有寫操作都是有序的,進而保證所有副本都是有序的。這意味着:single-leader 架構本身就具有全序傳遞的特性,只要在此基礎上解決了可靠傳輸問題,就能實現全序廣播

常規的 single-leader 架構需要在啟動時,人為指定一個 leader 節點,一旦這個節點失效,整個系統將會陷入一個不可用的狀態,直到人工介入指定一個新的 leader。這對系統的可用性無疑會造成嚴重的影響。

為了實現自動災備auto-failover,系統本身需要支持 leader 選舉功能:

當 leader 失效時,從健康的 follower 中選擇一個新的 leader,繼續對外提供服務。

選舉過程中需要防止腦裂的情況發生,避免同時出現多個個 leader,影響系統的一致性。

此類選舉場景中,不可避免的會用到布式一致性算法distributed consensus algorithm,后面我們將用獨立的篇幅對其進行介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM