天天看點

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

本文作者李宗偉,系思科工程師,是思科大資料架構團隊成員,目前主要負責OLAP平台搭建及客戶業務報表系統的研發。

我們是來自于Cisco大資料團隊的開發小組,其中一項業務是為客戶提供BI報表:客戶會登入報表系統查詢Cisco業務的使用情況,也會将它作為計費賬單的參考,這些報表對客戶而言是非常重要的業務功能。

這些報表資料來自于多張Oracle資料庫中的表,單張表單月的資料量在億級,也就是說,如果客戶想查詢一年的報表,至少需要對十億到二十億的資料做聚合查詢等操作,同時需要在很短的時間内得出結果。在我們的調研選型過程中發現了Apache Kylin這一基于預計算思想實作的海量資料分布式預處理引擎,它的一個亮點就是可以實作超大資料集的亞秒級查詢。

經過初步的資料模拟測試,我們發現Kylin确實可以在1秒内回報十億資料量的聚合查詢結果,很好地滿足了我們的業務需求。但是測試并沒有到此為止,我們展示給客戶的報表頁面包含了15張圖表,每一張圖表的展示BI系統都會異步的發送REST API請求到Kylin查詢資料,基于産線規模分析,如果短時間内有20個客戶(這個資料很保守)在單節點上同時查詢報表,會觸發15*20 = 300個請求,那麼Kylin在短時間内的并發響應性能就是我們需要測試的對象。

01 初步測試階段

前提

為了降低網絡開銷對并發性能測試結果的影響,我們将并發測試工具與Kylin部署在相同的網絡環境内。

測試工具

除了選用傳統的壓力測試工具Apache JMeter外,我們還使用了另一款開源工具Gatlin (https://gatling.io/) 測試相同的用例,對比排除測試工具的影響。

測試政策

通過累加并發線程數來模拟不同量級的使用者請求,觀察60秒内平均響應時間,确定Kylin的并發響應瓶頸,同時也需要觀察最大響應時間和成功率。為了確定不被緩存影響,整個測試我們都關閉了Kylin的query cache,確定每個查詢都被發送到底層執行。

測試結果

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

根據結果繪出趨勢圖:

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

測試結論

當并發數達到75時,Kylin的查詢響應數達到峰值90,即使進一步提高并發數,單秒的查詢響應數也并沒有提高。單個節點每秒90的并發查詢響應數隻能滿足此場景中90/15=6個客戶同時查詢報表,考慮到叢集内Kylin query node的數量為3,每秒18個客戶的查詢能力也遠遠不能滿足我們的業務需求。

02 定位問題

通過對Kylin Query子產品代碼的閱讀和分析,我們了解到Kylin的查詢是通過啟動HBase Coprocessor在HBase的region server中并行執行過濾和計算。基于這個資訊我們最初排查了測試環境HBase叢集的資源使用情況,觀察後發現高并發請求發生時,region server上處理的RPC Task數量并沒有與Kylin查詢請求數成線性增長,于是初步定位問題應該出在Kylin端,可能存線上程阻塞。

我們選用了火焰圖和JProfile對Kylin Query server進行了資料收集分析,結果都不是很理想,沒有定位到問題的源頭。之後我們嘗試通過jstack抓取Kylin的線程快照,分析jstack log後我們最終發現了造成并發查詢瓶頸問題的原因。這裡用其中一次測試的結果舉個例子(Kylin 版本 2.5.0)。

在一次快照中一個線程lock在sun.misc.URLClassPath.getNextLoader。此線程的 TID 是0x000000048007a180:

"Query e9c44a2d-6226-ff3b-f984-ce8489107d79-3425" #3425 daemon prio=5 os_prio=0 tid=0x000000000472b000 nid=0x1433 waiting }}{{for monitor entry [}}\\\{{0x00007f272e40d000}}{{]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at sun.misc.URLClassPath.getNextLoader(URLClassPath.java:469)
    - locked <0x000000048007a180> (a sun.misc.URLClassPath)
    at sun.misc.URLClassPath.findResource(URLClassPath.java:214)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:569)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:567)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findResource(URLClassLoader.java:566)
    at java.lang.ClassLoader.getResource(ClassLoader.java:1096)
    at java.lang.ClassLoader.getResource(ClassLoader.java:1091)
    at org.apache.catalina.loader.WebappClassLoaderBase.getResource(WebappClassLoaderBase.java:1666)
    at org.apache.kylin.common.KylinConfig.buildSiteOrderedProps(KylinConfig.java:338)
           

同一時刻有43 個其它線程waiting to lock <0x000000048007a180> 

"Query f1f0bbec-a3f7-04b2-1ac6-fd3e03a0232d-4002" #4002 daemon prio=5 os_prio=0 tid=0x00007f27e71e7800 nid=0x1676 waiting }}{{for monitor entry [}}\\\{{0x00007f279f503000}}{{]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at sun.misc.URLClassPath.getNextLoader(URLClassPath.java:469)
    - waiting to lock <0x000000048007a180> (a sun.misc.URLClassPath)
    at sun.misc.URLClassPath.findResource(URLClassPath.java:214)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:569)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:567)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findResource(URLClassLoader.java:566)
    at java.lang.ClassLoader.getResource(ClassLoader.java:1096)
    at java.lang.ClassLoader.getResource(ClassLoader.java:1091)
    at org.apache.catalina.loader.WebappClassLoaderBase.getResource(WebappClassLoaderBase.java:1666)
    at org.apache.kylin.common.KylinConfig.buildSiteOrderedProps(KylinConfig.java:338)
           

