如何構(gòu)建一個交易系統(tǒng)(八)

最近事情比較多萨赁, 所有擱了些日子才寫, 此篇主要講技術(shù)芥挣, 不喜繞開驱闷。

先聲明此篇文章和具體的語言無關(guān),語言之間的比較沒有太多的意義空免, 這里主要討論Java 語言空另, 這也是筆者比較熟悉的。

Java蹋砚,按筆者的經(jīng)驗扼菠,有幾大利器: 內(nèi)存管理、多線程坝咐、網(wǎng)絡(luò)通訊循榆;掌握好這幾個地方,方能在JAVA的世界暢游墨坚,此篇將主要討論JAVA多線程和內(nèi)存方面內(nèi)容秧饮, 以及在我們設(shè)計的系統(tǒng)中如何更好的運用。

Java 從誕生之初便有多線程支持, 但是不管語言級別支持多么完美盗尸,缺乏正確有效的使用柑船,都無法發(fā)揮最終的效用, 要了解多線程泼各,必須得知道計算機底層是如何運作的鞍时, 下面將結(jié)合計算機內(nèi)存、CPU扣蜻、 disruptor工具包的設(shè)計和他們后面的一些背景( Mechanical sympathy 逆巍, Blog )、技術(shù)和理念等弱贼,闡述在軟件設(shè)計需要保持一個謙卑的心態(tài)蒸苇,能夠從機器的角度去思考和權(quán)衡,更“機器化”設(shè)計和優(yōu)化你的代碼吮旅。

內(nèi)存和CPU

What Every Programmer Should Know About Memory, 如何合理的利用好內(nèi)存溪烤, 知曉內(nèi)存、CPU 運作機制庇勃, 對于所有的程序員都是繞不過的坎檬嘀, 計算機課班出生的人,可能在學(xué)校時學(xué)過责嚷, 但是真正到工作中鸳兽,這些理論基礎(chǔ)不會常用。一旦需要深入挖掘計算機的性能罕拂、或者debug往往需要反復(fù)查看這些技術(shù)揍异,而很多CPU、 內(nèi)存設(shè)計的思想爆班,在軟件應(yīng)用框架設(shè)計中都多有映射衷掷,由此可見,很多優(yōu)雅的設(shè)計思想和理念是通用的柿菩;比如分布式的緩存中如何保證一致性戚嗅, 如果看過上篇文章, 在多CPU系統(tǒng)中這樣的問題很久前其實就有研究和實現(xiàn)枢舶,套路異曲同工懦胞。

大家都知道摩爾定律,時至今日其實單個cpu 的性能發(fā)展已近達到一定的瓶頸(也許量子計算不一樣)凉泄,那么提升運算能力就不能完全依賴縱向擴展躏尉, 更多是橫向擴展,加更多的CPU后众;應(yīng)用層有同樣的做法醇份,通過普通的服務(wù)器集群服務(wù)以完成大量單機無法完成的運算稼锅。 一個普通的機器的架構(gòu)可能這個樣子:

圖一

北橋連接著內(nèi)存, CPU 通過FSB(Front side bus)僚纷,和RAM通訊, 南橋連接著硬盤和USB等其他其他設(shè)備拗盒, 南橋通過北橋和 CPU 交互怖竭。

可以看到這里有些明顯的缺陷, CPU得通過北橋和下面的設(shè)備打交道陡蝇, 數(shù)據(jù)會二次傳遞痊臭, 后來就出現(xiàn)了DMA(Direct Memory Access), 設(shè)備直接和北橋打交到,而不需要和CPU交互登夫, 但是這北橋的負(fù)擔(dān)就更重了广匙。

北橋和RAM之間的通道,同樣也有瓶頸恼策, 以前都是只有一條線連著鸦致, 現(xiàn)在多條也就是DDR2。

即使內(nèi)存現(xiàn)在有諸多的優(yōu)化涣楷, 但是還是遠(yuǎn)遠(yuǎn)不能趕上CPU的進化分唾,CPU更多時候需要的等著內(nèi)存的操作。特別在超線程狮斗、多核系統(tǒng)中并發(fā)訪問绽乔,這里瓶頸尤為突出。

為了增加內(nèi)存的帶寬碳褒,有更多的方案被提出來:

圖二

北橋沒有內(nèi)存管理器折砸, 而有獨立的內(nèi)存管理器,接到各自的RAM, 這樣大大增加了內(nèi)存帶寬沙峻, 還有一種做法直接放置CPU中:

圖三

這樣有多少核睦授,就帶多少內(nèi)存, 不好的地方在于专酗, 如果CPU1 要訪問CPU2內(nèi)數(shù)據(jù)需要一次跳轉(zhuǎn), 而如果是CPU4就需要2次跳轉(zhuǎn)睹逃。

