Event Sourcing在分散式系統中應用

banq發表於2015-01-14
本文來自原文:Event Sourcing at Global Scale,談論瞭如何在應用程式中透過開發人員自己實現基於ES的CQRS分散式系統,是一種犧牲一致性(最終一致性)換得低延遲高可用的分散式方案。實際是Vector Clock演算法應用,相當於自己實現型別Dynamo,Project Voldemort等NoSQL機制。

因為國際客戶的存在,我們開始探索如何基於Event Sourcing分佈化應用,主要驅動原因是地理位置距離的原因(稱為sites站點),從不同地理位置訪問應用應該有低延遲,每個站點site應該在最近的資料中心執行應用,應用資料應當跨所有sites複製,每個站點如果存在內部網路分割槽情況下應保持寫入的可用性,當分割槽修復後,從不同站點的更新應該能融合merge,衝突應該能自己解決。

Eventuate是這個專案的原型,繼續不斷完善推向生產環境。

這個案例中有6個站點,(A-F),以不同程式執行在本地localhost,透過改變配置,站點也可以分佈到多個主機上,站點A-F可透過雙向連線 非同步的事件複製連線:

A        E
 \      /    
  C -- D
 /      \
B        F
<p class="indent">


每個站點都必須配置:
1. 一個Akka Remoting 的hostname和port (akka.remote.netty.tcp.hostname 和akka.remote.netty.tcp.port)
2. 本地事件日誌複製的id(log.id 是站點的id)
3.到其他站點的複製連線列表(log.connections)

對於這個案例,site C配置如下:
akka.remote.netty.tcp.hostname = "127.0.0.1"
akka.remote.netty.tcp.port=2554

log.id = "C"
log.connections = ["127.0.0.1:2552", "127.0.0.1:2553", "127.0.0.1:2555"]

開始這個原型時,我們使用了akka-persistence,但是不久發現 akka-persistence (2.3.8)並不適合我們的概念和技術要求,於是決定對 akka-persistence API 進行一些修改結合我們的geo-replication。這些擴充不僅能適用geo-replication場合,而且可克服akka-persistence一些當前限制,比如event-sourced actor必須是叢集範圍內的單例,而我們的目標是,允許幾個actor例項在多個節點併發更新,如果衝突發生也能解決,這樣我們就支援來自幾個事件生產者的事件聚合與匯聚,這樣能確定地對所有這些事件進行重放。

在我們上面原型中 geo-replicated event-source應用是跨站點分佈,每個站點在一個獨立的資料中心,為了低延遲訪問,使用者選擇與其地理位置最近的一個站點互動訪問。

事件日誌
系統核心是全域性可複製的事件日誌,它擁有事件的happens-before關係,它是使用向量vector時間戳來跟蹤的,這些時間戳是由一個或更多向量時鐘vector clock在每個站點產生,並與事件一起儲存到事件日誌中,透過比較向量時間戳,一個站點可決定任何兩個事件的happens-before前後關係或同時發生。見這個案例原始碼

事件的部分順序是由向量時間戳決定的,儲存在每個本地日誌,如果e1 -> e2,那麼offset(e1) < offset(e2),這裡->表示happens-before關係,而offset(e)事件e的在事件日誌中的位置或索引,舉例,如果一個站點A寫入事件e1引起站點B的事件e2,那麼複製協議確保e1總是在所有站點本地日誌儲存中先於e2儲存。

儲存順序很重要,事件的生產和消費者才能從事件日誌獲得可靠複製,引起相應順序事件廣播,如果emit(e1) -> emit(e2),那麼在所有站點所有應用將消費e1先於e2,這裡->是happens-before 關係,emit(e)代表事件的寫入發生,儲存因果順序非常重要。

狀態複製
透過複製日誌記錄,應用狀態能夠在不同站點重新建立,在站點網路分割槽中,站點必須保留狀態更新和複製的可用性,因此,如果發現衝突,必須解決,更準確說,在內部站點網路分割槽中,衝突的更新不會發生。

舉例,站點A對領域物件x1做了更新,那麼更新事件e1被寫入事件日誌,一段時間後,站點A接受到來自站點B的更新事件e2,針對同樣領域物件x1,如果站點B在發射e2事件之前已經處理了e1,那麼e2因果關係是依賴e1,那麼站點A只是簡單將e2更新應用到x1,在這種情況下,兩個更新e1和e1都已經被應用到兩個站點的x1上,而x1的複製也會變成同樣值,另外一個方面,如果站點B與站點A同時更新x1,那麼衝突就發生。

同時發生事件是否衝突完全取決於應用邏輯,例如針對不同領域物件同時修改對於一個應用也許是可接受的,而針對同一個領域物件的同時修改會被看成是衝突,必須解決,任何兩個事件是同時或 happens-before前後順序關係都能透過比較它們的向量時鐘獲得。

衝突解決
如果應用狀態是使用 commutative replicated data types (CmRDTs) 建模的,狀態更新操作是透過事件複製的,併發修改一點不是問題,在這種情況下,事件複製甚至不需要保留因果順序,我們應用中許多狀態更新操作不是滿足互動律commutative 的,我們需要支援衝突解決的互動式和自動化兩個方式。

應用狀態衝突版本在一個同時併發版本樹中被跟蹤,這是一個根據事件的向量時鐘構建的資料結構,對於任何型別s的狀態值和任何型別A的更新,併發版本s能以資料型別

ConcurrentVersions[S, A]方式被跟蹤,併發版本能針對不同領域物件甚至領域物件欄位獨立跟蹤,這依賴於應用需要發現和解決衝突的粒度。

在互動式衝突解決中,一個使用者選擇一個衝突版本作為"winner",這種選擇是作為一個明確的衝突解決事件儲存到事件日誌中,這樣以後使用者互動介入時需要事件重放,這樣可能會進行人工干預下的衝突事件融合merge,這種情況下,衝突解決事件必須包含融合細節以便融合本身也可重新再來一次。

在自動化衝突解決下,為了選擇一個"winner",應用一個定製的衝突解決函式到衝突版本(vector clock演算法?),衝突解決函式會自動融合衝突版本,然後我們就進行了convergent replicated data types (CvRDTs).

注:按照CAP定理,CRDT是保證了可用性A和分割槽性P(AP),實現最終一致性;而Cassandro代表的Paxos是保證強一致性C和分割槽性P(CP)。

相關文章