LMAX架構(gòu)

LMAX是一種新型零售金融交易平臺(tái),它能夠以很低的延遲(latency)產(chǎn)生大量交易(吞吐量). 這個(gè)系統(tǒng)是建立在JVM平臺(tái)上,核心是一個(gè)業(yè)務(wù)邏輯處理器,它能夠在一個(gè)線(xiàn)程里每秒處理6百萬(wàn)訂單. 業(yè)務(wù)邏輯處理器完全是運(yùn)行在內(nèi)存中(in-memory),使用事件源驅(qū)動(dòng)方式(event sourcing). 業(yè)務(wù)邏輯處理器的核心是Disruptors,這是一個(gè)并發(fā)組件,能夠在無(wú)鎖的情況下實(shí)現(xiàn)網(wǎng)絡(luò)的Queue并發(fā)操作。他們的研究表明,現(xiàn)在的所謂高性能研究方向似乎和現(xiàn)代CPU設(shè)計(jì)是相左的。(JVM偽共享)

過(guò)去幾年我們不斷提供這樣聲音:免費(fèi)午餐已經(jīng)結(jié)束。我們不再能期望在單個(gè)CPU上獲得更快的性能,因此我們需要寫(xiě)使用多核處理的并發(fā)軟件,不幸的是, 編寫(xiě)并發(fā)軟件是很難的,鎖和信號(hào)量是很難理解的和難以測(cè)試,這意味著我們要花更多時(shí)間在計(jì)算機(jī)上,而不是我們的領(lǐng)域問(wèn)題。諸如各種并發(fā)模型,如Actors和軟事務(wù)STM(Software Transactional Memory), 目的是更加容易使用,但是任然還是帶來(lái)了bugs和復(fù)雜性.

我很驚訝聽(tīng)到去年3月QCon上一個(gè)演講, LMAX是一種新的零售的金融交易平臺(tái)。它的業(yè)務(wù)創(chuàng)新是他的一個(gè)零售平臺(tái),允許任何人交易一系列的金融衍生產(chǎn)品。這樣的平臺(tái)需要非常低的延遲,非??焖俚奶幚恚?yàn)槭袌?chǎng)變化很快,一個(gè)零售平臺(tái)增加了復(fù)雜性,因?yàn)樗仨殲楹芏嗳俗龅竭@一點(diǎn)。所以結(jié)果是更多的用戶(hù),有很多的交易,所有這些都需要被快速處理。

鑒于多核心思想的轉(zhuǎn)變,這種苛刻的性能自然會(huì)提出一個(gè)明確的并行編程模型,實(shí)際上這是他們的出發(fā)點(diǎn)。但QCon引起人們注意的是,這不是他們最終的目標(biāo)。事實(shí)上,他們提出僅僅使用一個(gè)線(xiàn)程處理所有的客戶(hù)的所有的交易,在通用的硬件上達(dá)到了每秒處理6百萬(wàn)訂單。

通過(guò)低延遲處理大量交易,取得低延遲和高吞吐量,而且沒(méi)有并發(fā)代碼的復(fù)雜性,他們是怎么做到呢?現(xiàn)在LMAX已經(jīng)產(chǎn)品化一段時(shí)間了,現(xiàn)在應(yīng)該可以揭開(kāi)其神秘而迷人的面紗了。

結(jié)構(gòu)如圖:

image

從最高層次看,架構(gòu)有三個(gè)部分:

  • 業(yè)務(wù)邏輯處理器business logic processor;
  • 輸入input disruptor;
  • 輸出output disruptors;

業(yè)務(wù)邏輯處理器處理所有的應(yīng)用程序的業(yè)務(wù)邏輯,這是一個(gè)單線(xiàn)程的Java程序來(lái)響應(yīng)方法調(diào)用并產(chǎn)生輸出事件,因此這是一個(gè)簡(jiǎn)單的Java程序,不需要任何平臺(tái)框架就可以運(yùn)行(需要JVM),這就保證其很容易運(yùn)行在測(cè)試環(huán)境中。

Input Disruptor是用來(lái)處理輸入消息的,輸入消息從網(wǎng)絡(luò)中接收,需要進(jìn)行反序列化(unmarshaled),需要進(jìn)行replicated避免單點(diǎn)故障,需要journaled來(lái)記錄消息日志從而能夠進(jìn)行故障恢復(fù)。Output Disruptors用來(lái)處理輸出消息,這些消息需要進(jìn)行序列化以便于網(wǎng)絡(luò)傳輸。Input Disruptor和output disruptor都是多線(xiàn)程的,因?yàn)樗麄冊(cè)O(shè)計(jì)到大量的IO操作,這些IO操作很慢而且相互獨(dú)立。

業(yè)務(wù)邏輯處理器 Business Logic Processor

全部駐留在內(nèi)存中 Keeping it all in memory

業(yè)務(wù)邏輯處理器按順序(方法調(diào)用的形式)接受輸入消息,然后運(yùn)行其中的業(yè)務(wù)邏輯,并產(chǎn)生輸出事件,整個(gè)操作都是在內(nèi)存中,沒(méi)有數(shù)據(jù)庫(kù)或其他持久化存儲(chǔ)。將所有數(shù)據(jù)駐留在內(nèi)存中有兩個(gè)重要好處:首先是快,沒(méi)有IO,也沒(méi)有事務(wù),因?yàn)樗械奶幚矶际前错樞驁?zhí)行的,第二個(gè)好處是簡(jiǎn)化編程,沒(méi)有對(duì)象/關(guān)系數(shù)據(jù)庫(kù)的映射,所有代碼都可以使用Java對(duì)象模型編寫(xiě),不必為映射到數(shù)據(jù)庫(kù)做任何妥協(xié)。

由于一切都在內(nèi)存中處理,因此需要任職考慮的問(wèn)題是萬(wàn)一Crash了怎么辦?可伸縮性再好的系統(tǒng)也會(huì)受到很多其他因素的影響,比如掉電。處理這個(gè)問(wèn)題的核心是“事件”(Event Sourcing )機(jī)制,這意味著業(yè)務(wù)邏輯處理器的當(dāng)前狀態(tài)完全可以通過(guò)處理輸入事件來(lái)推導(dǎo)出來(lái)。只要輸入事件保存在一個(gè)持久存儲(chǔ)(這是輸入Input Disruptor的作業(yè)之一)中,就可以通過(guò)重播事件來(lái)重新創(chuàng)建業(yè)務(wù)邏輯處理的當(dāng)前狀態(tài)。

可以基于NOSQL處理事務(wù)性事件存儲(chǔ)

理解這個(gè)的一個(gè)好方法就是對(duì)于一個(gè)版本控制系統(tǒng)。版本控制系統(tǒng)處理一系列的提交,任何時(shí)候你都可以通過(guò)應(yīng)用這些提交來(lái)構(gòu)建一個(gè)工作拷貝。VCS比業(yè)務(wù)邏輯處理器更復(fù)雜,因?yàn)樗鼈儽仨氈С址种?,而業(yè)務(wù)邏輯處理器是一個(gè)簡(jiǎn)單的序列。