CPU和主存的速度不匹配, 導(dǎo)致了引入更多的CPU緩存祷肯, 如一級沉填、二級甚至三級緩存,來匹配這里的差異佑笋, 基于temporal and spatial locality理論翼闹, 也就是時間和空間上的局部性(這個也是緩存的精華), 訪問最近訪問的數(shù)據(jù)蒋纬, 和訪問相鄰數(shù)據(jù)的概率比較大猎荠, 比如循環(huán)中坚弱,這樣緩存可以帶來很大的性能提升!

緩存用來提速度--當(dāng)然在命中率高的情況下关摇, 比如CPU主存訪問需要200個周期荒叶, CPU緩存只需要15周期,如果一段代碼需要對100個數(shù)據(jù)操作100次输虱, 主存中需要 100x100x200=2000000繁成; 而如果用CPU緩存 168500诸尽,可以提升91.5%。

寄存器、一級幌墓、二級沧卢、主存越小越好

CPU緩存使用SRAM(DRAM差別逻恐,刷電否)攒至,一般比主存小很多, 大概1/1000罪帖, 比如4M / 4G促煮, 抽象看CPU 緩存結(jié)構(gòu)如下:

圖四

這個就是個最簡單的結(jié)構(gòu), CPU不再直接連接內(nèi)存胸蛛,而是和緩存連接:

圖五
CPU緩存訪問延遲污茵,越短越好

有人可能問為什么不弄更多的緩存,這里有性能葬项、復(fù)雜性和經(jīng)濟上的考量和取舍參考給老婆普及計算機知識形象比喻:

  1. CPU就像是已經(jīng)在寶寶嘴里的奶一樣泞当,直接可以咽下去了。需要1秒鐘
  2. L1緩存就像是已沖好的放在奶瓶里的奶一樣民珍,只要把孩子抱起來才能喂到嘴里襟士。需要5秒鐘。
  3. L2緩存就像是家里的奶粉一樣嚷量,還需要先熱水沖奶陋桂,然后把孩子抱起來喂進去。需要2分鐘蝶溶。
  4. 內(nèi)存RAM就像是各個超市里的奶粉一樣嗜历,這些超市在城市的各個角落,有的遠(yuǎn)抖所,有的近梨州,你先要尋址,然后還要去商店上門才能得到田轧。需要1-2小時暴匠。
  5. 硬盤DISK就像是倉庫,可能在很遠(yuǎn)的郊區(qū)甚至工廠倉庫傻粘。需要大卡車走高速公路才能運到城市里每窖。需要2-10天帮掉。

所以,在這樣的情況下——

  1. 我們不可能在家里不存放奶粉窒典。試想如果得到孩子餓了蟆炊,再去超市買,這不更慢嗎瀑志?
  2. 我們不可以把所有的奶粉都沖好放在奶瓶里盅称,因為奶瓶不夠。也不可能把超市里的奶粉都放到家里后室,因為房價太貴,這么大的房子不可能買得起混狠。
  3. 我們不可能把所有的倉庫里的東西都放在超市里岸霹,因為這樣干成本太大。而如果超市的貨架上正好賣完了将饺,就需要從庫房甚至廠商工廠里調(diào)贡避,這在計算里叫換頁,相當(dāng)?shù)穆?/li>

當(dāng)然這里的緩存對于應(yīng)用這里的程序員是透明的予弧,當(dāng)今的CPU架構(gòu)更加復(fù)雜刮吧,可能有多CPU, 多核,多線程:

圖六

上圖我們有連個處理器掖蛤,每個處理器有4核杀捻,每個有兩個線程, 深灰色框子的是一個核蚓庭,他們有自己的一級緩存致讥,共享處理器內(nèi)的緩存(灰色框子);處理器之間不共享緩存(這個很重要)器赞, 他們共同連接到外面的主存上垢袱。

緩存要比主存小很多,所以從主存加載(映射)數(shù)據(jù)進來港柜,涉及到加載多少请契,和緩存的清理替換,上面說到 temporal and spatial locality, 所以在加載上面會有不同的策略加以優(yōu)化夏醉。

