COPS 性能和一致性之間的更貼合業(yè)務(wù)直覺、更易于編程的「中間地帶」
我們來深入探討一下 COPS 這篇經(jīng)典的分布式系統(tǒng)論文。在學(xué)習(xí) Spanner 這樣的強(qiáng)一致性系統(tǒng)和 Memcached 這樣的最終一致性系統(tǒng)之后,COPS 給我們展示了在性能和一致性之間,是否存在一個(gè)更貼合業(yè)務(wù)直覺、更易于編程的“中間地帶”。
設(shè)定場景:地理分布下的困境與抉擇
想象一下,我們正在構(gòu)建一個(gè)全球性的社交網(wǎng)絡(luò)或電商平臺(tái)。為了讓世界各地的用戶都能獲得飛快的訪問體驗(yàn),我們會(huì)在全球部署多個(gè)數(shù)據(jù)中心,比如在北美、歐洲和亞洲各部署一個(gè)。理想情況下,每個(gè)數(shù)據(jù)中心都擁有所有數(shù)據(jù)的完整副本,這樣用戶的讀請(qǐng)求就可以在本地?cái)?shù)據(jù)中心完成,延遲極低。
但問題來了: 寫操作怎么辦?
我們之前學(xué)過兩種主流方案:
- 強(qiáng)一致性方案(如 Goggle/Spanner) :用戶的寫操作需要通過 Paxos 或 Raft 這樣的共識(shí)算法,在多個(gè)數(shù)據(jù)中心的副本之間達(dá)成一致。這意味著,一次寫操作可能需要等待遠(yuǎn)在地球另一邊的數(shù)據(jù)中心的確認(rèn),延遲較高。
- 主從方案(如 Facebook/Memcache) :所有寫操作都必須發(fā)往一個(gè)指定的“主”數(shù)據(jù)中心。其他數(shù)據(jù)中心都是只讀副本,異步地從主數(shù)據(jù)中心同步數(shù)據(jù)。這同樣意味著非主數(shù)據(jù)中心的用戶無法享受低延遲的本地寫入。
這兩種方案都犧牲了某些東西。我們能不能設(shè)計(jì)一個(gè)系統(tǒng),它既能讓 任何數(shù)據(jù)中心都能處理寫操作 ,又能讓這些寫操作 在本地立即完成 ,無需等待其他數(shù)據(jù)中心?
這正是 COPS 想要實(shí)現(xiàn)的目標(biāo)。為了達(dá)到這個(gè)高性能目標(biāo),我們?cè)敢庠谝恢滦陨献鲆恍┩讌f(xié),但我們希望找到一種比“最終一致性”更強(qiáng)大、對(duì)程序員更友好的模型。
最終一致性的“陷阱”
讓我們先構(gòu)思一個(gè)最簡單的方案,稱之為“稻草人”設(shè)計(jì)一號(hào):每個(gè)數(shù)據(jù)中心都分片存儲(chǔ)著完整數(shù)據(jù)。用戶的讀寫請(qǐng)求都只發(fā)往本地?cái)?shù)據(jù)中心的分片,本地寫入成功后,再異步地把這次寫入“推送”給其他數(shù)據(jù)中心的對(duì)應(yīng)分片。
這個(gè)設(shè)計(jì)性能極好,但它屬于 最終一致性(Eventual Consistency) 。這意味著系統(tǒng)只保證,如果大家停止寫入,所有數(shù)據(jù)中心最終會(huì)同步到一樣的數(shù)據(jù)。但在寫入過程中,它可能會(huì)出現(xiàn)一些反直覺的“異常”現(xiàn)象(anomalies)。
舉個(gè)經(jīng)典的例子:
- 愛麗絲在她的社交網(wǎng)絡(luò)主頁上傳了一張美麗的風(fēng)景照。
- 然后,她把這張照片的鏈接添加到了自己的“公開相冊(cè)”里。
這個(gè)過程涉及兩個(gè)寫操作:put(photo_key, photo_data)
和 put(album_key, photo_link)
。在愛麗絲看來,這兩個(gè)操作是先后發(fā)生的。
現(xiàn)在,愛麗絲的朋友鮑勃刷新頁面,看到了“公開相冊(cè)”里新增的鏈接。他滿心歡喜地點(diǎn)進(jìn)去,結(jié)果卻看到了什么?一個(gè)“圖片不存在”的錯(cuò)誤!
愛麗絲 (數(shù)據(jù)中心A):
操作1: put(photo_key, data)
操作2: put(album_key, link_to_photo)
|
|--- (異步復(fù)制) --->
|
鮑勃 (數(shù)據(jù)中心B):
get(album_key) -> 成功, 拿到了 link_to_photo
get(photo_key) -> 失敗!
為什么會(huì)這樣?因?yàn)?/span>put(album)
的更新數(shù)據(jù)包可能比 put(photo)
的數(shù)據(jù)包更早地從數(shù)據(jù)中心 A 到達(dá)了數(shù)據(jù)中心 B。對(duì)于應(yīng)用開發(fā)者來說,這種不確定性非常頭疼,需要寫很多復(fù)雜的防御性代碼來處理“鏈接存在但內(nèi)容不存在”的窘境。
撥亂反正:因果一致性
為了解決上述問題,我們需要一個(gè)比最終一致性更強(qiáng)的一致性模型。COPS 提出的核心概念是 因果+一致性(Causal+ Consistency) 。
它的核心思想很簡單:如果操作 A 是操作 B 的“原因”,那么在系統(tǒng)中的任何觀察者看來,A 都必須發(fā)生在 B 之前。
什么是“因果關(guān)系”?論文定義了三條規(guī)則來確定操作之間的潛在因果關(guān)系
- 執(zhí)行線程(Execution Thread) :在同一個(gè)客戶端的執(zhí)行流中,先發(fā)生的操作是后發(fā)生操作的原因。比如,愛麗絲先
put(photo)
,再put(album)
,那么put(photo)
就因果上先于put(album)
。 - 讀寫關(guān)系(Gets From) :如果一個(gè)
get
操作讀到了某個(gè)put
操作寫入的值,那么這個(gè)put
操作就是該get
操作的原因。 - 傳遞性(Transitivity) :如果 A 是 B 的原因,B 是 C 的原因,那么 A 就是 C 的原因。
有了這個(gè)模型,put(photo)
和 put(album)
就有了明確的因果關(guān)系。因果一致性保證了,如果鮑勃能讀到 album
的更新,他一定也能讀到 photo
的內(nèi)容。
“+”號(hào)的含義:收斂的沖突處理
因果一致性只對(duì)有因果關(guān)系的操作進(jìn)行排序,對(duì)于并發(fā)(concurrent)的操作,它不作任何保證。例如,愛麗絲和查理同時(shí)修改同一個(gè)活動(dòng)的開始時(shí)間,一個(gè)改成晚上 8 點(diǎn),一個(gè)改成晚上 10 點(diǎn)。這兩個(gè) put
操作是并發(fā)的。
單純的因果一致性可能會(huì)導(dǎo)致一個(gè)嚴(yán)重問題: 數(shù)據(jù)副本永久分裂 。數(shù)據(jù)中心 A 可能永遠(yuǎn)顯示 8 點(diǎn),而數(shù)據(jù)中心 B 永遠(yuǎn)顯示 10 點(diǎn)。
Causal+ 中的“+”號(hào),代表了 收斂的沖突處理(Convergent Conflict Handling) 。它要求所有副本必須用同樣的方式處理并發(fā)沖突,最終達(dá)到一致的狀態(tài)。最常見的策略是 最后寫入者獲勝(last-writer-wins) 。
在生產(chǎn)實(shí)踐中,這是一個(gè)很重要但也很棘手的問題。對(duì)于計(jì)數(shù)器遞增、購物車加商品這類場景,“最后寫入者獲勝”顯然是不夠的。更高級(jí)的系統(tǒng)可能會(huì)采用:
- CRDTs (Conflict-free Replicated Data Types) :為特定數(shù)據(jù)類型(如計(jì)數(shù)器、集合)設(shè)計(jì)的、天然能夠收斂的數(shù)據(jù)結(jié)構(gòu)。
- 應(yīng)用層合并邏輯 :系統(tǒng)檢測到?jīng)_突后,交給應(yīng)用程序自己去定義如何合并,比如購物車的合并邏輯就是取兩個(gè)集合的并集。
COPS 默認(rèn)使用 last-writer-wins,并通過 蘭伯特時(shí)鐘(Lamport Clocks) 來為每個(gè)寫操作生成一個(gè)全局有序的版本號(hào),從而確定誰是“最后的寫入者”。
蘭伯特時(shí)鐘(Lamport Clocks)與最后寫入者獲勝(LWW)
“最后寫入者獲勝”(Last-Writer-Wins, LWW)是最簡單直接的沖突解決方法。當(dāng)兩個(gè)并發(fā)的寫操作修改同一個(gè) key 時(shí),系統(tǒng)選擇一個(gè)“勝利者”覆蓋另一個(gè)。但問題是,在沒有全局統(tǒng)一時(shí)鐘的分布式系統(tǒng)中,如何定義“最后”?
這就是 蘭伯特時(shí)鐘(Lamport Clocks) 發(fā)揮作用的地方。它不是一個(gè)物理時(shí)鐘,而是一個(gè)邏輯計(jì)數(shù)器,用來為分布式系統(tǒng)中的事件確定一個(gè)偏序關(guān)系。
- 工作原理 :每個(gè)節(jié)點(diǎn)都維護(hù)一個(gè)本地的邏輯時(shí)鐘(一個(gè)整數(shù))。
每當(dāng)節(jié)點(diǎn)內(nèi)部發(fā)生一個(gè)事件(比如處理一個(gè)寫請(qǐng)求),它就將本地時(shí)鐘加一。
當(dāng)節(jié)點(diǎn)發(fā)送消息時(shí),會(huì)附上自己當(dāng)前的邏輯時(shí)鐘。
當(dāng)節(jié)點(diǎn)接收到消息時(shí),它會(huì)將自己的本地時(shí)鐘更新為 max(本地時(shí)鐘, 接收到的時(shí)鐘) + 1
。
通過這個(gè)機(jī)制,如果事件 A 因果上先于事件 B,那么 A 的蘭伯特時(shí)間戳一定小于 B 的時(shí)間戳。
- 在 COPS 中的應(yīng)用 :COPS 為每個(gè)寫操作分配一個(gè)版本號(hào),這個(gè)版本號(hào)就是基于蘭伯特時(shí)鐘生成的。為了處理時(shí)間戳完全相同(雖然罕見)的情況,COPS 會(huì)在時(shí)間戳的低位附加一個(gè)唯一的節(jié)點(diǎn) ID 來打破平局。當(dāng)兩個(gè)對(duì)同一 key 的并發(fā)
put
操作到達(dá)一個(gè)副本時(shí),該副本只需比較它們的版本號(hào),保留版本號(hào)較大的那個(gè),丟棄較小的那個(gè)。由于所有副本都遵循同樣的規(guī)則,它們最終都會(huì)保留同一個(gè)“勝利”的版本,從而實(shí)現(xiàn)收斂。
例子 : 假設(shè)數(shù)據(jù)中心 A 和 B 的節(jié)點(diǎn)同時(shí)收到對(duì) key event_time
的寫請(qǐng)求。
- 節(jié)點(diǎn) A 的時(shí)鐘是 100,收到
put(event_time, "8pm")
。它將時(shí)鐘更新為 101,并標(biāo)記這次寫入的版本為101-A
。 - 節(jié)點(diǎn) B 的時(shí)鐘也是 100,收到
put(event_time, "10pm")
。它將時(shí)鐘更新為 101,標(biāo)記版本為101-B
。 - 當(dāng)節(jié)點(diǎn) A 收到來自 B 的
101-B
更新時(shí),它比較101-A
和101-B
。假設(shè)節(jié)點(diǎn) ID B > A,那么101-B
獲勝,event_time
被更新為 "10pm"。 - 同樣,當(dāng)節(jié)點(diǎn) B 收到來自 A 的
101-A
更新時(shí),它也會(huì)選擇101-B
。最終,所有副本都收斂到 "10pm"。
應(yīng)用層合并邏輯
LWW 雖然簡單,但對(duì)于很多業(yè)務(wù)場景來說過于粗暴。例如,合并兩個(gè)用戶的購物車,我們希望得到的是兩個(gè)購物車商品的并集,而不是一個(gè)覆蓋另一個(gè)。
應(yīng)用層合并邏輯 就是將沖突的決定權(quán)交給應(yīng)用程序。
- 工作原理 :存儲(chǔ)系統(tǒng)檢測到對(duì)同一個(gè) key 的并發(fā)寫入后,它不會(huì)擅自決定誰贏誰輸。相反,它會(huì)保留所有沖突的版本,或者將它們標(biāo)記為“沖突狀態(tài)”。當(dāng)客戶端下次讀取這個(gè) key 時(shí),系統(tǒng)會(huì)返回所有沖突的版本,并告知客戶端“這里有沖突,請(qǐng)解決”。應(yīng)用程序根據(jù)自己的業(yè)務(wù)邏輯來決定如何合并這些值,然后將合并后的結(jié)果寫回系統(tǒng)。
- 例子:購物車
- 愛麗絲在數(shù)據(jù)中心 A 將“牛奶”加入購物車。購物車狀態(tài)為
{"牛奶"}
。 - 鮑勃(使用同一賬號(hào))在數(shù)據(jù)中心 B 并發(fā)地將“面包”加入購物車。購物車狀態(tài)為
{"面包"}
。 - 當(dāng)這兩個(gè)更新在某個(gè)副本上相遇時(shí),系統(tǒng)檢測到?jīng)_突。
- 下次
get(cart)
時(shí),系統(tǒng)返回兩個(gè)版本:[{"牛奶"}, {"面包"}]
。 - 客戶端(或服務(wù)器端的業(yè)務(wù)邏輯)看到這個(gè)結(jié)果后,執(zhí)行合并操作:取兩個(gè)集合的并集,得到
{"牛奶", "面包"}
。 - 最后,將這個(gè)合并后的結(jié)果
put(cart, {"牛奶", "面包"})
寫回系統(tǒng),解決沖突。
Bayou 和 DynamoDB 等系統(tǒng)都支持這種自定義沖突解決策略。COPS 也可以被配置為顯式地檢測沖突并調(diào)用應(yīng)用定義好的處理函數(shù)。
CRDTs (Conflict-free Replicated Data Types)
CRDTs 是一種更優(yōu)雅的解決方案。它們是一類特殊的數(shù)據(jù)結(jié)構(gòu),其數(shù)學(xué)性質(zhì)保證了無論并發(fā)操作以何種順序應(yīng)用,所有副本最終都會(huì)收斂到相同的狀態(tài),并且這個(gè)過程 不需要額外的協(xié)調(diào)或復(fù)雜的合并邏輯 。
- 工作原理 : CRDTs 的操作被設(shè)計(jì)為天然滿足交換律、結(jié)合律和冪等性。這意味著操作的順序和重復(fù)執(zhí)行都不會(huì)影響最終結(jié)果。
- 例子 1:計(jì)數(shù)器 (PN-Counter) :一個(gè)支持增和減的計(jì)數(shù)器。它內(nèi)部由兩個(gè)普通計(jì)數(shù)器組成:一個(gè)只增不減的 P-Counter (Increments) 和一個(gè)只增不減的 N-Counter (Decrements)。
要增加總數(shù),就在 P-Counter 上加一。
要減少總數(shù),就在 N-Counter 上加一。
總值 = P-Counter 的值 - N-Counter 的值。
如果數(shù)據(jù)中心 A 執(zhí)行了兩次 increment
,它的狀態(tài)是 (P:2, N:0)
。數(shù)據(jù)中心 B 執(zhí)行了一次 decrement
,狀態(tài)是 (P:0, N:1)
。當(dāng)它們的狀態(tài)同步后,大家都會(huì)合并成 (P:2, N:1)
,最終總值都是 1,結(jié)果永遠(yuǎn)正確。
- 例子 2:集合 (G-Set, Grow-Only Set)
一個(gè)只能添加元素的集合。它的合并操作就是取兩個(gè)集合的并集。無論并發(fā)地添加了多少元素,最終所有副本的并集都是一樣的。
CRDTs 非常適合實(shí)現(xiàn)點(diǎn)贊數(shù)、在線用戶統(tǒng)計(jì)、集合標(biāo)簽等功能,它將沖突解決的復(fù)雜性隱藏在了數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)之中,極大地簡化了應(yīng)用開發(fā)。
COPS 的核心魔法:顯式依賴跟蹤
那么,COPS 是如何在不引入全局同步點(diǎn)(比如單點(diǎn)日志服務(wù)器)的情況下,實(shí)現(xiàn)可擴(kuò)展的因果一致性的呢?答案是: 顯式依賴跟蹤 。
它的工作流程是這樣的:
- 客戶端上下文(Client Context) :COPS 的客戶端庫會(huì)為每個(gè)執(zhí)行線程維護(hù)一個(gè)“上下文”。這個(gè)上下文記錄了該線程“看到”的所有鍵值對(duì)的最新版本,也就是它的因果歷史。
- 寫入時(shí)攜帶依賴 :當(dāng)客戶端執(zhí)行一個(gè)
put
操作時(shí),比如put(album_key, ...)
,客戶端庫會(huì)自動(dòng)將當(dāng)前上下文中的所有依賴項(xiàng),附加到這個(gè)寫請(qǐng)求上,一起發(fā)送給本地?cái)?shù)據(jù)中心的存儲(chǔ)節(jié)點(diǎn)。在我們的例子中,put(album)
請(qǐng)求會(huì)明確地告訴系統(tǒng):“我依賴于photo_key
的某個(gè)版本”。 - 本地立即寫入,遠(yuǎn)程延遲驗(yàn)證 :
- 這個(gè)
put(album)
操作在 本地?cái)?shù)據(jù)中心 會(huì) 立即執(zhí)行 并返回,保證了低延遲。 - 然后,本地存儲(chǔ)節(jié)點(diǎn)會(huì)將這個(gè)寫入(連同它的依賴列表)異步地復(fù)制到其他數(shù)據(jù)中心。
- 當(dāng)一個(gè) 遠(yuǎn)程數(shù)據(jù)中心 的節(jié)點(diǎn)收到這個(gè)
put(album)
請(qǐng)求時(shí),它不會(huì)立即寫入。它會(huì)先進(jìn)行一次 依賴檢查(dependency check) 。它會(huì)檢查該請(qǐng)求的所有依賴項(xiàng)(比如photo_key
)是否已經(jīng)在本地滿足了。 - 只有當(dāng)所有依賴項(xiàng)都已在本地存在時(shí),這個(gè)
put(album)
操作才會(huì)被應(yīng)用。如果依賴項(xiàng)還沒到,它就會(huì) 等待 。
數(shù)據(jù)中心A (寫入方)
Client: put(photo, v1) -> Context: {photo:v1}
Client: put(album, v2, deps:{photo:v1}) -> Context: {album:v2}
|
+--- 異步復(fù)制 put(photo, v1) ---> 數(shù)據(jù)中心B
|
+--- 異步復(fù)制 put(album, v2, deps:{photo:v1}) ---> 數(shù)據(jù)中心B
數(shù)據(jù)中心B (接收方)
NodeB: 收到 put(album, v2, deps:{photo:v1})
NodeB: 檢查依賴 {photo:v1}
- 如果 photo v1 已存在 -> 直接寫入 album v2
- 如果 photo v1 不存在 -> 等待... 直到 photo v1 到達(dá)并寫入
這個(gè)設(shè)計(jì)的精妙之處在于,它把保證因果序的責(zé)任從“寫入方”的同步等待,轉(zhuǎn)移到了“接收方”的異步驗(yàn)證。這使得系統(tǒng)可以水平擴(kuò)展,因?yàn)槊總€(gè)分片可以獨(dú)立地復(fù)制和驗(yàn)證自己的數(shù)據(jù),避免了中心化的瓶頸。
更進(jìn)一步:多 key 讀取與 Get 事務(wù)
因果一致性解決了單向依賴的問題,但對(duì)于需要同時(shí)讀取多個(gè) key 的場景,我們可能還需要更強(qiáng)的保證。
論文中舉了 訪問控制列表(Access Control List, ACL) 的例子:
愛麗絲想修改相冊(cè),她先把相冊(cè)設(shè)為私密,然后往相冊(cè)里添加了一張私密照片。
- 初始狀態(tài)(系統(tǒng)空白或之前已有一些公開照片,無關(guān)本例)。
- 操作 A:上傳私密照片
put(album, add photo?)
系統(tǒng)先讀當(dāng)前 ACL(此時(shí)是 v1=FRIENDS_ONLY
),
在新版本 Album@v1
里寫入條目 photo? (depends_on ACL=v1)
。
- 操作 B:切換為公開
put(ACL, PUBLIC)
- 生成新 ACL 版本
ACL@v2 = PUBLIC
,但 不 去改動(dòng)已有的Album@v1
內(nèi)容。 - 此時(shí),系統(tǒng)全局已有:
ACL@v2 = PUBLIC
(部分副本已可見)Album@v1
包含一條依賴ACL=v1(FRIENDS_ONLY)
的私密照片photo?
。
另一個(gè)用戶伊芙想查看這個(gè)相冊(cè)。她的代碼邏輯是:先檢查 ACL,如果公開,就去讀取相冊(cè)內(nèi)容。
// 伊芙的客戶端代碼
const acl = get("ACL"); // 讀到 ACL@v2 → "PUBLIC"
if (acl === "PUBLIC") {
const album = get("album"); // 可能從某副本讀到舊版 Album@v1
display(album); // 展示全部條目,包括 photo?
}
時(shí)序穿越
get("ACL")
讀到最新的ACL@v2 = PUBLIC
,Eve 認(rèn)為相冊(cè)已對(duì)所有人公開。- 隨后
get("album")
卻命中尚未同步到ACL@v2
的某個(gè)副本,返回舊的Album@v1
,其中含有原本“僅好友可見”的photo?
。
結(jié)果 :Eve 最終在“公開”條件下看到了本應(yīng)“私密”的照片,造成安全泄露。
為了解決這個(gè)問題,COPS 的擴(kuò)展版本 COPS-GT 引入了 get 事務(wù)(get transaction / get_trans) 。它能保證一次性返回多個(gè) key 的一個(gè)因果一致的快照。
它的實(shí)現(xiàn)是一個(gè)非常聰明的 兩階段非阻塞算法 :
- 第一輪:并行
get
:客戶端庫向本地?cái)?shù)據(jù)中心并行地為所有請(qǐng)求的 key(比如ACL
和album
)發(fā)出get_by_version
請(qǐng)求。這些請(qǐng)求會(huì)返回每個(gè) key 的最新值、版本號(hào)以及 完整的依賴列表 。 - 檢查與第二輪
get
(如果需要) :
- 客戶端庫拿到第一輪的所有結(jié)果后,開始檢查一致性。例如,它發(fā)現(xiàn)返回的
album
版本依賴于一個(gè)比它在第一輪中拿到的ACL
版本 更新的版本 。 - 如果發(fā)現(xiàn)這種不一致,它會(huì)發(fā)起第二輪
get_by_version
請(qǐng)求。但這次,它不是去獲取最新版本,而是去獲取在第一輪依賴中看到的、能滿足一致性需求的 那個(gè)特定版本 。
這個(gè)算法為什么最多兩輪就能搞定?因?yàn)橐蚬?一致性保證了,如果一個(gè)依賴(比如 ACL_v2
)被另一個(gè)值(album_v2
)所依賴,那么 ACL_v2
一定 已經(jīng)存在于本地?cái)?shù)據(jù)中心了。所以第二輪的 get
絕不會(huì)被阻塞等待,可以立即返回。
現(xiàn)實(shí)世界的考量:垃圾回收與系統(tǒng)開銷
COPS 的設(shè)計(jì)聽起來很美好,但在工程實(shí)踐中,魔鬼藏在細(xì)節(jié)里。
- 垃圾回收 (Garbage Collection) :依賴列表不能無限增長,否則客戶端上下文和存儲(chǔ)服務(wù)端的元數(shù)據(jù)都會(huì)爆炸。COPS 設(shè)計(jì)了一套垃圾回收機(jī)制。比如,當(dāng)一個(gè)寫操作已經(jīng)被所有數(shù)據(jù)中心確認(rèn)后,它就可以被標(biāo)記為“無需再依賴”,客戶端和服務(wù)器就可以清理掉相關(guān)的依賴信息。系統(tǒng)還會(huì)計(jì)算一個(gè) 全局檢查點(diǎn)時(shí)間(global checkpoint time) ,所有早于這個(gè)時(shí)間戳的依賴都可以被安全地清理掉。
- 性能與開銷 :
COPS 本身相比于純粹的最終一致性系統(tǒng),增加了跟蹤和檢查依賴的開銷,但評(píng)估顯示其性能依然很高,且擴(kuò)展性良好。
COPS-GT 的開銷更大,因?yàn)樗枰鎯?chǔ)每個(gè) key 的多個(gè)歷史版本和完整的依賴列表,以便響應(yīng) get_trans
。但在讀多寫少的典型 Web 負(fù)載下,它的性能可以接近 COPS 。
總結(jié)與反思
COPS 是一篇里程碑式的論文,它清晰地闡述了在強(qiáng)一致性和最終一致性之間,存在一個(gè)既實(shí)用又高效的中間地帶——因果一致性。
它的核心貢獻(xiàn)是:
- 定義了 Causal+ Consistency :一個(gè)比最終一致性更強(qiáng)、更符合直覺,但又比強(qiáng)一致性性能更高的模型。
- 提出了一種可擴(kuò)展的實(shí)現(xiàn) :通過客戶端跟蹤上下文和遠(yuǎn)程數(shù)據(jù)中心進(jìn)行依賴檢查,避免了單點(diǎn)瓶頸,實(shí)現(xiàn)了系統(tǒng)的水平擴(kuò)展。
- 設(shè)計(jì)了非阻塞的 Get 事務(wù):通過巧妙的兩階段算法,實(shí)現(xiàn)了多 key 讀取的一致性快照,且延遲可控。
啟發(fā):
- 理解權(quán)衡 :沒有銀彈。Spanner、Dynamo、COPS 代表了在一致性、可用性、分區(qū)容忍性和性能之間不同的設(shè)計(jì)哲學(xué)和權(quán)衡點(diǎn)。理解這些權(quán)衡是分布式系統(tǒng)設(shè)計(jì)的核心。
- 因果性的局限 :COPS 只能看到通過其 API 產(chǎn)生的因果關(guān)系。如果我
put
之后,用微信告訴你去看,這個(gè)“外部”的因果關(guān)系系統(tǒng)是不知道的。這是它和 Spanner 這種能保證外部一致性(或線性一致性)的系統(tǒng)的根本區(qū)別。 - 沖突處理是關(guān)鍵 :在允許任意節(jié)點(diǎn)寫入的系統(tǒng)中,如何優(yōu)雅地處理寫沖突,是一個(gè)繞不開的難題。last-writer-wins 是最簡單的,但往往不是最好的。
盡管因果一致性在學(xué)術(shù)界備受關(guān)注,但在工業(yè)界的大規(guī)模應(yīng)用還比較少見。這可能是因?yàn)樗鼘?duì)客戶端的侵入性較強(qiáng)(需要管理上下文),且對(duì)外部因果性的無能為力使其在某些場景下仍然不夠用。然而,COPS 的設(shè)計(jì)思想,尤其是其在去中心化系統(tǒng)中維護(hù)數(shù)據(jù)依賴關(guān)系的方法,至今仍對(duì)我們?cè)O(shè)計(jì)復(fù)雜的分布式系統(tǒng)有著深刻的啟發(fā)。
對(duì)比 Spanner / Memcache / COPS
維度 | Spanner | Facebook/Memcache (主從模型) | COPS |
一致性模型 | 線性一致性 (Linearizability) :提供全局、實(shí)時(shí)的操作順序,如同單機(jī)系統(tǒng)。 | 主從復(fù)制下的最終一致性 :寫操作在主庫是原子的,但讀操作可能從緩存或只讀副本讀到舊數(shù)據(jù)。 | 因果+一致性 (Causal+) :保證因果相關(guān)的操作順序,但并發(fā)操作無序。 |
寫操作路徑 | 全局同步 :寫操作需要在多個(gè)數(shù)據(jù)中心的 Paxos 副本中達(dá)成共識(shí),通常涉及兩階段提交。 | 主數(shù)據(jù)中心寫入 :所有寫操作必須路由到主數(shù)據(jù)中心的數(shù)據(jù)庫(如 MySQL),延遲較高。 | 本地寫入 :寫操作在任何數(shù)據(jù)中心都能立即完成,然后異步復(fù)制到其他地方。 |
寫延遲 | 高 :至少是一個(gè)廣域網(wǎng)的往返時(shí)延(RTT)。 | 高 (對(duì)于非主數(shù)據(jù)中心的用戶):需要一個(gè)到主數(shù)據(jù)中心的 RTT。 | 極低 :本地?cái)?shù)據(jù)中心內(nèi)的操作延遲,通常在毫秒級(jí)。 |
讀操作路徑 | 本地快照讀 :可以讀取一個(gè)時(shí)間戳快照,通常很快,但若需最新數(shù)據(jù)則可能變慢。 | 本地緩存優(yōu)先 :優(yōu)先從本地 Memcache 讀取,速度極快(百萬 QPS 級(jí))。緩存未命中則回源到只讀數(shù)據(jù)庫。 | 本地讀取 :所有讀操作都在本地?cái)?shù)據(jù)中心完成。 |
讀延遲 | 低 (對(duì)于快照讀)。 | 極低 (緩存命中時(shí))。 | 極低 。 |
可用性 | 高 :只要大多數(shù)副本存活,系統(tǒng)就能讀寫。但廣域網(wǎng)分區(qū)可能影響寫入。 | 寫可用性受限于主數(shù)據(jù)中心 :主數(shù)據(jù)中心宕機(jī),則無法寫入。讀可用性很高。 | 極高 :即使數(shù)據(jù)中心之間完全隔離,每個(gè)數(shù)據(jù)中心依然能獨(dú)立處理本地的讀寫請(qǐng)求。 |
可擴(kuò)展性 | 良好 :可以通過增加分片來水平擴(kuò)展。 | 良好 :緩存層 (Memcache) 和數(shù)據(jù)庫層都可以通過分片來擴(kuò)展。 | 良好 :通過將 key 分區(qū)到不同節(jié)點(diǎn),避免了單點(diǎn)瓶頸,可以水平擴(kuò)展。 |
開發(fā)者體驗(yàn) | 簡單直觀 :線性一致性最符合程序員的單機(jī)編程直覺,事務(wù)功能強(qiáng)大。 | 復(fù)雜 :需要處理緩存失效、讀到舊數(shù)據(jù)等問題。跨 key 操作通常需要應(yīng)用層自己保證。 | 中等 :比最終一致性簡單,因?yàn)橐蚬P(guān)系得到了保證。但需要理解因果上下文,且 Get 事務(wù)比標(biāo)準(zhǔn)事務(wù)更微妙。 |
典型用例 | 金融系統(tǒng)、關(guān)鍵元數(shù)據(jù)存儲(chǔ)、需要強(qiáng)一致性保證的全球性應(yīng)用。 | 讀取密集型的社交網(wǎng)絡(luò) Feed、新聞網(wǎng)站、內(nèi)容分發(fā)網(wǎng)絡(luò)。 | 協(xié)作編輯工具、有狀態(tài)的社交互動(dòng)(如評(píng)論回復(fù))、需要保證操作邏輯順序但對(duì)延遲非常敏感的場景。 |