永續性Akka、Kafka、Cassandra實現CQRS資料同步

banq發表於2018-06-29
本文是討論資料庫在讀寫分離情況下,如何實現寫資料庫的資料如何快速更新到讀資料庫的三種方式。

Akka Persistence(永續性Akka)是一個相當不錯的事件溯源EventSourcing實現。當我們選擇EventSourcing架構時,自然同時也會採用CQRS,CQRS是將查詢操作與永續性的寫操作分離,這樣事件儲存資料庫和事件查詢資料庫就是兩個不同資料庫,這種讀寫分離帶來的代價是最終一致性,所以最大的問題是:如何有效快速地更新讀取模型?

先看看CQRS/ES的基本要求:

1.讀資料庫應基於儲存事的資料庫進行更新,
2.必須保證沒有事件可以丟失,
3.事件順序必須保證。

前兩點非常明顯,如果無法實現事件的順序性,則根本不會選擇事件溯源這個架構。為滿足上述這些要求,選擇合適策略在很大程度上取決於你的領域(你擁有多少持久Actor,你正在製造多少事件等等)以及用於儲存事件的底層資料庫。在寫這篇文章的時候,我選擇儲存事件(和快照)的武器是Apache Cassandra--一個高度可擴充套件的分散式資料庫。現有的Cassandra外掛已經多次證明它是穩定的並且可以生產。有些傳聞說Scylla是更有效的儲存,但它仍然處於研發階段。

通常情況下,我們會有多個不同的讀模型。其中一些讀操作比其他更重要,一致性要求可能特別高,我們需要可以針對每個讀模型進行擴充套件,下面幾個方案是如何快速將持久的事件快速同步到查詢資料庫。

Akka Persistent Query
第一種方法非常簡單,我們可以使用Akka堆疊中的內建解決方案,即Persistence Query(永續性查詢)。這個想法如下:

1.連線到事件日誌資料庫並將事件作為流提供。
2.更新讀取模型資料庫。
3.儲存處理後的事件序列號。

val eventJournal  = PersistenceQuery(system).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier)

eventJournal
  .eventsByPersistenceId(persistenceId, startingSequenceNr, Long.MaxValue)
  .via(updateSingleReadModel)
  .mapAsync(1)(saveSequnceNr)
<p class="indent">


儲存序列號對於恢復階段是必需的,這樣您就不會從頭開始處理事件。

簡單而優雅的解決方案,對嗎?不幸的是這種方案不是響應式Rective的。預設情況下Cassandra時間間隔為3秒。起初,這可能很好,但假設生產中有10000個永續性Actor,10000雖然不是一個非常大的數量,但足以殺死你的應用程式。對於每個永續性Actor,都將需要啟動一個流,請相信我10000個流真不是最好的主意。實際上,如果要獨立更新讀取模型資料庫,應該將永續性Actor的數量乘以讀取模型的數量。

相比採用eventsByPersistenceId,可以使用使用eventsByTag查詢被標記的事件。在大多數情況下,這種方式這工作得也很好,但你可能會面臨事件分發的問題。假設大部分事件是由1%的永續性Actor產生的標記,可能導致其他99% 永續性Actor的事件處理滯後,因為所有事件都集中在同一個Actor源中了。解決方案可能是對標記進行分片,Lagom框架就是這麼實現的。

不幸的是,沒有任何技巧完美可以解決資料庫輪詢資料的問題。3秒滯後在其他情況下可能不成問題,對於某些情況即使0.5秒卻也不能接受,同時,太小的時間間隔也會造成底層資料庫不必要的負載,該分析是否有其他選項了?

CDC
是否可以直接從資料庫中透過流方式傳輸資料呢?Cassandra(與大多數資料庫一樣)支援CDC機制。理論上,很容易連線到Cassandra的更改日誌並使用這些日子來更新讀取模型。太好了,但這裡有一個問題。Cassandra是一個分散式資料庫,因此每個資料庫節點都有一個單獨的CDC日誌檔案,而且管理這些伺服器日誌以確保整體事件順序性會是一場噩夢。此時,CDC方式應該被認為是“有害的”。

