52
外匯
通過Axon和Disruptor處理1M tps
LMAX,一家在英國的金融公司,最近開源了其(新型零售金融交易平台的)核心組件之一:Disruptor。這個組件通過刪除必須的鎖來降低執行 開銷,且任然保證正確的處理訂單。如果你問我,我會說這是一個優美精巧的工程。我嚐試把Disruptor應用到Axon控製總線中,就是想看看它到底有 多大的潛力。結果相當驚人。
The Disruptor
Disruptor是一個並發編程框架,它允許開發者使用多線程技術去創建基於任務的工作流。Disruptor能用來並行創建任務,同時保證多個 處理過程的有序性。它刪除了保證處理有序性所需要的隊列。它擁有幾個技術上的特征,可以明確把它和其他並發編程框架區分開來,這些並發技術從java 5開始為程序員可用。主要的一個是沒有使用鎖機製。
考慮處理完成一個大任務,需要從A到D的4個步驟(4個子任務)。如果任務B和C都要依賴於任務A的執行結果,但B和C不依賴任何其他任務,它們可能被並行化執行。在這個例子中,任務D依賴任務B和C的執行結果。
如果在java5中要創建一個這樣的流式處理機製,需要使用諸如隊列這樣的技術。這會使任務的處理變的複雜;更重要的是,處理過程會比較慢。因為在向隊列put或poll元素時,線程需要獲取鎖。
Disruptor使用其他途徑來執行這些任務序列。它的主要組件是RingBuffer。RingBuffer實現了一個環形緩衝器,這個 RingBuffer能控製任務的執行。不同的任務讀/寫包含在緩衝器中的元素;一個任務的讀和寫,獨立於其他任務的讀和寫。這通常是令人滿意的。有時候 你需要去確認,在任務B 和C完成它們的工作之前,任務D有沒有開始執行。
環形緩衝器上麵的不同“消費者”,時刻觀察它們依賴的消費者的執行進展。這個功能是通過跟蹤其他消費者已經處理完成的任務的序列號來實現的。這允許 Disruptor最小化“內部消費者(inter-consumer)”的通信總量。舉一個例子,假設消費者D剛剛處理完了標號為8的任務,它將需要知 道是否允許處理9。它將詢問消費者B和C:“你們在哪裏”?B和C回答:“23”和“12”。在這種情況下,消費者D能明白它可以安全的處理9,10和 11。在這個期間,消費者D不需要詢問B和C的進展情況。消費者D處理完11之後,它將需要再次詢問消費者B 和C。
減少“內部線程(inter-thread)”的通信總量,可以讓CPU優化對緩存的使用。LMAX開發團隊稱這個機製為感應(Sympathy)。為了取得最好的結果,代碼已經根據CPU的工作機製做了優化。
我沒有試圖去解釋disruptor在技術上的所有來龍去脈,Trisha已經完成了一個係列的文章,這裏還有一篇技術論文。
Axon控製總線的基準測試
Disruptor模式適合在基於CQRS(命令查詢責任分類,Command Query Responsibility Segregation)架構的基礎上來處理命令。當某個進程“預加載(pre-load)”一個聚合對象群(aggregate)時,其他進程將執行命 令處理器(command handler),之後其它的存儲事件(store events)進入事件存儲(event store)中,且在事件總線(event bus)上發布這個存儲事件。我想嚐試一下,看能得到什麼樣的結果。
使用disruptor來實現一個概念驗證風格(proof-of-concept style)的命令總線,是非常容易的事情。disruptor的jar文件中有一個helper類,它可以幫助你優化處理速度(有些顯而易見的事情,比 如通過緩存行的填充來避免假共享)。
我的基準應用包含一個簡單的配置:一個命令處理器加載一個聚合對象群(在基準應用中隻使用了1個聚合對象)。並在聚合對象上執行 “doSomething”的方法。這個方法生成一個單一事件,這個事件需要存儲到在內存中(in-memory)的事件存儲中,而且這個事件被發布到一 個事件總線中(沒有監聽器監聽到這個事件)。這個基準應用的目標是關注“單純的命令處理”的速度。
我在筆記本電腦上運行了這個基準應用,這個筆記本的配置是:英特爾酷睿i7640M處理器(2.8 GHz,雙核,4線程)。
當使用SimpleCommandBus和CachingGenericEventSourcingRepository時,在我的電腦上得到了大 約每秒處理150 000個命令的成績。這個成績很可能遠遠超過了大多數應用程序可以取得的成績(吹牛吹出來的成績不算:))。
然後,我使用disruptor創建了一個基於命令總線的簡單應用。這個應用在一秒中執行了大約250 000個命令。這個應用差不多比上麵的基準應用快了一倍。但結果還是讓我失望。這裏必然有某種方法來提高命令的執行速度。
於是,我開始稍作調整。Axon使用java.util.UUID來獲得唯一事件標識符,我想徹底刪除它(別擔心,這隻是為了測試)。你猜怎麼了, 我取得了大約每秒處理700 000個命令的成績。現在取得了一些進展,但沒有標識符的應用是一個不真實的應用(應用使用的是隨機UUID)。
接下來,我改變UUID生成機製為一個基於時間的版本,得到的成績倒退到50K,但現在至少我有了我自己的標識符。
這時候,我發現我是在一個32位的JVM上運行應用。當我把同樣的基準程序在64位的JVM上運行時,執行結果幾乎翻了一番。這聽起來合乎邏輯,但 Oracle說遷移到64位JVM將降低性能。通過基於時間的UUID和其它的小優化,我取得了每秒處理1.3M (1 300 000)個命令的成績。這個成績比同樣的情況下使用鎖的機製取得的成績,高50多倍。
譯注–CQRS架構圖如下,供讀者參考:
總結
看起來,在同樣的硬件上處理同樣的邏輯,disruptor比基於鎖的實現機製更快。使用disruptor來編寫好的應用,需要一些編程的慣用法則。但一旦生產者,消費者和它們之間的依賴關係被確定,開始運行將非常簡單。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-23 10:02:50