LMAX系統架構

本文轉載自:LMAX系統架構 ,(很是感謝做者yfx416分享好文)php

  

  不少架構師都面臨這麼一個問題:如何設計一個高吞吐量,低延時的系統?面對這個問題,各位都有本身的答案。但面對這個問題,你們彷佛漸漸造成了一個共識:併發是解決之道。你們彷佛都這麼認爲:對於服務器而言,因爲多核愈來愈廣泛,所以咱們的程序必需要充分利用多線程,爲了讓多線程工做得更好,必須有一個與之匹配的高效的併發模型。因而各類各樣的併發模型被提出來,好比Actor模型,好比SEDA模型(Actor模型的表弟),好比Software Transactional Memory模型(準確得講,STM和其餘兩個模型所處在的視角是不同的,Actor和SEDA更可能是一種編程模型,而STM更相似一種思想,其實咱們經常使用到的Lock-Free機制都包含了STM的思想在裏頭)。html

這些模型獲得了普遍討論和應用。但這些模型都有一個討厭之處-麻煩。這個麻煩是由多線程複雜的天性帶來的,很難避免。除了麻煩,這些模型還忽視了另一個問題,因爲這個問題的忽視,可能致使這些模型在解決高性能問題的道路上走到了一個錯誤的方向。這個問題就是JVM的僞共享問題。所謂JVM的僞共享,簡單來講,就是JVM的每個操做指令都是基於一個緩存行,同一個緩存行中的數據是不能同時被多個線程同時修改的,也就是說,若是多線程各自操做的數據位於同一個緩存行,那麼這幾個線程訪問數據時實際上被加上了一把隱形的鎖,它們實際上在順序地訪問數據。(若是你看過JDK Concurrent的實現,你能夠看到有些類很奇怪得加了不少無用的padding成員,這就是爲了填充緩存行,從而繞過JVM的僞共享)。因爲JVM僞共享的存在,使得多線程在某些狀況下成了一個擺設。這也就是說大多數狀況下咱們的槍炮瞄錯了方向,咱們一般認爲沒有充分利用多線程壓榨多個CPU的能力是形成性能問題的緣由,實際上緩存問題纔是性能殺手。java

因而LMAX就作了一個大膽的嘗試。既然多線程在JVM中有可能成爲擺設,並且又這麼麻煩,那麼幹脆回到單線程來吧。用單線程來實現一個高吞吐量,低延時的系統?聽起來很瘋狂,但其實是可能的。LMAX就用單線程實現了一個吞吐量達到百萬TPS的系統。程序員

這裏講LMAX是單線程,並非它徹底只有一個線程,LMAX組件仍是有用到多線程。只不過LMAX充分認識到了單線程的意義,在某些組件中大膽得采用單線程的架構,這就是LMAX所謂的單線程。LMAX決定組件是否採用單線程的依據很簡單,若是某一個組件是IO密集型的,那麼這個組件的設計就使用多線程。若是某一個組件是CPU密集型的,那麼該組件就使用單線程的設計。這麼作的理由很簡單,IO密集型的組件的操做通常都很慢,每每會阻塞線程,所以使用多線程來競爭執行,有提升的餘地。而CPU密集型的操做,若是採用多線程的設計,一方面可能會陷入JVM僞共享的陷阱,另外一方面多線程之間的同步會帶來開發的複雜性,同時多線程會競爭某些資源,好比隊列等等,這些競爭會對計算機cache命中形成擾動,並且有可能引入鎖這種性能殺手,與這兩點相比,多線程帶來的好處至關有限,所以就採用單線程。數據庫

【LMAX的原則】編程

LMAX的設計使人耳目一新,它的設計也向咱們分享了高性能計算中的幾個重要經驗或者說原則:緩存

1. 全部的架構師和開發人員都應該具備良好的Mechanical Sympathy(這個單詞不太好翻譯,「機制共鳴」?)所謂Mechanical Sympathy,實際上就是指架構設計者應該對現代操做系統,現代服務器的底層運行機制有良好的理解和認識,設計的時候充分考慮到這些機制,可以和它們產生共鳴。這很容易理解,若是一個架構師對底層機制的認識不夠深刻或者還停留在過去,那麼很難想象這樣的架構師能設計出一個基於現代服務器的高效系統來。LMAX在文檔中向咱們分享了幾點對現代服務器的認識:安全

