PayPal將CRDT資料型別落實到生產環境

banq發表於2018-12-23

Dmitry Martyanov談到PayPal如何開發處理一致性問題的分散式系統,並分享他在開發基於最終一致資料儲存的系統中學到的經驗教訓。該解決方案利用無衝突,複製的資料型別CRDT和因果關係跟蹤,實現多主資料中心資料庫部署中關鍵資料的強大最終一致性。

我們都知道共享可變狀態是軟體中大多數問題的根本原因。我們儘量減少......它增加了意外的複雜性,它引入了副作用。良好的設計通常可以最小化共享的可變狀態,實現可變狀態共享的解決方案是互斥鎖或鎖。其目的是限制對資料點的訪問,以保證資料修改的一致性。鎖是一個很好的解決方案。他們工作得很快。它們非常可靠,隨著時間的推移得到了驗證。

當您在可變狀態和儲存資料庫領域內考慮這種概念時,其實就是透過事務處理來實現。事務保證了我們的操作原子性,一致性,隔離性和永續性ACID。當在同一個資料中心內並且沒有跨全域性訊息傳播時,ACID能很好地工作。

但是,當您遷移到地理上的分散式環境時,事務再也無法正常工作,因為資料中心之間的物理距離對事務時間的影響很大。如果您擁有高負載流量,則會開始發現事務失敗,事務重疊,這將成為系統可伸縮性的瓶頸。這正是CAP定理告訴我們的一致性和可用性之間的權衡。

最終一致性
犧牲一致性和增加可用性,對於大多數情況來說,它的效果非常好,但是如果你真的有很高的流量負載並且你的資料修改比複製更頻繁,那麼這個系統工作得更糟,因為不會出現事務故障和事務重疊,但你會看到資料丟失,這通常是無法控制的,而且非常困難追蹤。

這是因為分散式系統包含多個元件。它們中的每一個都有一個本地資料的的複製副本節點,稱為複製副本節點伺服器。元件本身是可靠的。它們保證了它內部的強一致性,並且它們透過非同步訊息傳遞相互通訊,實現資料複製。但這種通訊卻並不那麼可靠。您不能確保通訊所需的時間,也不能確保成功建立通訊連線。

如果在複製副本節點A中成功執行了一些put操作,並且在相同這個資料節點A實現後續讀取操作,你會看到put操作的結果。但是,如果從這個節點的另一個複製副本節點執行讀取操作,則可能不會得到預期結果,具體取決於這兩個節點之間的複製是否已經發生。這是一個非常重要的問題,因為從字面上看,這意味著每次讀取資料點時,都不知道它是否真實。你沒有辦法核實它。當然,如果將此資料值轉換為其他值,系統可能會開始出現分歧。所以我們開始考慮如何建立一個最終不會分歧的系統。

具有密切關係的方法
第一類方法是基於密切關係的方法(高聚合)。依賴的事實是,在同一個節點中的資料肯定具有很強的一致性。因此為資料的訪問模式引入一些約束,它無法同時與不同的副本節點進行通訊。
最受歡迎的密切關係解決方案是票務會話。在客戶會話中,您可以在同一個資料中心處理所有請求,並且您有閱讀許可權的情況。
基於密切關係方法存在的問題是,您的資料必須以這種方式切片Sharding。如果您的客戶端修改了他的個人資料並且沒有其他人接觸到這些資料,那麼它的效果非常好。但是當您有多個操作者在相同的資料點上進行互動時,定義這些約束以及如何在此處定義關聯性就不是一項簡單的任務。

基於協調器的方法
第二類方法是基於協調器的方法。這些方法依賴於一些負責管理環境衝突的協調器的存在。不一定意味著每個客戶端都與協調器互動,但協調器可以處理一些關鍵元件。我們非常認真地將協調器視為一種解決方案,但是當我們開始向我們的設計引入更多細節時,我們認識到處理協調生命週期的所有可能性非常複雜,因為他可能會失敗並且您需要在另一個生命週期中恢復它區。並且存在一些中間狀態,您需要以某種方式處理您的請求,或類似的事情將它們擱置,這就是我們放棄協調器的原因。

