最近居家中,對(duì)自己之前做的一些工作進(jìn)行總結(jié)。正好有Doris社區(qū)的小伙伴吐槽向量化的導(dǎo)入性能表現(xiàn)并不是很理想,就借這個(gè)機(jī)會(huì)對(duì)之前開發(fā)的向量化導(dǎo)入的工作進(jìn)行了性能調(diào)優(yōu),取得了不錯(cuò)的優(yōu)化效果。借用本篇手記記錄下一些性能優(yōu)化的思路,拋磚引玉,希望大家多多參與到性能優(yōu)化的工作總來。
1.看起來很慢的向量化導(dǎo)入
問題的發(fā)現(xiàn)
來自社區(qū)用戶的吐槽:向量化導(dǎo)入太慢了啊,我測試了xx數(shù)據(jù)庫,比Doris快不少啊。有招嗎?
啊哈?慢這么多嗎? 那我肯定得瞅一瞅了。
于是對(duì)用戶case進(jìn)行了復(fù)現(xiàn),發(fā)現(xiàn)用戶測試的是代碼庫里ClickBench的stream load,80個(gè)G左右的數(shù)據(jù),向量化導(dǎo)入耗時(shí)得接近1200s,而非向量化導(dǎo)入耗時(shí)為1400s。
| 向量化 | 非向量化 |
|---|---|
| 1230s | 1450s |
ClickBench是典型的大寬表的場景,并且為Duplicate Key的模型,原則上能充分發(fā)揮向量化導(dǎo)入的優(yōu)勢。所以看起來一定是有些問題的,需要按圖索驥的來定位熱點(diǎn):
定位熱點(diǎn)的技巧
筆者通常定位Doris代碼的熱點(diǎn)有這么幾種方式,通過這些方式共同組合,能幫助我們快速定位到代碼真正的瓶頸點(diǎn):
Profile: Doris自身記錄的耗時(shí),利用Profile就能分析出大致代碼部分的瓶頸點(diǎn)。缺點(diǎn)是不夠靈活,很多時(shí)候需要手動(dòng)編寫代碼,重新編譯才能添加我們需要進(jìn)行熱點(diǎn)觀察的代碼。
FlameGraph: 一旦通過Profile分析到大概的熱點(diǎn)位置,筆者通常會(huì)快速通讀一遍代碼,然后結(jié)合火焰圖來定位到函數(shù)熱點(diǎn)的位置,這樣進(jìn)行的優(yōu)化通常就有的放矢了。關(guān)于火焰圖的使用可以簡要參考Doris的官方文檔的開發(fā)者手冊(cè)。
Perf: 火焰圖只能大致定位到聚合函數(shù)的熱點(diǎn),而且編譯器經(jīng)過內(nèi)聯(lián),匯編優(yōu)化之后,單純通過火焰圖的函數(shù)級(jí)別就不一定夠用了。通常需要進(jìn)一步分析匯編代碼的問題,這時(shí)則可以用開發(fā)手記2中提到的perf來定位匯編語言的熱點(diǎn)。當(dāng)然,perf并不是萬能的,很多時(shí)候需要我們基于代碼本身的熟稔和一些優(yōu)化經(jīng)驗(yàn)來進(jìn)一步進(jìn)行調(diào)優(yōu)。
接下來我們就基于上述的調(diào)優(yōu)思路,來一起分析一下這個(gè)問題。
2.優(yōu)化與代碼解析
基于火焰圖,筆者梳理出在向量化導(dǎo)入時(shí)的幾部分核心的熱點(diǎn)。針對(duì)性的進(jìn)行了問題分析與解決:
緩慢的Cast與字符串處理
在CSV導(dǎo)入到Doris的過程之中,需要經(jīng)歷一個(gè)文本數(shù)據(jù)解析,表達(dá)式CAST計(jì)算的過程。顯然,這個(gè)工作從火焰圖中觀察出來,是CPU的耗損大戶

上面的火焰圖可以觀察出來,這里有個(gè)很反常的函數(shù)調(diào)用耗時(shí)FunctionCast::prepare_remove_prepare,這里需要根據(jù)源碼來進(jìn)一步分析。
在進(jìn)行cast過程之中需要完成null值拆分的工作,比如這里需要完成String Cast Int的操作流程如下圖所示:

這里會(huì)利用原始的block,和待cast的列建立一個(gè)新的臨時(shí)block來進(jìn)行cast函數(shù)的計(jì)算。