有三種方式將緩存槽(chunk, cache line)映射到主存塊中

  1. 直接映射(Direct mapped cache)
    每個內(nèi)存塊只能映射到一個特定的緩存槽爽锥。一個簡單的方案是通過塊索引chunk_index映射到對應(yīng)的槽位(chunk_index % cache_slots)。被映射到同一內(nèi)存槽上的兩個內(nèi)存塊是不能同時換入緩存的授舟。(譯者注:chunk_index可以通過物理地址/緩存行字節(jié)計算得到)
  2. N路組關(guān)聯(lián)(N-way set associative cache)
    每個內(nèi)存塊能夠被映射到N路特定緩存槽中的任意一路救恨。比如一個16路緩存,每個內(nèi)存塊能夠被映射到16路不同的緩存槽释树。一般地肠槽,具有一定相同低bit位地址的內(nèi)存塊將共享16路緩存槽擎淤。(譯者注:相同低位地址表明相距一定單元大小的連續(xù)內(nèi)存)
  3. 完全關(guān)聯(lián)(Fully associative cache)
    每個內(nèi)存塊能夠被映射到任意一個緩存槽。操作效果上相當(dāng)于一個散列表秸仙。
1&2 映射方式

不管何種方式映射這些內(nèi)存嘴拢, 最終替換清除工作都涉及到內(nèi)存的一致性, 如何判斷臟數(shù)據(jù)寂纪, 比較有名的有MESI 協(xié)議,單核Cache中每個Cache line有2個標(biāo)志:dirty和valid標(biāo)志席吴,它們很好的描述了Cache和Memory(內(nèi)存)之間的數(shù)據(jù)關(guān)系(數(shù)據(jù)是否有效,數(shù)據(jù)是否被修改)捞蛋,而在多核處理器中孝冒,多個核會共享一些數(shù)據(jù),MESI協(xié)議就包含了描述共享的狀態(tài)拟杉。
在MESI協(xié)議中庄涡,每個Cache line有4個狀態(tài),可用2個bit表示搬设,它們分別是(參考):

狀態(tài) 描述
M(Modified) 這行數(shù)據(jù)有效穴店,數(shù)據(jù)被修改了,和內(nèi)存中的數(shù)據(jù)不一致拿穴,數(shù)據(jù)只存在于本Cache中泣洞。
E(Exclusive) 這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致默色,數(shù)據(jù)只存在于本Cache中球凰。
S(Shared) 這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致该窗,數(shù)據(jù)存在于很多Cache中弟蚀。
I(Invalid) 這行數(shù)據(jù)無效。

在MESI協(xié)議中酗失,每個Cache的Cache控制器不僅知道自己的讀寫操作义钉,而且也監(jiān)聽(snoop)其它Cache的讀寫操作。每個Cache line所處的狀態(tài)根據(jù)本核和其它核的讀寫操作在4個狀態(tài)間進行遷移规肴。

MESI

這里諸多的思想可以為應(yīng)用程序開發(fā)所參考捶闸, 這里僅僅是CPU 緩存涉及技術(shù)的冰山一角, 上文摘取部分對于程序開發(fā)比較重要拖刃, 特別對cache line 導(dǎo)致false sharing (偽共享)等問題删壮。

當(dāng)一個處理器改變了屬于它自己緩存中的一個值,其它處理器就再也無法使用它自己原來的值兑牡,因為其對應(yīng)的內(nèi)存位置將被刷新(invalidate)到所有緩存央碟。而且由于緩存操作是以緩存行而不是字節(jié)為粒度,所有緩存中整個緩存行將被刷新均函!

False Sharing

上圖說明了偽共享的問題亿虽。在核心1上運行的線程想更新變量X菱涤,同時核心2上的線程想要更新變量Y。不幸的是洛勉,這兩個變量在同一個緩存行中粘秆。每個線程都要去競爭緩存行的所有權(quán)來更新變量。如果核心1獲得了所有權(quán)收毫,緩存子系統(tǒng)將會使核心2中對應(yīng)的緩存行失效攻走。當(dāng)核心2獲得了所有權(quán)然后執(zhí)行更新操作,核心1就要使自己對應(yīng)的緩存行失效此再。這會來來回回的經(jīng)過L3緩存昔搂,大大影響了性能。如果互相競爭的核心位于不同的插槽输拇,就要額外橫跨插槽連接巩趁,問題可能更加嚴(yán)重,有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現(xiàn)偽共享淳附。

基于上面的問題, JAVA 世界里面有添加緩沖行蠢古,來避免偽共享奴曙, 人為把變量擴充到64個字節(jié)。 比如在JAVA 中可以這樣做:

class LhsPadding
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

可能有人說加個鎖可以避免錯誤的數(shù)據(jù)分享草讶, 但是這將增加數(shù)據(jù)的移動量洽糟, 下面Disruptor 將講解一種無鎖化高效內(nèi)存隊列

Java memory model

既然我們使用的是JAVA, 所以無法避免討論JAVA 的內(nèi)存模型堕战。 套用上面CPU 內(nèi)存模型中的理論到JVM坤溃, 還是有同樣的問題:一個線程對共享變量的寫入何時對另一個線程可見。那么JMM 抽象了線程和主內(nèi)存之間的關(guān)系: 線程之間的共享變量存儲在主內(nèi)存(main memory)中嘱丢,每個線程都有一個私有的本地內(nèi)存(local memory)薪介,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念越驻,并不真實存在汁政。它涵蓋了緩存,寫緩沖區(qū)缀旁,寄存器以及其他的硬件和編譯器優(yōu)化记劈。