1.1 內存服務器

衡量內存有兩個指標:Bandwidth和Latency。所謂bandwidth指的是內存在單位時間內經過內存總線的數據量,它計算公式是bandwidth = 傳輸倍率*總線位寬*工做頻率/8,單位是Bytes/s(字節每秒)。傳輸倍率指的是內存在一個脈衝週期內傳輸數據的次數,好比DDR一個脈衝週期內能夠在上升沿傳遞一次數據,在降低沿傳遞一次數據,而SDRAM只能在脈衝週期的上升沿傳遞數據。工做頻率的是內存的工做頻率,好比133Mhz等等。網絡

而Latency是指內存總線發出訪問請求到內存總線返回數據之間的延遲時間,單位是納秒。

這兩個指標描述了內存性能的兩個方面,bandwidth描述了內存能夠以多快的速度來傳遞數據,反映了吞吐量,而latency從更底層的細節描述了內存的物理性能。一個內存的bandwidth說明的僅僅是內存在內存邊界的傳輸的速率,而數據在內存內部的流動速度是靠latency來決定。這就像趕飛機,bandwidth就好像是T3航站樓的門的大小,門的大小決定了T3每秒可以接納的旅客數量。而安檢的速度就是latency,它也會影響你最終登上飛機的時間。Bandwidth加上latency才能徹底描述內存整個環節的性能。最終內存的性能能夠用內存性能 = (bandwidth*latency)來近似描述(Little's Law)。

儘管硬件技術一日千里,但這些年來,服務器內存的延遲並無發生數量級的變化。可是內存的bandwidth仍是得到了很大的進步,所以總體而言內存的性能仍是有比較大的提升。

另外內存的容量也是愈來愈大,144G大小的內存配置也是至關廣泛。

1.2 CPU

對於CPU而言,單純的提升主頻的方法已經走到盡頭,Intel的主頻可能會在Ghz這個量級上停留很長的一段時間。

CPU的核數是愈來愈多,24核的服務器也很廣泛。

CPU的緩存機制也愈來愈強大,一方面CPU緩存變大了,另外一方面Intel又提出了Smart cache等概念,相比於傳統的L1 cache, L2 cache又提升了一步。

1.3 網絡

服務器本地的網絡響應時間很是快,處於sub 10 microseconds這個級別。(10 ms在操做系統中通常是一個時鐘滴答,sub 10 microseconds意味着小於一個時鐘滴答,咱們知道Linux的延時,線程切換都是基於時鐘滴答的,也就是說本地網絡速度是很快的,對於大多數的應用來說,幾乎能夠忽略不計)。

廣域網的帶寬是比較便宜的。

10GigE(10Gbps的以太網卡)的服務器很是廣泛。

Multi-cast技術愈來愈獲得關注,應用也愈來愈多。

1.4 存儲

硬盤是新一代的磁帶。磁盤對於順序訪問的速度是很是快的。
對於併發的隨機訪問,考慮採用SSD。

SSD的接口通常都是PCI總線接口,速度更快。

2. 把工做放到內存中來

儘量把一些數據都放到內存中來,避免和磁盤的低效交互。

3. 寫的代碼要緩存友好。

什麼樣的代碼是緩存友好的代碼?這個一言難盡。但總的原則就是,保持訪問的局部性,也就是說盡量使一段時間內的訪問保持在一個狹小的內存範圍內。經常使用的一個作法就是,先統一分配一個對象池,而後複用對象池中的對象,不要每次都是從新分配新的對象。

clip_image002

上圖顯示了各個層次的緩存的訪問效率,提醒咱們要對緩存敏感。

4. 要時刻牢記,代碼要乾淨,簡練

Hotspot虛擬機喜歡短小,簡練的代碼;

若是CPU的分支預測不許確,那麼CPU流水線會被阻斷;

複雜的代碼是一個危險的信號,這意味着你有可能沒有正確理解問題的領域(DDD裏的概念);

世界上的事情都不會很複雜,除了扣稅的方法。

5. 多花點時間考慮一下你的領域模型。

記住這麼幾個原則:

責任單一:一個類只幹一件事,一個方法也只幹一件事,不要臃腫的類或方法。

瞭解你的數據結構和關係基數(一對一的關係?一對多?仍是多對多)

讓關係來完成工做,好比「書架」和「書」之間存在一個「attach」的關係,既然如此,咱們可讓「書架」有一個方法叫attach,用來處理添加書本的工做,這就是讓關係來完成工做。這實際上也是DDD裏面的一些設計原則。

6. 採用正確的方法來實現併發。

實現併發須要考慮兩件事:

資源互斥和變化可見(讓結果以一個正確的順序出現)

併發的實現通常有兩種方法:

第一個方法是用鎖來保證,另外一個方法是藉助於CAS進行無鎖編程。

使用鎖會致使內核態的切換,但總能夠確保任什麼時候刻總有一個進程會被執行(相比之下Lock-Free若是代碼邏輯出現問題,有可能全部線程都處在自旋等待狀態,沒法前進),鎖也增長了編程的難度。

而藉助於CAS的Lock-Free則始終是運行在用戶態的(節省了效率,避免了無謂的上下文切換),相比於鎖,它的編程難度更加大。下面圖形象地表達了Lock和Lock-Free之間的區別:

clip_image004

這些原則大部分都是老生常談,但很容易被人忽略,總之這些原則提醒咱們:

1) 不少程序員對現代服務器的硬件有着一個錯誤的認識或者根本沒有認識,他們根本就不知道單線程所能達到的性能高度。