因此,從理論上講,您總是可以通過(guò)重新處理所有事件來(lái)重建業(yè)務(wù)邏輯處理器的狀態(tài),但是實(shí)踐中重建所有事件是耗時(shí)的,因此,正如版本控制系統(tǒng)一樣,LMAX可以創(chuàng)建業(yè)務(wù)邏輯處理器狀態(tài)的快照并從快照中恢復(fù)。在每天晚上系統(tǒng)不繁忙時(shí)構(gòu)建快照,通過(guò)快照重新啟動(dòng)業(yè)務(wù)邏輯處理器的速度很快,一個(gè)完整的重新啟動(dòng),包括重新啟動(dòng)JVM、加載最近的快照和重放一天事件,不到一分鐘。

如果業(yè)務(wù)邏輯處理器在下午2時(shí)崩潰,通過(guò)快照啟動(dòng)恢復(fù)的方式依然不夠快。LMAX的方案是保持多個(gè)業(yè)務(wù)邏輯處理器同時(shí)運(yùn)行,一個(gè)輸入事件由多個(gè)業(yè)務(wù)處理器處理,但只有一個(gè)業(yè)務(wù)處理輸出會(huì)被保留。如果一個(gè)處理器處理失敗,則切換到另外一個(gè),這種故障轉(zhuǎn)移是使用事件驅(qū)動(dòng)(Event Sourcing)的另外一個(gè)好處。

通過(guò)事件復(fù)制機(jī)制(replicas),多個(gè)業(yè)務(wù)處理器可以在微秒間切換。除了每天晚上創(chuàng)建快照,它們還每天晚上重啟業(yè)務(wù)邏輯處理器。復(fù)制機(jī)制(replicas)允許他們?cè)跊](méi)有停機(jī)的情況下這樣做,因此系統(tǒng)將7x24小時(shí)全天候處理交易。

事件機(jī)制的價(jià)值不僅體現(xiàn)在允許完全在內(nèi)存中進(jìn)行處理,對(duì)于系統(tǒng)診斷測(cè)試也是有相當(dāng)大的優(yōu)勢(shì)。如果系統(tǒng)發(fā)生意外的行為,團(tuán)隊(duì)會(huì)將事件順序復(fù)制到其開(kāi)發(fā)環(huán)境中,并在那里重播。 這使得他們能夠比在大多數(shù)環(huán)境中更容易地檢查發(fā)生的事情。

從基礎(chǔ)診斷功能可擴(kuò)展到業(yè)務(wù)診斷。有一些業(yè)務(wù)任務(wù),如風(fēng)險(xiǎn)管理,需要大量的計(jì)算,而不需要處理訂單。一個(gè)例子就是根據(jù)目前的交易倉(cāng)位,獲得風(fēng)險(xiǎn)狀況前20位客戶(hù)的名單。 團(tuán)隊(duì)通過(guò)復(fù)制域模型并在那里執(zhí)行計(jì)算來(lái)處理這個(gè)問(wèn)題,在那里它不會(huì)干擾核心訂單處理。這些分析域模型可以具有不同的數(shù)據(jù)模型,將不同的數(shù)據(jù)集保存在內(nèi)存中,并在不同的機(jī)器上運(yùn)行。

性能優(yōu)化 Tuning performance

到目前為止,我已經(jīng)解釋過(guò),業(yè)務(wù)邏輯處理器速度的關(guān)鍵是在內(nèi)存中順序地執(zhí)行所有的事情。只要做到這一點(diǎn)(并沒(méi)有什么真正愚蠢的,并行就聰明嗎?),開(kāi)發(fā)人員就可以編寫(xiě)能夠處理10K TPS的代碼。然后他們發(fā)現(xiàn)只需要使用良好的代碼和小方法就可以使其達(dá)到100K TPS的范圍。當(dāng)然,JVM Hotspot的緩存微調(diào),讓其更加優(yōu)化也是必須的。

再過(guò)一個(gè)數(shù)量級(jí)就更巧妙了一些。有幾件事LMAX團(tuán)隊(duì)發(fā)現(xiàn)有助于提升性能。一個(gè)是編寫(xiě)java集合的自定義實(shí)現(xiàn),這些實(shí)現(xiàn)被小心的設(shè)計(jì)為緩存友好。一個(gè)例子是使用原始的Java longs作為散列映射關(guān)鍵字,并帶有一個(gè)專(zhuān)門(mén)編寫(xiě)的數(shù)組支持的Map實(shí)現(xiàn)(LongToObjectHashMap)。一般來(lái)說(shuō),他們發(fā)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的選擇通常會(huì)造成很大的差別,大多數(shù)程序員只是抓住上次使用的List,而不是考慮哪種實(shí)現(xiàn)方式適合于這種場(chǎng)景。

達(dá)到頂級(jí)性能的另一個(gè)技術(shù)是性能測(cè)試。我早就注意到,人們談?wù)摵芏嗉夹g(shù)來(lái)提高性能,但真正有效的一件事是測(cè)試它。即使是優(yōu)秀的程序員也容易編寫(xiě)出性能差的應(yīng)用,所以最好的程序員更喜歡使用性能分析器和編寫(xiě)測(cè)試用例。 LMAX團(tuán)隊(duì)還發(fā)現(xiàn),將編寫(xiě)性能測(cè)試作為一項(xiàng)紀(jì)律能夠更易提升應(yīng)用性能。

編程模型 Programming Model