基於共識的方法
第三類方法是基於共識的方法。這個想法是複製副本節點之間彼此通訊,並且他們有一個協議來達成關於資料轉換的一些協議。這實際上是分散式系統中非常基本的主題。並且有一些非常可靠的產品建立在Google Spanner或FoundationDB等共識方法之上。但是,讓我告訴你為什麼它對我們不起作用。

如果您從事產品開發,您的服務堆疊看起來像處理客戶端請求的多個層。頂層是合規性的業務邏輯。例如,驗證需要哪些型別的文件。然後你有一個領域平臺,它引入了一些資料訪問函式,實體物件的描述和流程控制機制。而服務的最後一層就像服務基礎架構,它負責下游依賴項發現,不同的路由和平衡,以及重試請求等故障轉移策略。同時你有資料庫管理你的資料。與此同時,有大量與資料儲存配置,部署方式,故障轉移策略相關的工作。

當您在產品開發團隊中時,您通常擁有這些部件。作為服務提供的服務基礎結構,通常在需要呼叫某些下游依賴項時,您不一定知道如何組織此下游依賴項的池。在資料儲存中也是如此; 您不知道有多少實際硬體節點用於支援您的資料庫。

但是如果你想達成共識,你需要處理這些部分,因為你需要知道你的類堆疊的配置,有多少複製副本節點伺服器,它們之間如何相互互動,如果一個副本節點以某種方式失敗會做出什麼反應等等。

通常,在一些共享層中實現共識。對於大型企業來說,這很難做到,因為這個責任和它的實施將落在多個團隊之間,您需要將其注入到您的服務基礎架構路線圖中,,該路線圖有自己的計劃以實現他們想要的工作。

如果您擁有端到端的資料庫,那麼您才擁有與您的資料一起使用的每個流程,這樣共識才是一個很好的解決方案。這就是FoundationDB團隊和Google Spanner團隊所處的位置。

無衝突的複製資料型別
所以在這個時刻,我們開始尋找一個能夠存在於產品領域的解決方案。我們開始研究無衝突的複製資料型別。所以在那個時刻,我已經在Riak有過一些CRDT經驗。我還檢視了Akka分散式資料中的CRDT。

有兩大類CRDT。第一類是可交換的CRDT,當副本透過傳遞“更新操作”實現相互通訊時; 這種“更新操作”必須是可交換和關聯的,因此排序無關緊要。您的環境必須提供一次交付。交換式CRDT的一個非常粗略的例子是區塊鏈網路中的支付。因此,加減操作是可交換和關聯的,而塊鏈魔術可以幫助您只接收一次事件。

第二類是收斂CRDT,當副本透過傳送整個狀態相互通訊時,當另一個副本接收狀態時,它將狀態與本地狀態合併。此合併操作也必須是可交換和關聯的,但它也必須是冪等的,並且環境需要至少支援一次交付,這更容易。

收斂CRDT的經典例子是僅增長設定。這是一組有序的獨特元素,沒有刪除操作。您可以進行思考實驗並置換併發環境中資料結構可能發生的情況,並且您將認識到,最終,此資料結構是一致的和收斂的,並且它將包含中間階段中所有元素的並集。

收斂CRDT
我們需要為業務資料型別實現一些合併功能,這些功能將是可交換的,關聯的和冪等的。這不是一件容易的事,因為您的業務資料看起來不像學術資料結構一樣; 它包含欄位和引數之類的東西,這不是一件容易的事。但是我們非常鼓舞這樣的事實,因為這只是一種資料型別和操作,CRDT的實現類似於當您需要向資料模型新增一個欄位時的情況,您需要再新增一列到你的資料庫。您無需與DB管理員互動;您無需與服務基礎架構進行通訊,你只需修改你的物件,使用資料庫儲存新的資料模型。

航班訂座系統
假設我們在複製副本節點A有一個訂位12F,在複製副本節點B有一個訂位16D,跨資料中心複製後,副本節點B的值為12F。這是怎麼回事?
資料庫通常維護有關您的記錄的一些元數 它可能是上次修改或訪問記錄時的時間戳,也可能是某些生成計數器。當資料庫資料儲存看到值不同時,它會使用這些後設資料來解決您的衝突。
令人驚訝的是,這也是CRDT。這是“以最後一次寫為準”的策略,其合併函式等於取a,b中的最大值。220大於150,這就是12F超越16D的原因。

