前一節的一致性模型目標是提供系統層面的一致視角:假設多個行程可能同時更新資料儲存,必須在這種並行下保證一致性。例如物件式進入一致性保證:物件被呼叫時,呼叫端拿到的拷貝反映了至今所有(可能由其他行程做出的)修改,且呼叫期間享有互斥存取。

在並行操作共享資料的同時維持循序一致性,是分散式系統的根本能力;但基於效能考量,循序一致性往往只能在行程使用交易或鎖等同步機制時才有保障。本節轉向一類特殊的分散式資料儲存:幾乎沒有同時更新(或即使發生也容易化解),大多數操作是讀取。這類儲存提供一種非常弱的一致性模型——最終一致性(eventual consistency);而透過引入客戶端為中心的一致性模型,許多不一致可以用相對便宜的方式被遮蔽起來。

最終一致性#

許多系統中的並行只以受限的形式出現,寫寫衝突(write-write conflict)天然罕見:

  • 資料庫:多數行程幾乎不做更新、只讀資料,僅一個或極少數行程執行更新。問題只剩下「更新要多快讓只讀行程看到」。
  • DNS:名稱空間切分為網域(domain),每個網域由唯一的命名授權單位(naming authority)擁有並更新,寫寫衝突根本不會發生,只需處理讀寫衝突(一個行程更新時另一個行程正在讀)。事實證明,惰性(lazy)傳播更新往往可以接受——讀取方過一段時間後才看到更新。
  • Web:網頁幾乎都由單一權責者(如網站管理員或頁面擁有者)更新,通常沒有寫寫衝突;瀏覽器與 Web 代理(proxy)為了效率會快取頁面,代價是可能回傳過期版本——而許多使用者(在一定程度內)覺得這種不一致可以接受。

這些例子可視為容忍相當高度不一致的(大規模)分散式複製資料庫,共同點是:只要一段時間沒有更新,所有副本會逐漸趨於一致。這種一致性形式稱為最終一致性。

最終一致的資料儲存性質是:在沒有更新的情況下,所有副本收斂為彼此相同的拷貝。它本質上只要求更新保證會傳播到所有副本;在只有少數行程能更新的前提下,寫寫衝突通常容易解決,因此最終一致性往往實作成本低廉。

問題:行動使用者換副本#

只要客戶端始終存取同一個副本,最終一致的儲存運作良好;在短時間內存取不同副本時問題就來了。考慮行動使用者存取分散式資料庫:

  • 使用者透明地連上某個副本操作(應用程式不知道自己用的是哪個副本),執行了幾筆更新後離線。
  • 稍後他在別的地點(或用別的裝置)再度連線,這次可能連到另一個副本。若先前的更新尚未傳播過來,使用者會看到不一致的行為——他預期看到自己剛才的修改,結果卻彷彿什麼都沒發生過。

這個問題的根源是使用者有時會操作到不同副本。客戶端為中心的一致性(client-centric consistency)可以緩解它:對單一客戶端保證其對資料儲存的存取一致性;至於不同客戶端的並行存取,則不提供任何保證。

源起:Bayou 與記法#

客戶端為中心的一致性模型源自 Bayou 的研究(Terry 等人,1994、1998)。Bayou 是為行動運算開發的資料庫系統,假設網路連線不可靠且效能多變——無線網路與跨大範圍的網路(如網際網路)皆屬此類。Bayou 區分了四種一致性模型。

背景設定:資料儲存實體分散於多台機器;行程存取時連上本地(或最近的)拷貝,所有讀寫都在該本地拷貝上進行,更新最終會傳播出去。為簡化,假設每個資料項有一個擁有者,是唯一可修改該項的行程,藉此排除寫寫衝突。記法如下:

  • xi[t]:資料項 x 在本地拷貝 Li 於時刻 t 的版本,它是初始化以來在 Li 上一系列寫入的結果,該寫入集合記為 WS(xi[t])。
  • 若 WS(xi[t1]) 中的操作稍後(時刻 t2)也已在本地拷貝 Lj 執行,記為 WS(xi[t1]; xj[t2])。
  • 順序或時間從上下文可知時,省略時間索引。

單調讀#

第一種模型是單調讀(monotonic reads)。資料儲存提供單調讀一致性的條件:

若行程讀到資料項 x 的某個值,該行程之後對 x 的任何讀取,都會回傳同一個值或更新的值。

換言之,行程一旦在時刻 t 看過 x 的某個值,之後就絕不會看到更舊的版本

  • 圖示說明:行程 P 先在 L1 讀 x,得到由 WS(x1) 產生的值;稍後在 L2 執行 R(x2)。要保證單調讀,WS(x1) 的所有操作必須在第二次讀取前已傳播到 L2,即 WS(x1; x2)。反之,若 L2 上只執行了 WS(x2)、無法保證其中包含 WS(x1),單調讀就不成立。

圖 7-12:單一行程 P 在同一資料儲存的兩個不同本地拷貝上執行的讀取操作。(a) 單調讀一致的資料儲存。(b) 不提供單調讀的資料儲存