Business logic processor的一個(gè)重要特點(diǎn)就是不和任何外部服務(wù)進(jìn)行交互。因?yàn)檎{(diào)用外部服務(wù)的速度會(huì)比較慢,processor又是單線(xiàn)程的,因此外部服務(wù)會(huì)拖慢整個(gè)processor的速度。Processor只和event交互,要么接受一個(gè)event,要么產(chǎn)生一個(gè)event。怎么理解呢?舉個(gè)例子就明白了,比如電商網(wǎng)站通過(guò)信用卡來(lái)訂購(gòu)商品。普通青年的做法就很直接,先獲取訂單信息,通過(guò)銀聯(lián)的外部服務(wù)來(lái)驗(yàn)證信用卡信息是否有效(這意味著信用卡號(hào)如果有問(wèn)題,根本就不會(huì)生成訂單),然后生成訂單信息入庫(kù),這兩步放在一個(gè)操作里。由于信用卡驗(yàn)證服務(wù)是一個(gè)外部服務(wù),因此操作往往會(huì)被阻塞較長(zhǎng)的一段時(shí)間。

Lmax則另辟蹊徑,它把整個(gè)操作分為兩個(gè),第一個(gè)操作是獲取用戶(hù)填寫(xiě)的訂單。這個(gè)操作的結(jié)果是產(chǎn)生一個(gè)“信用卡驗(yàn)證請(qǐng)求”的事件。第二個(gè)操作是當(dāng)它接受一個(gè)“信用卡驗(yàn)證成功響應(yīng)”的事件,生成訂單入庫(kù)。Processor在完成第一個(gè)操作之后會(huì)接下來(lái)執(zhí)行另外其他的事件,直到“信用卡驗(yàn)證成功響應(yīng)”事件被插入input disruptor并被processor選取。至于lmax如何根據(jù)“信用卡驗(yàn)證請(qǐng)求”輸出事件生成另外一個(gè)輸入事件-“信用卡驗(yàn)證成功響應(yīng)”,這則是通過(guò)output disruptor的多線(xiàn)程來(lái)完成的。因此可以看出lmax青睞單線(xiàn)程的態(tài)度并不固執(zhí),而是有自己的原則:IO密集型操作用多線(xiàn)程,CPU密集型用單線(xiàn)程。

在這種事件驅(qū)動(dòng)的異步風(fēng)格下工作有些不尋常 - 盡管使用異步來(lái)提高應(yīng)用程序的響應(yīng)速度是一種熟悉的技術(shù)且它還有助于業(yè)務(wù)流程更具彈性,但您必須更加明確地考慮遠(yuǎn)程應(yīng)用程序可能發(fā)生的不同情況。

這個(gè)編程模型第二個(gè)特點(diǎn)在于錯(cuò)誤處理 - 傳統(tǒng)模式下會(huì)話(huà)和數(shù)據(jù)庫(kù)事務(wù)提供了一個(gè)有用的錯(cuò)誤處理能力。如果有什么出錯(cuò),很容易拋出任何東西,這個(gè)會(huì)話(huà)能夠被丟棄。如果一個(gè)錯(cuò)誤發(fā)生在數(shù)據(jù)庫(kù)端,你可以回滾事務(wù)。

LMAX的內(nèi)存結(jié)構(gòu)在輸入事件中是持久的,所以如果出現(xiàn)錯(cuò)誤,不要讓內(nèi)存處于不一致的狀態(tài)。但是沒(méi)有自動(dòng)回滾功能。因此,LMAX團(tuán)隊(duì)非常重視確保輸入事件在進(jìn)行內(nèi)存中持久狀態(tài)變化之前完全有效。他們發(fā)現(xiàn)測(cè)試是在投入生產(chǎn)之前消除這些問(wèn)題的關(guān)鍵。

Input and Output Disruptors

business logic processor是單線(xiàn)程工作的,,在processor可以正常進(jìn)行工作之前還是有很多任務(wù)需要做的。Processor的輸入本質(zhì)上是網(wǎng)絡(luò)消息,為了便于business logic processor處理,這些網(wǎng)絡(luò)消息在送達(dá)processor之前需要進(jìn)行反序列化(unmarshaled)。Event Sourcing的工作依賴(lài)于記錄輸入事件,因此輸入消息的日志需要被持久化。

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

如上圖,由于replicator和journaler涉及到大量的IO,因此速度相對(duì)比較慢。而business logic processor的中心思想就是避免任何IO。這三個(gè)任務(wù)相對(duì)比較獨(dú)立,它們必須在business logic processor處理消息之前完成,這些需要并發(fā)控制。

為了處理并發(fā),LMAX團(tuán)隊(duì)開(kāi)發(fā)了disruptor組件,并開(kāi)放了源代碼。

Disruptor可以看成一個(gè)事件監(jiān)聽(tīng)或消息機(jī)制,在隊(duì)列中一邊生產(chǎn)者放入消息,另外一邊消費(fèi)者并行取出處理。當(dāng)你進(jìn)入這個(gè)隊(duì)列內(nèi)部查看,發(fā)現(xiàn)其實(shí)是一個(gè)真正的單個(gè)數(shù)據(jù)結(jié)構(gòu):一個(gè)ring buffer。 每個(gè)生產(chǎn)者和消費(fèi)者都有一個(gè)序數(shù)計(jì)算器,以顯示當(dāng)前緩沖工作方式,每個(gè)生產(chǎn)者消費(fèi)者寫(xiě)入自己次序計(jì)數(shù)器,能夠讀取對(duì)方的計(jì)數(shù)器,通過(guò)這種方式,生產(chǎn)者能夠讀取消費(fèi)者的計(jì)算器確保其在沒(méi)有鎖的情況下使用,類(lèi)似地消費(fèi)者也要通過(guò)計(jì)數(shù)器在另外一個(gè)消費(fèi)者完成后確保它一次只處理一次消息。

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

如上圖,每個(gè)生產(chǎn)者/消費(fèi)者都擁有一個(gè)序號(hào),這個(gè)序號(hào)表示該生產(chǎn)者/消費(fèi)者正在處理ring buffer的哪個(gè)slot。每個(gè)生產(chǎn)者/消費(fèi)者都只能擁有自己序號(hào)的寫(xiě)權(quán)限,對(duì)于其它消費(fèi)者/生產(chǎn)者的序號(hào)只能讀取而不能更改?;谶@種方法,生產(chǎn)者可以不斷讀取其它消費(fèi)者的序號(hào)來(lái)檢查生產(chǎn)者想要寫(xiě)入的slot是否被占用,這種方法實(shí)際上就是的lock-free,避免了加鎖。類(lèi)似的,一個(gè)消費(fèi)者也可以通過(guò)觀(guān)察其他消費(fèi)者的序號(hào)來(lái)確保不會(huì)重復(fù)處理某些消息。

Output disruptors也類(lèi)似于此,只不過(guò)output disruptor的兩個(gè)消費(fèi)者marshaller和publisher必須是順序執(zhí)行的,也就是說(shuō)ring buffer里的消息必須經(jīng)過(guò)marshaller處理之后才能由publisher公布出去。Publisher發(fā)布出去的事件被組織成了若干個(gè)topics,每個(gè)事件只會(huì)被轉(zhuǎn)發(fā)到訂閱了該主題的receivers。

disruptor不但適合一個(gè)生產(chǎn)者多個(gè)消費(fèi)者,也適合多個(gè)生產(chǎn)者。在這種情況下它任然不需要加鎖。

disruptor設(shè)計(jì)的好處是如果遇到問(wèn)題導(dǎo)致消費(fèi)落后,消費(fèi)者就能更容易地趕上。比如在15號(hào)solt有一個(gè)unmarshaler(反序列化)問(wèn)題,而接受者在31號(hào)solt,它能夠從16-30號(hào)一次性批量抓取,這種數(shù)據(jù)批讀取能力加快消費(fèi)者處理,降低整體延遲性。

在Figure 3的例子中,journaler,replicator和un-marshaller各自只有一個(gè)實(shí)例,lmax在默認(rèn)設(shè)置下的確是這樣,但是lmax也可以運(yùn)行多個(gè)組件實(shí)例,比如journaller組件可以運(yùn)行兩個(gè)實(shí)例,一個(gè)處理奇數(shù)slot,一個(gè)處理偶數(shù)slot。是否運(yùn)行多個(gè)實(shí)例取決于IO操作的獨(dú)立性和IO的阻塞時(shí)間。

Ring buffer是很大的,input ring buffer擁有20 million個(gè)slot,每個(gè)output ring buffer也擁有4 million個(gè)slot。序號(hào)是一個(gè)64位的長(zhǎng)整形。Ring Buffer的大小為2的整數(shù)次方,這樣有利于做取余運(yùn)算(sequence number % buffer size)把序號(hào)映射成slot號(hào)碼。像很多其它的系統(tǒng)一樣,disruptors每天深夜做定期的重啟,這么做的主要原因是回收內(nèi)存,盡可能降低在繁忙時(shí)段的昂貴的垃圾回收的可能性。

Journaler的主要工作就是持久化存儲(chǔ)所有的事件,這樣便于當(dāng)系統(tǒng)出現(xiàn)故障時(shí)可以從日志進(jìn)行恢復(fù)。Lmax沒(méi)有用數(shù)據(jù)庫(kù)來(lái)作為持久化存儲(chǔ),而只是采用文件系統(tǒng)。它們把事件流寫(xiě)入磁盤(pán),由于現(xiàn)代磁盤(pán)對(duì)于順序存儲(chǔ)的速度很快,而對(duì)隨機(jī)存儲(chǔ)的速度很慢,因此lmax的這種做法的性能并不會(huì)很差,即使沒(méi)有用數(shù)據(jù)庫(kù)。

前面我提到lmax會(huì)運(yùn)行多個(gè)實(shí)例節(jié)點(diǎn)組成一個(gè)cluster來(lái)支持快速failover。Replicator用來(lái)保持這些實(shí)例節(jié)點(diǎn)的同步。Lmax節(jié)點(diǎn)之間的所有通訊采用的IP廣播,因此備用節(jié)點(diǎn)不需要知道主節(jié)點(diǎn)的IP地址。只有主節(jié)點(diǎn)運(yùn)行一個(gè)replicator并偵聽(tīng)輸入事件。Replicator負(fù)責(zé)廣播這些input event給備用節(jié)點(diǎn)。一旦主節(jié)點(diǎn)發(fā)生宕機(jī),主節(jié)點(diǎn)的心跳信號(hào)就會(huì)丟失,那么另一個(gè)備用節(jié)點(diǎn)就會(huì)變成主節(jié)點(diǎn),接著這個(gè)新的主節(jié)點(diǎn)就會(huì)開(kāi)始偵聽(tīng)輸入事件,并啟動(dòng)自己的replcator。每個(gè)節(jié)點(diǎn)是一個(gè)完整的lmax實(shí)例,有自己的disruptor,自己的journaler,自己的un-marshaller。

由于IP廣播消息并不能確保消息的到達(dá)順序。主節(jié)點(diǎn)負(fù)責(zé)決定廣播消息的順序。

Un-marshaller用于把網(wǎng)絡(luò)上的事件順序轉(zhuǎn)化成business logic processor可以調(diào)用的java對(duì)象。和其它的消費(fèi)者有所不同,un-marshaller需要改變r(jià)ing buffer中的數(shù)據(jù)。這里寫(xiě)(更改數(shù)據(jù))時(shí)需要遵守一個(gè)原則,那就是每個(gè)對(duì)象的writable field只能允許眾多并行消費(fèi)者(也就是un-marshaller)之中的一個(gè)來(lái)寫(xiě),這個(gè)原則的目的就是為了避免jvm的偽共享。

Figure 4: The LMAX architecture with the disruptors expanded

Disruptor可以作為一個(gè)單獨(dú)的組件被使用,而不只是用在lmax中,現(xiàn)在lmax已經(jīng)開(kāi)源了這個(gè)組件。作為一件金融交易軟件公司,lmax的行為的確令人稱(chēng)道,也希望更多的公司愿意交流或分享自己的架構(gòu),畢竟技術(shù)是在交流中促進(jìn)的?;剡^(guò)頭來(lái)看,樂(lè)意開(kāi)源或者愿意分享的公司(比如在infoQ中分享)往往技術(shù)上都比較領(lǐng)先。從個(gè)人來(lái)講,技術(shù)人員也應(yīng)該愿意進(jìn)行分享,畢竟這是一個(gè)在業(yè)界建立自己聲譽(yù)的好機(jī)會(huì)。

Queues and their lack of mechanical sympathy

LMAX架構(gòu)引起了人們的關(guān)注,因?yàn)檫@是一個(gè)與大多數(shù)人所想的高性能系統(tǒng)完全不同的實(shí)現(xiàn)方式。到目前為止,我已經(jīng)談到它是如何工作的,但是還沒(méi)有深入研究它是以這種方式開(kāi)發(fā)的。這個(gè)故事本身就很有趣,因?yàn)檫@個(gè)架構(gòu)并不只是拿來(lái)演講的。它是經(jīng)過(guò)很長(zhǎng)的時(shí)間,在團(tuán)隊(duì)意識(shí)到傳統(tǒng)方案的缺陷后設(shè)計(jì)的替代方案。