在我們的設計中,我們不希望任何資料被我們無法控制的資料庫元資訊丟棄,因為我們希望在現有資料庫之上構建我們的解決方案。
這就是為什麼我們這樣做的原因。我們不是儲存一些標量值,而是將資料型別擴充套件到某個Map。當它在12F之前時,就變為A1:12F和B1:16D,這裡的鍵key代表一個複製副本節點標識,操作的原子計數器,在每個節點內維護一個原子計數器,因為節點內部可以保證高一致性,下一次發生新資料插入,則A1將變成A2,如此累計計數器。

由於這些kley在全域性範圍內是唯一的,因此我們希望跨資料中心複製按鍵key合併這些map。在這種情況下,沒有任何值會丟失。所以我們仍然擁有兩個值。
如果你再看看這個Map,這些key是全域性唯一的,並且它們是不可變的,因為每次計數器都是新的。當我們只能插入一個具有某個值的新鍵時,只新增到map,這就是CRDT。這是可交換的,冪等的和聯合的,對嗎?

此Map的鍵key集合在某個時刻唯一地標識複製副本節點的狀態。這意味著我們可以使用這些資料來維持因果關係。那麼因果關係是什麼?如果你把Map的值的演變看作一個graph,因果關係就是你的優勢。例如,我們有一些初始值12F,一些客戶端將其改變到10A。因此,12F是10A的原因,那麼我們可以刪除12F。然後當客戶從10A改變到5C,則10A是5C的因,我們可以去除10A。

這有另一種情況:當12F是10A的原因時,我們可以放棄12F,但是另一個不知道這種轉變的客戶,他想將10A變到5C。在這種情況下,10A不是5C的因,我們不能放棄10A,在這種情況下,10A和5C是併發值。我們稱之為siblings兄弟姐妹,而且我們無法在當前時刻解決它們先後順序。
當我們總結這張地圖的所有鍵時,它會給我們一些因果關係向量。因此,要使用此因果關係向量,我們希望在讀取資料時將其返回給客戶。為此,我們需要擴充套件get和put操作的簽名。因此我們將因果向量新增到get操作的總型別中,並將因果向量新增到其中一個引數以進行操作。
當我們提供某個版本的值時,它與比較和設定的工作方式非常相似,然後當你想寫操作時,將這個版本與當前版本進行比較。

Aerospike資料儲存
我們想在一些可用的資料儲存上實現它,因為我們不想開發自己的資料儲存,我們使用了Aerospike。這是一個混合記憶體鍵值儲存。它具有非常高的效能和高吞吐量,低延遲。事實證明,大多數寫入和讀取操作將在一毫秒內完成。因此混合儲存器意味著鍵的索引儲存在操作儲存器中,但是記錄本身儲存在SSD盤上。Aerospike在其叢集中具有一致性強的模式,並且還具有跨資料中心複製功能。到目前為止,它看起來很合適。
Aerospike的資料模型是根據記錄Record的概念設計的。您可能會將記錄視為經典關聯式資料庫中的一行。記錄有一個鍵和後設資料,但值本身儲存在bin中。因此,您可能會將bin視為關聯式資料庫中的列,唯一的區別是Aerospike不要求您為所有記錄設定相同的bin。因此,每條記錄可能都有其獨特的箱櫃。而且,Aerospike為您提供了一個API來迭代記錄中的所有二進位制位。
我們還使用了Aerospike的另外兩個功能。第一個是使用者定義的函式。LUA操作是按記錄原子執行的。這些使用者定義的函式幫助我們維護了這個原子計數器。如果您不使用使用者定義的函式,則意味著在副本中,我們需要一些鎖定機制,如樂觀鎖或悲觀鎖來維護此計數器。但是在伺服器端更容易實現。
Aerospike中跨越資料中心複製的第二個好功能可以透過隔離複製bin的方式進行配置。如果你有兩個bin-bin one和bin two在某個副本A中,bin 2和bin 3在副本B中,交叉資料中心複製的結果將是所有這些bin的聯合,這是非常好的概念我們的設計。例如,如果您想在MongoDB中重複它並且您的結構類似於JSON,那麼它將不是那麼容易,因為您實際上需要構建一個新的JSON,它將是其中所有鍵的並集。所以它幫助我們用我之前談過的合併操作維護這個因果關係圖。

