小延遲大吞吐:LMAX架構
LMAX是一種新型零售金融交易平臺,它能夠以很低的延遲(latency)產生大量交易(吞吐量). 這個系統是建立在JVM平臺上,核心是一個業務邏輯處理器,它能夠在一個線程里每秒處理6百萬訂單. 業務邏輯處理器完全是運行在內存中(in-memory),使用事件源驅動方式(event sourcing). 業務邏輯處理器的核心是Disruptors,這是一個并發組件,能夠在無鎖的情況下實現網絡的Queue并發操作。他們的研究表明,現在的所謂高性能研究方向似乎和現代CPU設計是相左的。
過去幾年我們不斷提供這樣聲音:免費午餐已經結束。我們不再能期望在單個CPU上獲得更快的性能,因此我們需要寫使用多核處理的并發軟件,不幸的是, 編寫并發軟件是很難的,鎖和信號量是很難理解的和難以測試,這意味著我們要花更多時間在計算機上,而不是我們的領域問題,各種并發模型,如Actors 和軟事務STM(Software Transactional Memory), 目的是更加容易使用,但是按下葫蘆飄起瓢,還是帶來了bugs和復雜性.
我很驚訝聽到去年3月QCon上一個演講, LMAX是一種新的零售的金融交易平臺。它的業務創新 – 允許任何人在一系列的金融衍生產品交易。這就需要非常低的延遲,非常快速的處理,因為市場變化很快,這個零售平臺因為有很多人同時操作自然具備了復雜性,用戶越多,交易量越大,不斷快速增長。
鑒于多核心思想的轉變,這種苛刻的性能自然會提出一個明確的并行編程模型 ,但是他們卻提出用一個線程處理6百萬訂單,而且是每秒,在通用的硬件上。
通過低延遲處理大量交易,取得低延遲和高吞吐量,而且沒有并發代碼的復雜性,他們是怎么做到呢?現在LMAX已經產品化一段時間了,現在應該可以揭開其神秘而迷人的面紗了。
整體結構
結構如圖:
(圖一:LMAX架構的三大塊)
從最高層次看,架構有三個部分:
-
業務邏輯處理器business logic processor[5]
-
輸入input disruptor
-
輸出output disruptors
-
業務邏輯處理器處理所有的應用程序的業務邏輯,這是一個單線程的Java程序,純粹的方法調用,并返回輸出。不需要任何平臺框架,運行在JVM里,這就保證其很容易運行測試環境。
Although the Business Logic Processor can run in a simple environment for testing, there is rather more involved choreography to get it to run in a production setting. Input messages need to be taken off a network gateway and unmarshaled, replicated and journaled. Output messages need to be marshaled for the network. These tasks are handled by the input and output disruptors. Unlike the Business Logic Processor, these are concurrent components, since they involve IO operations which are both slow and independent. They were designed and built especially for LMAX, but they (like the overall architecture) are applicable elsewhere.
業務邏輯處理器
全部駐留在內存中
業務邏輯處理器有次序地取出消息,然后運行其中的業務邏輯,然后產生輸出事件,整個操作都是在內存中,沒有數據庫或其他持久存儲。將所有數據駐留在內存中有兩個重要好處:首先是快,沒有IO,也沒有事務,其次是簡化編程,沒有對象/關系數據庫的映射,所有代碼都是使用Java對象模型(廣告:開源框架Jdonframework和JiveJdon也是全部基于內存和事件源,內存領域對象+事件驅動,看來這條路的方向是對的)。
使用基于內存的模型有一個重要問題:萬一崩潰怎么辦?電源掉電也是可能發生的,“事件”(Event Sourcing )概念是問題解決的核心,業務邏輯處理器的狀態是由輸入事件驅動的,只要這些輸入事件被持久化保存起來,你就總是能夠在崩潰情況下,根據事件重演重新獲得當前狀態。(NOSQL存儲的基于事件的事務實現)
要很好理解這點可以通過版本控制系統來理解,版本控制系統提交的序列,在任何時候,你可以建立由申請者提交一個工作拷貝,版本控制系統是一個復雜的商業邏輯處理器,而這里的業務邏輯處理只是一個簡單的序列。
因此,從理論上講,你總是可以通過后處理的所有事件的商業邏輯處理器重建的狀態,但是實踐中重建所有事件是耗時的,需要切分,LMAX提供業務邏輯處理的快照,從快照還原,每天晚上系統不繁忙時構建快照,重新啟動商業邏輯處理器的速度很快,一個完整的重新啟動 – 包括重新啟動JVM加載最近的快照,和重放一天事件 – 不到一分鐘。
快照雖然使啟動一個新的業務邏輯處理器的速度,但速度還不夠快,業務邏輯處理器在下午2時就非常繁忙甚至崩潰,LMAX就保持多個業務邏輯處理器同時運行,每個輸入事件由多個處理器處理,只有一個處理器輸出有效,其他忽略,如果一個處理器失敗,切換到另外一個,這種故障轉移失敗恢復是事件源驅動 (Event Sourcing)的另外一個好處。
通過事件驅動(event sourcing)他們也可以在處理器之間以微秒速度切換,每晚創建快照,每晚重啟業務邏輯處理器, 這種復制方式能夠保證他們沒有當機時間,實現24/7.
事件方式是有價值的因為它允許處理器可以完全在內存中運行,但它有另一種用于診斷相當大的優勢:如果出現一些意想不到的行為,事件副本們能夠讓他們在開發環境重放生產環境的事件,這就容易使他們能夠研究和發現出在生產環境到底發生了什么事。
這種診斷能力延伸到業務診斷。有一些企業的任務,如在風險管理,需要大量的計算,但是不處理訂單。一個例子是根據其目前的交易頭寸的風險狀況排名前 20位客戶名單,他們就可以切分到復制好的領域模型中進行計算,而不是在生產環境中正在運行的領域模型,不同性質的領域模型保存在不同機器的內存中,彼此不影響。
性能優化
正如我解釋,業務邏輯處理器的性能關鍵是按順序地做事(其實并不愚蠢 并行做就聰明嗎?),這可以讓普通開發者寫的代碼處理10K TPS. 如果能精簡代碼能夠帶來100K TPS提升. 這需要良好的代碼和小方法,當然,JVM Hotspot的緩存微調,讓其更加優化也是必須的。
#p#
以下兩段未翻譯…….調試方面。
It took a bit more cleverness to go up another order of magnitude. There are several things that the LMAX team found helpful to get there. One was to write custom implementations of the java collections that were designed to be cache-friendly and careful with garbage[8]. An example of this is using primitive java longs as hashmap keys with a specially written array backed Map implementation (LongToObjectHashMap
). In general they’ve found that choice of data structures often makes a big difference, Most programmers just grab whatever List they used last time rather than thinking which implementation is the right one for this context.[9]
Another technique to reach that top level of performance is putting attention into performance testing. I’ve long noticed that people talk a lot about techniques to improve performance, but the one thing that really makes a difference is to test it. Even good programmers are very good at constructing performance arguments that end up being wrong, so the best programmers prefer profilers and test cases to speculation.[10] The LMAX team has also found that writing tests first is a very effective discipline for performance tests.
編程模型
以一個簡單的非LMAX的例子來說明。想象一下,你正在為糖豆使用信用卡下訂單。一個簡單的零售系統將獲取您的訂單信息,使用信用卡驗證服務,以檢查您的信用卡號碼,然后確認您的訂單 – 所有這些都在一個單一過程中操作。當進行信用卡有效性檢查時,服務器這邊的線程會阻塞等待,當然這個對于用戶來說停頓不會太長。
在MAX架構中,你將此單一操作過程分為兩個,第一部分將獲取訂單信息,然后輸出事件(請求信用卡檢查有效性的請求事件)給信用卡公司. 業務邏輯處理器將繼續處理其他客戶的訂單,直至它在輸入事件中發現了信用卡已經檢查有效的事件,然后獲取該事件來確認該訂單有效。
這種異步事件驅動方式確實不尋常,雖然使用異步提高應用程序的響應是一個熟悉的技術。它還可以幫助業務流程更彈性,因為你必須要更明確的思考與遠程應用程序打交道的不同之處。
這個編程模型第二個特點在于錯誤處理。傳統模式下會話和數據庫事務提供了一個有用的錯誤處理能力。如果有什么出錯,很容易拋出任何東西,這個會話能夠被丟棄。如果一個錯誤發生在數據庫端,你可以回滾事務。
LMAX的內存模式(in-memory structures)在于持久化輸入事件,如果有錯誤發生也不會從內存中離開造成不一致的狀態。但是因為沒有回滾機制,LMAX投入了更多精力,確保輸入事件在實施任何內存狀態影響前有效地持久化,他們發現這個關鍵是測試,在進入生產環境之前盡可能發現各種問題,確保持久化有效。
Disruptors的輸入和輸出
盡管業務邏輯是在單個線程中實現的,但是在我們調用一個業務對象方法之前,有很多任務需要完成. 原始輸入來自于消息形式,這個消息需要恢復成業務邏輯處理器能夠處理的形式。事件源Event Sourcing依賴于讓所有輸入事件持久化,這樣每個輸入消息需要能夠存儲到持久化介質上,最后整個架構還有賴于業務邏輯處理器的集群. 同樣在輸出一邊,輸出事件也需要進行轉換以便能夠在網絡上傳輸。
如圖復制和日志是比較慢的。所有業務邏輯處理器避免最任何IO處理,所有這些任務都應該相對獨立,他們需要在業務邏輯處理器處理之前完成,它們可以以任何次序方式完成,這不同意業務邏輯處理器需要根據交易自然先后進行交易,這些都是需要的并發機制。
為了這個并發機制,他們開發了disruptor的開源組件。
Disruptor可以看成一個事件監聽或消息機制,在隊列中一邊生產者放入消息,另外一邊消費者并行取出處理. 當你進入這個隊列內部查看,發現其實是一個真正的單個數據結構:一個ring buffer. 每個生產者和消費者都有一個次序計算器,以顯示當前緩沖工作方式.每個生產者消費者寫入自己次序計數器,能夠讀取對方的計數器,生產者能夠讀取消費者的計算器確保其在沒有鎖的情況下是可寫的,類似地消費者也要通過計算器在另外一個消費者完成后確保它一次只處理一次消息。
#p#
輸出disruptors也類似于此,但是只有兩個有順序的消費者,轉換和輸出。輸出事件被組織進入幾個topics, 這樣消息能夠被發送到只有感興趣的topic中,每個topic有自己的disruptor.
disruptor不但適合一個生產者多個消費者,也適合多個生產者。
disruptor設計的好處是能夠容易讓消費者快速抓取,如果發生問題,比如在15號位置有一個轉換問題,而接受者在31號,它能夠從16-30號一次性批量抓取,這種數據批讀取能力加快消費者處理,降低整體延遲性。
ring buffer是巨大的: 輸入2千萬號槽;4百萬輸出. 次序計算器是一個64bit long 整數型,平滑增長(banq注:大概這里發現了JVM的偽共享),象其他系統一樣disruptors過一個晚上將被清除,主要是擦除內存,以便不會產生代價昂貴的垃圾回收機制啟動(我認為重啟是一個好的習慣,以便你應付不時之需。)
日志工作是將事件存儲到持久化介質上,以便出錯是重放,但是他們沒有使用數據庫來實現,而是文件系統,他們將事件流寫到磁盤上,在現代概念看來,磁盤對于隨機訪問是非常慢,但是對于流操作卻很快,也就是說,磁盤是一種新式的磁帶。
之前我提到LMAX運行在集群多個系統拷貝能夠支持失敗回復,復制工作負責這些節點的同步,所有節點聯系是IP廣播, 這樣客戶端能夠不需要知道主節點的IP地址. 只有主節點直接聽取輸入事件,然后運行一個復制工作者,復制工作者將把輸入事件廣播到其他次要節點. 如果主節點當機,心跳機制將會發現, 另外一個節點就成為主節點,開始處理輸入事件,啟動復制工作者,每個節點都有自己的輸入disruptor這樣它有自己的日志處理和格式轉換。
即使有IP廣播,復制還是需要的,因為IP消息是以不同順序到達不同節點,主節點提供為其他處理提供一個確定順序。
格式轉換unmarshaler是將事件從其消息格式轉換到Java對象,這樣才能在業務邏輯處理器中使用,不同于其他消費者,它需要修改ring buffer中的數據以便能夠存入這個被轉換好的Java對象,這里有一個規則:并發地每次只有一個消費者能夠運行寫入,這實際上也符合單一寫入者原則。
disruptor組件可以用在LMAX系統以外,通常金融財務公司對他們的系統都保持隱秘,但是LMAX能夠開源,我很高興,這將允許其他組織使用disruptor,它也將允許其他人對其進行并發性能測試。
(banq注:disruptor看來是一種特殊的消息組件類似JMS東西)。
隊列和機制偏愛的缺乏
LMAX架構引起了人們的關注,因為它是一個非常不同的方式接近的高性能系統。到目前為止,我已經談到了它是如何工作的,但沒有太多深入探討了為什么它是這樣。這個故事本身是有趣的,意識到他們是有缺陷的。
許多商業系統都有自己的核心架構師,通過事務性數據庫實現多個會話事務(banq注:如EJB或Spring JTA等等),LMAX團隊也熟悉這些知識,但是確信這些不合適他們的系統。這個經驗是建立在LMAX母公司Betfair上 – 這是一家體育博彩公司,它處理很多人的體育投賭事件,這是一個相當大的并發訪問,傳統數據庫機制幾乎無法應付,這些讓他們相信必須尋找另外一個途徑來突破,他們現在接近目標了。
他們最初的想法為獲得高性能是使用現在流行的并發。這意味著允許多線程并行處理多個訂單。然而,在這種情況下是很難實現的,因為這些線程必須互相溝通。處理訂單變化的市場條件等都需要相互溝通。
早期他們探索了Actor模型和近親SEDA. Actor模型依賴于獨立,活躍的對象有其自己的線程,彼此之間是通過queue同學,很多人認為這種并發模型比基于原始鎖的方式易于處理。
這團隊就建立了一個actor模型原型,進行性能測試,他們發現的是處理器會花費更多時間在管理隊列,而不是去做真正應用邏輯,隊列訪問成了真正瓶頸(banq注:Scala的Actor模型很有名,不知這是否算Scala致命問題,怪不得很少人談Scala的Actor模型了).
當追求性能達到這種程度,現代硬件構造原理成為很重要的必須了解的知識了,馬丁湯普森喜歡用的一句話是“機制偏愛”,這詞來自賽車駕駛,它反映的是賽車手對汽車有一種與生俱來的感覺,使他們能夠感受到如何發揮它到最好狀態。許多程序員包括我承認我也陷入這樣一個陣營:不會認為編程如何與硬件等底層機制交互是值得研究的。
現代的CPU延遲是影響性能的主導因素之一,在CPU如何與內存交互,CPU具有多層次的緩存(一級二級),每級速度都明顯加快。因此,如果要提高速度,將您的代碼和數據加載到這些緩存中。
在某個層次, actor模型能夠幫助你,你能認為actor可以作為集群節點,是緩存的自然單元。但是actors需要相互聯系, 這是通過隊列的- 而LMAX團隊發現隊列會干擾緩存(banq注:JVM偽分享的問題)。
為什么隊列干擾了緩存呢?解釋是這樣的: 為了將數據放入隊列,你需要寫入隊列,類似地,為了從隊列取出數據,你需要移除隊列也是一種寫,客戶端也許不只一次寫入同樣數據結構,處理寫通常需要鎖,但是如果鎖使用了,會引起切換到底層系統的場景, 當這個發生后,處理器會丟失它的緩存中的數據。
他們得出的結論能夠獲得最好的緩存性能, 你需要設計一個CPU核寫任何內存,多個讀是好的,處理器會非常快,而隊列失敗在one-writer原則。
這樣的分析導致LMAX團隊得出一系列結論,導致他們設計出disruptor, 能夠遵循single-writer約束. 其次它導向留澳單個線程處理業務邏輯的新的目標, 問題是:一個線程如果從并發管理結構中脫離出來(沒有鎖機制),它到底能跑多快?
單個線程的本質是:確保你每個CPU核運行一個線程,緩存配合,盡可能的高速緩存訪問甚于主內存。這就意味著代碼和數據需要盡可能的一致,. 保持小的代碼對象和數據在一起,以便允許他們能夠調入到一個高速緩存單位中或者輪換,簡化高速緩存管理就是提高性能。
LMAX架構的路徑的一個重要組成部分是使用了性能測試。基于actor模型的放棄也是來自于測試原型的性能。同時也為改善的各個組成部分的性能步驟,啟用了性能測試。機械同情是非常寶貴的的 – 它有助于形成假設什么可以改進,并指導你前進 -最終測試提供了有說服力的證據。
兩段關于性能測試重要性,忽略…
你應當使用者架構嗎?
這個架構是適合非常小小眾,必須有很低的延遲獲得復雜大量的交易,大多數應用并不需要6百萬TPS。
但是我對這個架構著迷的原因是它的設計,它移除了很多其他大多數編程系統的復雜性,傳統圍繞事務性的關系數據庫會話并發模型是不是免費的麻煩? (banq注:因為都掌握都知道也就是免費的) 通常與數據庫打交道都有不尋常的付出和努力,對象/關系數據庫映射ORM工具Object/relational mapping tools能夠幫助減輕不少這種痛苦,但是它不能解決全部問題,大多數企業性能微調還是要糾結于SQL.
現在你能得到服務器更多的主內存,比我們過去這些老家伙得到的磁盤還要多,越來越多應用能夠將他們的工作全部置于內存中,這樣消除了復雜性和性能低問題. 事件源驅動Event Sourcing提供了一種內存in-memory系統的解決方案, 在單個線程運行業務解決了并發. LMAX 經驗建議只要你需要少于幾百萬TPS,你就有足夠的性能提升余地。
這里也是相似于CQRS. 一種事件驅動, in-memory風格自然的命令系統(盡管LMAX當前沒有使用CQRS.)
那么表示你是不是不應該走上這條道路呢?始終存在這樣鮮為人知的棘手的技術問題,這個行業需要更多的時間去探索它的邊界(注:老子思想的繳啊)?;境霭l點是鼓勵有自己特點的架構。
一個重要特點是處理一個交易總是潛在地影響其后面的處理方式,因為交易總是相互獨立的, 因為很少相互協調,那么使用分離單獨處理器分別處理并發運行也許更加有吸引力。
LMAX指出了“事件”概念是如何改變世界(banq注:hold住事件,而不是hold住數據,你就上了一個新層次,擺脫低級趣味的數據庫癖好)。 許多網站使用原有的信息存儲系統,然后渲染各種能夠吸引眼球的效果. 他們的架構挑戰就是如何正確使用緩存。
LMAX另外一個特點是這是一個后臺系統,有理由考慮如何在一個交互模型中應用它,比如日益增長的Web應用,當異步通訊在WEB應用越來越多時,這將改變我們的編程模型。
這個改變會影響很多團隊,大多數人傾向于認為同步編程,不習慣于異步處理。異步通訊是必不可少的響應工具,在javascript世界已經廣泛使用,如 AJAX 和 node.js, 這些鼓勵人們研究這些風格. LMAX團隊發現雖然要花費一定時間來適應異步編程模型,但是一旦習慣就成為自然,特別是錯誤處理上容易得多。
LMAX團隊當然感覺到花力氣協調事務性關系數據庫的日子已經屈指可數(banq老淚縱橫啊,05年喊出了數據庫時代的終結,08年我就喊出數據庫已死,被國內很多大牛譏笑為瘋子) 。你可以使用一種更加容易方式編寫程序而且比傳統集中式中央數據庫運行得更快,為什么視而不見呢?