LMAX是一種新型零售金融交易平臺巩步,它能夠以很低的延遲(latency)產(chǎn)生大量交易(吞吐量). 這個系統(tǒng)是建立在JVM平臺上,核心是一個業(yè)務邏輯處理器,它能夠在一個線程里每秒處理6百萬訂單. 業(yè)務邏輯處理器完全是運行在內存中(in-memory)骡湖,使用事件源驅動方式(event sourcing). 業(yè)務邏輯處理器的核心是Disruptors完沪,這是一個并發(fā)組件漓骚,能夠在無鎖的情況下實現(xiàn)網(wǎng)絡的Queue并發(fā)操作递沪。他們的研究表明丸冕,現(xiàn)在的所謂高性能研究方向似乎和現(xiàn)代CPU設計是相左的延赌。(JVM偽共享)
過去幾年我們不斷提供這樣聲音:免費午餐已經(jīng)結束除盏。我們不再能期望在單個CPU上獲得更快的性能,因此我們需要寫使用多核處理的并發(fā)軟件挫以,不幸的是者蠕, 編寫并發(fā)軟件是很難的,鎖和信號量是很難理解的和難以測試掐松,這意味著我們要花更多時間在計算機上踱侣,而不是我們的領域問題。諸如各種并發(fā)模型大磺,如Actors和軟事務STM(Software Transactional Memory), 目的是更加容易使用泻仙,但是任然還是帶來了bugs和復雜性.
我很驚訝聽到去年3月QCon上一個演講, LMAX是一種新的零售的金融交易平臺量没。它的業(yè)務創(chuàng)新是他的一個零售平臺玉转,允許任何人交易一系列的金融衍生產(chǎn)品。這樣的平臺需要非常低的延遲殴蹄,非尘孔ィ快速的處理,因為市場變化很快袭灯,一個零售平臺增加了復雜性刺下,因為它必須為很多人做到這一點。所以結果是更多的用戶稽荧,有很多的交易橘茉,所有這些都需要被快速處理。
鑒于多核心思想的轉變,這種苛刻的性能自然會提出一個明確的并行編程模型畅卓,實際上這是他們的出發(fā)點擅腰。但QCon引起人們注意的是,這不是他們最終的目標翁潘。事實上趁冈,他們提出僅僅使用一個線程處理所有的客戶的所有的交易,在通用的硬件上達到了每秒處理6百萬訂單拜马。
通過低延遲處理大量交易渗勘,取得低延遲和高吞吐量,而且沒有并發(fā)代碼的復雜性俩莽,他們是怎么做到呢旺坠?現(xiàn)在LMAX已經(jīng)產(chǎn)品化一段時間了,現(xiàn)在應該可以揭開其神秘而迷人的面紗了扮超。
結構如圖:
從最高層次看价淌,架構有三個部分:
- 業(yè)務邏輯處理器business logic processor;
- 輸入input disruptor;
- 輸出output disruptors;
業(yè)務邏輯處理器處理所有的應用程序的業(yè)務邏輯,這是一個單線程的Java程序來響應方法調用并產(chǎn)生輸出事件瞒津,因此這是一個簡單的Java程序蝉衣,不需要任何平臺框架就可以運行(需要JVM),這就保證其很容易運行在測試環(huán)境中巷蚪。
Input Disruptor是用來處理輸入消息的病毡,輸入消息從網(wǎng)絡中接收,需要進行反序列化(unmarshaled)屁柏,需要進行replicated避免單點故障啦膜,需要journaled來記錄消息日志從而能夠進行故障恢復。Output Disruptors用來處理輸出消息淌喻,這些消息需要進行序列化以便于網(wǎng)絡傳輸僧家。Input Disruptor和output disruptor都是多線程的,因為他們設計到大量的IO操作裸删,這些IO操作很慢而且相互獨立八拱。
業(yè)務邏輯處理器 Business Logic Processor
全部駐留在內存中 Keeping it all in memory
業(yè)務邏輯處理器按順序(方法調用的形式)接受輸入消息,然后運行其中的業(yè)務邏輯涯塔,并產(chǎn)生輸出事件肌稻,整個操作都是在內存中,沒有數(shù)據(jù)庫或其他持久化存儲匕荸。將所有數(shù)據(jù)駐留在內存中有兩個重要好處:首先是快爹谭,沒有IO,也沒有事務榛搔,因為所有的處理都是按順序執(zhí)行的诺凡,第二個好處是簡化編程东揣,沒有對象/關系數(shù)據(jù)庫的映射,所有代碼都可以使用Java對象模型編寫腹泌,不必為映射到數(shù)據(jù)庫做任何妥協(xié)嘶卧。
由于一切都在內存中處理,因此需要任職考慮的問題是萬一Crash了怎么辦真屯?可伸縮性再好的系統(tǒng)也會受到很多其他因素的影響,比如掉電穷娱。處理這個問題的核心是“事件”(Event Sourcing )機制绑蔫,這意味著業(yè)務邏輯處理器的當前狀態(tài)完全可以通過處理輸入事件來推導出來。只要輸入事件保存在一個持久存儲(這是輸入Input Disruptor的作業(yè)之一)中泵额,就可以通過重播事件來重新創(chuàng)建業(yè)務邏輯處理的當前狀態(tài)配深。
可以基于NOSQL處理事務性事件存儲
理解這個的一個好方法就是對于一個版本控制系統(tǒng)。版本控制系統(tǒng)處理一系列的提交嫁盲,任何時候你都可以通過應用這些提交來構建一個工作拷貝篓叶。VCS比業(yè)務邏輯處理器更復雜,因為它們必須支持分支羞秤,而業(yè)務邏輯處理器是一個簡單的序列缸托。
因此,從理論上講瘾蛋,您總是可以通過重新處理所有事件來重建業(yè)務邏輯處理器的狀態(tài)俐镐,但是實踐中重建所有事件是耗時的,因此哺哼,正如版本控制系統(tǒng)一樣佩抹,LMAX可以創(chuàng)建業(yè)務邏輯處理器狀態(tài)的快照并從快照中恢復。在每天晚上系統(tǒng)不繁忙時構建快照取董,通過快照重新啟動業(yè)務邏輯處理器的速度很快棍苹,一個完整的重新啟動,包括重新啟動JVM茵汰、加載最近的快照和重放一天事件枢里,不到一分鐘。
如果業(yè)務邏輯處理器在下午2時崩潰蹂午,通過快照啟動恢復的方式依然不夠快坡垫。LMAX的方案是保持多個業(yè)務邏輯處理器同時運行,一個輸入事件由多個業(yè)務處理器處理画侣,但只有一個業(yè)務處理輸出會被保留冰悠。如果一個處理器處理失敗,則切換到另外一個配乱,這種故障轉移是使用事件驅動(Event Sourcing)的另外一個好處溉卓。
通過事件復制機制(replicas)皮迟,多個業(yè)務處理器可以在微秒間切換。除了每天晚上創(chuàng)建快照桑寨,它們還每天晚上重啟業(yè)務邏輯處理器伏尼。復制機制(replicas)允許他們在沒有停機的情況下這樣做,因此系統(tǒng)將7x24小時全天候處理交易尉尾。
事件機制的價值不僅體現(xiàn)在允許完全在內存中進行處理爆阶,對于系統(tǒng)診斷測試也是有相當大的優(yōu)勢。如果系統(tǒng)發(fā)生意外的行為沙咏,團隊會將事件順序復制到其開發(fā)環(huán)境中辨图,并在那里重播。 這使得他們能夠比在大多數(shù)環(huán)境中更容易地檢查發(fā)生的事情肢藐。
從基礎診斷功能可擴展到業(yè)務診斷故河。有一些業(yè)務任務,如風險管理吆豹,需要大量的計算鱼的,而不需要處理訂單。一個例子就是根據(jù)目前的交易倉位痘煤,獲得風險狀況前20位客戶的名單凑阶。 團隊通過復制域模型并在那里執(zhí)行計算來處理這個問題,在那里它不會干擾核心訂單處理衷快。這些分析域模型可以具有不同的數(shù)據(jù)模型晌砾,將不同的數(shù)據(jù)集保存在內存中,并在不同的機器上運行烦磁。
性能優(yōu)化 Tuning performance
到目前為止养匈,我已經(jīng)解釋過,業(yè)務邏輯處理器速度的關鍵是在內存中順序地執(zhí)行所有的事情都伪。只要做到這一點(并沒有什么真正愚蠢的呕乎,并行就聰明嗎?)陨晶,開發(fā)人員就可以編寫能夠處理10K TPS的代碼猬仁。然后他們發(fā)現(xiàn)只需要使用良好的代碼和小方法就可以使其達到100K TPS的范圍。當然先誉,JVM Hotspot的緩存微調湿刽,讓其更加優(yōu)化也是必須的。
再過一個數(shù)量級就更巧妙了一些褐耳。有幾件事LMAX團隊發(fā)現(xiàn)有助于提升性能诈闺。一個是編寫java集合的自定義實現(xiàn),這些實現(xiàn)被小心的設計為緩存友好铃芦。一個例子是使用原始的Java longs作為散列映射關鍵字雅镊,并帶有一個專門編寫的數(shù)組支持的Map實現(xiàn)(LongToObjectHashMap)襟雷。一般來說,他們發(fā)現(xiàn)數(shù)據(jù)結構的選擇通常會造成很大的差別仁烹,大多數(shù)程序員只是抓住上次使用的List耸弄,而不是考慮哪種實現(xiàn)方式適合于這種場景。
達到頂級性能的另一個技術是性能測試卓缰。我早就注意到计呈,人們談論很多技術來提高性能,但真正有效的一件事是測試它征唬。即使是優(yōu)秀的程序員也容易編寫出性能差的應用捌显,所以最好的程序員更喜歡使用性能分析器和編寫測試用例。 LMAX團隊還發(fā)現(xiàn)鳍鸵,將編寫性能測試作為一項紀律能夠更易提升應用性能苇瓣。
編程模型 Programming Model
Business logic processor的一個重要特點就是不和任何外部服務進行交互尉间。因為調用外部服務的速度會比較慢偿乖,processor又是單線程的,因此外部服務會拖慢整個processor的速度哲嘲。Processor只和event交互贪薪,要么接受一個event,要么產(chǎn)生一個event眠副。怎么理解呢画切?舉個例子就明白了,比如電商網(wǎng)站通過信用卡來訂購商品囱怕。普通青年的做法就很直接霍弹,先獲取訂單信息,通過銀聯(lián)的外部服務來驗證信用卡信息是否有效(這意味著信用卡號如果有問題娃弓,根本就不會生成訂單)典格,然后生成訂單信息入庫,這兩步放在一個操作里台丛。由于信用卡驗證服務是一個外部服務耍缴,因此操作往往會被阻塞較長的一段時間。
Lmax則另辟蹊徑挽霉,它把整個操作分為兩個防嗡,第一個操作是獲取用戶填寫的訂單。這個操作的結果是產(chǎn)生一個“信用卡驗證請求”的事件侠坎。第二個操作是當它接受一個“信用卡驗證成功響應”的事件蚁趁,生成訂單入庫。Processor在完成第一個操作之后會接下來執(zhí)行另外其他的事件实胸,直到“信用卡驗證成功響應”事件被插入input disruptor并被processor選取荣德。至于lmax如何根據(jù)“信用卡驗證請求”輸出事件生成另外一個輸入事件-“信用卡驗證成功響應”闷煤,這則是通過output disruptor的多線程來完成的。因此可以看出lmax青睞單線程的態(tài)度并不固執(zhí)涮瞻,而是有自己的原則:IO密集型操作用多線程鲤拿,CPU密集型用單線程。
在這種事件驅動的異步風格下工作有些不尋常 - 盡管使用異步來提高應用程序的響應速度是一種熟悉的技術且它還有助于業(yè)務流程更具彈性署咽,但您必須更加明確地考慮遠程應用程序可能發(fā)生的不同情況近顷。
這個編程模型第二個特點在于錯誤處理 - 傳統(tǒng)模式下會話和數(shù)據(jù)庫事務提供了一個有用的錯誤處理能力。如果有什么出錯宁否,很容易拋出任何東西窒升,這個會話能夠被丟棄。如果一個錯誤發(fā)生在數(shù)據(jù)庫端慕匠,你可以回滾事務饱须。
LMAX的內存結構在輸入事件中是持久的,所以如果出現(xiàn)錯誤台谊,不要讓內存處于不一致的狀態(tài)蓉媳。但是沒有自動回滾功能。因此锅铅,LMAX團隊非常重視確保輸入事件在進行內存中持久狀態(tài)變化之前完全有效酪呻。他們發(fā)現(xiàn)測試是在投入生產(chǎn)之前消除這些問題的關鍵。
Input and Output Disruptors
business logic processor是單線程工作的盐须,玩荠,在processor可以正常進行工作之前還是有很多任務需要做的。Processor的輸入本質上是網(wǎng)絡消息贼邓,為了便于business logic processor處理阶冈,這些網(wǎng)絡消息在送達processor之前需要進行反序列化(unmarshaled)。Event Sourcing的工作依賴于記錄輸入事件塑径,因此輸入消息的日志需要被持久化女坑。
如上圖,由于replicator和journaler涉及到大量的IO晓勇,因此速度相對比較慢堂飞。而business logic processor的中心思想就是避免任何IO。這三個任務相對比較獨立绑咱,它們必須在business logic processor處理消息之前完成绰筛,這些需要并發(fā)控制。
為了處理并發(fā)描融,LMAX團隊開發(fā)了disruptor組件铝噩,并開放了源代碼。
Disruptor可以看成一個事件監(jiān)聽或消息機制窿克,在隊列中一邊生產(chǎn)者放入消息骏庸,另外一邊消費者并行取出處理毛甲。當你進入這個隊列內部查看,發(fā)現(xiàn)其實是一個真正的單個數(shù)據(jù)結構:一個ring buffer具被。 每個生產(chǎn)者和消費者都有一個序數(shù)計算器玻募,以顯示當前緩沖工作方式,每個生產(chǎn)者消費者寫入自己次序計數(shù)器一姿,能夠讀取對方的計數(shù)器七咧,通過這種方式,生產(chǎn)者能夠讀取消費者的計算器確保其在沒有鎖的情況下使用叮叹,類似地消費者也要通過計數(shù)器在另外一個消費者完成后確保它一次只處理一次消息艾栋。
如上圖,每個生產(chǎn)者/消費者都擁有一個序號蛉顽,這個序號表示該生產(chǎn)者/消費者正在處理ring buffer的哪個slot蝗砾。每個生產(chǎn)者/消費者都只能擁有自己序號的寫權限,對于其它消費者/生產(chǎn)者的序號只能讀取而不能更改携冤〉苛福基于這種方法,生產(chǎn)者可以不斷讀取其它消費者的序號來檢查生產(chǎn)者想要寫入的slot是否被占用噪叙,這種方法實際上就是的lock-free矮锈,避免了加鎖霉翔。類似的睁蕾,一個消費者也可以通過觀察其他消費者的序號來確保不會重復處理某些消息。
Output disruptors也類似于此债朵,只不過output disruptor的兩個消費者marshaller和publisher必須是順序執(zhí)行的子眶,也就是說ring buffer里的消息必須經(jīng)過marshaller處理之后才能由publisher公布出去。Publisher發(fā)布出去的事件被組織成了若干個topics序芦,每個事件只會被轉發(fā)到訂閱了該主題的receivers臭杰。
disruptor不但適合一個生產(chǎn)者多個消費者,也適合多個生產(chǎn)者谚中。在這種情況下它任然不需要加鎖渴杆。
disruptor設計的好處是如果遇到問題導致消費落后,消費者就能更容易地趕上宪塔。比如在15號solt有一個unmarshaler(反序列化)問題磁奖,而接受者在31號solt,它能夠從16-30號一次性批量抓取某筐,這種數(shù)據(jù)批讀取能力加快消費者處理比搭,降低整體延遲性。
在Figure 3的例子中南誊,journaler身诺,replicator和un-marshaller各自只有一個實例蜜托,lmax在默認設置下的確是這樣,但是lmax也可以運行多個組件實例霉赡,比如journaller組件可以運行兩個實例橄务,一個處理奇數(shù)slot,一個處理偶數(shù)slot穴亏。是否運行多個實例取決于IO操作的獨立性和IO的阻塞時間仪糖。
Ring buffer是很大的,input ring buffer擁有20 million個slot迫肖,每個output ring buffer也擁有4 million個slot锅劝。序號是一個64位的長整形。Ring Buffer的大小為2的整數(shù)次方蟆湖,這樣有利于做取余運算(sequence number % buffer size)把序號映射成slot號碼故爵。像很多其它的系統(tǒng)一樣,disruptors每天深夜做定期的重啟隅津,這么做的主要原因是回收內存诬垂,盡可能降低在繁忙時段的昂貴的垃圾回收的可能性。
Journaler的主要工作就是持久化存儲所有的事件伦仍,這樣便于當系統(tǒng)出現(xiàn)故障時可以從日志進行恢復结窘。Lmax沒有用數(shù)據(jù)庫來作為持久化存儲,而只是采用文件系統(tǒng)充蓝。它們把事件流寫入磁盤隧枫,由于現(xiàn)代磁盤對于順序存儲的速度很快,而對隨機存儲的速度很慢谓苟,因此lmax的這種做法的性能并不會很差官脓,即使沒有用數(shù)據(jù)庫。
前面我提到lmax會運行多個實例節(jié)點組成一個cluster來支持快速failover涝焙。Replicator用來保持這些實例節(jié)點的同步卑笨。Lmax節(jié)點之間的所有通訊采用的IP廣播,因此備用節(jié)點不需要知道主節(jié)點的IP地址仑撞。只有主節(jié)點運行一個replicator并偵聽輸入事件赤兴。Replicator負責廣播這些input event給備用節(jié)點。一旦主節(jié)點發(fā)生宕機隧哮,主節(jié)點的心跳信號就會丟失桶良,那么另一個備用節(jié)點就會變成主節(jié)點,接著這個新的主節(jié)點就會開始偵聽輸入事件近迁,并啟動自己的replcator艺普。每個節(jié)點是一個完整的lmax實例,有自己的disruptor,自己的journaler歧譬,自己的un-marshaller岸浑。
由于IP廣播消息并不能確保消息的到達順序。主節(jié)點負責決定廣播消息的順序瑰步。
Un-marshaller用于把網(wǎng)絡上的事件順序轉化成business logic processor可以調用的java對象矢洲。和其它的消費者有所不同,un-marshaller需要改變ring buffer中的數(shù)據(jù)缩焦。這里寫(更改數(shù)據(jù))時需要遵守一個原則读虏,那就是每個對象的writable field只能允許眾多并行消費者(也就是un-marshaller)之中的一個來寫,這個原則的目的就是為了避免jvm的偽共享袁滥。
Disruptor可以作為一個單獨的組件被使用盖桥,而不只是用在lmax中,現(xiàn)在lmax已經(jīng)開源了這個組件题翻。作為一件金融交易軟件公司揩徊,lmax的行為的確令人稱道,也希望更多的公司愿意交流或分享自己的架構嵌赠,畢竟技術是在交流中促進的塑荒。回過頭來看姜挺,樂意開源或者愿意分享的公司(比如在infoQ中分享)往往技術上都比較領先齿税。從個人來講,技術人員也應該愿意進行分享炊豪,畢竟這是一個在業(yè)界建立自己聲譽的好機會凌箕。
Queues and their lack of mechanical sympathy
LMAX架構引起了人們的關注,因為這是一個與大多數(shù)人所想的高性能系統(tǒng)完全不同的實現(xiàn)方式溜在。到目前為止陌知,我已經(jīng)談到它是如何工作的他托,但是還沒有深入研究它是以這種方式開發(fā)的掖肋。這個故事本身就很有趣,因為這個架構并不只是拿來演講的赏参。它是經(jīng)過很長的時間志笼,在團隊意識到傳統(tǒng)方案的缺陷后設計的替代方案。
許多商業(yè)系統(tǒng)都有自己的核心架構把篓,依賴于通過事務數(shù)據(jù)庫協(xié)調的多個活動會話纫溃,LMAX團隊也熟悉這些知識,并確信它不適用于LMAX韧掩。這個評估是建立在Betfair(成立LMAX的母公司)的經(jīng)驗基礎之上的紊浩,這是一家體育博彩公司,它處理很多人的體育投賭事件,這是一個相當大的并發(fā)訪問坊谁,鎖競爭十分激烈费彼,傳統(tǒng)數(shù)據(jù)庫幾乎無法應付,這些讓他們相信必須尋找另外一個途徑來突破口芍,他們現(xiàn)在接近目標了箍铲。
他們最初的想法與大多數(shù)業(yè)界方案一樣,為獲得高性能是使用現(xiàn)在流行的并發(fā)編程鬓椭。這意味著允許多線程并行處理多個訂單颠猴。而這些線程必須互相通信。處理訂單的條件等都需要進行線程間通信來確定小染。
早期他們探索了Actor模型和SEDA翘瓮。Actor模型依靠獨立的活動對象和自己的線程通過隊列相互通信。很多人發(fā)現(xiàn)這種并發(fā)模型要比基于鎖定原語的事情更容易處理裤翩。
該團隊建立了一個Actor模型原型春畔,進行性能測試,他們發(fā)現(xiàn)的是處理器會花費更多時間在管理隊列岛都,而不是去做真正應用邏輯律姨,隊列訪問成了真正瓶頸。
當追求性能達到這種程度臼疫,現(xiàn)代硬件構造原理成為很重要的必須了解的知識了择份,馬丁湯普森喜歡用的一句話是“機制偏愛(mechanical sympathy)”,這詞來自賽車駕駛烫堤,它反映的是賽車手對汽車有一種與生俱來的感覺荣赶,使他們能夠感受到如何發(fā)揮它到最好狀態(tài)。許多程序員包括我承認我也陷入這樣一個陣營:不會認為編程如何與硬件等底層機制交互是值得研究的鸽斟。
現(xiàn)代的CPU延遲是影響性能的主導因素之一拔创,在CPU如與內存的交互中,CPU具有多層次的緩存(一級二級)富蓄,每級速度都明顯加快剩燥。因此,如果要提高速度立倍,將您的代碼和數(shù)據(jù)加載到這些緩存中很重要灭红。
在某個層次, Actor模型能夠幫助你,你可以把一個演員當作自己的對象口注,把代碼和數(shù)據(jù)聚類在一起变擒,這是緩存的一個自然單位。但Actor需要溝通寝志,他們通過排隊來進行溝通 - 而LMAX團隊則認為這是干擾緩存的隊列娇斑。(JVM偽分享的問題)策添。
為什么隊列干擾了緩存呢?解釋是這樣的: 為了將數(shù)據(jù)放入隊列毫缆,你需要寫入隊列舰攒,類似地,為了從隊列取出數(shù)據(jù)悔醋,你需要移除隊列 - 也是一種寫摩窃,客戶端也許不只一次寫入同樣數(shù)據(jù)結構享郊,處理寫通常需要鎖们颜,但是如果鎖使用了,會引起切換到底層系統(tǒng)的場景他巨,當這個發(fā)生后账阻,處理器會丟失它的緩存中的數(shù)據(jù)蒂秘。
他們得出的結論是為了夠獲得最好的緩存性能, 你需要設計一個CPU核寫任何內存的設計,多個讀是良好的淘太,處理器會非骋錾快,而隊列失敗在one-writer原則蒲牧,隊列不符合單寫者原則撇贺。(JVM偽共享)
這樣的分析導致LMAX團隊得出一系列結論,導致他們設計出disruptor, 能夠遵循single-writer約束冰抢。 其次松嘶,它引發(fā)了探索單線程業(yè)務邏輯方法的想法,提出了一個單線程如果沒有并發(fā)管理挎扰,可以走多快的問題翠订。
單個線程的本質是:確保你每個CPU核運行一個線程,緩存配合遵倦,盡可能的使用高速緩存而不是主內存尽超。這就意味著代碼和數(shù)據(jù)需要盡可能的一致訪問,將代碼和數(shù)據(jù)放在一起的小對象也可以作為一個單元在緩存之間進行交換梧躺,從而簡化緩存管理并提高性能似谁。
LMAX體系結構的一個重要組成部分是使用性能測試≡镎考慮和放棄基于Actor的方法來自對原型進行構建和性能測試棘脐。類似地,通過性能測試來實現(xiàn)提高各種組件性能很重要龙致。Mechanical sympathy是非常有價值的 - 它有助于形成你能做出什么改進的假設,并且引導你向前邁進顷链,而不是落后 - 最終的測試將給你令人信服的證據(jù)目代。
然而,這種風格的性能測試并不是一個很好理解的話題。 LMAX團隊經(jīng)常強調榛了,開發(fā)有意義的性能測試往往比開發(fā)產(chǎn)品代碼更困難在讶。除非考慮到CPU的高速緩存行為,否則測試低級并發(fā)組件是沒有意義的霜大。
一個特別的經(jīng)驗是編寫針對零組件的測試的重要性构哺,以確保性能測試足夠快以真正衡量真實組件正在做什么。編寫快速的測試代碼并不比編寫快速生產(chǎn)代碼容易战坤,而且容易得到錯誤的結果曙强,因為測試的速度并不像要測試的組件那么快。
Should you use this architecture途茫?
乍一看碟嘴,這個架構是非常小眾的,驅動低延遲的交易系統(tǒng)囊卜,大多數(shù)應用并不需要6百萬TPS娜扇。
但是我對這個架構著迷的原因是它的設計,它移除了很多其他大多數(shù)編程系統(tǒng)的復雜性栅组,傳統(tǒng)圍繞事務性的關系數(shù)據(jù)庫會話并發(fā)模型并不簡單雀瓢,對象/關系數(shù)據(jù)庫映射ORM工具Object/relational mapping tools能夠幫助減輕這種麻煩,但是它不能解決全部問題玉掸,大多數(shù)企業(yè)性能微調還是要糾結于SQL.
現(xiàn)在你能得到服務器更多的主內存致燥,比我們過去這些老家伙得到的磁盤還要多,越來越多應用能夠將他們的工作全部置于內存中排截,這樣消除了復雜性和性能低問題. 事件源驅動(Event Sourcing)提供了一種內存(in-memory)系統(tǒng)的解決方案, 在單個線程運行業(yè)務解決了并發(fā)性能嫌蚤。LMAX的經(jīng)驗建議只要你需要少于幾百萬TPS,你就有足夠的性能提升余地断傲。
這里也是相似于CQRS脱吱,一種事件驅動, in-memory風格的命令系統(tǒng)(盡管LMAX當前沒有使用CQRS)
那么是不是表示你不應該走上這條道路呢?對于像這樣鮮為人知的技術來說认罩,這總是一個棘手的問題箱蝠,因為這個行業(yè)需要更多的時間去探索它的界限。
一個例子是垦垂,一個領域模型宦搬,處理一個事務總是有可能改變如何處理另一個事務。對于彼此獨立的事務來說劫拗,不太需要協(xié)調间校,因此使用并行運行的獨立處理器變得更具吸引力。
LMAX致力于研究事件如何改變世界页慷。許多網(wǎng)站更多的是關于獲取現(xiàn)有的信息存儲憔足,并將這些信息的各種組合呈現(xiàn)出來 - 例如媒體網(wǎng)站胁附。這里的構建挑戰(zhàn)往往集中在如何正確的使用緩存。
LMAX另外一個特點是這是一個后臺系統(tǒng)滓彰,有理由考慮如何在一個交互模型中應用它控妻,比如日益增長的Web應用,當異步通訊在WEB應用越來越多時揭绑,這將改變我們的編程模型弓候。
對于大多數(shù)團隊來說,這些變化需要一些習慣他匪。大多數(shù)人傾向于用同步的方式來理解編程菇存,而不習慣處理異步。然而诚纸,異步通信是提升響應速度的重要工具撰筷。看看在javascript世界中廣泛使用地異步通信(使用AJAX和node.js)是否會鼓勵更多的人了解異步編程畦徘。 LMAX團隊發(fā)現(xiàn)毕籽,雖然花了一些時間來適應異步風格,但它很快變得自然而且容易井辆。特別是在這種方法下关筒,錯誤處理更容易。
LMAX團隊當然認為花力氣協(xié)調事務性關系數(shù)據(jù)庫的日子已經(jīng)屈指可數(shù)杯缺。你可以使用一種更加容易方式編寫程序而且比傳統(tǒng)集中式中央數(shù)據(jù)庫運行得更快蒸播,為什么視而不見呢?
就我而言萍肆,我覺得這是一個非常令人興奮的故事袍榆。我的大部分目標是專注于復雜的業(yè)務領域軟件。像這樣的架構提供了很好的問題分離塘揣,使人們可以專注于領域驅動設計包雀,并保持很好的平臺復雜性。領域對象和數(shù)據(jù)庫之間的緊密耦合一直是一個惱人的方法 - 這樣的方法提出了一個出路亲铡。
附錄
LMAX設計
很多架構師都面臨這么一個問題:如何設計一個高吞吐量才写,低延時的系統(tǒng)?面對這個問題奖蔓,各位都有自己的答案赞草。但面對這個問題,大家似乎漸漸形成了一個共識:并發(fā)是解決之道吆鹤。大家似乎都這么認為:對于服務器而言厨疙,由于多核越來越普遍,因此我們的程序必須要充分利用多線程檀头,為了讓多線程工作得更好轰异,必須有一個與之匹配的高效的并發(fā)模型岖沛。于是各種各樣的并發(fā)模型被提出來暑始,比如Actor模型搭独,比如SEDA模型(Actor模型的表弟),比如Software Transactional Memory模型(準確得講廊镜,STM和其他兩個模型所處在的視角是不一樣的牙肝,Actor和SEDA更多是一種編程模型,而STM更類似一種思想嗤朴,其實我們常用到的Lock-Free機制都包含了STM的思想在里頭)配椭。
這些模型得到了廣泛討論和應用。但這些模型都有一個討厭之處-麻煩雹姊。這個麻煩是由多線程復雜的天性帶來的股缸,很難避免。除了麻煩吱雏,這些模型還忽視了另外一個問題敦姻,由于這個問題的忽視,可能導致這些模型在解決高性能問題的道路上走到了一個錯誤的方向歧杏。這個問題就是JVM的偽共享問題镰惦。所謂JVM的偽共享,簡單來說犬绒,就是JVM的每一個操作指令都是基于一個緩存行旺入,同一個緩存行中的數(shù)據(jù)是不能同時被多個線程同時修改的,也就是說凯力,如果多線程各自操作的數(shù)據(jù)位于同一個緩存行茵瘾,那么這幾個線程訪問數(shù)據(jù)時實際上被加上了一把隱形的鎖,它們實際上在順序地訪問數(shù)據(jù)咐鹤。(如果你看過JDK Concurrent的實現(xiàn)拗秘,你可以看到有些類很奇怪得加了很多無用的padding成員,這就是為了填充緩存行慷暂,從而繞過JVM的偽共享)聘殖。由于JVM偽共享的存在,使得多線程在某些情況下成了一個擺設行瑞。這也就是說大多數(shù)情況下我們的槍炮瞄錯了方向奸腺,我們通常認為沒有充分利用多線程壓榨多個CPU的能力是造成性能問題的原因,實際上緩存問題才是性能殺手血久。
于是LMAX就做了一個大膽的嘗試突照。既然多線程在JVM中有可能成為擺設,而且又這么麻煩氧吐,那么干脆回到單線程來吧讹蘑。用單線程來實現(xiàn)一個高吞吐量末盔,低延時的系統(tǒng)?聽起來很瘋狂座慰,但實際上是可能的陨舱。LMAX就用單線程實現(xiàn)了一個吞吐量達到百萬TPS的系統(tǒng)。
這里講LMAX是單線程版仔,并不是它完全只有一個線程游盲,LMAX組件還是有用到多線程。只不過LMAX充分認識到了單線程的意義蛮粮,在某些組件中大膽得采用單線程的架構益缎,這就是LMAX所謂的單線程。LMAX決定組件是否采用單線程的依據(jù)很簡單然想,如果某一個組件是IO密集型的莺奔,那么這個組件的設計就使用多線程。如果某一個組件是CPU密集型的变泄,那么該組件就使用單線程的設計令哟。這么做的理由很簡單,IO密集型的組件的操作一般都很慢杖刷,往往會阻塞線程励饵,因此使用多線程來競爭執(zhí)行,有提高的余地滑燃。而CPU密集型的操作役听,如果采用多線程的設計,一方面可能會陷入JVM偽共享的陷阱表窘,另一方面多線程之間的同步會帶來開發(fā)的復雜性典予,同時多線程會競爭某些資源,比如隊列等等乐严,這些競爭會對計算機cache命中造成擾動瘤袖,而且有可能引入鎖這種性能殺手,與這兩點相比昂验,多線程帶來的好處相當有限捂敌,因此就采用單線程。
LMAX的原則
LMAX的設計令人耳目一新既琴,它的設計也向我們分享了高性能計算中的幾個重要經(jīng)驗或者說原則:
1. 所有的架構師和開發(fā)人員都應該具有良好的Mechanical Sympathy(這個單詞不太好翻譯占婉,“機制共鳴”?)所謂Mechanical Sympathy甫恩,實際上就是指架構設計者應該對現(xiàn)代操作系統(tǒng)逆济,現(xiàn)代服務器的底層運行機制有良好的理解和認識,設計的時候充分考慮到這些機制,能夠和它們產(chǎn)生共鳴奖慌。這很容易理解抛虫,如果一個架構師對底層機制的認識不夠深入或者還停留在過去,那么很難想象這樣的架構師能設計出一個基于現(xiàn)代服務器的高效系統(tǒng)來简僧。LMAX在文檔中向我們分享了幾點對現(xiàn)代服務器的認識:
1.1 內存
衡量內存有兩個指標:Bandwidth和Latency建椰。所謂bandwidth指的是內存在單位時間內通過內存總線的數(shù)據(jù)量,它計算公式是bandwidth = 傳輸倍率總線位寬工作頻率/8涎劈,單位是Bytes/s(字節(jié)每秒)广凸。傳輸倍率指的是內存在一個脈沖周期內傳輸數(shù)據(jù)的次數(shù)阅茶,比如DDR一個脈沖周期內可以在上升沿傳遞一次數(shù)據(jù)蛛枚,在下降沿傳遞一次數(shù)據(jù),而SDRAM只能在脈沖周期的上升沿傳遞數(shù)據(jù)脸哀。工作頻率的是內存的工作頻率蹦浦,比如133Mhz等等。
而Latency是指內存總線發(fā)出訪問請求到內存總線返回數(shù)據(jù)之間的延遲時間撞蜂,單位是納秒盲镶。
這兩個指標描述了內存性能的兩個方面,bandwidth描述了內存可以以多快的速度來傳遞數(shù)據(jù)蝌诡,反映了吞吐量溉贿,而latency從更底層的細節(jié)描述了內存的物理性能。一個內存的bandwidth說明的僅僅是內存在內存邊界的傳輸?shù)乃俾势趾担鴶?shù)據(jù)在內存內部的流動速度是靠latency來決定宇色。這就像趕飛機,bandwidth就好像是T3航站樓的門的大小颁湖,門的大小決定了T3每秒能夠接納的旅客數(shù)量宣蠕。而安檢的速度就是latency,它也會影響你最終登上飛機的時間甥捺。Bandwidth加上latency才能完全描述內存整個環(huán)節(jié)的性能抢蚀。最終內存的性能可以用內存性能 = (bandwidth*latency)來近似描述(Little’s Law)。
盡管硬件技術一日千里镰禾,但這些年來皿曲,服務器內存的延遲并沒有發(fā)生數(shù)量級的變化。但是內存的bandwidth還是獲得了很大的進步吴侦,因此整體而言內存的性能還是有比較大的提高屋休。另外內存的容量也是越來越大,144G大小的內存配置也是相當普遍妈倔。
1.2 CPU
對于CPU而言博投,單純的提高主頻的方法已經(jīng)走到盡頭,Intel的主頻可能會在Ghz這個量級上停留很長的一段時間盯蝴。CPU的核數(shù)是越來越多毅哗,24核的服務器也很普遍听怕。CPU的緩存機制也越來越強大,一方面CPU緩存變大了虑绵,另一方面Intel又提出了Smart cache等概念尿瞭,相比于傳統(tǒng)的L1 cache, L2 cache又提高了一步翅睛。
1.3 網(wǎng)絡
服務器本地的網(wǎng)絡響應時間非成椋快,處于sub 10 microseconds這個級別捕发。(10 ms在操作系統(tǒng)中一般是一個時鐘滴答疏旨,sub 10 microseconds意味著小于一個時鐘滴答,我們知道Linux的延時扎酷,線程切換都是基于時鐘滴答的檐涝,也就是說本地網(wǎng)絡速度是很快的,對于大多數(shù)的應用來講法挨,幾乎可以忽略不計)谁榜。
廣域網(wǎng)的帶寬是比較便宜的。10GigE(10Gbps的以太網(wǎng)卡)的服務器非常普遍凡纳。Multi-cast技術越來越得到關注窃植,應用也越來越多。
1.4 存儲
硬盤是新一代的磁帶荐糜。磁盤對于順序訪問的速度是非诚锪快的。對于并發(fā)的隨機訪問狞尔,考慮采用SSD丛版。SSD的接口一般都是PCI總線接口,速度更快偏序。
2. 把工作放到內存中來页畦。
盡可能把一些數(shù)據(jù)都放到內存中來,避免和磁盤的低效交互研儒。
3. 寫的代碼要緩存友好豫缨。
什么樣的代碼是緩存友好的代碼?這個一言難盡端朵。但總的原則就是好芭,保持訪問的局部性,也就是說盡可能使一段時間內的訪問保持在一個狹小的內存范圍內冲呢。常用的一個做法就是舍败,先統(tǒng)一分配一個對象池,然后復用對象池中的對象,不要每次都是重新分配新的對象邻薯。
上圖顯示了各個層次的緩存的訪問效率裙戏,提醒我們要對緩存敏感。
4. 要時刻牢記厕诡,代碼要干凈累榜,簡練。
- Hotspot虛擬機喜歡短小灵嫌,簡練的代碼壹罚;
- 如果CPU的分支預測不準確,那么CPU流水線會被阻斷寿羞;
- 復雜的代碼是一個危險的信號猖凛,這意味著你有可能沒有正確理解問題的領域(DDD里的概念);
- 世界上的事情都不會很復雜稠曼,除了扣稅的方法形病。
- 多花點時間考慮一下你的領域模型。
采用正確的方法來實現(xiàn)并發(fā)霞幅。
記住這么幾個原則:
- 責任單一:一個類只干一件事,一個方法也只干一件事量瓜,不要臃腫的類或方法司恳。
- 了解你的數(shù)據(jù)結構和關系基數(shù)(一對一的關系?一對多绍傲?還是多對多)
- 讓關系來完成工作扔傅,比如“書架”和“書”之間存在一個“attach”的關系,既然如此烫饼,我們可以讓“書架”有一個方法叫attach猎塞,用來處理添加書本的工作,這就是讓關系來完成工作杠纵。這實際上也是DDD里面的一些設計原則荠耽。
實現(xiàn)并發(fā)需要考慮兩件事:資源互斥和變化可見(讓結果以一個正確的順序出現(xiàn))。
并發(fā)的實現(xiàn)一般有兩種方法:第一個方法是用鎖來保證比藻,另一個方法是借助于CAS進行無鎖編程铝量。使用鎖會導致內核態(tài)的切換,但總可以確保任何時刻總有一個進程會被執(zhí)行(相比之下Lock-Free如果代碼邏輯出現(xiàn)問題银亲,有可能所有線程都處在自旋等待狀態(tài)慢叨,無法前進),鎖也增加了編程的難度务蝠。而借助于CAS的Lock-Free則始終是運行在用戶態(tài)的(節(jié)省了效率拍谐,避免了無謂的上下文切換),相比于鎖,它的編程難度更加大轩拨。下面圖形象地表達了Lock和Lock-Free之間的區(qū)別:
這些原則大部分都是老生常談力穗,但很容易被人忽略,總之這些原則提醒我們:
- 很多程序員對現(xiàn)代服務器的硬件有著一個錯誤的認識或者根本沒有認識气嫁,他們根本就不知道單線程所能達到的性能高度当窗。
- 對于現(xiàn)代處理器,緩存丟失才是性能的最大殺手寸宵。
- 架構設計時崖面,把并發(fā)放到infrastructure層里去考慮,這樣一方面使得應用層的編寫避免了并發(fā)編程的復雜性梯影,另一方面由于并發(fā)放在了相對單純的infrastructure層巫员,避免了來自應用層的亂七八糟的干擾,更容易優(yōu)化甲棍。
- 牢記上述3條原則简识,一旦你實現(xiàn)了這3條,那恭喜你感猛,你已經(jīng)進入了理想王國:
單線程:
- 所有的一切都在內存中七扰;
- 優(yōu)雅的模式;
- 易于測試的代碼陪白;
- 不用擔心infrastructure和集成的問題颈走。
JVM偽共享
偽共享False sharing。內存緩存系統(tǒng)中基本單元是高速緩存行(Cache lines)咱士,CPU會把數(shù)據(jù)從內存加載到高速緩存中 ,這樣可以獲得更好的性能立由,高速緩存默認大小是64 Byte為一個區(qū)域,一個區(qū)域在一個時間點只允許一個核心操作序厉,也就是說不能有多個核心同時操作一個緩存區(qū)域锐膜。
因為高速緩存是64字節(jié),而Hotspot JVM的對象頭是兩個部分組成弛房,第一部分是由24字節(jié)的hash code和8字節(jié)的鎖等狀態(tài)標識組成道盏,第二部分是指向該對象類的引用⊥ピ伲基本類型字節(jié)如下:
doubles (8) and longs (8)
ints (4) and floats (4)
shorts (2) and chars (2)
booleans (1) and bytes (1)
references (4/8)
因此捞奕,一個高速緩存64字節(jié)可以放下多個字段,如果這多個字段位于同一個高速緩存區(qū)拄轻,雖然它們是類的不同字段颅围,如下代碼:
Class A{
int x;
int y;
}
x和y被放在同一個高速緩存區(qū),如果一個線程修改x恨搓;那么另外一個線程修改y院促,必須等待x修改完成后才能實施筏养。雖然兩個線程修改各種獨立變量,但是因為這些獨立變量被放在同一個高速緩存區(qū)常拓,性能就影響了渐溶。當多核CPU線程同時修改在同一個高速緩存行各自獨立的變量時,會不自不覺地影響性能弄抬,這就發(fā)生了偽共享False sharing茎辐,偽共享是性能的無聲殺手。
解決方案是將高速緩存剩余的字節(jié)填充填滿(padding)掂恕,確保不發(fā)生多個字段被擠入一個高速緩存區(qū)拖陆。Disruptor在RingBuffer中實現(xiàn)了填充,用1毫秒的延時得到100K+ TPS吞吐量懊亡。