JMM

如果線程A 和 B 需要 通訊修改某個變量他們需要:

  1. 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去并巍。
  2. 然后目木,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。

Java 內(nèi)存中對象懊渡, 和系統(tǒng)之間的映射關(guān)系:

JMM2.png
JMM3.png

當(dāng)對象和變量存儲到計算機的各個內(nèi)存區(qū)域時刽射,必然會面臨一些問題军拟,其中最主要的兩個問題是:

  1. 共享對象對各個線程的可見性
  2. 共享對象的競爭現(xiàn)象

支撐Java內(nèi)存模型的基礎(chǔ)原理

  1. 指令重排序
  2. 內(nèi)存屏障
  3. happen before

具體可以參考JMM specification . 這里主要講下內(nèi)存屏障(memory barrier)。

它是一個CPU指令柄冲。它是這樣一條指令:

  1. 確保一些特定操作執(zhí)行的順序吻谋;
  2. 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)。

編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對指令重排序现横,使性能得到優(yōu)化漓拾。插入一個內(nèi)存屏障,相當(dāng)于告訴CPU和編譯器先于這個命令的必須先執(zhí)行戒祠,后于這個命令的必須后執(zhí)行

MemoryBarrier

內(nèi)存屏障另一個作用是強制更新一次不同CPU的緩存骇两。例如,一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到緩存姜盈,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值低千,而不用考慮到底是被哪個CPU核心或者哪顆CPU執(zhí)行的.

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數(shù)據(jù)馏颂,因此示血,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。

如果一個變量是volatile修飾的救拉,JMM會在寫入這個字段之后插進一個Write-Barrier指令难审,并在讀這個字段之前插入一個Read-Barrier指令。這意味著亿絮,如果寫入一個volatile變量告喊,就可以保證:

  1. 一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值派昧。
  2. 在你寫入前黔姜,會保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的蒂萎,因為內(nèi)存屏障會把之前的寫入值都刷新到緩存秆吵。

volatile 在concurrent 包中被大量使用, 在下面的disruptor 中我們將繼續(xù)講解五慈。

Disruptor

關(guān)于Disruptor 的介紹網(wǎng)上比較多: 并發(fā)框架Disruptor譯文 & 一起聊聊Disruptor帮毁。

Disruptor 是 LMAX一種新型零售金融交易平臺, 后臺架構(gòu)的核心組件之一豺撑,能夠在無鎖的情況下實現(xiàn)網(wǎng)絡(luò)的Queue并發(fā)操作烈疚, 官方描述: 一個線程里每秒處理6百萬訂單。

在設(shè)計Disruptor時要避免寫競爭聪轿,讓數(shù)據(jù)更久的留在cache里爷肝, 避免JMV 過度GC。

Disruptor

核心數(shù)據(jù)對象, RINGBUFFER

Ringbuffer

Disruptor的核心是一個circular array灯抛,有個cursor金赦,里面有sequence number,數(shù)據(jù)類型是long对嚼。如果不考慮consumer夹抗,只有一個producer在寫,就是不停的往entry里寫東西纵竖,然后增加cursor上的sequence number漠烧。為了避免cursor里的sequence number和其他variable造作false sharing,disruptor定義了7個long型靡砌,并沒有給它們賦值已脓,然后再定義cursor。這樣cursor就不會和其他variable同時出現(xiàn)在一個cache line里通殃,內(nèi)存填充技術(shù)度液。

這個Ring buffer 是循環(huán)使用,如果producer在寫的過程中画舌,超出了原來的長度堕担,就不停地覆蓋原來的數(shù)據(jù),增加cursor里的sequence number曲聂。bucket里的entry都是pre-allocated照宝,避免每次都new一個object。因為disruptor是用java寫的句葵,這樣可以避免garbage collection。producer寫的過程是two phase commit兢仰。

Consumer每次在訪問時需要先檢查sequence number是否available乍丈,如果不available,會有多種策略把将。latency最高的一種是盲等轻专。producer在寫的時候,需要檢查最低的sequence number在哪兒察蹲。這里不需要lock的原因是sequence number是遞增的请垛。producer不需要趕在最低sequence number前面,因而沒有write contention洽议。此外宗收,disruptor使用memory barrier通知數(shù)據(jù)的更新。

Disruptor 使用:

  1. 內(nèi)存填充放在偽共享
  2. 預(yù)分配內(nèi)存減少GC
  3. CAS
  4. 內(nèi)存屏障亚兄,無鎖保證數(shù)據(jù)可見性

