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