開源地址:https://github.com/tangxuehua/enode
本文想介紹一下enode框架要實現的目標以及部分實現分析思路剖析。總體來說enode框架是一個基於cqrs架構和消息驅動的應用開發框架。在說實現思路之前,我們先看一下enode框架希望實現的一些目標吧!
框架總體目標
- 高吞吐量(High Throughput)、低延遲(Low Latency)、高可用性(High Availability);
- 需要能充分利用CPU,即要允許方便配置需要使用的並行處理線程數,以提高單台機器的command處理能力;
- 支持command的同步和異步處理,同步處理時要允許客戶端捕獲異常,異步處理時要允許客戶端設置回調函數;
- 應用編程模型要統一,框架api要簡單、好用、一致、好理解;
- 能讓開發人員只關注業務,不用關心數據哪里來,以及如何保存,也不用關心並發、重試、超時等技術相關的問題;
- 基於消息驅動的架構,那消息投遞方面,要能做到:至少投遞一次(即如果宕機了消息也不能丟)、且能做到最多投遞一次,因為有時我們無法做到消息的等冪處理;
- 要足夠可擴展,框架中每個組件都要允許用戶自定義並替換掉,包括IOC容器;
- 因為是CQRS架構,那必須要確保單個聚合根的事件的持久化順序與分發給查詢端的順序要完全一致,否則會出現嚴重的數據不一致的問題;
實現高吞吐量、低延遲、高可用的思路分析
關於性能的幾個重要概念
吞吐量是指系統每秒可以處理的請求數;延遲是指系統在處理一個請求時的延遲;一般來說,一個系統的性能受到這兩個條件的約束,缺一不可。比如,我的系統可以頂得住一百萬的並發,但是系統的延遲是2分鍾以上,那么,這個一百萬的負載毫無意義。系統延遲很短,但是吞吐量很低,同樣沒有意義。所以,一個好的系統的性能測試必然受到這兩個條件的同時作用。有經驗的朋友一定知道,這兩個東西的一些關系:Throughput越大,Latency會越差。因為請求量過大,系統太繁忙,所以響應速度自然會低。Latency越好,能支持的Throughput就會越高。因為Latency短說明處理速度快,性能高,於是就可以處理更多的請求。所以,可以看出,最根本的,我們是要盡量縮短單次請求處理的時間。另外,可用性是指系統的平均無故障時間,系統的可用性越高,平均無故障時間越長。如果你的系統能保持一年365天都能7*24全天候正常運行,那說明你的系統可用性非常高。
思路分析
要實現高可用,要怎么辦?簡單的辦法就是主備模式,即一份站點同時運行在主備服務器上,主服務器如果正常,那所有請求都由主服務器處理,當主服務器掛了,那自動切換到備服務器;這種方式能確保高可用;甚至我們還能設置多台備的服務器增加可用性;但是主備模式解決不了高吞吐量的問題,因為一台機器能處理的請求數總是有限的,那怎么辦呢?我覺得就需要讓我們的系統支持集群部署了,也就是說,不是只有一台機器在服務,而是同時有很多台機器在服務,這些同時服務的機器稱為一個集群。而且為了能讓集群中的服務器的負載能平衡,為了盡量避免某台服務器很忙,其他服務器很空的情況,我們還需要負載均衡技術。當然,真正的高可用同樣意味着不能有單點故障問題,就是不能因為集群中的一個點掛了導致整個集群掛掉,所以我們要杜絕所有的數據都要經過某個點的設計;相反,要做到每個點都能橫向擴展,web應用站點(enode框架支持)、內存緩存(memcached,redis都支持)、持久化(mongodb支持),都要能支持集群與負載均衡。好,整個系統所有層次都支持集群+負載均衡解決了高吞吐高可用無單點的問題,但並沒有解決低延遲的問題,那怎么辦呢?如何才能盡量快的處理一個用戶請求呢?我覺得關鍵是三個方面:In Memory+盡量快的IO+無阻賽,也就是內存模式加很快的數據持久化加無阻塞的編程模型。
In Memory
in memory是什么意思呢?在enode框架中,主要的體現是,當我們要獲取領域聚合根對象然后進行一些業務邏輯操作時,是從內存獲取,而不是從數據庫。這樣的好處就是快。那這樣做要面臨的一些問題,如內存不夠怎么辦?用分布式緩存,如memcached, redis這樣的成熟基於key-value模式的nosql產品。redis服務器掛了怎么辦?沒關系,我們可以讓框架自動處理,即當發現內存緩存中不存在時,自動在從eventstore取,就是取出當前聚合根的所有事件,然后使用事件溯源(event sourcing,簡稱ES)的機制還原聚合根,然后嘗試更新到緩存,然后返回給用戶。這樣就解決了緩存掛了的問題,當redis緩存服務器重啟后,又能繼續從緩存中取聚合根了;實際上,我們也要根據情況進行分布式集群部署redis服務器,這樣一方面是為了能將數據sharding,另一方面能提高緩存的可用性,因為不會因為一台redis緩存服務器掛了導致整個系統所有的緩存數據都丟失了。另外,你可能會奇怪,redis緩存服務器里的數據哪里來呢?同樣利用ES模式,因為我們在eventstore中存儲了所有聚合根的所有的事件,所以我們就能在redis緩存服務器啟動時,對所有需要放在緩存中的聚合根根據ES模式來得到。
盡量快的持久化
怎樣才能盡量快的持久化呢?我們先分析下enode框架需要持久化的關鍵數據是什么,就是事件。因為enode框架是一個基於event sourcing架構模式的,我們不會存儲對象的最終狀態,而是存儲對象每次發生的事件;並且,每次事件都是append的方式追加到eventstore。我們唯一需要確保的是eventstore中的事件表中的聚合根ID+事件版本號唯一即可;通過這個唯一索引,我們能檢測同一個聚合根是否有並發沖突產生。除了這個唯一性索引的要求外,我們不需要事務的支持,因為我們每次總是只插入一條記錄;好了,那這樣的話,我們要選擇傳統的關系型數據庫來持久化事件嗎?顯然不太合適,因為慢!更明智的選擇是用性能更高的NoSQL DB。如MongoDB,MongoDB默認的持久化是先放入內存,然后每隔100毫秒寫入日志,然后可能60秒寫入一次磁盤。這樣的特性使得我們可以非常快速的持久化事件,因為持久化事件實際上只是寫到mongodb server的內存中而已。另外,當數據被寫入到日志后,我們就可以認為數據已經被安全的持久化了,因為即使斷電了,mongodb也能將數據從日志恢復。當然你的疑問是,那如果斷電了,那理論上這100毫秒的數據不是就丟了,沒關系,我們還可以同時把數據寫入到多台mongodb server,也就是我們可以部署一個MongoDB server的集群,一般整個集群的所有機器都同時掛掉的可能性是很低的,所以我們可以認為這樣的思路是可行的。當然,這里所說的一切要能實現,還需要很過重要的細節問題要考慮。本文主要是給出思路。我一直覺得解決問題的思路最重要,是嗎?另外,mongodb是介於key-value結構的NoSQL產品和關系型DB之間,它是一個文檔型數據庫,最主要的是它也支持像數據庫一樣的關系查詢、更新、刪除等操作,再加上高性能以及支持集群分布式等特性;所以我覺得非常適合用來作為eventstore。
另外,還有一個問題很重要,那就是序列化。數據存儲到mongodb時,要被序列化,而.net自帶的二進制序列化類(BinaryFormatter)不是太快,所以會成為持久化的瓶頸,那怎么辦呢?呵呵,當然也是去找一個更高效的二進制序列化類庫了。目前為止,我找到的是一個開源的NetSerializer,測試下來發現是.net自帶的10倍左右,這樣的性能完全可以滿足我們的要求了;再簡單談一下為什么NetSerializer能這么快呢?很簡單,.net自帶的BinaryFormatter每次都需要反射,而NetSerializer在程序啟動時已經將所有要序列化的類型的元數據都一次性生成了,所以系列化或反序列化的時候就不用再做這一步耗時的操作,所以當然就快了。當然像google protocol buffer也性能非常高,也很成熟,對,總之序列化方面我們還有很多解決方案來優化。
無阻塞的編程模型
接下來我們來看看如何實現無阻塞。先想一下為什么要無阻賽?舉個例子:比如電商網站通過信用卡來訂購商品。一般的做法就很直接,就是先獲取訂單信息,通過銀聯的外部服務來驗證信用卡信息是否有效(這意味着信用卡號如果有問題,根本就不會生成訂單),然后生成訂單信息入庫,這兩步放在一個操作里。這樣做的問題是,由於信用卡驗證服務是一個外部服務,因此操作往往會被阻塞較長的一段時間。這樣就導致整個系統無法高效的運行。
無阻賽的方式是:把整個操作分為兩個,第一個操作是獲取用戶填寫的訂單。這個操作的結果是產生一個“信用卡驗證請求”的事件。第二個操作是當它接受一個“信用卡驗證成功響應”的事件,生成訂單入庫。我們的系統在完成第一個操作之后會接下來執行另外其他的事件,也就是不會依賴於信用卡驗證的結果了,直到“信用卡驗證成功響應”事件產生了,我們的系統才會繼續處理后續的創建訂單的事情。
可以看出,這樣的設計實際上就是一種事件驅動(event-driven)的思想。基於這樣的思想,我們的系統一直在不停的運轉,不會因為和外部系統的交互而要同步等待外部系統的處理結果。同樣,對於一個用戶操作如果涉及多個聚合根的修改的情況,也是采用這樣的事件驅動的思想,采用我常提到的saga的思想。我們不會在一個command中把所有事情都做完,而是會通過command+event不斷串聯的無阻塞的方式來實現整個過程。這一點在我之前的博文中應該已經做了比較詳細討論了。
目前只能想到這么多分析思路吧,希望對大家有幫助。為了篇幅不要太長的原因,框架的其他一些目標的分析思路只能在后續的文章中慢慢討論了。希望我能堅持下去。我個人能思考到的問題畢竟有限,希望大家看了后能多多提一些問題,然后大家討論解決,這樣才能讓框架不斷完善起來。