Disruptor 也在我們使用的框架混稽, Axonframework 和 我們熟悉的log4j2 中大量使用, 后面在具體的業(yè)務(wù)中再細(xì)看。

Martin fowler

在軟件架構(gòu)界 Martin fowler 是個比較有名的角匈勋,他的這篇文章 The LMAX Architecture 對于Disrutpor CQRS, Event source 有很多的解讀礼旅, 我們設(shè)計的交易系統(tǒng)架構(gòu)也部分參考了此理論, 這篇文章Martin fowler 根據(jù)LMAX 中對disruptor 的使用洽洁, 以及衍生到我們普遍的架構(gòu)上面的問題進行了敘述痘系, 這里僅做摘要, 具體細(xì)節(jié)大家翻閱原文饿自。

LMAX 是一個新零售金融公司(估計針對散戶)汰翠,這樣在峰值交易量會比較大, LMAX母公司是betfair 也就是個賭*球的公司璃俗,賭*球在有大事件的時候同樣交易量非常大奴璃, 那么在傳統(tǒng)的解決方案, 很多事圍繞一個強勁的DB來做城豁, 維持高并發(fā)下的事務(wù)苟穆。 于是促成了LMAX嘗試 actor model 以及他類似的SEDA ,我們比較熟悉的AKKA 的actor model 就是一個常見的應(yīng)用。 Actor模型=數(shù)據(jù)+行為+消息唱星。Actor模型內(nèi)部的狀態(tài)由自己的行為維護雳旅,外部線程不能直接調(diào)用對象的行為,必須通過消息才能激發(fā)行為间聊,這樣就保證Actor內(nèi)部數(shù)據(jù)只有被自己修改,很多人喜歡使用這樣的模型攒盈,這樣避免在同步原語上的糾結(jié)。

同樣LMAX團隊也試圖使用此模型哎榴, 但是他們發(fā)現(xiàn)了主要的瓶頸發(fā)生在消息通道型豁,也就是隊列Queue上面, 對性能追求極致的時候, 參考Martin Thompson 提出的 "mechanical sympathy":如賽車手一樣能個和自己賽車融為一體尚蝌,感知機械細(xì)微之處迎变,LAMX主要針對這個方向進行重現(xiàn)設(shè)計和架構(gòu)。

由于如上文CPU 和內(nèi)存之間的工作模式飘言, 導(dǎo)致緩存衣形、主存中間的競爭, Actor model 天然集成數(shù)據(jù)的操作姿鸿, 可以很好解決緩存問題谆吴, 但是一個Acotor 還是需要和其他Actor 交流,也就Queue. 有Queue,就有寫入和讀出操作苛预,就有對隊列的資源競爭句狼, 最簡單的解決方案就是加個鎖, 這將導(dǎo)致更多上下文的切換热某,LMAX 團隊創(chuàng)建了新數(shù)據(jù)結(jié)構(gòu) Disruptor: 單核寫入鲜锚,多個讀突诬,以實現(xiàn)無鎖化操作,單線程寫入瓶頸就在于:單線程的性能在當(dāng)今的架構(gòu)下能達到多大芜繁? 于是Disruptor 做了大量的測試: “The 6 million(600萬) TPS benchmark was measured on a 3Ghz dual-socket quad-core Nehalem based Dell server with 32GB RAM.”

LMAX 最基本的架構(gòu)

LAMX 架構(gòu)
  1. 業(yè)務(wù)處理邏輯(核心業(yè)務(wù))
  2. 輸入disruptor
  3. 輸出disruptor

業(yè)務(wù)處理邏輯旺隙,為單線程不需要依賴任何框架,核心可以在任何JVM 上面運行骏令。
真正的業(yè)務(wù)處理邏輯蔬捷,可能么有那么簡單, 需要從網(wǎng)絡(luò)上收到消息榔袋,然后反序列化周拐,冗余備份等等操作, 輸出消息同樣序列化傳播出去凰兑, 這些任務(wù)都由輸入妥粟、輸出disruptor來處理。由于他們涉及到大部分是獨立的IO 操作吏够, 所以可以多線程并行處理勾给,這個架構(gòu)雖然為LMAX定制, 但是可以為其他架構(gòu)借鑒锅知。

業(yè)務(wù)處理邏輯

業(yè)務(wù)處理邏輯單線程的接收消息(輸入)播急,處理業(yè)務(wù)邏輯,然后把處理結(jié)果以 Event 方式發(fā)送給輸出Disruptor, 所有的這些過程都是在內(nèi)存中處理售睹, 沒有落地處理桩警, 這里有幾個好處, 首先都在內(nèi)存會非巢茫快捶枢, 其二由于是單線程序列化順序(sequentially)處理,不需要事務(wù)管理飞崖。這使整個架構(gòu)非常簡單明了烂叔, 減低了編程的難度,也就減少了潛在的錯誤概率蚜厉。