分析代碼棧我們可以追溯到最近的Kylin的邏輯在org.apache.kylin.common.KylinConfig.buildSiteOrderedProps(KylinConfig.java:338),然後再結合Kylin源代碼進一步分析,成功就離我們不遠了。

03 代碼分析

Kylin query engine 建構查詢請求時, 會導出Kylin properties (Kylin裡的各種配置)發送給HBase Coprocessor,在KylinConfig.class中有這麼一個方法:

function private static OrderedProperties buildSiteOrderedProps() 
           

它的執行邏輯是這樣的:

1. 對于每個線程, 會調用getResouce 去讀取”kylin-defaults.properties”(預設配置檔案,使用者不可修改) 的内容。

// 1. load default configurations from classpath.
// we have a kylin-defaults.properties in kylin/core-common/src/main/resources
URL resource = Thread.currentThread().getContextClassLoader().getResource("kylin-defaults.properties");
Preconditions.checkNotNull(resource);
logger.info("Loading kylin-defaults.properties from {}", resource.getPath());
OrderedProperties orderedProperties = new OrderedProperties();
loadPropertiesFromInputStream(resource.openStream(), orderedProperties);
           

2. 循環10次去讀取”kylin-defaults” +(i)+ “.properties”, 線程阻塞就發生在這裡。

for (int i = 0; i < 10; i++) {

String fileName = "kylin-defaults" +  + ".properties";

 URL additionalResource = Thread.currentThread().getContextClassLoader().getResource(fileName);

 if (additionalResource != null) {

        logger.info("Loading {} from {} ", fileName, additionalResource.getPath());

 loadPropertiesFromInputStream(additionalResource.openStream(), orderedProperties);

 }
           

通過版本追溯,這段邏輯是在2017/6/7 引入的,對應的JIRA ID 是KYLIN-2659。

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

04 問題解決

針對第一段邏輯,因為kylin-defaults.properties是打包在kylin-core-common-xxxx.jar中,在Kylin啟動後是不會改變的,是以不需要每次查詢時都從檔案讀取。可以将這段邏輯挪至 getInstanceFromEnv(),這個靜态方法隻會在服務加載時調用一次。

在修改這塊邏輯時遇到一個坑。在Coprocessor中的類CubeVisitService,它會調用KylinConfig作為工具類去生成KylinConfig 對象,引入讀取properties檔案的邏輯是危險的,因為 Coprocessor中沒有打包kylin.properties檔案。

buildDefaultOrderedProperties();
           

對于第二塊邏輯,設計的最初應該是為了未來擴充,允許使用者定于多達10個default properties檔案(彼此覆寫),但是經曆了一年半的版本疊代,這段邏輯似乎沒有被使用。但是為了降低風險,在這次修複中暫時保留這段邏輯,因為前面的改動後,這段邏輯隻會在服務加載時執行一次,是以它的時間損耗基本可以忽略。

05 修複後性能測試

基于修複後的版本在同樣的資料量和環境中進行測試,結果如下:

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

同樣地繪制出趨勢圖:

【開發實踐】思科大資料團隊如何将 Kylin 吞吐率提高 5 倍?

當并發數達到150時,Kylin每秒能處理的查詢請求數可以達到467,與此bug修複前并發處理能力提高了5倍左右,趨勢圖也是呈線性增長的,可以看到瓶頸基本消除了。我們沒有再進一步提高并發數測試的原因是Kylin Query engine都是做叢集負載均衡配置,一味地增加單節點的并發連接配接數反而會增加Tomcat伺服器的壓力(Tomcat 預設最大線程數為150)。

重新收集分析jstack日志,再沒有發現線程阻塞的問題。

根據現在的測試結果,單個Kylin節點每秒可以處理 467/15= 31個客戶查詢,是滿足目前業務需求的。此外,如果開啟Kylin的查詢緩存,單節點 QPS 還可以提升若幹倍,足以滿足我們的需要。

06 總結

Kylin的一大亮點就是提供亞秒級的海量資料集的查詢,而實作這個目标既得益于Cube預計算的設計,以及Query時Apache Calcite算子的優化,同時在2.5.0版本中也引入了PreStatement Cache來減少Calcite語義解析的消耗。每一點的查詢性能優化都是來之不易的,在引入新功能、bug fix等代碼改動的時候,大家要額外注意這些改動對 Kylin query engine的影響,有時可能會牽一發而動全身。這些在高并發下的問題,往往是比較難重制和分析的。

另外,查詢性能測試不能僅局限在單次或少量查詢,可以結合實際業務需求估算并發請求數做相應的高并發性能測試。對于企業級的報表系統,客戶的新頁面載入忍耐度在3秒鐘,這包括了頁面渲染和網絡消耗,是以背景資料服務的查詢響應最好控制在1秒以内。這在基于大資料集的業務場景下确實是不小的挑戰,Kylin則很好的滿足了這一需求。

目前這個問題已經作為KYLIN-3672在JIRA上送出,并在Kylin 2.5.2版本釋出,感謝這一過程中來自Kyligence團隊史少鋒同學的幫助。

參考文獻:

【1】https://issues.apache.org/jira/browse/KYLIN-3672

繼續閱讀