最近事情比較多萨赁, 所有擱了些日子才寫, 此篇主要講技術(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)存胸蛛,而是和緩存連接:
有人可能問為什么不弄更多的緩存,這里有性能葬项、復(fù)雜性和經(jīng)濟上的考量和取舍參考給老婆普及計算機知識形象比喻:
- CPU就像是已經(jīng)在寶寶嘴里的奶一樣泞当,直接可以咽下去了。需要1秒鐘
- L1緩存就像是已沖好的放在奶瓶里的奶一樣民珍,只要把孩子抱起來才能喂到嘴里襟士。需要5秒鐘。
- L2緩存就像是家里的奶粉一樣嚷量,還需要先熱水沖奶陋桂,然后把孩子抱起來喂進去。需要2分鐘蝶溶。
- 內(nèi)存RAM就像是各個超市里的奶粉一樣嗜历,這些超市在城市的各個角落,有的遠(yuǎn)抖所,有的近梨州,你先要尋址,然后還要去商店上門才能得到田轧。需要1-2小時暴匠。
- 硬盤DISK就像是倉庫,可能在很遠(yuǎn)的郊區(qū)甚至工廠倉庫傻粘。需要大卡車走高速公路才能運到城市里每窖。需要2-10天帮掉。
所以,在這樣的情況下——
- 我們不可能在家里不存放奶粉窒典。試想如果得到孩子餓了蟆炊,再去超市買,這不更慢嗎瀑志?
- 我們不可以把所有的奶粉都沖好放在奶瓶里盅称,因為奶瓶不夠。也不可能把超市里的奶粉都放到家里后室,因為房價太貴,這么大的房子不可能買得起混狠。
- 我們不可能把所有的倉庫里的東西都放在超市里岸霹,因為這樣干成本太大。而如果超市的貨架上正好賣完了将饺,就需要從庫房甚至廠商工廠里調(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)映射到主存塊中
- 直接映射(Direct mapped cache)
每個內(nèi)存塊只能映射到一個特定的緩存槽爽锥。一個簡單的方案是通過塊索引chunk_index映射到對應(yīng)的槽位(chunk_index % cache_slots)。被映射到同一內(nèi)存槽上的兩個內(nèi)存塊是不能同時換入緩存的授舟。(譯者注:chunk_index可以通過物理地址/緩存行字節(jié)計算得到) - N路組關(guān)聯(lián)(N-way set associative cache)
每個內(nèi)存塊能夠被映射到N路特定緩存槽中的任意一路救恨。比如一個16路緩存,每個內(nèi)存塊能夠被映射到16路不同的緩存槽释树。一般地肠槽,具有一定相同低bit位地址的內(nèi)存塊將共享16路緩存槽擎淤。(譯者注:相同低位地址表明相距一定單元大小的連續(xù)內(nèi)存) - 完全關(guān)聯(lián)(Fully associative cache)
每個內(nèi)存塊能夠被映射到任意一個緩存槽。操作效果上相當(dāng)于一個散列表秸仙。
不管何種方式映射這些內(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)間進行遷移规肴。
這里諸多的思想可以為應(yīng)用程序開發(fā)所參考捶闸, 這里僅僅是CPU 緩存涉及技術(shù)的冰山一角, 上文摘取部分對于程序開發(fā)比較重要拖刃, 特別對cache line 導(dǎo)致false sharing (偽共享)等問題删壮。
當(dāng)一個處理器改變了屬于它自己緩存中的一個值,其它處理器就再也無法使用它自己原來的值兑牡,因為其對應(yīng)的內(nèi)存位置將被刷新(invalidate)到所有緩存央碟。而且由于緩存操作是以緩存行而不是字節(jié)為粒度,所有緩存中整個緩存行將被刷新均函!
上圖說明了偽共享的問題亿虽。在核心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)化记劈。
如果線程A 和 B 需要 通訊修改某個變量他們需要:
- 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去并巍。
- 然后目木,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
Java 內(nèi)存中對象懊渡, 和系統(tǒng)之間的映射關(guān)系:
當(dāng)對象和變量存儲到計算機的各個內(nèi)存區(qū)域時刽射,必然會面臨一些問題军拟,其中最主要的兩個問題是:
- 共享對象對各個線程的可見性
- 共享對象的競爭現(xiàn)象
支撐Java內(nèi)存模型的基礎(chǔ)原理
- 指令重排序
- 內(nèi)存屏障
- happen before
具體可以參考JMM specification . 這里主要講下內(nèi)存屏障(memory barrier)。
它是一個CPU指令柄冲。它是這樣一條指令:
- 確保一些特定操作執(zhí)行的順序吻谋;
- 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)。
編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對指令重排序现横,使性能得到優(yōu)化漓拾。插入一個內(nèi)存屏障,相當(dāng)于告訴CPU和編譯器先于這個命令的必須先執(zhí)行戒祠,后于這個命令的必須后執(zhí)行
內(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變量告喊,就可以保證:
- 一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值派昧。
- 在你寫入前黔姜,會保證所有之前發(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。
核心數(shù)據(jù)對象, 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 使用:
- 內(nèi)存填充放在偽共享
- 預(yù)分配內(nèi)存減少GC
- CAS
- 內(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)
- 業(yè)務(wù)處理邏輯(核心業(yè)務(wù))
- 輸入disruptor
- 輸出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 這些輸入消息斩个, 同樣對輸出端也是。
這里的journal, replicate,unmarshall驯杜, 都涉及到IO 操作受啥,都比較耗時,他們不像業(yè)務(wù)邏輯層艇肴, 需要嚴(yán)格單線程處理:比如交易系統(tǒng)腔呜,每個單子先來后到都影響后續(xù)成交的價格等。 這些操作可以并行再悼, 于是用到了Disruptor, 也就是下圖:
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 中寫東西而晒, 同時只能由一個消費者寫入,
其它
并行和并發(fā)
并行并發(fā)是兩種不同概念螃壤, 這里Concurrency vs Parallelism - What is the difference?有討論抗果,不再細(xì)述,里面go 小人畫不錯奸晴,解釋也比較形象到位冤馏。
纖程和其他
quasar 如何和golang 比較, Quasar 在一些框架中已經(jīng)使用寄啼, 比如 corda宿接。
這片文章比較長,下篇將具體講解 Axon中的一些細(xì)節(jié)和坑辕录。
參考
- What Every Programmer Should Know About Memory
- 并發(fā)框架Disruptor譯文
- The LMAX Architecture
- 一起聊聊Disruptor
- 7個示例科普CPU CACHE
- 給老婆普及計算機知識
- 關(guān)于CPU Cache:程序猿需要知道的那些
- 計算機組成與體系結(jié)構(gòu)
- Cacheline技術(shù)淺析
- 《大話處理器》Cache一致性協(xié)議之MESI
- 全面理解Java內(nèi)存模型
- Concurrency vs Parallelism - What is the difference?
- 并行并發(fā)
- quasar
- 關(guān)于 AKKA 和 actor model
GoXTX 下一代交易平臺技術(shù)供應(yīng)商
GoXTX one-stop solution for neXT generation eXchange