如果崩潰了如何處理? 即使系統(tǒng)再健壯畜眨,也避免不了比如停電等等不可控制因素昼牛,采用了Event Source(參考前幾篇) 方式, 當(dāng)前業(yè)務(wù)驅(qū)動都是由輸入Event 觸發(fā)康聂, 然后生成輸出Event, 只要輸入Event 已經(jīng)落地贰健, 就沒有這個問題了, 你都可以replay事件流恢復(fù)到當(dāng)前狀態(tài)(這里需要保持冪等操作L裰)伶椿。

在具體Event Source 的操作上面,有很多的方案,基本的邏輯也都比較直觀易懂(我們采用Axon框架)脊另, 為了加快狀態(tài)的恢復(fù)导狡, 可以定期的做Snapshot, 這樣恢復(fù)就非常迅速不需要從第一個事件開始偎痛, LAMX 每天晚上做一個Snapshot, 這樣重啟服務(wù)旱捧,加載Snapshot, 重發(fā)后續(xù)事件都能很快完成。

Snapshot 還不足夠維持7X24踩麦,特別在比如你半夜東西壞掉枚赡, LMAX采用兩個業(yè)務(wù)模塊并行處理, 每個事件都由會兩個業(yè)務(wù)模塊處理谓谦,一個業(yè)務(wù)模塊的處理結(jié)果會被拋棄贫橙, 如果一個廢掉,另外一個直接切換過去反粥, 據(jù)說可以做到毫秒級別,目前我們的做法是減少Snapshot 周期卢肃, 比如1024個版本一個,集群做parittion,壞掉在另外一臺機上恢復(fù)星压。

Eventsource, 好處不僅記錄所有的狀態(tài)轉(zhuǎn)變(Event)践剂,同時多個下游可以針對不同的業(yè)務(wù)場景消費這些Event, 比如監(jiān)控娜膘、分析逊脯、聚合、風(fēng)控等等竣贪, 互不干擾军洼。

性能優(yōu)化

如上面描述, 這里的核心點是演怎, 業(yè)務(wù)邏輯在內(nèi)存中匕争, 單線程,序列化處理爷耀, 可以很容易讓程序突破10K TPS限制甘桑, 如果刻意優(yōu)化想可以達到100K, 只要你的代碼夠精簡, 更和合理的內(nèi)存分配和CPU使用策略。

LMAX 對數(shù)據(jù)結(jié)構(gòu)的使用進行了優(yōu)化歹叮,這樣對內(nèi)存和GC 更友好跑杭,比如用原生的long 值作為hashmap 的key, LongToObjectHashMap(fastutils 有更多)咆耿, 一個好的數(shù)據(jù)格式德谅,對性能優(yōu)化至關(guān)重要, 但是很多程序員都使用更方便的數(shù)據(jù)結(jié)構(gòu)萨螺, 信手拈來什么方便用上面窄做, 最好掂量平衡下,小優(yōu)化可能帶來大性能提升愧驱。

編程模型

基于上面我們所說的處理方式, 將對你的業(yè)務(wù)邏輯層帶來不少限制椭盏, 你需要剔除對遠(yuǎn)程服務(wù)的調(diào)用组砚, 一個遠(yuǎn)程服務(wù)的調(diào)用,將拉慢你的處理效率庸汗, 單線程處理業(yè)務(wù)邏輯可能會被掛起惫确,這樣整個處理的速度會降下來,所以你不能在業(yè)務(wù)邏輯層調(diào)用其他服務(wù)蚯舱,取而代之改化,你發(fā)送事件到output event, 等待一個回調(diào)的input event枉昏。

打個簡單的例子陈肛, 比如你用信用卡買罐豆子,一般的流程是兄裂,零售系統(tǒng)會檢查的訂單信息句旱, 然后給信用卡檢查服務(wù)發(fā)送請求,檢查你的信用卡號晰奖,然后再確認(rèn)你的訂單谈撒,所有的一步完成,如果遠(yuǎn)程反饋緩慢你的訂單的線程將被阻塞住匾南,所以你啟動多個線程來滿足更多用戶的請求啃匿。

但是在LMAX 的架構(gòu)中,你需要將這個操作分層兩組蛆楞,第一個來獲取用戶的訂單信息溯乒, 完成后,發(fā)送一個事件出來(信用卡檢查請求)豹爹,發(fā)送給信用卡公司裆悄。而業(yè)務(wù)邏輯層不會阻塞,繼續(xù)處理其他用戶的請求臂聋, 直到收到一個信用卡驗證完的事件回到input 事件流中光稼,然后繼續(xù)下面的確認(rèn)事宜。

