查看原文
其他

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

李宗伟 apachekylin 2020-08-20


本文作者李宗伟,系思科工程师,是思科大数据架构团队成员,目前主要负责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 版本中也引入了 Prepared Statement Cache 来减少 Calcite 语义解析的消耗。每一点的查询性能优化都是来之不易的,在引入新功能、bug fix 等代码改动的时候,大家要额外注意这些改动对 Kylin query engine 的影响,有时可能会牵一发而动全身。这些在高并发下的问题,往往是比较难重现和分析的。

另外,查询性能测试不能仅局限在单次或少量查询,可以结合实际业务需求估算并发请求数做相应的高并发性能测试。对于企业级的报表系统,客户的新页面载入忍耐度在 3 秒钟,这包括了页面渲染和网络消耗,所以后台数据服务的查询响应最好控制在 1 秒以内。这在基于大数据集的业务场景下确实是不小的挑战,Kylin 则很好的满足了这一需求。


目前这个问题已经作为 KYLIN-3672 在 JIRA 上提交,并在 Kylin 2.5.2 版本发布,感谢这一过程中来自 Kyligence 团队史少锋同学的帮助。


参考文献:

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

点击阅读原文

进入 Apache Kylin 官网,了解更多内容!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存