許多商業(yè)系統(tǒng)都有自己的核心架構(gòu),依賴(lài)于通過(guò)事務(wù)數(shù)據(jù)庫(kù)協(xié)調(diào)的多個(gè)活動(dòng)會(huì)話(huà),LMAX團(tuán)隊(duì)也熟悉這些知識(shí),并確信它不適用于LMAX。這個(gè)評(píng)估是建立在Betfair(成立LMAX的母公司)的經(jīng)驗(yàn)基礎(chǔ)之上的,這是一家體育博彩公司,它處理很多人的體育投賭事件,這是一個(gè)相當(dāng)大的并發(fā)訪(fǎng)問(wèn),鎖競(jìng)爭(zhēng)十分激烈,傳統(tǒng)數(shù)據(jù)庫(kù)幾乎無(wú)法應(yīng)付,這些讓他們相信必須尋找另外一個(gè)途徑來(lái)突破,他們現(xiàn)在接近目標(biāo)了。

他們最初的想法與大多數(shù)業(yè)界方案一樣,為獲得高性能是使用現(xiàn)在流行的并發(fā)編程。這意味著允許多線(xiàn)程并行處理多個(gè)訂單。而這些線(xiàn)程必須互相通信。處理訂單的條件等都需要進(jìn)行線(xiàn)程間通信來(lái)確定。

早期他們探索了Actor模型和SEDA。Actor模型依靠獨(dú)立的活動(dòng)對(duì)象和自己的線(xiàn)程通過(guò)隊(duì)列相互通信。很多人發(fā)現(xiàn)這種并發(fā)模型要比基于鎖定原語(yǔ)的事情更容易處理。

該團(tuán)隊(duì)建立了一個(gè)Actor模型原型,進(jìn)行性能測(cè)試,他們發(fā)現(xiàn)的是處理器會(huì)花費(fèi)更多時(shí)間在管理隊(duì)列,而不是去做真正應(yīng)用邏輯,隊(duì)列訪(fǎng)問(wèn)成了真正瓶頸。

當(dāng)追求性能達(dá)到這種程度,現(xiàn)代硬件構(gòu)造原理成為很重要的必須了解的知識(shí)了,馬丁湯普森喜歡用的一句話(huà)是“機(jī)制偏愛(ài)(mechanical sympathy)”,這詞來(lái)自賽車(chē)駕駛,它反映的是賽車(chē)手對(duì)汽車(chē)有一種與生俱來(lái)的感覺(jué),使他們能夠感受到如何發(fā)揮它到最好狀態(tài)。許多程序員包括我承認(rèn)我也陷入這樣一個(gè)陣營(yíng):不會(huì)認(rèn)為編程如何與硬件等底層機(jī)制交互是值得研究的。

現(xiàn)代的CPU延遲是影響性能的主導(dǎo)因素之一,在CPU如與內(nèi)存的交互中,CPU具有多層次的緩存(一級(jí)二級(jí)),每級(jí)速度都明顯加快。因此,如果要提高速度,將您的代碼和數(shù)據(jù)加載到這些緩存中很重要。