延伸案例:分散式電子郵件信箱

信箱資料庫分散且複製於多台機器上,郵件可在任一地點插入信箱,更新以惰性(隨需)方式傳播。假設使用者在舊金山讀信(且讀信不改動信箱:不刪信、不歸檔、不標已讀),之後飛到紐約再開信箱——單調讀一致性保證:舊金山信箱裡有的訊息,在紐約打開時也一定在。

單調寫#

許多情況下,寫入必須以正確順序傳播到所有拷貝。**單調寫(monotonic writes)**一致性的條件:

同一行程對資料項 x 的寫入操作,必須在該行程對 x 的任何後續寫入之前完成

「完成」的意思是:後續寫入所在的拷貝,必須已反映前一次寫入的效果——不論前一次寫入發起於何處。若有需要,新寫入必須等舊寫入結束。

  • 這與資料為中心的 FIFO 一致性相似:同一行程的寫入在各處以正確順序執行;差別在於這裡只考慮單一行程的一致性,而非一組並行行程。
  • 若每次寫入都完全覆蓋 x 的值,其實不必先把拷貝帶到最新;但寫入常只修改資料項的部分狀態。例如軟體程式庫的更新常是替換一或多個函式而產生新版本:單調寫保證對某拷貝更新前,所有先前更新都會先施加,結果才會真正是包含所有歷史更新的最新版。
  • 圖示說明:P 在 L1 執行 W(x1),稍後在 L2 執行 W(x2)。要保證單調寫,W(x1) 必須先傳播到 L2 執行完,W(x2) 才進行。缺了這步傳播,就無法保證第二次寫入所在的拷貝已含前一次寫入的效果。

圖 7-13:單一行程 P 在同一資料儲存的兩個不同本地拷貝上執行的寫入操作。(a) 單調寫一致的資料儲存。(b) 不提供單調寫一致性的資料儲存

按定義,單調寫要求同一行程的寫入依發起順序執行。還有一種較弱形式:只要求寫入生效前所有先前寫入都已施加,但順序可以不同——適用於寫入操作可交換(commutative)、順序其實不重要的場合。細節見 Terry 等人(1994)。

讀己之寫#

與單調讀密切相關的是**讀己之寫(read-your-writes)**一致性:

行程對資料項 x 的寫入效果,一定會被同一行程後續對 x 的讀取看到。

亦即:寫入總是在同一行程的後續讀取之前完成,不論讀取發生在哪裡。

缺少讀己之寫一致性的日常體驗:

  • 更新網頁後看不到效果:編輯器把新版本存到 Web 伺服器共享的檔案系統,但瀏覽器(或伺服器)快取了舊拷貝,於是更新後仍顯示舊頁面。若編輯器與瀏覽器整合為單一程式,讀己之寫一致性可保證頁面更新時快取作廢、重新抓取並顯示新檔。
  • 改密碼後暫時登不進去:例如網路數位圖書館改密碼後可能要幾分鐘才生效,因為密碼由獨立伺服器管理,(加密後的)密碼要花時間傳播到構成圖書館的各伺服器。

圖示上,它與單調讀非常相似,只是決定一致性的是行程 P 的最後一次寫入而非最後一次讀取:P 執行 W(x1) 後在另一拷貝上讀取,讀己之寫保證 W(x1) 屬於 WS(x2),記為 WS(x1; x2);若 W(x1) 不在 WS(x2) 中,該模型即不成立。

圖 7-14:(a) 提供讀己之寫一致性的資料儲存。(b) 不提供讀己之寫一致性的資料儲存

寫追隨讀#

最後一種模型是寫追隨讀(writes-follow-reads):更新作為先前讀取的結果被傳播。條件如下:

行程對資料項 x 的寫入,若跟在該行程先前對 x 的讀取之後,則保證發生在「與所讀值相同或更新」的 x 值之上。

亦即:行程後續的寫入,會施加在「至少和該行程最近讀到的值一樣新」的拷貝上。

典型應用是網路新聞群組(newsgroup):保證使用者先看到原文、才看到回應(Terry 等人,1994)。使用者讀了文章 A 之後發表回應 B;寫追隨讀要求 B 只會被寫入「已經寫入 A」的拷貝。只讀不寫的使用者則不需要任何特定的客戶端為中心模型。

  • 圖示說明:行程在 L1 讀 x,導致該讀值的寫入操作也出現在 L2 的寫入集合中,該行程稍後才在 L2 寫入(L2 上的其他行程也看得到那些寫入);反之若無此保證,L2 上的寫入可能施加在與剛讀到的值不一致的拷貝上。

圖 7-15:(a) 寫追隨讀一致的資料儲存。(b) 不提供寫追隨讀一致性的資料儲存

這四種模型(單調讀、單調寫、讀己之寫、寫追隨讀)各自對「同一個客戶端跨副本操作」給出不同的最低保證。實作方式將在本章稍後的一致性協定部分討論。