- 問題背景
- 優化1 將精確去重指標拆分HBase列族
- 優化2 移除不必要的toString避免bitmap deserialize
- 優化3 獲取bitmap的字節長度時避免deserialize
- 優化4 無需上卷聚合的精確去重查詢優化
- 總結
- 反思
- 相關Kylin JIRA
本文記錄了我將Apache Kylin超高基數的精確去重指標查詢提速數十倍的過程,大家有任何建議或者疑問歡迎討論。
問題背景
某業務方的cube有12個維度,35個指標,其中13個是精確去重指標,並且有一半以上的精確去重指標單天基數在千萬級別,cube單天數據量1.5億行左右。
但是業務方的一個結果僅有21行的精確去重查詢竟然需要12秒多:
SELECT A, B, count(distinct uuid),
FROM table
WHERE dt = 17150
GROUP BY A, B
其中HBase端耗時6秒多,Kylin的query server端耗時5秒多。
精確去重指標已經在美團點評生產環境大規模使用,我印象中精確去重的查詢的確比普通的Sum指標慢一點,但也是挺快的。這個查詢慢的如此離譜,我就決定分析一下,這個查詢到底慢在哪。
優化1 將精確去重指標拆分HBase列族
我首先確認了這個cube的維度設計是合理的,這個查詢也精準匹配了cuboid,並且在HBase端也只掃描了21行數據。
那麼問題來了, 爲什麼在HBase端只掃描21行數據需要6秒多?一個顯而易見的原因是Kylin的精確去重指標是用bitmap存儲的明細數據,而這個cube有13個精確去重指標,並且基數都很大。
我從兩方面驗證了這個猜想:
- 同樣SQL的查詢Sum指標只需要120毫秒,並且HBase端Scan僅需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文件就可以進行性能分析。
熱點代碼的分析如圖:
從圖中我們可以發現,耗時最多的代碼竟然是一個毫無意義的toString。
Preconditions.checkState(comparator.compare(last, fetched) <= 0, "Not sorted! last: " + last + " fetched: " + fetched);
代碼中的last
和 fetched
就是一個bitamp。 去掉這個toString之後,query server的耗時直接減少1秒多。
優化3 獲取bitmap的字節長度時避免deserialize
在優化2去掉無意義的toString之後,熱點代碼已經變成了對bitmap的deserialize。
不過bitmap的deserialize共有兩處,一處是bitmap本身的deserialize,一處是在獲取bitmap的字節長度時。
於是很自然的想法就是是在獲取bitmap的字節長度時避免deserialize bitmap,當時有兩種思路:
- 在serialize bitmap時就寫入bitmap的字節長度
- 在
MutableRoaringBitmap
序列化的頭信息中獲取bitmap的字節長度。(Kylin的精確去重使用的bitmap是RoaringBitmap)
我最終確認思路2不可行,採用了思路1。
思路1中一個顯然的問題就是如何保證向前兼容,我向前兼容的方法就是根據MutableRoaringBitmap
deserialize時的cookie頭信息來確認版本,並在新的serialize方式中寫入了版本號,便於之後序列化方式的更新和向前兼容。
經過這個優化後,Kylin query server端的耗時再次減少1秒多。
優化4 無需上卷聚合的精確去重查詢優化
從精確去重指標在美團點評大規模使用以來,我們發現部分用戶的應用場景並沒有跨segment上卷聚合的需求,即只需要查詢單天的去重值,或是每次全量構建的cube,也無需跨segment上卷聚合。
所以我們希望對無需上卷聚合的精確去重查詢進行優化,當時我考慮了兩種可行的方案:
方案1: 精確去重指標新增一種返回類型
一個極端的做法是對無需跨segment上卷聚合的精確去重查詢,我們只存儲最終的去重值。
優點:
- 存儲成本會極大降低。
- 查詢速度會明顯提高。
缺點:
- 無法支持上卷聚合,與Kylin指標的設計原則不符合。
- 無法支持segment的merge,因爲要進行merge必須要存儲明細的bitmap。
- 新增一種返回類型,對不清楚的用戶可能會有誤導。
- 查詢需要上卷聚合時直接報錯,用戶體驗不好,儘管使用這種返回類型的前提是無需上聚合卷。
實現難點: 如果能夠接受以上缺點,實現成本並不高,目前沒有想到明顯的難點。
方案2:serialize bitmap的同時寫入distinct count值。
優點:
- 對用戶無影響。
- 符合現在Kylin指標和查詢的設計。
缺點:
- 存儲依然需要存儲明細的bitmap。
- 查詢速度提升有限,因爲即使不進行任何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
倍左右的提升。
大家有任何好的建議或者疑問歡迎在相關的JIRA中討論。
反思
- 我們應該善於利用各類命令和工具,快速分析和定位問題。
- 重寫
toString
,hashCode
,equals
等方法時一定要輕量化,不要有複雜的操作。 - 設計序列化,通信協議,存儲格式時,一定要有版本信息,便於之後的更新和向前兼容。