在某個(gè)層次, Actor模型能夠幫助你,你可以把一個(gè)演員當(dāng)作自己的對(duì)象,把代碼和數(shù)據(jù)聚類(lèi)在一起,這是緩存的一個(gè)自然單位。但Actor需要溝通,他們通過(guò)排隊(duì)來(lái)進(jìn)行溝通 - 而LMAX團(tuán)隊(duì)則認(rèn)為這是干擾緩存的隊(duì)列。(JVM偽分享的問(wèn)題)。

為什么隊(duì)列干擾了緩存呢?解釋是這樣的: 為了將數(shù)據(jù)放入隊(duì)列,你需要寫(xiě)入隊(duì)列,類(lèi)似地,為了從隊(duì)列取出數(shù)據(jù),你需要移除隊(duì)列 - 也是一種寫(xiě),客戶(hù)端也許不只一次寫(xiě)入同樣數(shù)據(jù)結(jié)構(gòu),處理寫(xiě)通常需要鎖,但是如果鎖使用了,會(huì)引起切換到底層系統(tǒng)的場(chǎng)景,當(dāng)這個(gè)發(fā)生后,處理器會(huì)丟失它的緩存中的數(shù)據(jù)。

他們得出的結(jié)論是為了夠獲得最好的緩存性能, 你需要設(shè)計(jì)一個(gè)CPU核寫(xiě)任何內(nèi)存的設(shè)計(jì),多個(gè)讀是良好的,處理器會(huì)非??欤?duì)列失敗在one-writer原則,隊(duì)列不符合單寫(xiě)者原則。(JVM偽共享)

這樣的分析導(dǎo)致LMAX團(tuán)隊(duì)得出一系列結(jié)論,導(dǎo)致他們?cè)O(shè)計(jì)出disruptor, 能夠遵循single-writer約束。 其次,它引發(fā)了探索單線(xiàn)程業(yè)務(wù)邏輯方法的想法,提出了一個(gè)單線(xiàn)程如果沒(méi)有并發(fā)管理,可以走多快的問(wèn)題。

單個(gè)線(xiàn)程的本質(zhì)是:確保你每個(gè)CPU核運(yùn)行一個(gè)線(xiàn)程,緩存配合,盡可能的使用高速緩存而不是主內(nèi)存。這就意味著代碼和數(shù)據(jù)需要盡可能的一致訪(fǎng)問(wèn),將代碼和數(shù)據(jù)放在一起的小對(duì)象也可以作為一個(gè)單元在緩存之間進(jìn)行交換,從而簡(jiǎn)化緩存管理并提高性能。

LMAX體系結(jié)構(gòu)的一個(gè)重要組成部分是使用性能測(cè)試??紤]和放棄基于A(yíng)ctor的方法來(lái)自對(duì)原型進(jìn)行構(gòu)建和性能測(cè)試。類(lèi)似地,通過(guò)性能測(cè)試來(lái)實(shí)現(xiàn)提高各種組件性能很重要。Mechanical sympathy是非常有價(jià)值的 - 它有助于形成你能做出什么改進(jìn)的假設(shè),并且引導(dǎo)你向前邁進(jìn),而不是落后 - 最終的測(cè)試將給你令人信服的證據(jù)。

然而,這種風(fēng)格的性能測(cè)試并不是一個(gè)很好理解的話(huà)題。 LMAX團(tuán)隊(duì)經(jīng)常強(qiáng)調(diào),開(kāi)發(fā)有意義的性能測(cè)試往往比開(kāi)發(fā)產(chǎn)品代碼更困難。除非考慮到CPU的高速緩存行為,否則測(cè)試低級(jí)并發(fā)組件是沒(méi)有意義的。

一個(gè)特別的經(jīng)驗(yàn)是編寫(xiě)針對(duì)零組件的測(cè)試的重要性,以確保性能測(cè)試足夠快以真正衡量真實(shí)組件正在做什么。編寫(xiě)快速的測(cè)試代碼并不比編寫(xiě)快速生產(chǎn)代碼容易,而且容易得到錯(cuò)誤的結(jié)果,因?yàn)闇y(cè)試的速度并不像要測(cè)試的組件那么快。

Should you use this architecture?

乍一看,這個(gè)架構(gòu)是非常小眾的,驅(qū)動(dòng)低延遲的交易系統(tǒng),大多數(shù)應(yīng)用并不需要6百萬(wàn)TPS。

但是我對(duì)這個(gè)架構(gòu)著迷的原因是它的設(shè)計(jì),它移除了很多其他大多數(shù)編程系統(tǒng)的復(fù)雜性,傳統(tǒng)圍繞事務(wù)性的關(guān)系數(shù)據(jù)庫(kù)會(huì)話(huà)并發(fā)模型并不簡(jiǎn)單,對(duì)象/關(guān)系數(shù)據(jù)庫(kù)映射ORM工具Object/relational mapping tools能夠幫助減輕這種麻煩,但是它不能解決全部問(wèn)題,大多數(shù)企業(yè)性能微調(diào)還是要糾結(jié)于SQL.

現(xiàn)在你能得到服務(wù)器更多的主內(nèi)存,比我們過(guò)去這些老家伙得到的磁盤(pán)還要多,越來(lái)越多應(yīng)用能夠?qū)⑺麄兊墓ぷ魅恐糜趦?nèi)存中,這樣消除了復(fù)雜性和性能低問(wèn)題. 事件源驅(qū)動(dòng)(Event Sourcing)提供了一種內(nèi)存(in-memory)系統(tǒng)的解決方案, 在單個(gè)線(xiàn)程運(yùn)行業(yè)務(wù)解決了并發(fā)性能。LMAX的經(jīng)驗(yàn)建議只要你需要少于幾百萬(wàn)TPS,你就有足夠的性能提升余地。

這里也是相似于CQRS,一種事件驅(qū)動(dòng), in-memory風(fēng)格的命令系統(tǒng)(盡管LMAX當(dāng)前沒(méi)有使用CQRS)

那么是不是表示你不應(yīng)該走上這條道路呢?對(duì)于像這樣鮮為人知的技術(shù)來(lái)說(shuō),這總是一個(gè)棘手的問(wèn)題,因?yàn)檫@個(gè)行業(yè)需要更多的時(shí)間去探索它的界限。

一個(gè)例子是,一個(gè)領(lǐng)域模型,處理一個(gè)事務(wù)總是有可能改變?nèi)绾翁幚砹硪粋€(gè)事務(wù)。對(duì)于彼此獨(dú)立的事務(wù)來(lái)說(shuō),不太需要協(xié)調(diào),因此使用并行運(yùn)行的獨(dú)立處理器變得更具吸引力。

LMAX致力于研究事件如何改變世界。許多網(wǎng)站更多的是關(guān)于獲取現(xiàn)有的信息存儲(chǔ),并將這些信息的各種組合呈現(xiàn)出來(lái) - 例如媒體網(wǎng)站。這里的構(gòu)建挑戰(zhàn)往往集中在如何正確的使用緩存。

LMAX另外一個(gè)特點(diǎn)是這是一個(gè)后臺(tái)系統(tǒng),有理由考慮如何在一個(gè)交互模型中應(yīng)用它,比如日益增長(zhǎng)的Web應(yīng)用,當(dāng)異步通訊在WEB應(yīng)用越來(lái)越多時(shí),這將改變我們的編程模型。