以事件驅(qū)動模型孩等,異步處理方式艾君,是挺普遍的一種方式來增加吞吐量,同時讓你的業(yè)務(wù)邏輯層瞎访, 更有彈性擴展性腻贰,只需要處理自己部分東西吁恍, 不需要關(guān)注外圍的系統(tǒng)扒秸。

當(dāng)然這種方式要有很好的應(yīng)錯能力播演, 不能做到強事務(wù),需要比如事務(wù)補償機制伴奥, 達到最終一致性(筆者加的)写烤, LMAX 在事件輸入輸出端都用了Disruptor, 所以一旦錯誤發(fā)生拾徙,在內(nèi)存中保持狀態(tài)一致性很重要洲炊,但是這里沒有一個自動回滾措施, 所以 LMAX 團隊十分注意輸入事件的驗證措施尼啡, 來保證任何對內(nèi)存數(shù)據(jù)狀態(tài)的更新保證一致性暂衡,同時在上線前做大量的測試, 在我們系統(tǒng)崖瞭,更多的解決方案是做transaction 補償措施狂巢。

輸入輸出Disruptor

業(yè)務(wù)邏輯層是單線程, 但是在數(shù)據(jù)進入業(yè)務(wù)邏輯層前需要做很多工作书聚, 比如消息反序列化唧领, Eventsource將消息落地, 最終需要一個集群化的業(yè)務(wù)邏輯層來支撐這個架構(gòu)雌续,這樣我們可以集群中replicate 這些輸入消息斩个, 同樣對輸出端也是。

輸入Disruptor 需要做的事情

這里的journal, replicate,unmarshall驯杜, 都涉及到IO 操作受啥,都比較耗時,他們不像業(yè)務(wù)邏輯層艇肴, 需要嚴(yán)格單線程處理:比如交易系統(tǒng)腔呜,每個單子先來后到都影響后續(xù)成交的價格等。 這些操作可以并行再悼, 于是用到了Disruptor, 也就是下圖:

Ringbuffer

Disruptor 是一個生產(chǎn)者們用來放置對象以供多個消費者并行消費的queue,如果你仔細(xì)看的話他就是一個 Ring Buffer(環(huán)狀緩沖)核畴, 每個生產(chǎn)者和消費者都有一個序列號, 表示他們當(dāng)前正在處理的槽冲九,每個人只能修改自己的序列號谤草, 但是可以讀所有其它人的序列號, 這樣的生產(chǎn)者可以查看消費者的計數(shù)來保證莺奸,他要寫入的槽沒有被人占用丑孩。同樣一個消費者也可以監(jiān)控其它消費者的計數(shù), 來處理只有經(jīng)過其它人處理過的槽灭贷。

輸出端的 disruptors 也非常類似温学,他們用來處理序列化,和向網(wǎng)絡(luò)上發(fā)送的任務(wù)甚疟。外輸?shù)南⒖梢苑植煌膖opic, 每個topic 用一個disruptor 來分開發(fā)送仗岖,增加并發(fā)量逃延。

Disruptor 可以支持一個生產(chǎn)者和多個消費者,也支持多個生產(chǎn)者多個消費者模式轧拄。

Disruptor有個好處是如果消費者跟不上速度揽祥,比如一個反序列化比較慢,可以批量的處理數(shù)據(jù)檩电。 比如現(xiàn)在反序列化到15槽拄丰, 處理完已經(jīng)到31槽來, 他可以批量的讀取16~30數(shù)據(jù)俐末,這樣處理方式可以讓比較慢的消費者可以跟上來料按,以減少延遲。

增加更多的消費者卓箫, 也可以增加并發(fā)量站绪, 比如落地消費者比較慢, 可以增加兩個丽柿, 一個專門消費下標(biāo)是偶數(shù)的槽恢准, 另外一個處理下標(biāo)是奇數(shù)的槽,這樣就可以大大增加并發(fā)量甫题。 這個也是Axon里面使用到的一個方法馁筐,多消費者分不同segement, 根據(jù)aggregate identifier 的hash值取模坠非,消費者來處理敏沉,這樣既保證來每個aggregate處理在同一個線程, 也增加來并發(fā)量炎码, 這也是為什么disruptor可以帶來4倍的性能提升盟迟。

Ringbuffer 的大小需要是2的次方, 這樣好處可以快速的取模潦闲, 在LMAX和其它的系統(tǒng)一樣Disruptor也要被每晚重啟攒菠, 重啟的主要目的是清理內(nèi)存,這樣大大減少在交易時間由于gc導(dǎo)致昂貴的垃圾回收開銷--定時重啟是個很好的方法歉闰, 不僅僅是這里避免垃圾回收辖众,其實也是對你系統(tǒng)的一個鍛煉,應(yīng)急反應(yīng)和敬。

