微服務的最終一致性與事件流

banq發表於2016-04-21
微服務是指一個個單個小型業務功能的服務,由於各個微服務開發部署都是獨立的,因此微服務天然是分散式的,因此,分散式系統的設計問題如CAP定理同樣適合微服務架構,雖然微服務本身是無狀態的,但是微服務是需要管理狀態的。這些狀態是指領域模型的狀態或儲存在自己的專有資料庫中。

雖然我們使用微服務必須面對分散式系統,但是好的一方面是有很多關於如何建立複雜分散式系統的成熟模式和最佳實踐。

典型的問題是微服務之間如果需要共享狀態怎麼辦?實際是在分散式節點之間需要共享或複製狀態。關於共享狀態有幾個解決方案:
1.微服務之間透過共享同一個資料庫實現狀態共享,但是因為微服務是使用自己專用的資料庫,因此,資料庫共享方案在微服務中是不適用的,違背了微服務架構宗旨。

2.透過呼叫同一個微服務實現狀態共享,比如A服務和B服務需要共享C資料狀態,而C資料狀態是由C服務管理的,那麼,A服務和B服務共同呼叫C服務不就是獲得同一個C狀態嗎?
但是考慮到分散式系統下,A服務和B服務可能不在同一個節點伺服器上,或者不同Docker VM中,那麼服務之間呼叫就需要網路通訊,通常RPC是一種透過網路呼叫遠端伺服器上其他服務的同步方式,但是,RPC雖然將網路程式設計藏起來,其實藏是藏不住,結果造成抽象洩漏了。

"Asynch message-passing makes constraints of network programming firstclass instead of hiding them behind the RPC leaky abstraction"非同步訊息傳遞使得網路程式設計變成第一公民(顯式),而不是像RPC隱藏了網路程式設計卻造成抽象洩漏。

在分散式系統中使用非同步訊息必然會遭遇最終一致性。甚至可以說微服務是使用最終一致性的(microservices use eventual consistency)

最終一致性Eventual Consistency

最終一致性是一種用於描述在分散式系統中資料的操作模型,在分散式系統中狀態是被複制然後跨網路多節點儲存,其實在關聯式資料庫叢集中,最終一致性被用來在叢集多個節點之間協調資料複製的寫操作,資料庫叢集中這種寫操作挑戰是:各個節點接受到的寫操作必須嚴格按照複製的次序進行,這個次序是有時間損耗的,從這個角度看,資料庫在叢集節點之間的這種狀態複製還是可以被認為是一種最終一致性,所有節點狀態在未來某個時刻最終匯聚到一個一致性狀態,也就是說,最終達成狀態一致性。

當構建微服務時,最終一致性是開發者 DBA和架構師頻繁打交道的問題,當開始在分散式系統中進行狀態處理時,頭疼問題更加嚴重。核心問題是:

如何在保證資料一致性基礎上保證高可用性呢?

事務日誌

幾乎所有資料庫都支援高可用性叢集,大多數資料庫對系統一致性模型提供一個易於理解的方式,保證強一致性模型的安全方式是維持資料庫事務操作的有序日誌,理論上理由非常簡單,一個事務日誌是一系列資料更新操作的動作有序記錄集合,當其他節點從主節點獲得這個事務日誌時,能夠按照這種有序動作集合重新播放這些操作,從而更新自己所在節點的資料庫狀態,當這個事務日誌完成後,次節點的狀態最終會和主節點狀態一致,

這種事務日誌非常類似於財務中記賬模型,或者類似銀行儲蓄卡列印出來的流水賬,哪天存入一筆鈔票(更新操作),哪天又提取了一筆鈔票(更新操作),最後當前餘額是多少(代表資料庫當前狀態)。
  

Event Sourcing
Event sourcing事件溯源是借鑑資料庫事務日誌的一種資料持久方式,在ES中,事務單元變得更細粒度,使用一系列有序的事件來代表儲存在資料庫中的領域模型狀態,一旦一個事件被加入事件日誌,它就不能被移走或重新排序,事件被認為是不可變的,事件序列只能被追加方式儲存。

因為微服務將系統切分成一個個松耦合的小系統,每個系統後面都獨佔自己的資料庫,雖然,微服務是無態的,但是它需要操作自己資料庫的狀態,如何保證微服務之間運算元據庫資料的一致性成了微服務實踐中重要問題,使用ES能夠幫助我們實現這點。.