Kafka作為一個資料庫
如果從Cassandra的讀取效率確實是一個大問題,也許我們可以使用像卡夫卡Kafka這樣的訊息佇列作為事件儲存?從卡夫卡中讀取事件流是非常有效的。每個讀模型資料庫將由不同的卡夫卡消費者更新,每個都是一個獨立的過程,完全自主,獨立和獨立可擴充套件。整個概念在我們之前的部落格文章中有詳細描述。對於某些應用,這種方法可以順利執行。但是,在某些情況下,Kafka(或實際上任何訊息佇列)作為資料庫可能帶來很多其他問題:

1.快照管理。
2.Retention滯留管理(Retention應該可能被禁用)。
3.Kafka分割槽 - 為了保持順序,來自聚合的所有事件必須放入單個分割槽,該分割槽必須適合單個節點。在一些過載情況下,這可能是一個阻礙或需要解決的挑戰。
4.Akka Persistence不支援。

卡桑德拉,卡夫卡和至少一次交付
如何將兩個概念結合在一起?Cassandra用作儲存事件的資料庫(真相的來源),Kafka負責將資料處理寫入到讀取模型的資料庫。

[img index=1]

理論上,這是完美的。唯一的問題是如何有效地把事件發給卡夫卡,可以使用前面所述持久Actor的eventsByPersistenceId或eventsByTag方式將Cassandra的事件發給Kafka,但滯後3秒和分發不平均問題仍將存在。

這裡還有一種方法是:在永續性Actor儲存事件到Cassandra資料庫以後立即向Kafka傳送事件。永續性Actor的演算法很簡單:

1.接收命令
2.儲存事件(S)
3.插入Cassandra資料庫
4.傳送事件給卡夫卡

我們來看看這裡可能存在的問題:為了確保事件順序性,傳送給Kafka必須阻塞整個Actor,這當然也是一個壞主意,因為它會降低永續性Actor的效能。我們可以將卡夫卡生產者委託給另外一個獨立的child actor(我們稱之為KafkaSender)。這樣就太棒了,但是我們還必須確保這兩個Actor之間的訊息傳遞。這可以透過使用AtLeastOnceDelivery特徵來完成。至此,你已經可能覺得好像有些複雜,其實沒有什麼是免費的,確實,更多的訊息會在我們的Actor叢集中迴圈傳播,我們也可能會失去順序:

至少一次投遞意味著原始郵件傳送順序並不總是保留。

考慮使用一些事件緩衝機制以確保事件順序?請停止!至少一次交付能以不同的、更樂觀的方式完成。您可以將事件傳送到KafkaSender而無需確認傳送,但您需要監視事件序列號,如果序列號有任何差異,則需要採取額外的措施:

1.如果序列號低於當前序列號 - 事件已經處理完畢,可以跳過

2.在間隔高於1的情況下,儲存訊息並啟動eventsByPersistenceId以填補空白,並清除掛起的訊息

當然,當前序列號應該在KafkaSender失敗或持久Actor重啟後保持並恢復。聽起來很複雜?誠然,但沒有人說高可擴充套件性是便宜的。(banq注:可以採取Kafka 1.0以後的事務性訊息)

結論
最後一個方案顯然是最真實、最有效、也可擴充套件的,但卻最難實施,如果你確實需要這種級別的可擴充套件性,那麼也要接受不可避免的複雜性。這裡我們討論了幾種如何將儲存資料庫的事件同步到讀資料庫的方案,只要能滿足預期的延遲和吞吐量,按照你自己的實際情況選擇即可,如果需要強一致性,比如將事件溯源架構的內部延遲壓縮到最後一毫秒,那麼這篇文章也提供了這種複雜的解決方案。


Scalable read model updates in Akka Persistence –

[該貼被banq於2018-06-29 19:39修改過]

[該貼被admin於2018-07-01 08:59修改過]

[該貼被admin於2018-07-01 09:03修改過]

相關文章