對(duì)于大多數(shù)團(tuán)隊(duì)來(lái)說(shuō),這些變化需要一些習(xí)慣。大多數(shù)人傾向于用同步的方式來(lái)理解編程,而不習(xí)慣處理異步。然而,異步通信是提升響應(yīng)速度的重要工具。看看在javascript世界中廣泛使用地異步通信(使用AJAX和node.js)是否會(huì)鼓勵(lì)更多的人了解異步編程。 LMAX團(tuán)隊(duì)發(fā)現(xiàn),雖然花了一些時(shí)間來(lái)適應(yīng)異步風(fēng)格,但它很快變得自然而且容易。特別是在這種方法下,錯(cuò)誤處理更容易。

LMAX團(tuán)隊(duì)當(dāng)然認(rèn)為花力氣協(xié)調(diào)事務(wù)性關(guān)系數(shù)據(jù)庫(kù)的日子已經(jīng)屈指可數(shù)。你可以使用一種更加容易方式編寫(xiě)程序而且比傳統(tǒng)集中式中央數(shù)據(jù)庫(kù)運(yùn)行得更快,為什么視而不見(jiàn)呢?

就我而言,我覺(jué)得這是一個(gè)非常令人興奮的故事。我的大部分目標(biāo)是專(zhuān)注于復(fù)雜的業(yè)務(wù)領(lǐng)域軟件。像這樣的架構(gòu)提供了很好的問(wèn)題分離,使人們可以專(zhuān)注于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),并保持很好的平臺(tái)復(fù)雜性。領(lǐng)域?qū)ο蠛蛿?shù)據(jù)庫(kù)之間的緊密耦合一直是一個(gè)惱人的方法 - 這樣的方法提出了一個(gè)出路。

附錄

LMAX設(shè)計(jì)

很多架構(gòu)師都面臨這么一個(gè)問(wèn)題:如何設(shè)計(jì)一個(gè)高吞吐量,低延時(shí)的系統(tǒng)?面對(duì)這個(gè)問(wèn)題,各位都有自己的答案。但面對(duì)這個(gè)問(wèn)題,大家似乎漸漸形成了一個(gè)共識(shí):并發(fā)是解決之道。大家似乎都這么認(rèn)為:對(duì)于服務(wù)器而言,由于多核越來(lái)越普遍,因此我們的程序必須要充分利用多線(xiàn)程,為了讓多線(xiàn)程工作得更好,必須有一個(gè)與之匹配的高效的并發(fā)模型。于是各種各樣的并發(fā)模型被提出來(lái),比如Actor模型,比如SEDA模型(Actor模型的表弟),比如Software Transactional Memory模型(準(zhǔn)確得講,STM和其他兩個(gè)模型所處在的視角是不一樣的,Actor和SEDA更多是一種編程模型,而STM更類(lèi)似一種思想,其實(shí)我們常用到的Lock-Free機(jī)制都包含了STM的思想在里頭)。

這些模型得到了廣泛討論和應(yīng)用。但這些模型都有一個(gè)討厭之處-麻煩。這個(gè)麻煩是由多線(xiàn)程復(fù)雜的天性帶來(lái)的,很難避免。除了麻煩,這些模型還忽視了另外一個(gè)問(wèn)題,由于這個(gè)問(wèn)題的忽視,可能導(dǎo)致這些模型在解決高性能問(wèn)題的道路上走到了一個(gè)錯(cuò)誤的方向。這個(gè)問(wèn)題就是JVM的偽共享問(wèn)題。所謂JVM的偽共享,簡(jiǎn)單來(lái)說(shuō),就是JVM的每一個(gè)操作指令都是基于一個(gè)緩存行,同一個(gè)緩存行中的數(shù)據(jù)是不能同時(shí)被多個(gè)線(xiàn)程同時(shí)修改的,也就是說(shuō),如果多線(xiàn)程各自操作的數(shù)據(jù)位于同一個(gè)緩存行,那么這幾個(gè)線(xiàn)程訪(fǎng)問(wèn)數(shù)據(jù)時(shí)實(shí)際上被加上了一把隱形的鎖,它們實(shí)際上在順序地訪(fǎng)問(wèn)數(shù)據(jù)。(如果你看過(guò)JDK Concurrent的實(shí)現(xiàn),你可以看到有些類(lèi)很奇怪得加了很多無(wú)用的padding成員,這就是為了填充緩存行,從而繞過(guò)JVM的偽共享)。由于JVM偽共享的存在,使得多線(xiàn)程在某些情況下成了一個(gè)擺設(shè)。這也就是說(shuō)大多數(shù)情況下我們的槍炮瞄錯(cuò)了方向,我們通常認(rèn)為沒(méi)有充分利用多線(xiàn)程壓榨多個(gè)CPU的能力是造成性能問(wèn)題的原因,實(shí)際上緩存問(wèn)題才是性能殺手。

于是LMAX就做了一個(gè)大膽的嘗試。既然多線(xiàn)程在JVM中有可能成為擺設(shè),而且又這么麻煩,那么干脆回到單線(xiàn)程來(lái)吧。用單線(xiàn)程來(lái)實(shí)現(xiàn)一個(gè)高吞吐量,低延時(shí)的系統(tǒng)?聽(tīng)起來(lái)很瘋狂,但實(shí)際上是可能的。LMAX就用單線(xiàn)程實(shí)現(xiàn)了一個(gè)吞吐量達(dá)到百萬(wàn)TPS的系統(tǒng)。

這里講LMAX是單線(xiàn)程,并不是它完全只有一個(gè)線(xiàn)程,LMAX組件還是有用到多線(xiàn)程。只不過(guò)LMAX充分認(rèn)識(shí)到了單線(xiàn)程的意義,在某些組件中大膽得采用單線(xiàn)程的架構(gòu),這就是LMAX所謂的單線(xiàn)程。LMAX決定組件是否采用單線(xiàn)程的依據(jù)很簡(jiǎn)單,如果某一個(gè)組件是IO密集型的,那么這個(gè)組件的設(shè)計(jì)就使用多線(xiàn)程。如果某一個(gè)組件是CPU密集型的,那么該組件就使用單線(xiàn)程的設(shè)計(jì)。這么做的理由很簡(jiǎn)單,IO密集型的組件的操作一般都很慢,往往會(huì)阻塞線(xiàn)程,因此使用多線(xiàn)程來(lái)競(jìng)爭(zhēng)執(zhí)行,有提高的余地。而CPU密集型的操作,如果采用多線(xiàn)程的設(shè)計(jì),一方面可能會(huì)陷入JVM偽共享的陷阱,另一方面多線(xiàn)程之間的同步會(huì)帶來(lái)開(kāi)發(fā)的復(fù)雜性,同時(shí)多線(xiàn)程會(huì)競(jìng)爭(zhēng)某些資源,比如隊(duì)列等等,這些競(jìng)爭(zhēng)會(huì)對(duì)計(jì)算機(jī)cache命中造成擾動(dòng),而且有可能引入鎖這種性能殺手,與這兩點(diǎn)相比,多線(xiàn)程帶來(lái)的好處相當(dāng)有限,因此就采用單線(xiàn)程。

LMAX的原則