它是如何工作的?
當你想選擇座位時,您將轉到瀏覽器並選擇一個座位,12F。由於沒有初始狀態,我們執行put操作,包含12F和空因果向量,空是因為沒有初始狀態。我們在副本A節點:A1中建立一個新的bin,其中包含12F。
我們在資料庫上成功執行了它,但您的瀏覽器已掛起卡住,出了點問題,你不想失去選擇正確座位的機會,你決定使用移動應用程式,移動應用程式連線到副本B節點服務,而副本B中沒有初始狀態,因此您在帳戶中看不到你已經選的任何席位,這樣就新選擇了另一個座位,10D,移動應用程式執行值為10D且無空因果向量的put操作。我們在副本B中建立了bin B1,它從零開始包含值10D。
在這個時刻,兩個副本每個都有一個值,但是他們彼此不瞭解,對吧?
這完全是最終的一致性。然後交叉資料中心複製發生,bin A被複制到副本B,bin B1被複制到副本A.
所以現在,每個副本都有相同的bin組,但它們都不是彼此的因果關係。它們彼此平行。
然後你的瀏覽器有正常了,它說,“嘿,你成功選擇了你的座位,12F。”它還會返回因果關係資訊,因為在那個時刻,只有一個bin A1,你的因果關係向量將是A1。
你很驚訝:“我剛剛選了一個地方,10D。所以我想至少驗證系統工作正常”,你決定又選擇一個新的位置:10F,期待它會覆蓋前面其他一切選擇的座位號。
這個10Fput操作: 
這個10F具有因果關係上下文A1。而這正是我們的CRDT魔法發生的時刻。我們有一個新的bin: A2,其值為10F:A1。並且資料庫理解該因果關係向量A1大於或等於bin: A2。這意味著bin A1是A2的原因。這意味著我們可以刪除bin A1,因為它在我們的邏輯中被覆蓋了。我們的客戶也知道有bin A1,因為知道有這個資料值,所以想覆蓋它;所以刪除以前的版本並新增一個新版本吧?
你仍然有B1 bin,它與A2平行,但A1是A2的原因,這就是我們放棄A1 bin的原因。
你乘坐航班時候,要求航空公司改為商務艙,它是從副本A讀取你的資料。他決定給你另一個座位,5C,這是商務艙。所以他用因果向量A2,B1執行put操作,並且它對副本A是持久的。
所以我們建立了bin A3,因為這是副本A中的第三個操作,它具有因果向量A2和B1。CRDT魔法再次發生。我們知道A2,B1向量大於B1並且大於A2。這就是為什麼我們可以丟棄這些因,我們仍然有兩個位置:A1和B1,然後再次發生複製,因此,bin A3被複制到副本B,A2具有因果向量A2,B1。即使在複製品B中根本沒有A1,我們也可以做一個簡單的向量比較,說A2,B1大於B1和A1。
所以最終,你會看到我們的狀態是收斂的。如果你嘗試另外一個操作來置換它,它也會收斂。

學習收穫
我在2016年開始研究這個專案,現在有一年多一點,我們開始投入生產。它已經工作了一年。所以我從中學到了:CRDT是真實的,他們是可行的。這不僅僅是一篇學術論文。它們肯定要求您重新考慮如何處理併發,但它允許您實現資料的收斂可預測狀態,而不會在叢集之間實現強同步。

第二個學習是當你想要處理你的併發性時,需要教育自己在一致性和正確性之間的正確權衡。

第三種學習永遠不會低估併發資料訪問。當我們開始這個專案時,當然,我們做了一些關於我們應該面對的併發資料操作率的評估。一旦我們上線,我們就認識到這個速度要高得多。問題是這種向分散式部署模式的過渡已經影響了許多其他團隊。其中一些可能會錯誤地解釋下游依賴關係如何工作的概念,或者它們如何處理流量。例如,我們遇到一種情況,即兩個資料中心的訊息都出了兩次。

所以我想說的是,通常我們不會過度設計解決方案。但與此同時,我們設計它們時假設天氣好; 每個人都工作正常,資料庫總是響應,訊息總是隻出列一次。這並非總是如此,正是我想說的。保證解決方案足夠耐用。

相關文章