2) 對於現代處理器,緩存丟失纔是性能的最大殺手。

3)架構設計時,把併發放到infrastructure層裏去考慮,這樣一方面使得應用層的編寫避免了併發編程的複雜性,另外一方面因爲併發放在了相對單純的infrastructure層,避免了來自應用層的亂七八糟的干擾,更容易優化。

4) 牢記上述3條原則,一旦你實現了這3條,那恭喜你,你已經進入了理想王國:

單線程;

全部的一切都在內存中;

優雅的模式;

易於測試的代碼;

不用擔憂infrastructure和集成的問題。

太完美了!

【LMAX的架構

接下來就來談談LMAX具體的架構,LMAX正是基於上述原則下的產物。

clip_image006

LMAX的整體架構就如上圖,它分爲三個部分:

l Business logic processor

l Input disruptor

l Output disruptors

Business logic processor是一個單線程的java進程,用來處理方法調用,產生輸出事件。因爲它是一個單線程的簡單的java程序,所以它除了JVM自己以外不依賴於任何其餘的framework,這就使得咱們很容易把它放入一個測試環境進行測試,這就是所謂的「易於測試的代碼」。

Input Disruptor是用來處理輸入消息的,輸入消息從網絡中接收,須要進行反序列化(unmarshaled),須要進行replicated避免單點故障,須要journaled來記錄消息日誌從而可以進行故障恢復。

Output Disruptors用來處理輸出消息,這些消息須要進行序列化以便於網絡傳輸。

Input Disruptor和output disruptor都是多線程的,由於他們設計到大量的IO操做,這些IO操做很慢並且相互獨立。

Business logic processor

Business logic processor的整個處理都是放在內存中的,這樣帶來的好處不少:

l 速度快,一切盡在內存,沒有緩慢的磁盤IO

l 簡單,由於所處理的都是java對象模型,不須要進行數據庫和java對象之間的映射

因爲一切盡在內存中,所以一個須要認真考慮的問題是若是Business logic processor發生了crash怎麼辦?Lmax解決這個問題的思路很直接,它在input distruptor上運行了一個按期的任務,該任務(journal任務)的職責就是在一個合適的時間(好比天天的夜裏12點)生成一個輸入信息的快照,只要有了這些輸入信息的快照,processor在crash以後只要一被重啓,它會從新再處理一遍這些輸入信息,處理以後它就可以把整個系統帶回到crash以前的最新狀態。

Business logic processor還有一個優點就是易於診斷,好比團隊發現了在線的產品上的一些問題,一個簡單的作法是利用replicated任務產生的副本,把它放到一個安全的測試機器上,而後從新運行processor進行debug,這不會影響產品。也就是說business logic processor好像一臺錄像機,只要記錄了拷貝,它能夠在任何地方進行回放。這個特性很是吸引人,這也是爲何近些年來event sourcing這樣的方法獲得重視的緣由。