LMAX的設(shè)計(jì)令人耳目一新,它的設(shè)計(jì)也向我們分享了高性能計(jì)算中的幾個(gè)重要經(jīng)驗(yàn)或者說(shuō)原則:

1. 所有的架構(gòu)師和開(kāi)發(fā)人員都應(yīng)該具有良好的Mechanical Sympathy(這個(gè)單詞不太好翻譯,“機(jī)制共鳴”?)所謂Mechanical Sympathy,實(shí)際上就是指架構(gòu)設(shè)計(jì)者應(yīng)該對(duì)現(xiàn)代操作系統(tǒng),現(xiàn)代服務(wù)器的底層運(yùn)行機(jī)制有良好的理解和認(rèn)識(shí),設(shè)計(jì)的時(shí)候充分考慮到這些機(jī)制,能夠和它們產(chǎn)生共鳴。這很容易理解,如果一個(gè)架構(gòu)師對(duì)底層機(jī)制的認(rèn)識(shí)不夠深入或者還停留在過(guò)去,那么很難想象這樣的架構(gòu)師能設(shè)計(jì)出一個(gè)基于現(xiàn)代服務(wù)器的高效系統(tǒng)來(lái)。LMAX在文檔中向我們分享了幾點(diǎn)對(duì)現(xiàn)代服務(wù)器的認(rèn)識(shí):

1.1 內(nèi)存

衡量?jī)?nèi)存有兩個(gè)指標(biāo):Bandwidth和Latency。所謂bandwidth指的是內(nèi)存在單位時(shí)間內(nèi)通過(guò)內(nèi)存總線(xiàn)的數(shù)據(jù)量,它計(jì)算公式是bandwidth = 傳輸倍率總線(xiàn)位寬工作頻率/8,單位是Bytes/s(字節(jié)每秒)。傳輸倍率指的是內(nèi)存在一個(gè)脈沖周期內(nèi)傳輸數(shù)據(jù)的次數(shù),比如DDR一個(gè)脈沖周期內(nèi)可以在上升沿傳遞一次數(shù)據(jù),在下降沿傳遞一次數(shù)據(jù),而SDRAM只能在脈沖周期的上升沿傳遞數(shù)據(jù)。工作頻率的是內(nèi)存的工作頻率,比如133Mhz等等。

而Latency是指內(nèi)存總線(xiàn)發(fā)出訪(fǎng)問(wèn)請(qǐng)求到內(nèi)存總線(xiàn)返回?cái)?shù)據(jù)之間的延遲時(shí)間,單位是納秒。

這兩個(gè)指標(biāo)描述了內(nèi)存性能的兩個(gè)方面,bandwidth描述了內(nèi)存可以以多快的速度來(lái)傳遞數(shù)據(jù),反映了吞吐量,而latency從更底層的細(xì)節(jié)描述了內(nèi)存的物理性能。一個(gè)內(nèi)存的bandwidth說(shuō)明的僅僅是內(nèi)存在內(nèi)存邊界的傳輸?shù)乃俾?,而?shù)據(jù)在內(nèi)存內(nèi)部的流動(dòng)速度是靠latency來(lái)決定。這就像趕飛機(jī),bandwidth就好像是T3航站樓的門(mén)的大小,門(mén)的大小決定了T3每秒能夠接納的旅客數(shù)量。而安檢的速度就是latency,它也會(huì)影響你最終登上飛機(jī)的時(shí)間。Bandwidth加上latency才能完全描述內(nèi)存整個(gè)環(huán)節(jié)的性能。最終內(nèi)存的性能可以用內(nèi)存性能 = (bandwidth*latency)來(lái)近似描述(Little’s Law)。

盡管硬件技術(shù)一日千里,但這些年來(lái),服務(wù)器內(nèi)存的延遲并沒(méi)有發(fā)生數(shù)量級(jí)的變化。但是內(nèi)存的bandwidth還是獲得了很大的進(jìn)步,因此整體而言?xún)?nèi)存的性能還是有比較大的提高。另外內(nèi)存的容量也是越來(lái)越大,144G大小的內(nèi)存配置也是相當(dāng)普遍。

1.2 CPU

對(duì)于CPU而言,單純的提高主頻的方法已經(jīng)走到盡頭,Intel的主頻可能會(huì)在Ghz這個(gè)量級(jí)上停留很長(zhǎng)的一段時(shí)間。CPU的核數(shù)是越來(lái)越多,24核的服務(wù)器也很普遍。CPU的緩存機(jī)制也越來(lái)越強(qiáng)大,一方面CPU緩存變大了,另一方面Intel又提出了Smart cache等概念,相比于傳統(tǒng)的L1 cache, L2 cache又提高了一步。

1.3 網(wǎng)絡(luò)

服務(wù)器本地的網(wǎng)絡(luò)響應(yīng)時(shí)間非???,處于sub 10 microseconds這個(gè)級(jí)別。(10 ms在操作系統(tǒng)中一般是一個(gè)時(shí)鐘滴答,sub 10 microseconds意味著小于一個(gè)時(shí)鐘滴答,我們知道Linux的延時(shí),線(xiàn)程切換都是基于時(shí)鐘滴答的,也就是說(shuō)本地網(wǎng)絡(luò)速度是很快的,對(duì)于大多數(shù)的應(yīng)用來(lái)講,幾乎可以忽略不計(jì))。

廣域網(wǎng)的帶寬是比較便宜的。10GigE(10Gbps的以太網(wǎng)卡)的服務(wù)器非常普遍。Multi-cast技術(shù)越來(lái)越得到關(guān)注,應(yīng)用也越來(lái)越多。

1.4 存儲(chǔ)

硬盤(pán)是新一代的磁帶。磁盤(pán)對(duì)于順序訪(fǎng)問(wèn)的速度是非??斓?。對(duì)于并發(fā)的隨機(jī)訪(fǎng)問(wèn),考慮采用SSD。SSD的接口一般都是PCI總線(xiàn)接口,速度更快。

2. 把工作放到內(nèi)存中來(lái)。

盡可能把一些數(shù)據(jù)都放到內(nèi)存中來(lái),避免和磁盤(pán)的低效交互。

3. 寫(xiě)的代碼要緩存友好。

什么樣的代碼是緩存友好的代碼?這個(gè)一言難盡。但總的原則就是,保持訪(fǎng)問(wèn)的局部性,也就是說(shuō)盡可能使一段時(shí)間內(nèi)的訪(fǎng)問(wèn)保持在一個(gè)狹小的內(nèi)存范圍內(nèi)。常用的一個(gè)做法就是,先統(tǒng)一分配一個(gè)對(duì)象池,然后復(fù)用對(duì)象池中的對(duì)象,不要每次都是重新分配新的對(duì)象。

image

上圖顯示了各個(gè)層次的緩存的訪(fǎng)問(wèn)效率,提醒我們要對(duì)緩存敏感。

4. 要時(shí)刻牢記,代碼要干凈,簡(jiǎn)練。

  1. Hotspot虛擬機(jī)喜歡短小,簡(jiǎn)練的代碼;
  2. 如果CPU的分支預(yù)測(cè)不準(zhǔn)確,那么CPU流水線(xiàn)會(huì)被阻斷;
  3. 復(fù)雜的代碼是一個(gè)危險(xiǎn)的信號(hào),這意味著你有可能沒(méi)有正確理解問(wèn)題的領(lǐng)域(DDD里的概念);
  4. 世界上的事情都不會(huì)很復(fù)雜,除了扣稅的方法。
  5. 多花點(diǎn)時(shí)間考慮一下你的領(lǐng)域模型。

