Kylin性能調優記——業務技術兩手抓

背景

最近開始使用了新版本的Kylin,在此之前對於新版本的瞭解只是代碼實現和一些簡單的新功能測試,但是並沒有導入實際場景的數據做分析和查詢,線上Hadoop穩定之後,逐漸得將一些老需求往新的環境遷移,基於以前的調研,新版本(V2,版本爲1.5.2)的Kylin提供了幾個比較顯著的功能和優化:

  • 新的度量類型,包括TOPN、基於bitmap的精確distinct count和RAW。
  • 自定義度量框架,用戶可以定義一些特殊的度量需求。
  • Fast Cubing算法,減少MR任務,提升build性能。
  • 查詢優化,Endpoint Coprocessor,並行scan和數據壓縮,這部分對於查詢性能提升還是比較顯著的。
  • shard hbase存儲,基於一致性哈希的rawkey分佈,儘可能的將大的cuboid分散到不同的region上,增加並行掃描度。
  • spark計算引擎,in memory準實時計算,這兩項目前還處於試驗階段。
  • 新的aggregation group分區算法。

有了這麼多新的特性和性能提升,經常拿新版本來應付用戶的需求:新版本實現了xxx功能,肯定性能會很好,等到新版本穩定了再來搞這個需求吧。等到我們的新版本上線了,業務需求真正上來之後,才發現用起來沒有相當的那麼簡單,對於Kylin這種直接面向用戶需求的服務,對於一些特殊的需求更是要不斷打磨和調整才能達到更好的性能。本文整理在接入雲音樂一個較爲複雜的需求的實現和中間調優的過程,一方面開拓一下自己的思路,另外也使得讀者能改更好的使用Kylin。

業務需求

數據通過日誌導入到Hive中查詢,經過一定的聚合運算,整理成如圖1中的格式。這個數據源是針對歌曲統計的,每一首歌曲包含歌曲屬性信息(歌曲名、歌手ID,專輯ID等)、支付方式、所屬圈子ID、標籤信息等,需要統計的指標包括每一首歌曲的各種操作的PV和UV,操作包括播放、收藏、下載等10種。因此一共20左右個指標,然後需要按照這20個指標的任意一個指標計算TOP N歌曲和其他統計信息(N可能是1000,1W等),除此之外,在計算TOP N的時候還需要支持字段的過濾,可以執行過濾的字段包括支付方式、圈子ID、標籤等。歌曲ID全部的成員數大概在千萬級別,該表的數據每天導入到hive,經過初步的聚合計算生成圖1中的格式,基本上保證了每天的每一個用戶對於每一首歌曲只保留一條記錄,每天的記錄數大概在幾億條(聚合之後),查詢通常只需要查詢不同條件下的TOPN的song_id。查詢性能儘可能的高(秒級),需要提供給分析人員使用。

簡單聚合的數據

基於kylin 1.x版本的實現

Kylin 1.3.0之前的版本對於這種需求的確有點無能爲力,我們使用了最簡單的方式實現,創建一個包含所有維度、所有度量的cube,其中10個度量爲count distinct,使用hyperloglog實現的近似去重計數算法,這種做法能夠可以說是最原始的,每次查詢時如果不進行任何過濾則需要從hbase中掃描幾百萬條記錄才能滿足TOPN的查詢,每一條記錄還都包含hyperloglog序列化的值(讀取到內存之後還需要對hyperloglog進行反序列化),TOP的查詢速度可想而知,正常情況(沒有併發)下能夠在1、2分鐘出結果,偶爾還會有一個查詢把服務搞掛(順便吐槽一下我們使用對的虛擬機)。查詢以天的數據就這種情況了,對於跨天的數據更不敢嘗試了。

這種狀況肯定無法滿足需求方,只能等着新版本落地之後使用TOPN度量。

需求簡化

面對這樣的現實,感覺對於去重計數無能爲力了,尤其像這種包含10個distinct count度量的cube,需求方也意識到這種困境,對需求做出了一定的讓步:不需要跨天計算TOPN的song_id了,只需要根據每一天的PV或者UV值計算TOPN,這樣就不需要distinct count了,而且還能保證每天的統計值是精確的。如果希望計算大的時間週期的TOPN,則通過創建一個新的cube實現每個自然周、自然月的統計。對於這樣的需求簡化,可以大大提升cube的創建,首先需要對數據源做一些改變(這種改變通常是view來完成),將原始表按照歌曲ID、用戶ID和其它的維度進行聚合(group by,保證轉換後的新表每一個用戶對於每一首歌曲只保存一條記錄),PV則直接根據計算SUM(操作次數),UV通過表達式if(SUM(操作次數) > 0, 1, 0)確定每一個userid對每一個songid在當天是否進行了某種操作,這樣我們就得到如圖2中格式的表:

歌曲和用戶進行聚合

這個表中可以保證歌曲ID+用戶ID是唯一的(其它的維度是歌曲的屬性,一個歌曲只對應相同的屬性),對於Kylin來說原始數據量也變小了(還是每天幾億條記錄),接下來新建的Cube由於數據源保證每一條記錄不是同一個用戶對同一首歌曲的操作,因此所有的度量都可以用SUM來實現(去重計數也可以通過對sum(是否xxx)統計精確的計數值),這樣對於Kylin的負擔減少了一大部分,但是掃描記錄數還是這麼多(Cube中最大的維度爲歌曲,每次查詢都需要攜帶,查詢時掃描記錄數始終保持在歌曲ID的個數),但是Build性能提升了,查詢性能也由於掃描數據量減少(一個SUM度量佔用的存儲空間只有8字節)而提升,但是還是需要30+秒。

此時,新版本終於在公司內部落地了,彷彿看到了新的曙光。

Kylin新版本(2.x)的實現

新版本有了TOPN度量了,但是目前只支持最大TOP 1000,可以通過直接改cube的json定義改變成最大得到5000,於是嘗試了幾種使用TOPN的方案。

第一種是將所有維度放在一個Cube中然後對於每一個PV和UV創建一個TOPN度量。這樣build速度還是挺快的,但是查詢時大約需要25秒+,最坑爹的是,創建TOPN時需要指定一個指標列(做SUM,根據該列的SUM值排序)和一個聚合列(歌曲ID),使用一個聚合列創建的多個TOPN度量居然只有查詢第一個度量時纔有結果,使用其他指標查詢TOPN時返回的聚合值總是null,難道我要爲每一個指標都建一個cube,每一個只包含一個topn度量?

第二種方案是不使用全部維度,而只使用在計算TOPN時需要做過濾的列,例如支付類型、圈子ID等維度建包含TOPN的Cube,這樣通過抽取部分維度創建一個小Cube用來計算TOPN,然後再使用計算出來的一批批歌曲ID從大Cube(包含全部維度的cube)中取出這個ID對應的其它指標和屬性信息。這樣做的弊端是對於查詢不友好,查詢時需要執行兩條SQL並且在前端進行分頁,如果性能可以接受也就認了,但是現實總是那麼殘酷,當創建了一個這樣的Cube(包含歌曲ID和其他幾個成員值很少的維度)進行build數據時,讓人意想不到的事情發生了,計算第二層Cuboid的任務居然跑了3個小時才勉強跑完,接下來的任務就更不敢想象了,MR任務的task使用CPU總是100%,通過單機的測試發現多個TOPN的值進行merge的性能非常差,尤其是N比較大的時候(而且Kylin內部會把N放大50倍以減少誤差),看來這個方案連測試查詢性能的機會都沒有了。

嘗試了這兩個方案之後我開始慢慢的失去信心了,但是想到了Kylin新版本的並行掃描會對性能有所提升,於是就創建了一個小cube(包含歌曲ID和幾個過濾維度,和全部的SUM度量)用於計算TOPN,整個cube計算一天的數據只需要不到1G的存儲,想必掃描性能會更好吧,查詢測試發現在不加任何過濾條件的情況下對任意指標的TOPN查詢大約不到15s,但是掃描的記錄數仍然是幾百W(歌曲ID的數目),可見並行掃描帶來的性能提升,但是掃描記錄數仍然是一個繞不過去的坎。

在經歷這麼多打擊之後,實在有點不知所措了,感覺歌曲ID的數目始終是限制查詢性能的最大障礙,畢竟要想計算TOPN肯定要查看每一個歌曲ID的統計值,然後再排序,避免不了這麼多的掃描,唯一的方法也就是減小region的大小,使得一天的數據分佈在更多的region中,增大並行度,但這也不是最終的解決方案啊,怎麼減小掃描記錄數呢?

從業務出發

