美團(tuán)Apache Kylin精確去重指標(biāo)優(yōu)化歷程
康凱森,美團(tuán)點(diǎn)評(píng)大數(shù)據(jù)工程師,Apache Kylin commiter,目前主要負(fù)責(zé)Apache Kylin在美團(tuán)點(diǎn)評(píng)的平臺(tái)化建設(shè)。
問(wèn)題背景
本文記錄了我將Apache Kylin超高基數(shù)的精確去重指標(biāo)查詢提速數(shù)十倍的過(guò)程,大家有任何建議或者疑問(wèn)歡迎討論。
某業(yè)務(wù)方的cube有12個(gè)維度,35個(gè)指標(biāo),其中13個(gè)是精確去重指標(biāo),并且有一半以上的精確去重指標(biāo)單天基數(shù)在千萬(wàn)級(jí)別,cube單天數(shù)據(jù)量1.5億行左右。業(yè)務(wù)方一個(gè)結(jié)果僅有21行的精確去重查詢竟然耗時(shí)12秒多,其中HBase端耗時(shí)6秒多,Kylin的query server端耗時(shí)5秒多:
- SELECT A, B, count(distinct uuid), FROM table WHERE dt = 17150 GROUP BY A, B
精確去重指標(biāo)已經(jīng)在美團(tuán)點(diǎn)評(píng)生產(chǎn)環(huán)境大規(guī)模使用,我印象中精確去重的查詢的確比普通的Sum指標(biāo)慢一點(diǎn),但也挺快的。這個(gè)查詢慢的如此離譜,我就決定分析一下,這個(gè)查詢到底慢在哪。
優(yōu)化1 將精確去重指標(biāo)拆分HBase列族
我首先確認(rèn)了這個(gè)cube的維度設(shè)計(jì)是合理的,這個(gè)查詢也精準(zhǔn)匹配了cuboid,并且在HBase端也只掃描了21行數(shù)據(jù)。
那么問(wèn)題來(lái)了,為什么在HBase端只掃描21行數(shù)據(jù)卻需要6秒多?一個(gè)顯而易見的原因是Kylin的精確去重指標(biāo)是用bitmap存儲(chǔ)的明細(xì)數(shù)據(jù),而這個(gè)cube有13個(gè)精確去重指標(biāo),并且基數(shù)都很大。我從兩方面驗(yàn)證了這個(gè)猜想:
1.同樣SQL的查詢Sum指標(biāo)只需要120毫秒,并且HBase端Scan僅需2毫秒。
2.我用HBase HFile命令行工具查看并計(jì)算出HFile中單個(gè)KeyValue的大小,發(fā)現(xiàn)普通指標(biāo)的列族中每個(gè)KeyValue平均大小是29B,精確去重指標(biāo)列族的每個(gè)KeyValue平均大小卻有37M。
所以我第一個(gè)優(yōu)化就是將精確去重指標(biāo)拆分到多個(gè)HBase列族,優(yōu)化后的效果十分明顯。查詢時(shí)間從12秒多減少到5.7秒左右,HBase端耗時(shí)從6秒多減少到1.3秒左右,不過(guò)query server耗時(shí)依舊有4.5秒多。
優(yōu)化2 移除不必要的toString避免bitmap deserialize
Kylin的query server耗時(shí)依舊有4.5秒多,我猜測(cè)肯定還是和bitmap比較大有關(guān),但是為什么bitmap大會(huì)導(dǎo)致如此耗時(shí)呢?為了分析query server端查詢處理的時(shí)間到底花在了哪,我利用Java Mission Control進(jìn)行了性能分析。
JMC分析很簡(jiǎn)單,在Kylin的啟動(dòng)進(jìn)程中增加以下參數(shù):
- -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
- -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints
- -XX:StartFlightRecording=delay=20s,duration=300s,name=kylin,filename=myrecording.jfr,settings=profile
獲得myrecording.jfr文件后,我們?cè)诒緳C(jī)執(zhí)行jmc命令,然后打開myrecording.jfr文件就可以進(jìn)行性能分析。從jmc的熱點(diǎn)代碼圖中我們發(fā)現(xiàn),耗時(shí)最多的代碼竟然是一個(gè)毫無(wú)意義的toString。去掉這個(gè)toString之后,query server的耗時(shí)直接減少了1秒多。
優(yōu)化3 獲取bitmap的字節(jié)長(zhǎng)度時(shí)避免deserialize
在優(yōu)化2去掉無(wú)意義的toString之后,熱點(diǎn)代碼已經(jīng)變成了對(duì)bitmap的deserialize。不過(guò)bitmap的deserialize共有兩處,一處是bitmap本身的deserialize,一處是在獲取bitmap的字節(jié)長(zhǎng)度時(shí)。于是很自然的想法就是是在獲取bitmap的字節(jié)長(zhǎng)度時(shí)避免deserialize bitmap,當(dāng)時(shí)有兩種思路:
1.在serialize bitmap時(shí)就寫入bitmap的字節(jié)長(zhǎng)度。
2.在MutableRoaringBitmap序列化的頭信息中獲取bitmap的字節(jié)長(zhǎng)度。(Kylin的精確去重使用的bitmap是RoaringBitmap)
我最終確認(rèn)思路2不可行,采用了思路1。
思路1中一個(gè)顯然的問(wèn)題就是如何保證向前兼容,我向前兼容的方法就是根據(jù)MutableRoaringBitmap deserialize時(shí)的cookie頭信息來(lái)確認(rèn)版本,并在新的serialize方式中寫入了版本號(hào),便于之后序列化方式的更新和向前兼容。
經(jīng)過(guò)這個(gè)優(yōu)化后,Kylin query server端的耗時(shí)再次減少1秒多。
優(yōu)化4 無(wú)需上卷聚合的精確去重查詢優(yōu)化
從精確去重指標(biāo)在美團(tuán)點(diǎn)評(píng)大規(guī)模使用以來(lái),我們發(fā)現(xiàn)部分用戶的應(yīng)用場(chǎng)景并沒有跨segment上卷聚合的需求,即只需要查詢單天的去重值,或是每次全量構(gòu)建的cube,也無(wú)需跨segment上卷聚合。所以我們希望對(duì)無(wú)需上卷聚合的精確去重查詢進(jìn)行優(yōu)化,當(dāng)時(shí)我考慮了兩種可行的方案:
方案1: 精確去重指標(biāo)新增一種返回類型
一個(gè)極端的做法是對(duì)無(wú)需跨segment上卷聚合的精確去重查詢,我們只存儲(chǔ)最終的去重值。
優(yōu)點(diǎn):
1.存儲(chǔ)成本會(huì)極大降低。
2.查詢速度會(huì)明顯提高。
缺點(diǎn):
1.無(wú)法支持上卷聚合,與Kylin指標(biāo)的設(shè)計(jì)原則不符合。
2.無(wú)法支持segment的merge,因?yàn)橐M(jìn)行merge必須要存儲(chǔ)明細(xì)的bitmap。
3.新增一種返回類型,對(duì)不清楚的用戶可能會(huì)有誤導(dǎo)。
4.查詢需要上卷聚合時(shí)直接報(bào)錯(cuò),用戶體驗(yàn)不好,盡管使用這種返回類型的前提是無(wú)需上聚合卷。
實(shí)現(xiàn)難點(diǎn):
如果能夠接受以上缺點(diǎn),實(shí)現(xiàn)成本并不高,目前沒有想到明顯的難點(diǎn)。
方案2:serialize bitmap的同時(shí)寫入distinct count值。
優(yōu)點(diǎn):
1.對(duì)用戶無(wú)影響。
2.符合現(xiàn)在Kylin指標(biāo)和查詢的設(shè)計(jì)。
缺點(diǎn):
1.存儲(chǔ)依然需要存儲(chǔ)明細(xì)的bitmap。
2.查詢速度提升有限,因?yàn)榧词共贿M(jìn)行任何bitmap serialize,bitmap本身太大也會(huì)導(dǎo)致HBase scan,網(wǎng)絡(luò)傳輸?shù)冗^(guò)程變慢。
實(shí)現(xiàn)難點(diǎn):
如何根據(jù)是否需要上卷聚合來(lái)確定是否需要serialize bitmap?
解決過(guò)程:
我開始的思路是從查詢過(guò)程入手,確認(rèn)在整個(gè)查詢過(guò)程中,哪些地方需要進(jìn)行上卷聚合。為此,我仔細(xì)閱讀了Kylin query server端的查詢代碼,HBase Coprocessor端的查詢代碼,Calcite的example例子。發(fā)現(xiàn)在HBase端,Kylin query server端,cube build時(shí)都有可能需要指標(biāo)的聚合。
此時(shí)我又意識(shí)到一個(gè)問(wèn)題:即使我清晰的知道了何時(shí)需要聚合,我又該如何把是否聚合的標(biāo)記傳遞到精確去重的反序列方法中呢?現(xiàn)在精確去重的deserialize方法參數(shù)只有一個(gè)ByteBuffer,如果加參數(shù),就要改變整個(gè)kylin指標(biāo)deserialize的接口,這將會(huì)影響所有指標(biāo)類型,并會(huì)造成大范圍的改動(dòng)。所以我把這個(gè)思路放棄了。
后來(lái)我"靈光一閃",想到既然我的目標(biāo)是優(yōu)化無(wú)需上卷的精確去重指標(biāo),那為什么還要費(fèi)勁去deserialize出整個(gè)bitmap呢,我只要個(gè)distinct count值不就完了。所以我的目標(biāo)就集中在BitmapCounter本身的deserialize上,并聯(lián)想到我最近提升了Kylin前端加載速度十倍以上的核心思想:延遲加載,就改變了BitmapCounter的deserialize方法,默認(rèn)只讀出distinct count值,不進(jìn)行bitmap的deserialize,并將那個(gè)buffer保留,等到的確需要上卷聚合的時(shí)候再根據(jù)buffer deserialize 出bitmap。
當(dāng)然,這個(gè)思路可行有一個(gè)前提,就是buffer內(nèi)存拷貝的開銷是遠(yuǎn)小于bitmap deserialize的開銷,慶幸的是事實(shí)的確如此。最終經(jīng)過(guò)這個(gè)優(yōu)化,對(duì)于無(wú)需上卷聚合的精確去重查詢,查詢速度也有了較大提升。顯然,如你所見,這個(gè)優(yōu)化加速查詢的同時(shí)加大了需要上卷聚合的精確去重查詢的內(nèi)存開銷。我的想法是首先對(duì)于超大數(shù)據(jù)集并且需要上卷的精確去重查詢,用戶在分析查詢時(shí)返回的結(jié)果行數(shù)應(yīng)該不會(huì)太多,其次我們需要做好query server端的內(nèi)存控制。
總結(jié)
我通過(guò)總共4個(gè)優(yōu)化,在向前兼容的前提下,后端僅通過(guò)100多行的代碼改動(dòng),對(duì)Kylin超高基數(shù)的精確去重指標(biāo)查詢有了明顯提升,測(cè)試中最明顯的查詢有50倍左右的提升。