爲了達到性能最大化,Lmax甚至實現了本身的數據結構,好比本身的List等等。

Business logic processor的一個重要特色就是不和任何外部服務進行交互。由於調用外部服務的速度會比較慢,processor又是單線程的,所以外部服務會拖慢整個processor的速度。Processor只和event交互,要麼接受一個event,要麼產生一個event。怎麼理解呢?舉個例子就明白了,好比電商網站經過信用卡來訂購商品。普通青年的作法就很直接,先獲取訂單信息,經過銀聯的外部服務來驗證信用卡信息是否有效(這意味着信用卡號若是有問題,根本就不會生成訂單),而後生成訂單信息入庫,這兩步放在一個操做裏。因爲信用卡驗證服務是一個外部服務,所以操做每每會被阻塞較長的一段時間。

Lmax則另闢蹊徑,它把整個操做分爲兩個,第一個操做是獲取用戶填寫的訂單。這個操做的結果是產生一個「信用卡驗證請求」的事件。第二個操做是當它接受一個「信用卡驗證成功響應」的事件,生成訂單入庫。Processor在完成第一個操做以後會接下來執行另外其餘的事件,直到「信用卡驗證成功響應」事件被插入input disruptor並被processor選取。至於lmax如何根據「信用卡驗證請求」輸出事件生成另一個輸入事件-「信用卡驗證成功響應」,這則是經過output disruptor的多線程來完成的。所以能夠看出lmax青睞單線程的態度並不執拗,而是有本身的原則:IO密集型操做用多線程,CPU密集型用單線程。

這樣異步的工做方式帶來一個問題是如何實現事務?相比傳統作法,lmax須要作更多的工做。

Input and Output Disruptor

business logic processor是單線程工做的,在processor能夠正常進行工做以前仍是有不少任務須要作的。Processor的輸入本質上是網絡消息,爲了便於business logic processor處理,這些網絡消息在送達processor以前須要進行反序列化(unmarshaled)。Event Sourcing的工做依賴於記錄輸入事件,所以輸入消息的日誌須要被持久化。

clip_image007

Figure 2: The activities done by the input disruptor (using UML activity diagram notation)

如上圖,因爲replicator和journaler涉及到大量的IO,所以速度相對比較慢。而business logic processor的中心思想就是避免任何IO。這三個任務相對比較獨立,它們必須在business logic processor處理消息以前完成,所以這三個任務很適合併發。

爲了處理這個併發,lmax開發了一個特殊的併發組件 – disruptor。

粗略理解,你能夠把disruptor看做一組隊列,生產者向某些隊列中放入對象,這些對象會被廣播發送到若干個獨立分開的下行隊列中,消費者就會並行得從這些下行隊列中獲取對象進行處理。若是你進一步深刻到disruptor的內部,你會發現實際上並非一組隊列,而只是一個單獨的數據結構-ring buffer。

clip_image009

Figure 3: The input disruptor coordinates one producer and four consumers

如上圖,每一個生產者/消費者都擁有一個序號,這個序號表示該生產者/消費者正在處理ring buffer的那個slot。每一個生產者/消費者都只能擁有本身序號的寫權限,對於其它消費者/生產者的序號只能讀取而不能更改。基於這種方法,生產者能夠不斷讀取其它消費者的序號來檢查生產者想要寫入的slot是否被佔用,這種方法實際上就是的lock-free,避免了加鎖。相似的,一個消費者也能夠經過觀察其餘消費者的序號來確保不會重複處理某些消息。

Output disruptor和input disruptor是相似的,只不過output disruptor的兩個消費者marshaller和publisher必須是順序執行的,也就是說ring buffer裏的消息必須通過marshaller處理以後才能由publisher公佈出去。Publisher發佈出去的事件被組織成了若干個topics,每一個事件只會被轉發到訂閱了該主題的receivers。

clip_image011

Figure 4: The LMAX architecture with the disruptors expanded

