關(guān)聯(lián)比賽:??2021第二屆云原生編程挑戰(zhàn)賽1:針對冷熱讀寫場景的RocketMQ存儲系統(tǒng)設(shè)計
引子
在一個渾渾噩噩的下午宵蛀,百無聊賴的我像往常一樣點開了劃水交流群现喳,細(xì)細(xì)品味著老哥們關(guān)于量子力學(xué)的討論飞蚓。嬉戲間渔伯,平常水不拉幾的群友張三忽然發(fā)了一張大大的橙圖来涨,我啪的一下點開了尽纽,很快啊它褪,仔細(xì)觀摩后發(fā)現(xiàn)原來是2021第二屆云原生編程挑戰(zhàn)賽報名的海報俭驮,暗暗的想起了被我鴿掉的前幾屆擂送,小手不自覺地打開了鏈接并且一鍵三連悦荒。
每個人的心里都有一個童心未泯的自己,這次比賽就像一場游戲一樣讓我深陷其中嘹吨,三岔路口搬味,我選擇了存儲領(lǐng)域,誰承想這決定會讓我在接下來的兩個月里減少百分之N的發(fā)量。
讀題
賽題目的是實現(xiàn)簡單的消息讀取與存儲碰纬,程序需要實現(xiàn)append和getRange方法萍聊,并依次通過性能評測與正確性評測,性能評測耗時最少者居高悦析。
評測環(huán)境
Linux下的4核8G服務(wù)器寿桨,配置400GESSD PL1云盤,吞吐可達(dá)320MiB/s强戴,60GIntel 傲騰持久內(nèi)存PMem(Persistent Memory)亭螟,由參考文檔可推測為第一代持久內(nèi)存,代號為AEP骑歹。
賽題編程語言限制為Java8媒佣,JVM配置為6G堆內(nèi)+2G堆外。
性能評測
評測程序首先會創(chuàng)建10~50個不等的線程陵刹,每個線程隨機(jī)分配若干個topic進(jìn)行寫入,topic總數(shù)量不超過100個欢嘿。每個topic之下又分為若干個queue衰琐,總數(shù)量不超過5000個,調(diào)用append方法后返回當(dāng)前數(shù)據(jù)在queue中的offset炼蹦,由0開始羡宙。每次寫入數(shù)據(jù)大小為100B-17KiB區(qū)間隨機(jī),當(dāng)寫滿75G數(shù)據(jù)后掐隐,會挑選一半的queue由下標(biāo)0(頭)開始讀取狗热,另外一半從當(dāng)前最大下標(biāo)(尾)開始讀取,并保持之前的寫入壓力繼續(xù)寫入50G數(shù)據(jù)虑省,最后一條數(shù)據(jù)讀取完畢后停止計時匿刮。
正確性評測
同樣會使用N個線程寫入數(shù)據(jù),在寫入過程中會重啟ECS探颈,之后再讀取之前寫入成功的數(shù)據(jù)(返回offset即視為成功)熟丸,要求嚴(yán)格一致。
持久內(nèi)存
本次比賽多了一個比較陌生的存儲介質(zhì)PMem伪节,它結(jié)合了內(nèi)存的讀寫性能和持久化的特性光羞,可以在延遲可以控制在納秒級。
目前主流的實現(xiàn)為非易失性雙列直插式內(nèi)存模塊NVDIMM(Non-Volatile Dual In-Line Memory Module怀大,NVDIMM)纱兑,它是持久內(nèi)存的一種實現(xiàn),目前有三種實現(xiàn)標(biāo)準(zhǔn):
NVDIMM-N:配置同等容量的DRAM和NAND Flash化借,另外還有一個超大電容潜慎,當(dāng)主機(jī)斷電后,PMem設(shè)備會使用電容中保留的電量保證DRAM的數(shù)據(jù)同步到閃存中。
NVDIMM-F:使用了適配DDR規(guī)格的NAND Flash勘纯,通過多個控制器和橋接器將DDR總線信息轉(zhuǎn)化為SATA協(xié)議信息來操作閃存的讀寫局服。
NVDIMM-P:同樣配置了DRAM和NAND Flash,只不過DRAM容量會比閃存少很多驳遵,DRAM在其中作為閃存上層的緩存以優(yōu)化讀寫性能淫奔,同樣使用超大電容來保障斷電后的臟數(shù)據(jù)持久。
Intel傲騰第一代持久內(nèi)存AEP遵循NVDIMM-P標(biāo)準(zhǔn)堤结,實現(xiàn)了非易失性唆迁,可以按字節(jié)尋址(Byte Addressable)操作,小于1μs的延時竞穷,以及集成密度高于或等于DRAM等特性唐责。不同于傳統(tǒng)的NAND Flash實現(xiàn),傲騰持久內(nèi)存使用了新型非易失性存儲器3D-XPoint瘾带,其內(nèi)部是一種全新的存儲介質(zhì)鼠哥。
Intel傲騰持久內(nèi)存提供多種操作模式:
內(nèi)存模式:此模式下持久內(nèi)存被當(dāng)做超大容量的易失性內(nèi)存使用,其中DRAM被稱為近內(nèi)存(Near Memory)看政,持久化介質(zhì)被稱為遠(yuǎn)內(nèi)存(Far Memory)朴恳,讀寫性能取決于讀寫時命中近內(nèi)存還是遠(yuǎn)內(nèi)存。
AD模式:此模式下持久內(nèi)存直接暴露給用戶態(tài)的應(yīng)用程序直接調(diào)用允蚣,應(yīng)用程序通過持久內(nèi)存感知文件系統(tǒng)(PMEM-Aware File System)將用戶態(tài)的內(nèi)存空間直接映射到持久內(nèi)存設(shè)備上于颖,從而應(yīng)用程序可以直接進(jìn)行加載(Load)和存儲(Store)操作。這種形式也被稱作DAX嚷兔,意為直接訪問森渐。目前主流的文件系統(tǒng)ext4, xfs 都支持Direct Access的選項(-o dax),英特爾也提供了用于在持久內(nèi)存上進(jìn)行編程的用戶態(tài)軟件庫PMDK冒晰。
本次比賽使用AD模式同衣。
分析
首先關(guān)注的是正確性評測,寫入過程會重啟ECS翩剪,那么就要保證在append方法return之前數(shù)據(jù)要落盤乳怎,也就是說每個寫入請求都要fsync刷盤。另外在重啟ECS之后前弯,會清理PMem上的數(shù)據(jù)蚪缀,所以數(shù)據(jù)肯定要在ESSD上保存一份。
總寫入數(shù)據(jù)量為125G恕出,而ESSD提供400G容量询枚,正常寫入的情況下不用考慮硬盤GC的問題。除了ESSD空間外浙巫,我們還有60G的PMem可用金蜀,而且文件系統(tǒng)通常會預(yù)留一部分文件空間作緊急情況使用刷后,所以PMem可用容量會更高(實測真實容量為62G左右)。DRAM內(nèi)存也要盡可能利用起來渊抄,首選不受JVM限制的2G堆外尝胆,剩下的6G堆內(nèi)如何使用就要在GC和整體性能之間做抉擇了。
文件寫入
方案1:每個queue一個文件护桦,這樣可以保證順序讀寫含衔,但最壞的情況下需要創(chuàng)建100 * 5000 = 500,000個文件,操作系統(tǒng)默認(rèn)每個用戶進(jìn)程1024個句柄肯定會超限二庵。
方案2:每個topic一個文件贪染,那么最壞只需要創(chuàng)建100個文件,可以接受催享,但這意味著多個queue的數(shù)據(jù)要寫入同一個文件中杭隙,無法保證順序讀寫,不過可以是使用稀疏索引來做塊存儲因妙。另外因為正確性評測的限制痰憎,我們需要在每次寫入后手動fsync,所以這種設(shè)計下會導(dǎo)致頻繁的fsync攀涵,也就意味著用戶態(tài)與內(nèi)核態(tài)之間要頻繁的切來切去信殊,另外數(shù)據(jù)大小范圍為100B~17KiB,ESSD在一次寫入32K以上數(shù)據(jù)時才能發(fā)揮最優(yōu)性能汁果,很明顯當(dāng)前設(shè)計是打不滿ESSD PL1的吞吐的。
方案3:所有topic共用一個文件玲躯,通過對以上弊端的思考据德,我們應(yīng)該盡可能每次fsync時寫入更多的數(shù)據(jù),由于N個線程并發(fā)寫同一個文件跷车,所以我們可以將N個線程的數(shù)據(jù)先寫入聚合緩沖中后并掛起棘利,等待將緩沖中的數(shù)據(jù)刷盤后再取消阻塞。這個方案可以保證順序?qū)戨S機(jī)讀朽缴,每次寫入數(shù)據(jù)足夠多善玫,并且減少了核態(tài)的切換次數(shù),但是刷盤變成了串行密强,或許能得到一個不錯的ESSD吞吐茅郎,但是對CPU造成了浪費。
在上一個假設(shè)上做優(yōu)化或渤,因為評測環(huán)境配置4核CPU系冗,我們將所有線程分為4組,每組對應(yīng)一個文件薪鹦,這樣既可以保證ESSD的性能掌敬,又可以在無法綁核的情況下盡可能壓榨所有CPU的性能惯豆。
文件讀寫的API方面,首先放棄傳統(tǒng)的FileWriter/FileRead奔害,相比而言楷兽,F(xiàn)ileChannel提供雙向讀寫能力且更易操控讀寫數(shù)據(jù)精度。MMap是另外一種方案华临,因為它只在創(chuàng)建的時候需要切態(tài)芯杀,理論上它的讀寫速度會比FileChannel更快,但是由于種種原因银舱,MMap映射大小受限瘪匿,這無疑增加了程序設(shè)計上的維護(hù)成本,另外最終場景每次寫入數(shù)據(jù)量平均在64KB左右寻馏,通過Benchmark棋弥,F(xiàn)ileChannel在這種場景下性能總是優(yōu)于MMap。最終選定使用FileChannel進(jìn)行文件讀寫诚欠,另外為了減少用戶與向內(nèi)核態(tài)的內(nèi)存復(fù)制顽染,使用DirectByteBuffer用作寫入緩沖。
最終方案:將所有線程分為4組轰绵,充分利用多核CPU粉寞,每組對應(yīng)一個AOF數(shù)據(jù)文件,每組線程的數(shù)據(jù)寫入緩沖后并掛起左腔,緩沖刷盤后再取消阻塞唧垦,返回offset。
緩存利用
首先要明確一點液样,在本次賽題中振亮,無論是DRAM還是PMem,都不能利用它們用來做數(shù)據(jù)的持久化(PMem正確性階段重啟后會做數(shù)據(jù)清理)鞭莽,ESSD是必須要求寫入的坊秸。因此,緩存的主要利用方向在于提高讀性能澎怒。
首先是性能最快但是容量最小的DRAM褒搔,官方不允許使用unsafe來額外分配堆外的堆外內(nèi)存,所以可供我們使用的DRAM只有2G的堆外以及6G的堆內(nèi)喷面,又由于JVM的GC機(jī)制外加程序本身的業(yè)務(wù)流程需要一定的內(nèi)存開銷星瘾,所以6G的堆內(nèi)可供我們用來做數(shù)據(jù)存儲的部分大打折扣(實際測下來可以用到3.2G),而堆外內(nèi)存會有一部分用于文件讀寫緩沖惧辈,所以堆外內(nèi)存可用量也會小于2G死相。另外就是62G的持久內(nèi)存PMem,由于其性能優(yōu)于ESSD數(shù)百倍咬像,容量遠(yuǎn)大于DRAM算撮,且ext4支持dax模式生宛,可直接用FileChannel操作讀寫,對于它的合理使用直接決定了最終成績的好壞肮柜。
再回到性能評測上進(jìn)行分析陷舅,我們將整個過程分為是三個階段(重點,下文要考):
一階段:先寫入75G的數(shù)據(jù)审洞。
二階段:評測程序隨機(jī)挑選一半的queue從頭開始讀莱睁,另一半從結(jié)尾開始讀,并在讀的同時芒澜,繼續(xù)寫入50G的數(shù)據(jù)仰剿。
三階段:隨著時間的推移,最終讀取的offset點位會慢慢追趕上當(dāng)前寫入的點位痴晦,此階段中剛寫入的數(shù)據(jù)有可能下一刻被讀取南吮。
經(jīng)過分析,我們需要在一階段盡可能的將數(shù)據(jù)寫入緩存誊酌,這樣二階段讀取時可以減少ESSD的命中率部凑。由于二階段會有一半的queue從結(jié)尾開始讀數(shù)據(jù),這也就意味著這些queue之前的數(shù)據(jù)可以被淘汰碧浊,淘汰后的緩存可以復(fù)用于之后寫入的數(shù)據(jù)涂邀。另外由于二階段的過程是邊讀邊寫,讀后的緩存也可以投入復(fù)用箱锐。
所以理論上二階段所有寫入的數(shù)據(jù)全部可以復(fù)用到淘汰后的緩存比勉。到了三階段后,應(yīng)該盡可能使用性能最高的DRAM來存儲熱數(shù)據(jù)驹止。
最終方案:一階段首先將緩存寫入大約5G的DRAM中敷搪,之后的數(shù)據(jù)寫入62G的PMem中(此過程的ESSD一直保持著寫入),每個記錄的緩存信息保存在對應(yīng)的queue中幢哨。來到二階段后,將淘汰的緩存按介質(zhì)類型及大小放入不同的緩存池嫂便,之后寫入的數(shù)據(jù)會優(yōu)先向DRAM緩存池申請緩存塊捞镰,其次是PMem緩存池。
當(dāng)然毙替,前期的分析也只能基于理論岸售,最終方案的背后是無數(shù)個日日夜夜的測試和思考(卷就完了。
整體方案
一階段開始厂画,將所有線程隨機(jī)分為4組凸丸,每組對應(yīng)1個AOF文件,在寫入ESSD的同時袱院,異步寫入DRAM或PMem中屎慢。理論上在寫入5G + 62G = 67G數(shù)據(jù)后緩存用盡瞭稼,從此刻開始到寫滿75G之前都只是單純寫硬盤前普,所有的異步任務(wù)也將在此期間全部執(zhí)行完畢裁良。
二階段開始,每次讀取都會淘汰失效的緩存并放入緩存池中揭璃,寫入過程中會優(yōu)先按照記錄大小從緩存池中獲取到相應(yīng)的緩存塊集灌,理想情況下每次都能申請到對應(yīng)的緩存塊并寫入悔雹,Missing時記錄數(shù)據(jù)在ESSD上的位置索引。
每次讀取時欣喧,根據(jù)offset從獲取對應(yīng)的數(shù)據(jù)索引腌零,到索引指定的介質(zhì)中讀取數(shù)據(jù)并返回。
緩存池
本次賽題一共有DRAM唆阿,PMem以及ESSD三種介質(zhì)益涧,而讀寫的最小顆粒度為100B-17KiB的數(shù)據(jù),我們將之抽象為Data類酷鸦,它提供單個數(shù)據(jù)讀取功能饰躲,定義如下:
public abstract class Data {
? ? ? ? // 緩存塊大小
? ? protected int capacity;
? ? ? ? // 數(shù)據(jù)在文件中開始存儲的位置
? ? protected long position;
? ? // 從介質(zhì)中讀取
? ? public abstract void get(ByteBuffer buffer);
? ? ? // 從介質(zhì)中寫入
? ? ? ? public abstract void set(ByteBuffer buffer);
? ? // 從介質(zhì)中清除
? ? ? ? public abstract void clear();
}
在一階段中,會按照寫入大小創(chuàng)建對應(yīng)介質(zhì)的Data臼隔,它記錄了這條數(shù)據(jù)在當(dāng)前介質(zhì)中的索引信息(如果是DRAM則直接存放ByteBuffer指針)嘹裂,例如當(dāng)DRAM和PMem寫滿時,Data記錄的是當(dāng)前數(shù)據(jù)在ESSD中的position以及capacity摔握。
二階段開始時寄狼,隨著queue的讀寫會淘汰無效的DRAM和PMem Data并放入對應(yīng)的緩存池中,二階段過程中的寫入會優(yōu)先從DRAM緩存池中獲取閑置的Data氨淌,如果獲取失敗則從PMem緩存池獲取泊愧,如果依然失敗會降級為SSD Data(相當(dāng)于不走緩存)。如果獲取成功盛正,則將數(shù)據(jù)寫入到當(dāng)前緩存塊中并記錄在Queue索引中删咱。
由于二階段中的緩存塊都是從緩存池中獲取,因此緩存塊大小是固定的豪筝,會出現(xiàn)塊大小小于當(dāng)前寫入數(shù)據(jù)大小的情況痰滋,當(dāng)發(fā)生此類情況時,不足的大小會使用預(yù)留的堆外內(nèi)存補救续崖,這塊數(shù)據(jù)被稱為ext敲街,調(diào)用clear()方法同時會釋放ext。
而且严望,為了減少使用額外的ext多艇,緩存池會根據(jù)Data的capacity大小將之進(jìn)行分組,當(dāng)從緩存池獲取閑置緩存塊時像吻,會根據(jù)寫入數(shù)據(jù)的大小到緩存池分組中進(jìn)行匹配峻黍,取出合適區(qū)間中的緩存塊進(jìn)行使用复隆。
// 17K / 5 五組內(nèi)存回收池
public LinkedBlockingQueue<Data> getReadBuffer(int cap){
? ? return cap < Const.K * 3.4 ? null : cap < Const.K * 6.8
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? readBuffers2 : cap < Const.K * 10.2
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? readBuffers3 : cap < Const.K * 13.6
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? readBuffers4: readBuffers5;
}
數(shù)據(jù)索引
程序執(zhí)行過程中,數(shù)據(jù)寫入后會記錄一條索引到具體的queue中奸披,由于offset從0開始并有序的特性昏名,每個queue中會實例化一個ArrayList來記錄該索引,下標(biāo)即是offset阵面,value的話則為Data:
private final List<Data> records;
AOF中的數(shù)據(jù)格式
由于準(zhǔn)確性階段需要數(shù)據(jù)的recover轻局,所以直接存儲在AOF中的數(shù)據(jù)需要記錄一些額外的索引信息:
當(dāng)recover時,首先會讀取9個Byte來獲取頭信息样刷,當(dāng)校驗通過后仑扑,會根據(jù)Data Len來繼續(xù)讀取真實的數(shù)據(jù),之后根據(jù)TopicId置鼻,QueueId镇饮,Offset等信息找到目標(biāo)隊列預(yù)先建立索引。
文件預(yù)分配
根據(jù)官方渠道得知箕母,評測環(huán)境使用的文件系統(tǒng)為ext4储藐,在ext4文件系統(tǒng)下,每次創(chuàng)建一個物理文件會子啊系統(tǒng)中注冊一個inode來記錄文件的元數(shù)據(jù)信息以及block索引樹的根節(jié)點嘶是。
當(dāng)我們對文件進(jìn)行讀寫時钙勃,首先會從extent tree中尋找合適的block邏輯地址,再從block中拿到硬盤設(shè)備中的物理地址方可操作聂喇。如果找不到合適的extent或block則需要創(chuàng)建辖源,此過程還涉及到inode中元數(shù)據(jù)的變動,對內(nèi)核代碼簡單追蹤可知希太,最終會調(diào)用ext4_do_update_inode方法完成inode的更新克饶。
ext4_write_begin
? ? __block_write_begin
? ? ? ? get_block -> ext4_get_block_unwritten
? ? ? ? ? ? _ext4_get_block
? ? ? ? ? ? ? ? ext4_map_blocks
? ? ? ? ? ? ? ? ? ? ext4_ext_map_blocks
? ? ? ? ? ? ? ? ? ? ? ? ext4_ext_insert_extent
? ? ? ? ? ? ? ? ? ? ? ? ? ? ext4_ext_dirty
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ext4_mark_inode_dirty
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ext4_mark_iloc_dirty
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ext4_do_update_inode
其內(nèi)部實現(xiàn)過程中會先上文件內(nèi)全局的自旋鎖spin_lock(),在設(shè)置完新的block并更新inode元數(shù)據(jù)后調(diào)用spin_unlock()解鎖誊辉,之后處理臟元數(shù)據(jù)矾湃,這個過程需要記錄journal日志。
對于一個空文件進(jìn)行持續(xù)的寫入堕澄,每當(dāng)ext4_map_blocks()獲取block失敗邀跃,就會執(zhí)行復(fù)雜的流程來創(chuàng)建新的邏輯空間到物理空間的block映射,這種開銷對于性能的影響是非常致命的奈偏,對于分秒必爭的比賽更是如此。
為了避免這段開銷躯护,我們可以在寫入空白文件之前預(yù)先寫入足夠多的數(shù)據(jù)惊来,讓inode預(yù)熱一下,之后再從position 0開始寫入棺滞。這種方法稱為預(yù)分配裁蚁,Linux中提供fallocate命令完成這種操作矢渊,在Java中可以手動完成:
void fallocate(FileChannel channel, long allocateSize) throws IOException {
? ? ? if (channel.size() == 0){
? ? ? ? ? int batch = (int) (Const.K * 4);
? ? ? ? ? int size = (int) (allocateSize / batch);
? ? ? ? ? ByteBuffer buffer = ByteBuffer.allocateDirect(batch);
? ? ? ? ? for (int i = 0; i < batch; i ++){
? ? ? ? ? ? ? buffer.put((byte) 0);
? ? ? ? ? }
? ? ? ? ? for (int i = 0; i < size; i ++){
? ? ? ? ? ? ? buffer.flip();
? ? ? ? ? ? ? channel.write(buffer);
? ? ? ? ? }
? ? ? ? ? channel.force(true);
? ? ? ? ? channel.position(0);
? ? ? ? ? Utils.recycleByteBuffer(buffer);
? ? ? }
? }
當(dāng)然,預(yù)分配不是適用于所有場景枉证,本次賽題的計時從第一次append開始矮男,所以有足夠的時間在程序初始化過程中完成預(yù)分配。再者就是SSD硬盤空間的容量最好足夠大室谚,如果容量與要寫入的數(shù)據(jù)相當(dāng)毡鉴,預(yù)分配后再進(jìn)行寫入時,會導(dǎo)致SSD內(nèi)部頻繁的Foreground GC秒赤,性能下降猪瞬。
4K對齊
傳統(tǒng)HDD扇區(qū)單位一直習(xí)慣于512Byte,有些文件系統(tǒng)默認(rèn)保留前63個扇區(qū)入篮,也就是前512 * 63 / 1024 = 31.5KB陈瘦,假設(shè)閃存Page和簇(OS讀寫基本單位)都大小為4KB,那么一個Page對應(yīng)著8個扇區(qū)潮售,用戶數(shù)據(jù)將于第8個Page的第3.5KB位置開始寫入痊项,導(dǎo)致之后的每一個簇都會跨兩個Page,讀寫處于超界處酥诽,這對于閃存會造成更多的讀損及讀寫開銷鞍泉。
除了OS層的4K對齊至關(guān)重要以外,在文件寫入過程中仍然需要關(guān)注4K對齊的問題盆均。假設(shè)Page大小仍然為4KB塞弊,向一個空白文件寫入5KB數(shù)據(jù),此時需要2個Page來存儲數(shù)據(jù)泪姨,Page 1寫滿了4KB游沿,而Page2只寫入1KB,當(dāng)再次向文件順序?qū)懭霐?shù)據(jù)時肮砾,需要將Page2數(shù)據(jù)預(yù)先讀出來诀黍,然后與新寫入數(shù)據(jù)在內(nèi)存中合并后再寫入新的Page 3中,之前的Page 2則標(biāo)記為stale等待被GC仗处。這種帶來的開銷被稱為寫入放大WA(Write Amplification)眯勾。
為了減小WA,我們可以人工補充缺少的數(shù)據(jù)婆誓。對于本次賽題吃环,當(dāng)寫入緩沖刷盤前,將寫入Buffer的position右移至最近的4KB整數(shù)倍點位即可洋幻。
預(yù)讀取
二階段中郁轻,我們需要做的是從queue中獲取請求區(qū)間所有的Data,并根據(jù)Data中的索引信息將真實數(shù)據(jù)從對應(yīng)介質(zhì)中讀取出來,而且這個過程通常是批量的好唯,具體數(shù)量由入?yún)?b>fetchNum控制竭沫。
最開始我使用Semaphore對批量數(shù)據(jù)多線程并發(fā)讀,并且得到了不錯的效果骑篙。但是背后卻埋著不小的坑蜕提,由于每次getRange要頻繁的對多個線程阻塞和取消阻塞,線程上下文切換帶來開銷非常嚴(yán)重靶端,有興趣的讀者可以運行以下測試代碼(并把我不能接受打在彈幕里):
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
public class Test {
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? int count = 100 * 10000;
? ? ? ? int batch = 1;
? ? ? ? ThreadPoolExecutor pools =
? ? ? ? ? ? ? ? (ThreadPoolExecutor) Executors.newFixedThreadPool(batch);
? ? ? ? Semaphore semaphore = new Semaphore(0);
? ? ? ? long start = System.currentTimeMillis();
? ? ? ? for (int i = 0; i < count; i ++){
? ? ? ? ? ? for (int j = 0; j < batch; j ++){
? ? ? ? ? ? ? ? pools.execute(semaphore::release);
? ? ? ? ? ? }
? ? ? ? ? ? semaphore.acquire(batch);
? ? ? ? }
? ? ? ? long end = System.currentTimeMillis();
? ? ? ? System.out.println(end - start);
? ? }
}
我不能接受谎势,但是又要保證getRange階段盡可能并發(fā)讀取,于是乎我將思路轉(zhuǎn)向了預(yù)讀取躲查,方法與Page Cache預(yù)讀類似它浅,舉個栗子:當(dāng)getRange讀第0 ~ 10條數(shù)據(jù)的時候,從線程池中取個線程預(yù)讀取第10 ~ 20條數(shù)據(jù)镣煮,并將這些數(shù)據(jù)存儲在緩存塊中姐霍,實際測試中,足夠多的PMem緩存塊使我們不用擔(dān)心緩存池匱乏的問題典唇。
順帶一提
評測階段線程數(shù)量不固定镊折,好在所有線程幾乎同時執(zhí)行,所以在寫入時阻塞一段時間獲取到線程數(shù)量介衔,之后再對其進(jìn)行分組恨胚。
每個線程要持續(xù)運行,所以將線程內(nèi)數(shù)據(jù)存入ThreadLocal中炎咖,并盡可能復(fù)用赃泡。
數(shù)據(jù)格式中的offset或許可以拿掉,每條記錄可以省去4 Byte的空間乘盼。
兩個方法的入?yún)⒅猩埽琓opic的類型為String,但是格式固定為TopicN绸栅,可以搞個超大switch方法將其轉(zhuǎn)為int類型级野,方便之后的存儲與讀取。
結(jié)束
不知不覺粹胯,比賽已經(jīng)結(jié)束蓖柔,寫這篇文章的時候明天就要上交的PPT還未開工,這次比賽收獲很多风纠,遺憾也不少况鸣,收獲了很多卷友,遺憾自己未能如心竹观。
從第一個方案出分的驚喜若狂到優(yōu)化過程中的絞盡腦汁镐捧,每一秒的進(jìn)步都帶來了無與倫比的成就感。從為了給女朋友買個電瓶車代步的決心下定開始,仿佛就以注定要在這條道路上一卷無前愤估。
來年,希望張三再發(fā)一次橙圖(也不一定是橙色)速址,到時候如果我心有余力玩焰,肯定很快點進(jìn)來,然后一鍵三連芍锚。
倉庫地址:https://github.com/ainilili/tianchi-race-2021
參考