采用正確的方法來(lái)實(shí)現(xiàn)并發(fā)。

記住這么幾個(gè)原則:

  • 責(zé)任單一:一個(gè)類(lèi)只干一件事,一個(gè)方法也只干一件事,不要臃腫的類(lèi)或方法。
  • 了解你的數(shù)據(jù)結(jié)構(gòu)和關(guān)系基數(shù)(一對(duì)一的關(guān)系?一對(duì)多?還是多對(duì)多)
  • 讓關(guān)系來(lái)完成工作,比如“書(shū)架”和“書(shū)”之間存在一個(gè)“attach”的關(guān)系,既然如此,我們可以讓“書(shū)架”有一個(gè)方法叫attach,用來(lái)處理添加書(shū)本的工作,這就是讓關(guān)系來(lái)完成工作。這實(shí)際上也是DDD里面的一些設(shè)計(jì)原則。

實(shí)現(xiàn)并發(fā)需要考慮兩件事:資源互斥和變化可見(jiàn)(讓結(jié)果以一個(gè)正確的順序出現(xiàn))。

并發(fā)的實(shí)現(xiàn)一般有兩種方法:第一個(gè)方法是用鎖來(lái)保證,另一個(gè)方法是借助于CAS進(jìn)行無(wú)鎖編程。使用鎖會(huì)導(dǎo)致內(nèi)核態(tài)的切換,但總可以確保任何時(shí)刻總有一個(gè)進(jìn)程會(huì)被執(zhí)行(相比之下Lock-Free如果代碼邏輯出現(xiàn)問(wèn)題,有可能所有線(xiàn)程都處在自旋等待狀態(tài),無(wú)法前進(jìn)),鎖也增加了編程的難度。而借助于CAS的Lock-Free則始終是運(yùn)行在用戶(hù)態(tài)的(節(jié)省了效率,避免了無(wú)謂的上下文切換),相比于鎖,它的編程難度更加大。下面圖形象地表達(dá)了Lock和Lock-Free之間的區(qū)別:

image

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

  1. 很多程序員對(duì)現(xiàn)代服務(wù)器的硬件有著一個(gè)錯(cuò)誤的認(rèn)識(shí)或者根本沒(méi)有認(rèn)識(shí),他們根本就不知道單線(xiàn)程所能達(dá)到的性能高度。
  2. 對(duì)于現(xiàn)代處理器,緩存丟失才是性能的最大殺手。
  3. 架構(gòu)設(shè)計(jì)時(shí),把并發(fā)放到infrastructure層里去考慮,這樣一方面使得應(yīng)用層的編寫(xiě)避免了并發(fā)編程的復(fù)雜性,另一方面由于并發(fā)放在了相對(duì)單純的infrastructure層,避免了來(lái)自應(yīng)用層的亂七八糟的干擾,更容易優(yōu)化。
  4. 牢記上述3條原則,一旦你實(shí)現(xiàn)了這3條,那恭喜你,你已經(jīng)進(jìn)入了理想王國(guó):

單線(xiàn)程:

  • 所有的一切都在內(nèi)存中;
  • 優(yōu)雅的模式;
  • 易于測(cè)試的代碼;
  • 不用擔(dān)心infrastructure和集成的問(wèn)題。

JVM偽共享

偽共享False sharing。內(nèi)存緩存系統(tǒng)中基本單元是高速緩存行(Cache lines),CPU會(huì)把數(shù)據(jù)從內(nèi)存加載到高速緩存中 ,這樣可以獲得更好的性能,高速緩存默認(rèn)大小是64 Byte為一個(gè)區(qū)域,一個(gè)區(qū)域在一個(gè)時(shí)間點(diǎn)只允許一個(gè)核心操作,也就是說(shuō)不能有多個(gè)核心同時(shí)操作一個(gè)緩存區(qū)域。

因?yàn)楦咚倬彺媸?4字節(jié),而Hotspot JVM的對(duì)象頭是兩個(gè)部分組成,第一部分是由24字節(jié)的hash code和8字節(jié)的鎖等狀態(tài)標(biāo)識(shí)組成,第二部分是指向該對(duì)象類(lèi)的引用?;绢?lè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)

因此,一個(gè)高速緩存64字節(jié)可以放下多個(gè)字段,如果這多個(gè)字段位于同一個(gè)高速緩存區(qū),雖然它們是類(lèi)的不同字段,如下代碼:

Class A{
   int x;
   int y;
}

x和y被放在同一個(gè)高速緩存區(qū),如果一個(gè)線(xiàn)程修改x;那么另外一個(gè)線(xiàn)程修改y,必須等待x修改完成后才能實(shí)施。雖然兩個(gè)線(xiàn)程修改各種獨(dú)立變量,但是因?yàn)檫@些獨(dú)立變量被放在同一個(gè)高速緩存區(qū),性能就影響了。當(dāng)多核CPU線(xiàn)程同時(shí)修改在同一個(gè)高速緩存行各自獨(dú)立的變量時(shí),會(huì)不自不覺(jué)地影響性能,這就發(fā)生了偽共享False sharing,偽共享是性能的無(wú)聲殺手。

解決方案是將高速緩存剩余的字節(jié)填充填滿(mǎn)(padding),確保不發(fā)生多個(gè)字段被擠入一個(gè)高速緩存區(qū)。Disruptor在RingBuffer中實(shí)現(xiàn)了填充,用1毫秒的延時(shí)得到100K+ TPS吞吐量。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • CPU Cache 今天的CPU比25年前更復(fù)雜。那時(shí)候,CPU內(nèi)核的頻率與內(nèi)存總線(xiàn)的頻率相當(dāng)。內(nèi)存訪(fǎng)問(wèn)只比寄存器...
    blueshadow閱讀 3,216評(píng)論 0 5
  • 最近事情比較多, 所有擱了些日子才寫(xiě), 此篇主要講技術(shù), 不喜繞開(kāi)。 先聲明此篇文章和具體的語(yǔ)言無(wú)關(guān),語(yǔ)言之間的比...
    企開(kāi)老K閱讀 4,712評(píng)論 0 214
  • 轉(zhuǎn)載自https://tech.meituan.com/disruptor.html Disruptor是英國(guó)外匯...
    檀文淵閱讀 2,701評(píng)論 0 25
  • 第五十八章 張書(shū)記去墓地看前妻 市政法委張書(shū)記早晨坐著車(chē)來(lái)到單位,并沒(méi)上樓,而是讓司機(jī)下車(chē),自己當(dāng)起了駕...
    風(fēng)雲(yún)獨(dú)攬閱讀 1,215評(píng)論 200 71
  • 減肥這種事情,日記怎么行, 要時(shí)時(shí)記,分分記,秒秒記, 因?yàn)橐粋€(gè)不留神,就能吃他個(gè)昏天暗地。 正義可能會(huì)遲到,但是...
    小艾心3707閱讀 294評(píng)論 0 2

友情鏈接更多精彩內(nèi)容