Journaler 的工作主要是把所有的事情落地化凹炸, 在出錯的時候可以replay恢復(fù)狀態(tài),LMAX沒有用數(shù)據(jù)庫落地昼弟, 而是簡簡單單的文件系統(tǒng)啤它,在大家看來這個可能不可思議, 但是順序?qū)懭胛募到y(tǒng)的性能可能不遜于內(nèi)存, 這點可以參考kafka的解讀变骡。

先前我們討論來LMAX救欧,用多臺機器集群以更快的進行failover, replicator 的用途就是保證這些機器之間的同步, 所有LMAX集群交流使用來IP廣播锣光,這樣每個客戶端不需要知道那個節(jié)點是master, 這個由master幾點自己控制選擇監(jiān)聽,然后在replicator, replicator把收到的事件廣播給其它的slave節(jié)點铝耻, 如果master節(jié)點死掉來誊爹,比如沒有來心跳, 另外一個節(jié)點將編程master節(jié)點瓢捉, 開始消費輸入事件频丘, 啟動他自己的replicator, 每個節(jié)點都由這里全套的組件,序列化泡态,落地搂漠,反序列化。(這里如何選擇master, 腦裂如何解決某弦? )

反序列化把網(wǎng)絡(luò)收到的數(shù)據(jù)桐汤,轉(zhuǎn)化成java 對象,一共業(yè)務(wù)邏輯層使用靶壮,這個消費者和其它消費者不一樣, 他需要修改ring buffer 里的東西, 這里需要遵循的規(guī)則是瞒大,消費者可以往ring buffer 中寫東西而晒, 同時只能由一個消費者寫入,

完整架構(gòu)

其它

并行和并發(fā)
并行并發(fā)是兩種不同概念螃壤, 這里Concurrency vs Parallelism - What is the difference?有討論抗果,不再細(xì)述,里面go 小人畫不錯奸晴,解釋也比較形象到位冤馏。

纖程和其他

quasar 如何和golang 比較, Quasar 在一些框架中已經(jīng)使用寄啼, 比如 corda宿接。

這片文章比較長,下篇將具體講解 Axon中的一些細(xì)節(jié)和坑辕录。

參考

  1. What Every Programmer Should Know About Memory
  2. 并發(fā)框架Disruptor譯文
  3. The LMAX Architecture
  4. 一起聊聊Disruptor
  5. 7個示例科普CPU CACHE
  6. 給老婆普及計算機知識
  7. 關(guān)于CPU Cache:程序猿需要知道的那些
  8. 計算機組成與體系結(jié)構(gòu)
  9. Cacheline技術(shù)淺析
  10. 《大話處理器》Cache一致性協(xié)議之MESI
  11. 全面理解Java內(nèi)存模型
  12. Concurrency vs Parallelism - What is the difference?
  13. 并行并發(fā)
  14. quasar
  15. 關(guān)于 AKKA 和 actor model

GoXTX 下一代交易平臺技術(shù)供應(yīng)商
GoXTX one-stop solution for neXT generation eXchange

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睦霎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子走诞,更是在濱河造成了極大的恐慌副女,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚣旱,死亡現(xiàn)場離奇詭異碑幅,居然都是意外死亡戴陡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門沟涨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恤批,“玉大人,你說我怎么就攤上這事裹赴∠才樱” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵棋返,是天一觀的道長延都。 經(jīng)常有香客問我,道長睛竣,這世上最難降的妖魔是什么晰房? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮射沟,結(jié)果婚禮上殊者,老公的妹妹穿的比我還像新娘。我一直安慰自己验夯,他們只是感情好幽污,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著簿姨,像睡著了一般距误。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扁位,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天准潭,我揣著相機與錄音,去河邊找鬼域仇。 笑死刑然,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的暇务。 我是一名探鬼主播泼掠,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼垦细!你這毒婦竟也來了择镇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤括改,失蹤者是張志新(化名)和其女友劉穎腻豌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡吝梅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年虱疏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苏携。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡做瞪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出右冻,到底是詐尸還是另有隱情装蓬,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布国旷,位于F島的核電站,受9級特大地震影響茫死,放射性物質(zhì)發(fā)生泄漏跪但。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一峦萎、第九天 我趴在偏房一處隱蔽的房頂上張望屡久。 院中可真熱鬧,春花似錦爱榔、人聲如沸被环。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筛欢。三九已至,卻和暖如春唇聘,著一層夾襖步出監(jiān)牢的瞬間版姑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工迟郎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剥险,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓宪肖,卻偏偏與公主長得像表制,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子控乾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容