【开发实践】思科大数据团队如何将 Kylin 吞吐率提高 5 倍?

01 - 07 - 2019

本文作者李宗伟,系思科工程师,是思科大数据架构团队成员,目前主要负责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,确保每个查询都被发送到底层执行。

 

测试结果

 

根据结果绘出趋势图:

 

 

测试结论

 

当并发数达到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。

 

04 问题解决

 

针对第一段逻辑,因为kylin-defaults.properties是打包在kylin-core-common-xxxx.jar中,在Kylin启动后是不会改变的,因此不需要每次查询时都从文件读取。可以将这段逻辑挪至 getInstanceFromEnv(),这个静态方法只会在服务加载时调用一次。

 

在修改这块逻辑时遇到一个坑。在Coprocessor中的类CubeVisitService,它会调用KylinConfig作为工具类去生成KylinConfig 对象,引入读取properties文件的逻辑是危险的,因为 Coprocessor中没有打包kylin.properties文件。

 

buildDefaultOrderedProperties();

 

对于第二块逻辑,设计的最初应该是为了未来扩展,允许用户定于多达10个default properties文件(彼此覆盖),但是经历了一年半的版本迭代,这段逻辑似乎没有被使用。但是为了降低风险,在这次修复中暂时保留这段逻辑,因为前面的改动后,这段逻辑只会在服务加载时执行一次,因此它的时间损耗基本可以忽略。

 

05 修复后性能测试

 

基于修复后的版本在同样的数据量和环境中进行测试,结果如下:

 

 

同样地绘制出趋势图:

 

 

当并发数达到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

近期文章

在青云 AppCenter 上使用 Kyligence Enterprise

在青云 AppCenter 上使用 Kyligence Enterprise

本章节主要介绍如何在青云 AppCenter 上部署 Kyligence Enterprise 并使用样例 C […]

详细阅读

【案例】Cognos + Kyligence:打造大数据时代的OLAP解决方案

【案例】Cognos + Kyligence:打造大数据时代的OLAP解决方案

作为 Hadoop 原生的企业级 OLAP 大数据分析产品,Kyligence 是 IBM Cognos 在大 […]

详细阅读

【开发实践】美团为什么开发 Kylin On Druid(下)?

【开发实践】美团为什么开发 Kylin On Druid(下)?

作者:敏丞   前言   在上篇文章里,我们比较了 Kylin 和 Druid 这两个重要 […]

详细阅读