天天看點

ENode 1.0 - 架構的總體目标架構總體目标實作高吞吐量、低延遲、高可用的思路分析

本文想介紹一下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是什麼意思呢?在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也性能非常高,也很成熟,對,總之序列化方面我們還有很多解決方案來優化。

接下來我們來看看如何實作無阻塞。先想一下為什麼要無阻賽?舉個例子:比如電商網站通過信用卡來訂購商品。一般的做法就很直接,就是先擷取訂單資訊,通過銀聯的外部服務來驗證信用卡資訊是否有效(這意味着信用卡号如果有問題,根本就不會生成訂單),然後生成訂單資訊入庫,這兩步放在一個操作裡。這樣做的問題是,由于信用卡驗證服務是一個外部服務,是以操作往往會被阻塞較長的一段時間。這樣就導緻整個系統無法高效的運作。

無阻賽的方式是:把整個操作分為兩個,第一個操作是擷取使用者填寫的訂單。這個操作的結果是産生一個“信用卡驗證請求”的事件。第二個操作是當它接受一個“信用卡驗證成功響應”的事件,生成訂單入庫。我們的系統在完成第一個操作之後會接下來執行另外其他的事件,也就是不會依賴于信用卡驗證的結果了,直到“信用卡驗證成功響應”事件産生了,我們的系統才會繼續處理後續的建立訂單的事情。

目前隻能想到這麼多分析思路吧,希望對大家有幫助。為了篇幅不要太長的原因,架構的其他一些目标的分析思路隻能在後續的文章中慢慢讨論了。希望我能堅持下去。我個人能思考到的問題畢竟有限,希望大家看了後能多多提一些問題,然後大家讨論解決,這樣才能讓架構不斷完善起來。