陷入了上面的思考之後,提升查詢性能只有通過減少掃描記錄數實現,而記錄數最少也要等於歌曲ID的成員數,是否可以減少歌曲ID的成員數呢?需求方的一個提醒啓發了我們,全部的20個指標最關鍵的是播放次數,一般播放次數在TOPN的歌曲,也很有可能在其他指標的TOPN中。那麼是否可以通過去除長尾數據來減少歌曲ID的個數呢?首先查了一下每天播放次數大於50的個數數量,查詢結果對於沒接觸過業務的我來說還是很喫驚的,居然還剩幾十W,也就意味着通過排除每天播放次數小於50的歌曲,可以減少將近80%+的歌曲ID,而這些歌曲ID對於任何指標的TOPN查詢都是沒有任何作用的,於是通過view把數據源進行在過濾和聚合,創建包含全部維度的cube,查詢速度果然槓槓的,只需要2s左右了!畢竟只需要掃描的記錄數只有幾十萬,並且都是SUM的度量。

終於達成目標了,但是這種查詢其實是對原始數據的過濾,萬一需求方需要查詢這些播放次數比較少的歌曲ID的其他指標呢?完全不能從Kylin中獲取啊。此時,業務方的哥們根據數據特性想到了一個很好的辦法:爲播放次數建索引!根據播放次數的數值創建一個新的維度,這個維度的值等於播放次數所在的區間,例如播放次數是幾位數這個維度的值就是多少,或者使用case when sum(播放次數)自定義不同的範圍。例如某一個歌曲在當天SUM(播放次數)爲千萬級,那麼這個歌曲對應的index維度值爲8,百萬級則值爲7,以此類推。然後查詢的時候根據index過濾確定播放次數的範圍,這樣可以大大減少歌曲ID的數目,進而減少掃描記錄數。

此時需要對圖2中的記錄格式再進行整理,通過group by 歌曲ID,xxx(其他維度)統計出每一首歌曲的操作次數和操作人數,然後再根據操作次數的值確定index的值。得到新的記錄格式如圖3.

說幹就幹,整理完數據之後再添加一個index維度創建新的Cube,查詢的時候可以先查詢select 歌曲ID, sum(指標) as c from table where index >= 7 group by 歌曲ID order by c desc limit 100,使用着這種查詢可以直接過濾掉所有播放次數小於百萬的歌曲ID,但是經過測試發現,這種查詢時間還是15s左右,和上面的方案性能並無改善。整得我們開始懷疑人生了。

聚合並加index

迴歸技術

接下來感覺是走投無路之後的各種猜測,例如猜測shard by設置true or false是否對性能有很大的影響,經過查看源碼發現shard by只是爲了決定該列的值是否參與一致性哈希中桶號的計算,只不過是爲了更好的打散rowkey的分佈,對於存儲和查詢性能提升不是太明顯,思來想去還是不合理,既然加了過濾不應該還要掃描全部的歌曲ID,是不是Cube中rowkey的順序錯了,查了一下cube的定義,發現index維度被放在rowkey的最後一行,這顯然是不瞭解Kylin是如何確定掃描範圍的!Kylin存儲在hbase中的記錄是rowkey是根據定義Cube時確定的rowkey順序確定的,把如果查詢的時候對某一個維度進行範圍過濾,Kylin則會根據過濾的範圍確定掃描的區間以減小掃描記錄數,如果把這個維度放到rowkey的最後,則將這種過濾帶來的減小掃描區間的作用降到了最低。

重新修改Cube把index維度在rowkey中的位置提升到最前面之後重新build,此後再執行相同的查詢速度有了質的飛躍,降低到了1s以內。根據index的範圍不同掃描記錄數也隨着改變,但是對於查詢TOPN一般只需要使用index >= 6的過濾條件,這種條件下掃描記錄數可以降低到幾萬,並且保證了全部歌曲ID可以被查詢。

總結

本文記錄了雲音樂的一個比較常見的TOPN的需求的優化,在整個優化的過程中在技術和業務上都給與了充分的考慮,在滿足查詢需求的前提下,查詢性能從最初的上百秒提升到1秒以內,這個過程讓我充分意識到了解數據特徵和業務的重要性,以前遇到問題總是想着是否能夠進行算法的優化,是否可以加機器解決,但是當這些都不能再有改進的空間時,轉換一下思路,思考一下是否可以從業務數據入手進行優化,可能會取得更好的效果。

隨着Kylin 2.x的落地使用,對於業務的支持也更有信心了,尤其是並行查詢和Endpoint Coprocessor對性能的提升,但是對於開源技術來說,如何正確的使用纔是最困難的問題,我們也會在探索Kylin使用的路上越走越遠。

最後,通過本次調優,我發現了一個以前沒有注意到的問題:薛之謙最近真的挺火,不相信的話可以查看一下網易雲音樂熱歌榜。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章