聚合可以被認為是產生任何物件的一致性狀態,它提供校訂方法用來進行重播產生物件中狀態變化的歷史。它能使用事件流提供分析資料許多必要輸入,能夠採取補償方式對不一致應用狀態實現事件回滾。


事件流共享
我們在微服務之間相互呼叫中透過引入非同步機制,如果不同微服務之間存在共享的狀態,或者說需要訪問其他微服務的專用資料庫,那麼我們無需將本來專有的資料庫共享出來,也無需在服務層使用2PC+RPC進行效能很慢的跨機同步呼叫,而是將改變這些共享狀態的事件儲存並共享,將領域事件以事務日誌的方式記錄下來,儲存在一個統一的儲存庫,現在EventSourcing標準的儲存庫是 Apache Kafka。

也就是說,微服務之間共享的不是傳統資料庫,而是Apache Kafka,透過讀取ES的事務日誌和重新播放,我們可以得到任何時間內的狀態,從而實現狀態的時間旅行。

時間旅行概念非常類似前端的Redux模式,Redux是Facebook的Flux模式的改進,將可變狀態和函式不變性進行分離,狀態值一旦建立就無法被可變的,如果需要改變狀態值,只能透過重新建立新的狀態值實現,將舊的狀態和新的狀態透過樹形結構連線起來,因此遍歷樹形結構就能回到歷史上任何狀態,從而實現了時間旅行,

我們將微服務之間共享狀態透過共享事件流實現,這點符合函數語言程式設計的宣告風格,在微服務中,我們不是需要狀態時就發出命令從資料庫中查詢獲得,這樣,可變的狀態會遍佈微服務程式碼中,帶來很多副作用,而我們將這些狀態操作統一為事件流宣告訂閱,訂閱了某個事件流,透過重播事件流中各個事件一直到最新最後的事件,也就獲得了最終的狀態。函數語言程式設計Stream風格為這種播放提供了方便。

這種實現其實已經在Reactive前端中有著同樣實現思路,見:為什麼要使用GraphQL和Falcor?,應用程式(微服務)將可變的狀態被限定在一個單個的序列化物件中,從而整個應用就變成了無態,可變狀態不會擴散到整個應用程式碼的各個本地變數中。

在後端Reactive中,我們可以透過一些Reactive框架來實現事件Stream,比如RxJava 或Spring的Reactor,比如我們為了獲得一個購物車的當前狀態,透過使用Spring Reactor如下程式碼實現ES重播:

// 從kafka獲得某個使用者的購物車操作事件流,Flux是Reactor的庫包,cartEventRepository是kafka倉儲

Flux<CartEvent> cartEvents = 

           Flux.fromStream(cartEventRepository.getCartEventStreamByUser(user.getId())); 



//執行事件流的事件直至最後一個事件發生的最終狀態。也就是購物車的最終狀態

ShoppingCart shoppingCart = cartEvents

             .takeWhile(cartEvent -> !ShoppingCart.isTerminal(cartEvent.getCartEventType())) 

             .reduceWith(() -> new ShoppingCart(catalog), ShoppingCart::incorporate) 

             .get(); 
<p class="indent">

以上程式碼詳細見這裡
該案例原始碼來自於基於Spring Cloud+Event Sourcing實現案例:https://github.com/kbastani/spring-cloud-event-sourcing-example

上面程式碼中ShoppingCart是包含使用者當前放入購物車的商品種類和數量以及總價,這些都是購物車的狀態,如果不使用ES事件流,我們通常是使用一個資料表來儲存購物車這些資料狀態,或者放在Redis等記憶體快取中,但是在微服務架構中,共享的資料表或記憶體系統會成為分散式的瓶頸,不符合真正橫向擴充套件分佈的理論定義,當然實際中這麼做是比比皆是。而在這裡我們使用了事件流的事件播放來獲得最終購物車的狀態,共享的只是訊息系統kafka,那麼如果為了完成真正沒有共享的分散式,將共享的訊息系統Kafka去除了怎麼辦?


事件流複製
上面我們展示了透過微服務之間共享訊息系統Kafka來實現事件事務日誌的共享,除了共享方案以外,還有複製方案,也就是將需要共享的事務日誌在多個微服務節點之間複製,每個節點上都有一份共享事務日誌的複製,比特幣的區塊鏈技術正是基於此方案:比特幣區塊鏈是一種分 布 式的事件流日誌


[該貼被banq於2016-04-21 18:19修改過]

相關文章