如上圖,深綠色的模塊表示生產者,深藍色的模塊表示消費者,output disruptor實際上包含了若干個ring buffer,每一個ring buffer對應一個topic,output disruptors 中的publisher和input disruptor中的receiver構成了一個典型的pub/sub系統,這個系統並無在圖中顯式註明。

上圖中描述看起來整個系統彷佛都是單生產者+多消費者的模式,但實際上disruptor也可配置成多生產者+多消費者的模式,在這種模式下,input disruptor/output disruptor上的消息接收組件能夠有若干個實例(每一個實例也有多是多線程的),即便在多生產者+多消費者模式下,disruptor依然不須要鎖。

Lmax的這種設計帶來一個好處,若是某個消費者發生了問題從而成爲其它消費者的拖累,它也可以很快遇上來。仍是看Figure 3中的例子,假設un-marshaller在處理slot 15時產生了問題,速度特別慢,但一旦un-marshaller處理完了slot 15從其中脫身,那麼下一次執行,它就會一次性讀取slot 16到slot 31之間全部的數據從而加快消息的消費速度(至於消息的實際處理速度則是另一回事,一旦消息被讀走了,它至少在ring buffer中再也不是拖累者)。

在Figure 3的例子中,journaler,replicator和un-marshaller各自只有一個實例,lmax在默認設置下的確是這樣,可是lmax也能夠運行多個組件實例,好比journaller組件能夠運行兩個實例,一個處理奇數slot,一個處理偶數slot。是否運行多個實例取決於IO操做的獨立性和IO的阻塞時間。

Ring buffer是很大的,input ring buffer擁有20 million個slot,每一個output ring buffer也擁有4 million個slot。序號是一個64位的長整形。Ring Buffer的大小爲2的整數次方,這樣有利於作取餘運算(sequence number % buffer size)把序號映射成slot號碼。像不少其它的系統同樣,disruptors天天深夜作按期的重啓,這麼作的主要緣由是回收內存,儘量下降在繁忙時段的昂貴的垃圾回收的可能性。

Journaler的主要工做就是持久化存儲全部的事件,這樣便於當系統出現故障時能夠從日誌進行恢復。Lmax沒有用數據庫來做爲持久化存儲,而只是採用文件系統。它們把事件流寫入磁盤,因爲現代磁盤對於順序存儲的速度很快,而對隨機存儲的速度很慢,所以lmax的這種作法的性能並不會不好,即便沒有用數據庫。

前面我提到lmax會運行多個實例節點組成一個cluster來支持快速failover。Replicator用來保持這些實例節點的同步。Lmax節點之間的全部通信採用的IP廣播,所以備用節點不須要知道主節點的IP地址。只有主節點運行一個replicator並偵聽輸入事件。Replicator負責廣播這些input event給備用節點。一旦主節點發生宕機,主節點的心跳信號就會丟失,那麼另外一個備用節點就會變成主節點,接着這個新的主節點就會開始偵聽輸入事件,並啓動本身的replcator。每一個節點是一個完整的lmax實例,有本身的disruptor,本身的journaler,本身的un-marshaller。

因爲IP廣播消息並不能確保消息的到達順序。主節點負責決定廣播消息的順序。

Un-marshaller用於把網絡上的事件順序轉化成business logic processor能夠調用的java對象。和其它的消費者有所不一樣,un-marshaller須要改變ring buffer中的數據。這裏寫(更改數據)時須要遵照一個原則,那就是每一個對象的writable field只能容許衆多並行消費者(也就是un-marshaller)之中的一個來寫,這個原則的目的就是爲了不jvm的僞共享。

Disruptor能夠做爲一個單獨的組件被使用,而不僅是用在lmax中,如今lmax已經開源了這個組件。做爲一件金融交易軟件公司,lmax的行爲的確使人稱道,也但願更多的公司願意交流或分享本身的架構,畢竟技術是在交流中促進的。回過頭來看,樂意開源或者願意分享的公司(好比在infoQ中分享)每每技術上都比較領先。從我的來說,技術人員也應該願意進行分享,畢竟這是一個在業界創建本身聲譽的好機會。

 

延伸閱讀:Disruptor:High performance alternative to bounded queues for exchanging data between concurrent threads

相關文章
相關標籤/搜索