导读:本文将从四个方面来进行介绍。首先是分析在网易NDH中使用 Impala 过程遇到的一些痛点;第二个部分是基于这些痛点问题,我们提出了建设高性能SQL引擎的方案,以及这些方案是基于什么原则来创建的;第三个是基于这些原则,我们做了哪些的优化实践的尝试;最后会举一个线上的使用案例,看一下具体优化效果。
▌Impala介绍
在开始正式议题前,首先简单介绍一下Impala。Impala是一个存算分离的MPP架构数据查询分析引擎,它的特点是有比较高的查询性能。同时 Impala 的历史非常悠久,所以在很多的企业是有非常多的使用,功能特性比较符合企业级的一些应用的场景,比如丰富完善的鉴权、认证等一些能力。
网易 NDH 主要使用 Impala 做BI 报表、数据抽取、自助分析,还有跑批任务这四类场景,下表对这四种场景从 SQL 模式查询、磁场资源消耗查询并发以及元数据变更等特点来进行总结:
▌Impala痛点分析
本章节主要会介绍下我们在混合负载、资源管理、元数据异常以及查询性能方面的遇到痛点。
混合负载场景是我们遇到比较多的,比如在BI报表对性能还有成功率要求都很高,但是在混合场景下,它的成功率往往是没办法保证的,原因比较多,这里分成两个:
一是 BI 报表本身原因,产品侧对不同的 BI 的查询类型没有进行资源的隔离,比如 BI 可能既支持报表类的应用,也可能支持数据抽取,就是把一个数据源的数据抽取到另外一个数据源,有点类似于数据加工。这两种不同类型的查询,如果共用同一个资源队列,就会导致资源竞争比较严重,出现一些排队等等问题。
第二个原因是因为 Impala或其他的一些查询引擎,往往会基于资源队列进行隔离,但是本身隔离是不够彻底的。比如队列里面的查询只支持FIFO先进先服务。对应 Impala来说,队列也没办法对 CPU 资源进行隔离,如果存在多个队列,所有队列加起来配置的总资源可能是超过物理内存的。这个时候就会出现一些问题,比如并发太高,或者内存不足都有可能导致查询排队等待,对性能也有比较大的影响,也有可能导致查询出错。
资源管理粗放是一个共性的问题,不仅仅是 Impala 这样,有些关系数据库也存在资源预估不准的问题。我们在生产环境遇到的问题有两类:
其中一类就是内存的预估值远远大于实际查询的使用,虽然有很多的内存是可以使用的,但却没办法真正地用起来,所以效率比较低。这就进一步导致因为没有实际可用的内存,或者没办法分配出来的内存,出现查询的排队。这里举了一个例子,预估值和实际的使用它差了将近 20 倍。
第二类是内存的预估又过小了,过小就可能出现查询实际需要的内存超过预估值,会导致执行的过程中报出内存超了 limit 的错误。
元数据异常主要表现在两个方面:
第一个是元数据缓存过旧问题,之前一直是认为它是 Impala 特有的一类问题,因为其他SQL 引擎存算分离的比较少,一般都是存算一体的,自己带存储并直接跑 ETL 任务,因为元数据本来就是引擎内部,就不存在元数据缓存这种问题。但是在存算分离这种架构下,可能是通过 spark 或者 Hive 来跑批的,然后用 Impala 做查询,如果跑批过程增加了一个分区,Impala 并不知道分区增加了,就会导致 Impala 的元数据缓存过旧。到现在我们已经进入数据湖的时代,在数据湖里存算分离本来就是一个公共的属性,在这种情况下如何解决缓存的有效性能、缓存什么时候更新以及怎么失效等问题是大部分查询引擎需要共同面对的一些问题。
第二类元数据异常是元数据同步的阻塞,是 Impala 自己特有的一类问题。阻塞表现出来典型的现象就是coordinator 上有大量的查询,或者基本上所有的查询都是处于 creative 的状态。这个原因是在跑批的任务中对表进行了一个变更,比如增加了分区需要更新元数据,那么在 catalog 上就需要对表加写锁。 Impala元数据同步的机制是从 catalog 到coordinator,通过中间的statestore,依次会收集一批的表,多个表同时发给coordinator。在收集元数据的时候就需要为每个表加一个读锁,如果这时候某一个表有跑批的任务,或者加了写锁,加读锁的时候就会阻塞住,这就导致对应的 coordinator上的这些查询就得不到执行,因为都在等待元数据,就阻塞在这一步了。
查询性能不足这里也分了两类:
第一类是对于一些复杂的查询性能本身就比较低,这是Impala自身的原因,不受部署环境等其他因素影响。Impala制约性能因素是由于目前比较欠缺向量化执行的能力以及在 SQL 优化,尤其 join 次序的调整能力上有所欠缺。因为这些不足,会导致在进行大表扫描和多表 join 的场景性能比较差。
第二类性能差的原因是受外界的因素的影响,跟部署的方式有关系。比如我们大部分线上的部署方式还是存算分离,就是 Impala 的节点跟 HDFS 的节点是不在同一台机器上的,读数据以及打开文件都是跨网络的,跨网络就会导致网络的性能的影响。还有比如使用公共存储,就会导致大家共用存储的 IO 资源。这里举了两个例子:
▌高性能SQL引擎的建设方案
上一节列了一些Impala使用的痛点,但我们遇到的痛点还远不止这些。这一部分介绍我们怎么建设一个高性能的 SQL 引擎,它的建设的方案指导思想是什么?主要列举三方面:
1、增强引擎的内核
我们希望Impala能够提供更高的性能,更强的功能,还能够带给用户更好的体验,针对社区的Impala也做了很多的优化工作,后面会进行展开介绍。
2、智能化优化
第二点是希望 Impala 能够更加的智能,能够自我学习。我们通过分析历史查询进行一些尝试,之前做了一个Impala的新模块叫做管理服务器(或者叫manager):第一个作用是提供了集群层面的查询展示的平台,通过机器人的统一web UI,能够汇集所有 coordinator 上正在进行的查询,以及最近执行完的查询,这样在看集群有哪些查询正在执行的时候,就不需要每个coordinator 跳过去了。第二个作用是会把所有 Impala 查询过的这些信息都持久化下来,存到 MySQL 里面,这样就可以后续异步的对这些持久化的查询信息进行分析,来得出一些规律,支撑我们的集群优化,发挥大平台的优势。
3、发挥网易数帆大数据平台的优势
NDH 是网易数帆的一个独立的大数据的基础设施产品。它由很多组件组成,包括Hadoop,Impala、spark, Hive ,还有Artic等等。 Impala 跟这些组件协同打造高性能的数仓,发挥各自的优势。
第二点我们跟网易数仓的另外一大产品有数 BI进行上下游产品的垂直整合。比如 BI通过提供 SQL 的注解,来让 Impala 知道 SQL 具体是干嘛用的,我们就可以做一些自适应的优化。同时 Impala 也可以提供给有数 BI 一些能力,比如一些性能诊断的结果,有数 bi 可以基于这些打造数据医生等高价值的一些特性。
还有就是网易数帆另外一大产品,数据开发治理平台EasyData,也可以做一些协同的优化。
▌优化实践
上一节介绍了我们优化的原则,本节会介绍基于这些原则我们都做了哪些事情,也举例做一些说明。下面是优化汇总的信息,主要有 4 个方面:第一个方面是希望提高 Impala 使用的易用性以及稳定性;第二个方面是提升 Impala 计算资源的利用效率,计算资源尽可能的高效使用起来;第三个方面是提高查询性能,补齐Impala的一些短板,把瓶颈给补上;第四个方面是提高集群的运维以及管理的效率,让大家更好地使用。
接下来就挑几个优化点来展开介绍。
Apache Impala虽然一个集群能够部署多个coordinator,但多个 coordinator 怎么访问,并没有提供一个官方或者标准的解决方案,官方只是建议部署 HA proxy 等来进行高可用和负载均衡。我们在这方面做的优化是参考 Hive 的高可用方式,实现了基于 ZK 的 coordinator 的高可用和 Impala查询的负载均衡。
这个说起来比较简单,就是把各个 coordinator 的地址注册到同一个 ZK 的 Znode 上,然后客户端可以通过 Hive 的 JDBC 来访问 Impala 集群,它的访问模式会先访问 ZK 里面的地址,随机取某一个 coordinator地址来建立连接,这样就达到了高可用的目的。某一个 coordinator挂掉对于整体Impala 的服务没有多少影响。
基于 ZK 的高可用和负载均衡的特性,我们进一步其实是做了另外一个新的特性,就是虚拟数仓。虚拟数仓是从 Snowflake 来的,但我们做的这个特性其实是早于 Snowflake 的。在 Snowflake 没有叫虚拟数仓之前,我们这个特性其实叫做 Impala 的。
Impalad分组把一个Impala集群分成很多个组,不同组的 Impalad相互之间是看不到的,每个组只能看到自己组内部的Impalad,这样就达到物理资源的隔离。每个组可以选择注册同一个或者不同Znode,如果是不同的组在不同的 ZK 地址上就可以承接不同类型的查询服务。比如有一个组是跑批的,有一个组可能是做 BI查询的,有些是做数据抽取的等。
相比于多个集群来说,虚拟数仓本质上还是一个集群,它共用的是同一份元数据,同一个 catalogd 和statestored,所以会更轻量化。在运维层面更加灵活和简单,比如元数据同步不需要给每个集群都配一个。
虚拟数仓我们也做了一些增强,扩展性比较灵活,如果业务发展得比较好,负载以及并发一直在提高,我们可以基于虚拟数仓进行扩展,相当于加一组 Impala d,注册到同一个 ZK 的地址上,就达到了服务能力的扩展。
另外虚拟数仓我们又做了负载查询跨虚拟数仓调度的能力,目的也是尽可能充分地利用有限的计算资源。
接下来举一个虚拟数仓的使用场景案例。比如有两个虚拟数仓,分别是用来跑批和 BI 报表,BI 报表一般是在上班时间,查询的人会比较多,跑批一般是在晚上或者凌晨,这两类任务高峰期是错开的。这就意味着有一个虚拟数仓处于高峰期的时候,另外一个虚拟数仓处于低峰期。但其实完全可以把一些高峰期虚拟数仓的查询路由到另外一个很空的虚拟数仓上,这样达到充分的利用计算资源。
我们在 Impala 原生的队列管理上也做了一些增强,主要提供了两种新的能力,分别是基于优先级的能力,以及跨队列的动态路由的能力。
优先级又分成两种:第一种是队列里的查询是有优先级的,支持为不同类型查询配置执行优先级,保障重要的查询先被执行,如果资源出现瓶颈,高优先级的查询可以抢先排到队列的前面,更快地得到执行。还有一个优先级是队列的优先级,也就是一个 Impala 分组或者集群里面有很多的队列,可以为这些队列划分不同的优先级。在负载非常高的时候,可以先保证优先级最高的队列里面的查询是能够得到先执行的,其他队列会限制执行时间。
中间是跨队列的路由能力,支持将特定类型的查询路由到其他资源队列单独进行配额的管理。这个是业务需求触发我们来做的,比如不同用户下发的查询可以放到不同的资源队列,因为业务层面不一定对 Impala 非常了解,可能不会使用队列,所有任务都下发到default队列,这个功能就可以在 Impala 集群层面来进行查询的分发,放到不同的队列里面进行配额的控制。
基于历史的查询优化是我们基于管理服务器做的一个新的特性,我们叫HBO,它的原理是利用前面提到的管理服务器持久化保存所有的历史查询信息,这个信息包括 SQL 语句的内容、任务执行预估内存以及实际消耗的内存等。我们新增了一个 HBO 的模块,它会提取这些 SQL 的模板,以及涉及到的表、分区字段和分区个数等信息,进行加工保存在 HBO元数据表里面。通过优化修改coordinator缓存 HBO元数据表,在执行查询的时候,先提取查询SQL 模板,判断它是不是跟HBO 元数据表里的模板匹配,如果匹配就意味着命中了HBO,然后使用已经执行过的这同类的 SQL 的内存的实际使用值,来作为当前查询的一个预估值。
目前 HBO 能力在很多的业务集群已经使用了,效果非常明显。下面例子是前面提到的差了 20 倍的这种预估值和实际使用值,启用了 HBO 以后,就变成只有实际使用值的 2 倍,所以资源使用效率提高的非常明显,同时因为内存资源被充分地利用起来,导致查询排队的概率就变得很比较小,排队的时间少了,平均的查询性能也就进一步的得到了提升。
元数据管理层面的优化第一个方面是元数据缓存,解决 Impala 集群跟外部组件的元数据同步问题,比如在 spark 和 Hive 上做表数据加工时,需要及时地把元数据变更同步过来。
开源社区Impala3.x版本以后支持了元数据同步,也就是列出来的第三种方式,我们基于社区版的实现做了一些优化,解决了一些BUG并做了增强。这种元数据同步的方式对于原来CDH 的用户迁到NDH上是比较有用的,因为涉及到集群迁移NDH 的Impala底层依赖的Hive 以及HDFS还是 CH 的版本,这种情况只能是用社区版的实现。
另外两种元数据同步都是网易数帆自研的,分别是基于 HiveMetaStore的DDL 变更日志和基于EasyData数据开发平台离线任务的调度加工识别表的数据变更,然后通过订阅进行同步。
元数据管理第二类增强是元数据在 Impala 集群内部的缓存,或者是广播时候的增强。首先做了分区元数据裁剪,解决元数据的缓存空间的问题。如果Impala 集群有非常多的表,有非常多的分区,它需要缓存的元数据会非常大。虽然Apache Impala 做了很多优化,但还是有一些做得不够,还是可以进一步优化的。
Impala 4.x版本的local catalog 模式是一个非常好的特性,对它减少 coordinator侧的元数据缓存的大小是非常有帮助的,因为 local的特点就是 coordinator 是按需的,从 catalog 获取元数据信息。但是 local catalog 它还是没有解决 catalog的元数据缓存空间的问题。
如果有很多大表,有非常多的分区,小文件问题比较严重,同样的数据量文件数会很多,这可能会导致需要缓存的元数据膨胀,因为空间越大catalogd配置的 JVM 内存就越大,也很难能够精确地量化出来,如果配得小可能就会导致比较严重的 GC 问题,进而导致 coordinator 的查询出现异常,比如查询卡住等等。
我们做的优化就是减少 catalog 的元数据缓存量,着眼点是能够减少大表分区数非常多的表分区的元数据缓存空间,只缓存Impala 可能会查到的这些分区的元数据,查不到的元数据就不缓存了,偶尔查询的时候再从MetaStore和 NameNode 上加载。例如有张 5 年的分区的表,一般情况下会只会查询最近的一年或者一个月的数据,比较旧的分区缓存也基本不用,所以我们做了部分老分区元数据的裁剪的优化。
元数据在内部进行通信时,比如从 catalog 到 coordinator 这个过程中,前面提到的使用痛点就是阻塞 coordinator侧会出现很多处于 creative 状态的查询,它是因为 Impala 的元数据收集机制是一批一批的多个表一起收集,如果有一个表加了写锁就会被阻塞。Apache Impala 在这一块做了很多优化,也在 4.x版本提供了等待超时等新的能力。对于加了锁的这些表,元数据收集线程就不是一直等待,而是会等待超时。如果超时了以后,对应表的元数据就跳过了,只收集其他没有锁的表。这样大大提高了它的元数据收集和广播的效率,但是这种情况毕竟还是要等一段时间,所以还是有一些影响,虽然可以把等待的时长超时时长调小,但有些场景下,还是需要等待比较长的时长。
我们做的优化是会根据表的更新频率,例如更新、添加分区、删除分区这些数据更新的频率以及查询频率等,来决定要不要在元数据收集逻辑上把表锁给去掉,当然去掉元数据收集的时候,跳过对应的加锁逻辑不收集的元数据意味着可能会导致这个表的元数据更新会丢失掉, coordinator就看不到这个表的最新的元数据了。我们做的补偿是会把coordinator上这张表的元数据缓存失效掉,来确保正确性没有问题。通过这种方式,某些场景下可以提升查询的性能。
我们从Impala 3.4版本升级到 4.1以后,主要用了两个跟性能相关的新特性:第一个是Multi-thread Execution(MT_DOP),即进程内多线程执行查询;第二个是异步的动态代码生成,同步动态代码生成的查询在执行之前,需要等到动态代码生成的逻辑完成以后才能执行,异步方式不管是不是动态代码生成,会先基于原来的非动态代码生成的函数的版本开始执行,如果动态代码生成逻辑已经完成了,通过指针的切换再使用最新的更高效率的模式来加速。
这两个特性我们线上都开启了,在开启的过程中发现MT_DOP特性还有不够完善的地方,比如我们可以在查询低峰期或者是集群CPU 资源比较空的时候设置比较高的并发度,但是如果集群的负载上来了再设置这么高的查询并发可能就会导致整个 CPU 负载变得过于高,反而对性能是有害的。所以我们引入了一个基于 CPU 负载的一个 MT_DOP的自适应的能力,就是我们会监测 impalad 的当前CPU 负载情况,如果 CPU 的负载情况超过某个阈值,就会对应地把 MT_DOP 的值给调小,如果负载是处于比较低的水平,查询进来就可以设置更高的线程并发度,让它更快地执行完,利用更多的计算资源,确保 CPU 资源是被充分利用但又不至于过载。
我们线上的业务集群,从去年9月底上线到 12 月份做了个评估,刚开始查询的性能平均是 4 秒多,使用前面的两个特性优化后,平均的查询性能是降到 2 秒以下,各个分层的查询1秒一下的百分比都提高了很多。
多表 join 的物化视图之前介绍过如何实现,这里就不展开来说。它的适用场景是那些 SQL 模式比较固定,出现的重复频率比较高的场景的效果是非常好的。在实现上分了两个部分:
第一个是物化视图的创建、数据更新、删除等。我们自研了一个独立服务,通过前面提到的多种元数据同步的方式来驱动物化视图的数据的更新。
第二个部分是 SQL 的透明改写。透明改写是做到 Impala 的coordinator 里面的,基于Apache Calcite物化视图的能力进行透改写的实现做了很多的增强,比如原来Calcite 不支持 outerjoin 语法、group by limit 等算子以及Impala 特有的一些udf 等都做了支持。下图是我们去年在业务场景落地的效果,如果是命中的物化视图效果非常明显,类似于kylin模式进行预计算。
DataCache在Impala3.x版本就已经支持了,到了 4.x版本就更加的完善了。我们在这个基础上做了一些增强,重点解决了缓存不命中的问题。原始流程是查询时如果缓存不命中,需要从 HDFS 里面把数据读出来,先放到缓存里面,然后再返回到查询。我们做的优化是数据读出来以后直接返回给查询,再填充到缓存里面,这样可以减少对查询性能的影响。
除此之外,我们还提供了缓存白名单功能,可以选择只缓存某些表。同时对 Parquet、ORC 这种文件格式提供了 footer 的缓存,并通过把元数据缓存持久化解决了DataCache 在 Impalad 重启以后缓存失效的问题。这些优化工作我们正在贡献给社区,通过 DataCache能够解决存算分离模式下存储层性能波动导致的影响,优化后的效果如下图。
查询成功率保障也跟 Impala 4.x版本的一个新特性有关系,新版本提供了一个查询透明重试的能力,但它目前只是在Impalad宕机的时候才会进行查询重试。这里提到的第二点就是 executor的节点宕机等这种场景,另外我们增强的是如果因为元数据缓存过旧导致的查询错误,我们会选择先把表的元数据刷新再执行。
另外就是前面提到的内存预估不准,预估值小于实际查询时需要使用的内存值导致查询失败,我们可以把预估值给调高再做重试。关于预估内存方面还有其他的一些优化,比如直接放宽memory limit,通过引入一个新的参数,确定一个比较宽泛的最大内存消耗值。
▌使用案例
本节通过一个我们外部客户的业务案例来进行简单的说明优化效果,这个业务本来是Oracle 上的BI 报表,迁移到我们的 Impala 上。迁移过来的时候还有一些的优化没有启用,表现的性能远远不符合预期。
第二个就是之前提到的,因为内存预估不准问题,导致内存资源浪费非常严重,并发高了以后排队问题很严重,比如预估值可能是超过了几十G,其实只用了几个G。还有个痛点是并发的查询性能下降很明显,并发度高了以后甚至整个所有的查询都卡死了。
上面的各种原因就导致查询的成功率和用户的满意度比较差,比如没有并发的时候能够 45 秒之内返回,本身已经比较慢了,但如果并发上来以后性能线性的下降,比如到 10 个并发的时候就将近要 200 秒。
我们优化的过程首先规范了一下集群的配置,比如在执行过程中有详细的日志输出在 web UI 上会保留已经执行完的查询,因为这些查询是后续管理服务器来基于历史查询进行优化的,因此会从 web UI 上进行拉取保留更多的信息,方便后续问题的排查。另外是coordinator负载均衡,因为他们的 BI 工具不支持 Hive JDBC,所以业务层直接连一个固定的coordinator上,导致整个集群同时执行不了几个查询。我们刚开始是使用HAProxy 机制,后来通过调研发现 BI产品本身是支持JDBC 的,然后就改造成基于 ZK 的这种模式。
第二个优化是启用管理服务器来收集持久化这些历史查询信息,从中挑选出性能比较差的来分析性能差的原因。这个过程我们发现了比较多的可优化点,比如查询因为有排序等操作,就存在比较严重的数据溢出,后来还发现在 Impalad跟 Impalad之间做数据交换 exchange 环节,网络的拥塞非常严重。通过排查发现数据溢出避免不了,因为排序的时候确实数据集比较大,然后就把数据溢出的盘换成更高性能的 SSD 盘。网络拥塞的问题是因为这个集群里面有些节点是万兆网卡,有些节点因为错配的原因是千兆的网卡,就导致这类问题的出现,识别出来了以后就比较好优化,优化以后对性能的提升就非常的明显了。
第三个优化就是前面提到启用虚拟数仓,因为业务场景可以区分出是普通的报表和复杂的报表,复杂报表倾向于数据抽取,会有很大的结果集,它会把这个结果集导出到比如 Excel 做进一步分析。我们通过虚拟数仓把这两类隔离开来,避免相互之间的干扰。
最后做的事情也是在这个基础上通过启用HBO尽可能地提高资源利用率,下面是我们启用 HBO 以后的效果,没有启用之前,预估值是好几十个 g 的。启用以后,跟实际的使用值就差得很少了。
下图是普通报表的 SQL以及复杂报表这种场景优化前后的整体性能对比,很明显看出来有非常大的提升。
▌总结与展望
最后说一下总结与展望。我们前面提到网易 NDH 基于 Impala 做的一个高性能的 SQL 引擎,整体架构如下:
首先在 Impala 集群内部我们做的一些事情,包括虚拟数仓、本地的DataCache、物化视图、元数据缓存等,同时还提供了管理服务器以及其他的管控平台进行集群管理,还做了一些性能分析和诊断的能力,并且这块的能力我们目前还在持续地加强,最终的目标是提供给业务更好用的一个 SQL 的引擎。
目前NDH Impala 的使用版本主要是以 3.4 和 4.1 为主,还有少量2.12版本在不断地升级,最终我们希望都能升到 4.x 版本,因为相比早期版本具有很多新的特性,对业务开发是非常有价值的。
我们尽可能的希望用社区的一些最新的功能,所以会跟进社区的版本升级,包括一些场景的自研也更多地贡献给社区,让社区变得更加强大。
还有很多的内部和外部的客户就不展开介绍,目前最大的集群超过 300 多个节点。
痛点与方案上,比如通过虚拟数仓查询优先级解决混合负载场景,通过HBO 调整查询的多线程并发数以及跨队列跨虚拟数仓的调度解决计算资源利用效率低的问题。对于异常比较多的问题,我们通过元数据层面的优化以及查询的重试进行解决。查询性能没有办法满足要求,可以通过预计算、数据的缓存等进行优化。
未来的计划有三点: