# 通過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來編寫好的應用,需要一些編程的慣用法則。但一旦生產者,消費者和它們之間的依賴關系被確定,開始運行將非常簡單。
- 首頁
- 剖析Disruptor為什么會這么快
- 1.1 鎖的缺點
- 1.2 神奇的緩存行填充
- 1.3 偽共享
- 1.4 揭秘內存屏障
- Disruptor如何工作和使用
- 2.1 Ringbuffer的特別之處
- 2.2 如何從Ringbuffer讀取
- 2.3 寫入Ringbuffer
- 2.4 解析Disruptor關系組裝
- 2.5 Disruptor(無鎖并發框架)-發布
- 2.6 LMAX Disruptor 一個高性能、低延遲且簡單的框架
- 2.7 Disruptor Wizard已死,Disruptor Wizard永存!
- 2.8 Disruptor 2.0更新摘要
- 2.9 線程間共享數據不需要競爭
- Disruptor的應用
- 3.1 LMAX的架構
- 3.2 通過Axon和Disruptor處理1M tps