事件流如何提高應用程序的擴展性、可靠性和可維護性
關于事件流處理,在不同的場景中有不同的概念。有人稱之為流處理,有人稱之為事件溯源或CQRS,還有人稱之為“復雜事件處理(Complex Event Processing)”。不管名稱是什么,它們的基本原則都是一樣的。Martin Kleppmann是Apache Samza的貢獻者。在本文中,我們將跟隨他的思路深入理解這些概念,以便幫助我們設計更好的系統。
“流處理(stream processing)”源于LinkedIn構建大規模數據系統的經驗,并在開源項目Apache Kafka和Apache Samza中實現。Martin以Google Analytics為例具體介紹了這一概念。Google Analytics是一小段JavaScript代碼,可以追蹤哪個訪問者訪問了哪個網頁。然后,系統管理員可以研究這些數據,并按照時間段、URL等劃分這些數據。為了實現這個目的,每次用戶訪問一個頁面時,就需要記錄一個事件來反映這個事實。頁面訪問事件可能是(圖1)這樣的結構:
每個事件都是包含上述信息的一個簡單不變的事實。它只簡單地記錄已發生的事情。然后,我們就可以從這些頁面訪問事件中生成圖形儀表板。通常來說,這些事件可以使用(圖2)所示的其中一種方式存儲:
選項(a):在每個事件進來的時候將其存儲,并把它們全部轉存到一個大型的數據庫、數據倉庫或Hadoop集群中。在需要時,就可以在數據集上執行查詢。這個過程會掃描所有事件,或者至少是某個大型的數據子集,并動態地完成聚合。
選項(b):如果每個事件都存儲數據量太大的話,可以選擇存儲事件的聚合結果。比如,如果要記錄某個事件的發生次數,那么就可以在這個事件進來時將計數器加1。我們還可以將多個計數器保存在OLAP立方中。有了OLAP立方,當需要查找一個URL在某一天的訪問量時,直接讀取相應URL和日期組合的計數器就可以了。這樣就只需要讀取一個值,而不需要掃描一個很長的事件列表。
選項(a)的好處是,存儲原始事件數據可以最大化分析的靈活性。比如,可以跟蹤某個人以什么順序訪問了哪些頁面,采用選項(b)就無法實現。這種分析對于一些離線處理任務非常重要,比如訓練一個推薦系統。在這種應用場景下,最好是保存原始事件。
不過,選項(b)也有它的用途,尤其是需要實時決策或響應的時候。比如,為了防止別人破壞網站,可能需要引入一個訪問頻率限制,在一個小時內一個特定的IP只允許請求100次;如果客戶端超出這個限制,就阻塞它。這時,通過原始數據存儲實現效率將非常低下,因為系統需要不斷地重新掃描事件歷史才能確定某個人是否超出了限制。而針對每個IP每個時間窗口維護一個計數器將會更高效。總之,存儲原始事件和存儲聚合結果都是有用的,只不過應用場景不同。
對于選項(b),在最簡單的情況下,可以讓Web服務器直接更新聚合結果。這時,可以將計數器保存在像memcached或Redis這樣具有原子增量操作的緩存中。每次Web服務器處理一個請求,就直接向緩存發送一條增量命令。更復雜一點,可以引入事件流(如圖3),或者消息隊列,或者事件日志。流上的事件與(圖1)中PageViewEvent記錄相同。
這種架構的好處是,同樣的事件數據可以供多個消費者使用,不同的消費者完成不同的任務,非常靈活和易于擴展。
“事件溯源(Event sourcing)”是一個同流處理類似的概念,只不過它出自領域驅動設計社區。它關注數據在數據庫中的存儲結構。這里將以電商網站的購物車為例:
如果用戶123將產品999的數量改成了3,那么系統將通過UPDATE操作實現數據修改:
不過,按照事件溯源的思想,這不是一個好的數據庫設計方式,因為它沒有記錄購物車每次變化的信息,即丟失了歷史操作信息。因此,在用戶123初次添加產品999的時候,系統應該記錄AddToCart事件;當用戶改變主意想買3個999時,系統接著記錄UpdateCartQuantity事件。總之,用戶對購物車的每次操作都記為一個單獨的事件。這就是事件溯源的本質:將每次寫操作記為一個不可變事件,而不是對數據庫執行破壞性寫入。
可以發現,它同流處理的例子(關于Google Analytics)一樣:(a)存儲原始事件;(b)存儲聚合結果。
通過進一步思考可以觀察到,(a)是理想的數據寫入形式,只需要將事件追加到日志尾部,而不需要更新多個不同的表。這對數據庫而言是一種最簡單、最快速的寫入方式。另一方面,(b)是理想的數據讀取形式。比如,在用戶想知道購物車中有什么的時候,他并不會關心購物車中產品的變化歷史,所以直接讀取聚合結果會獲得最好的性能。
為了幫助我們更深入的理解上述概念,Martin又分別舉了Twitter、Facebook和Wikipedia的例子。本文就不一一贅述了,感興趣的讀者可以查看原文。
現在,讓我們回到有關事件流的討論。不管是流處理,還是事件溯源,只要有了事件流,就可以完成以下工作:
- 獲取所有的原始事件(也許還要做一點轉換),然后將它們加載到一個大型的數據倉庫中供分析人員使用;
- 更新全文搜索索引,使用戶可以搜索最新數據;
- 更新緩存,使系統可以從快速緩存中讀取數據,并保證緩存中的數據是最新的;
- 通過對事件流進行處理創建一個新的事件流,然后將后者作為另一個系統的輸入。
與傳統的數據庫使用方法相比,采用類似事件溯源的方法是一個重大的變革。這項變革帶來了如下好處:
- 松耦合——數據讀寫使用不同的數據庫模式,讀取的數據經由寫入的數據轉換而來,應用程序不同部分之間的耦合度降低了;
- 讀寫性能——規范化(寫入快)和非規范化(讀取快)的爭論源于數據讀寫使用同一模式的假設,如果數據讀寫使用不同的數據庫模式,讀寫速度都會得到提升;
- 擴展性——因為事件流是一種簡單的抽象,而且允許開發人員將應用程序分解成流的生產者和消費者,所以很容易跨機器并行和擴展;
- 靈活性——原始事件簡單、明確,“模式遷移”不會造成多大影響;而向用戶展示數據要復雜得多,但如果有一個轉換過程可以實現從原始事件到緩存內容的轉換,那么當需要新的用戶界面時,只需要使用新的邏輯構建新的緩存;
- 錯誤場景——原始事件是不變的事實,如果系統出現問題,那么開發人員總是可以用相同的順序將事件重放。
這里需要注意,實際上,數據庫寫操作通常都有一個類似事件的不變性,大部分數據庫都有的“寫前日志(write-ahead log)”本質上就是一個寫操作的事件流,雖然在不同的數據庫中實現形式可能不同,如PostgreSQL、InnoDB和Oracle中的MVCC機制,CouchDB、Datomic和LMDB中的追加式B樹。
接下來,Martin介紹了如何在應用程序層面上使用事件流。
他用的比較多的是Apache Kafka和Apache Samza。前者是一個消息代理,就像一個發布-訂閱消息隊列,一秒鐘可以處理包含數百萬條消息的事件流,并將它們永久存儲到磁盤上及跨機器復制。后者是與Kafka搭配使用的處理過程,開發人員可以用它編寫代碼,消費輸入流,生產輸出流。
除了Samza之外,開發人員還可以選擇Storm或Spark Streaming這兩種最流行的流處理框架。關于它們之間的區別,感興趣的讀者可以查看Samza文檔。這些分布式流處理框架均源于互聯網公司。它們都關注底層的一些事情:如何將流處理擴展到多臺機器;如何將Job部署到集群;如何處理故障;如何在多租戶環境下實現可靠的性能。它們像MapReduce更多一些,而像數據庫更少一些。
相比之下,還有一些面向流處理的高級語言,如復雜事件處理(CEP)。使用CEP,可以編寫查詢或規則來匹配滿足特定模式的事件。這些查詢或規則與SQL查詢類似,只不過CEP引擎會不斷的查找事件流來匹配查詢,并在匹配成功時發送通知。這對于欺詐檢測或業務流程監控非常有用。
還有一個相關概念是在流上進行全文搜索。它是說,在流上事先注冊一個查詢,當有事件匹配查詢時發送通知。這里有一些與此相關的試驗性工作。以下是其它一些與流處理相關的概念:
- Actor框架——像Akka、Orleans和Erlang OTP等框架也是基于不可變事件的流。不過,它們更多的是一種并發機制,而不是數據管理機制;
- “響應式(Reactive)”——它似乎是一個定義松散的概念集合,像函數響應式編程,主要是將事件流提供給用戶界面使用;
- 變更數據捕獲(CDC)——按照我們熟悉的方式使用數據庫,但要將任何插入、更新和刪除操作抽取到一個數據變更事件流中。