上面標(biāo)紅的代碼會(huì)對(duì)std::set進(jìn)行大量的CPU計(jì)算工作,影響的向量化導(dǎo)入的性能。在導(dǎo)入表本身是大寬表的場景下,這個(gè)問題的嚴(yán)重性會(huì)進(jìn)一步放大。
進(jìn)行了問題定位之后,優(yōu)化工作就顯得很簡單了。顯然進(jìn)行cast的時(shí)候,我們僅僅只需要進(jìn)行cast計(jì)算的相關(guān)列,而并不需要整個(gè)block中所有的列都參與進(jìn)來。所以筆者這里實(shí)現(xiàn)了一個(gè)新的函數(shù) create_block_with_nested_columns_only_args來替換create_block_with_nested_columns_impl,原本對(duì)100列以上的計(jì)數(shù)問題,減少為對(duì)一個(gè)列進(jìn)行處理,問題得到了顯著的改善。
| 優(yōu)化前 | 優(yōu)化后 |
|---|---|
| 1230s | 980s |
缺頁中斷的優(yōu)化
解決了上面問題之后,繼續(xù)來對(duì)火焰圖進(jìn)行分析,發(fā)現(xiàn)了在數(shù)據(jù)寫入memtable時(shí),產(chǎn)生了下面的熱點(diǎn):缺頁中斷。

這里得先簡單了解一下什么是缺頁中斷:

如上圖所示:CPU對(duì)數(shù)據(jù)進(jìn)行計(jì)算時(shí),會(huì)請(qǐng)求獲取內(nèi)存中的數(shù)據(jù)。而CPU層級(jí)看的內(nèi)存地址是:Virtual Address,需要經(jīng)過特別的CPU結(jié)構(gòu)MMU進(jìn)行虛擬地址到物理地址的映射。而MMU會(huì)到TLB(Translation lookaside buffer,記住這個(gè)是個(gè)緩存),查找對(duì)應(yīng)的虛擬地址到物理地址的映射。由于操作系統(tǒng)中,內(nèi)存都是通過頁進(jìn)行管理的,地址都是基于頁內(nèi)存地址的偏移量,所以這個(gè)過程變成了查找起始頁地址的一個(gè)工作。如果目標(biāo)虛存空間中的內(nèi)存頁,在物理內(nèi)存中沒有對(duì)應(yīng)的頁映射,那么這種情況下,就產(chǎn)生了缺頁中斷(Page Fault)。
缺頁中斷顯然會(huì)帶來一些額外的開銷:
- 用戶態(tài)到內(nèi)核態(tài)的切換
- 內(nèi)核處理缺頁錯(cuò)誤
所以,頻繁的出現(xiàn)缺頁中斷,對(duì)導(dǎo)入的性能產(chǎn)生了不利的影響,需要嘗試解決它。
內(nèi)存復(fù)用
這里大量的內(nèi)存使用,取址都是對(duì)于Column進(jìn)行操作導(dǎo)致的,所以得嘗試從內(nèi)存分配的源頭來解決這個(gè)問題。
解決思路也很簡單,既然缺頁中斷是內(nèi)存沒有映射引起的,那這里就盡量復(fù)用之前已經(jīng)使用過的內(nèi)存,這樣,自然也不會(huì)引起缺頁中斷的問題了,對(duì)于TLB的緩存訪問也有了更高的親和度。
Doris內(nèi)部本身支持了ChunkAlloctor的類來進(jìn)行內(nèi)存分配,復(fù)用,綁核的邏輯,通過ChunkAlloctor能大大提升內(nèi)存申請(qǐng)的效率,對(duì)于當(dāng)前case的缺頁中斷也能起到規(guī)避的效果:

通過替換podarray的內(nèi)存分配的邏輯之后,效果也很符合預(yù)期,通過火焰圖進(jìn)行觀察,缺頁中斷的占比大量的減少,性能上也獲得了可觀的收益。
| 優(yōu)化前 | 優(yōu)化后 |
|---|---|
| 980s | 776s |
3.一些相關(guān)的優(yōu)化的TODO:
CSV的數(shù)據(jù)格式解析:通過4kb的cache 來預(yù)取多行數(shù)據(jù),利用并SIMD指令集來進(jìn)一步性能優(yōu)化
缺頁中斷的優(yōu)化:部分內(nèi)存分配拷貝過程之中的page fault的問題, 可以考慮引入大頁內(nèi)存機(jī)制來進(jìn)一步進(jìn)行缺頁中斷,頁內(nèi)存cache的優(yōu)化
4.小結(jié)
當(dāng)然,筆者進(jìn)行的向量化導(dǎo)入工作只是Doris向量化導(dǎo)入中的一部分工作。很多社區(qū)的同學(xué)也深入?yún)⑴c了相關(guān)工作,在當(dāng)前的基礎(chǔ)上又有得到了更為理想的性能表現(xiàn)??傊?,性能優(yōu)化的工作是永無止境的.
這里也特別鳴謝社區(qū)的兩位同學(xué)的code review和分析幫助:@xinyiZzz, @Gabriel
Bingo!請(qǐng)大家期待下一個(gè)1.2版本全面向量化的Doris,相信在性能和穩(wěn)定性上,一定會(huì)帶給各位驚喜。
最后,也希望大家多多支持Apache Doris,多多給Doris貢獻(xiàn)代碼,感恩~~