本文概述了面对公司数据报表平台遇到的查询性能挑战,数据平台组围绕数据缓存、物化视图、查询策略、SQL质量等方向所做的一系列治理工作,以提升平台的查询效率和稳定性。通过这些工作,平台的查询响应时间得到了显著的改善,其中平均响应时间从原来的8秒降低至4秒,响应时间90线由原先的约18秒降低至约8秒,总体性能指标提升幅度达50%以上。本文在各个小节中对各治理策略的关键原理和思路进行了阐述,希望能够为读者提供一定的参考和启发。
一、背景
数据报表平台(代称Nova,后同)用于支持携程内部数据分析、数据挖掘、数据可视化等业务需求,目前每日承载数十万Hive表AP查询,所涉数据量达万亿级别。随着用户基数逐步提升,承载查询量不断增大,平台查询性能面临挑战,具体表现如下:
1)平均响应时间延长,大查询在业务高峰期存在阻塞现象,超时数量增多;
2)查询所需时间不稳定,性能波动较大,在业务高峰期可能出现响应时间突增现象;
3)查询负载集群资源占用率高,CPU、内存资源吃紧,I/O 请求排队等待,进而导致集群稳定性下降,时有节点宕机现象出现。
针对上述现象,我们从平台自身服务、SQL路由分发组件、SQL执行引擎等方面入手,采用了一套“全方位组合拳”对平台的查询性能进行治理,目标有二:
1)从用户体验角度:改善查询性能,提升查询效率和稳定性;
2)从集群维护角度:提升集群稳定性,增强查询结果复用能力,提高算力使用效率。
二、平台设计概览
数据报表平台执行查询的主要链路如图1所示,其中有几个关键构件:
图1:Nova 数据查询链路
1)Nova:应用本体,提供可视化用户界面,包含报表即时查询、执行离线定时任务等功能;
2)Router:用于分离指向不同引擎的查询请求,起到SQL路由功能;
3)Starrocks & Hive:平台使用Starrocks作为主要查询引擎,向Hive外表发起查询请求。
三、多维度数据缓存
在硬件资源有限的情况下,要提升查询性能,最直观的思想是对重复的查询进行结果复用。在对平台的查询请求数据进行统计分析后,可发现存在相当数量的查询请求在不同时段内重复出现,这为我们引入缓存机制提供了实践基础。
若将在执行过程中可能遭遇瓶颈的查询进行划分,可将大致分为I/O型、计算型和高频型三类,其中I/O型查询对网络和磁盘带宽的要求较高,往往涉及大规模数据的扫描;计算型查询对CPU和内存资源的要求较高,往往涉及大量连接、分组、聚合、筛选、再计算操作;高频型查询的单次调用开销可能较小,但在单位时间内发起的次数显著高于均值,在涉及远程调用(如元数据获取)的环节可能遭遇性能瓶颈,且在单位时间造成的资源开销可能与大查询相当。
图2:受限查询分类
目前,在整个数据查询链路中,我们在以下几个环节引入了缓存机制,以应对不同类型查询所带来的挑战。
3.1 底表Data Cache预热
当Starrocks从Hive外表进行数据查询时,Scan算子会将所需数据文件以块的形式读取至本地。对于典型的I/O型查询而言,这个过程所需时间可达整个查询流程耗时的70%以上。在业务高峰期,由于大量查询请求同时发起,I/O型查询的堆积将导致其他查询请求读取数据文件的等待时间增长,进而影响查询响应时间。
针对这类情况,我们在Starrocks集群中开启了Data Cache,将每次读取得到的文件块标识并临时存储在本地磁盘中,在下次查询请求需要相同文件块时,若发现该文件块没有更新,则直接从本地磁盘读取,避免了经由网络和Hive带来的文件读取延迟。
通过Data Cache缓存的文件块在Starrocks中由带冷热分区的LRU队列维护,当队列满时,将根据文件块的访问频率和时间戳进行淘汰,以保证缓存的命中率。
从缓存一致性角度,Starrocks在使用Data Cache时,会通过元数据判断底表数据是否发生更新,若发现数据文件已更新,则将废弃缓存数据,重新拉取底表数据文件,以保证查询结果的准确性。
3.1.1 预热机制
通过对查询请求命中底表的情况进行统计,可发现其中热点表的使用呈现一定的规律性(如:每日相近时刻、每周固定几日访问量达峰等)。为此,我们为统计得到的各热点表建立了用户画像,记录并预测其访问高峰。
图3:预热机制
通过在业务高峰到来前将热点表数据主动Cache预热,可进一步分散业务高峰期的I/O压力。如图4 所示,这部分主动指定Cache的数据文件将会优先被置入LRU队列的热区,以保证其在高峰期的查询中能够被快速命中。
图4:Data Cache LRU 队列
3.2 HDFS元数据缓存
在Starrocks查询引擎执行查询时,需要获取Hive表的元数据信息,如表的列信息、分区信息、表的存储格式;HDFS File的元数据信息,如block块属性等。Starrocks将通过这些信息来生成最优的执行计划。在业务高峰期,大量并发查询可能导致Hadoop Namenode的元数据请求压力过大,进而影响查询的执行效率。
Starrocks原有的元数据缓存时间较短,这是因为其无法实时感知HDFS File文件变化。为防止缓存不一致,原有的Remote File元数据缓存时间不宜设置过长,但这会导致即使元数据未更新,在某些场景下Starrocks也会频繁发起重复的元数据请求。
为此,我们选择在Starrocks的FE侧通过元数据接口(该接口调用开销远小于元数据的获取)对元数据的新鲜度进行检测,仅在远端元数据发生更新时拉取元数据,此功能可使得Remote File缓存时长延长至6小时,在保证缓存一致性的同时,提升相同元数据的复用程度。
3.3 Router Redis 缓存
当数据从查询引擎返回至Router时,Router会将查询结果进行缓存,以便后续面对完全相同的查询请求,可直接从缓存中获取结果,避免重复计算。
在缓存一致性方面,Router同样可通过血缘信息实时获取底表数据的更新时间,从而判断缓存数据是否过期。在缓存数据过期时,Router将弃用缓存数据,重新执行查询,以保证查询结果的准确性。
3.4 Download 缓存
对于查得的报表数据,平台支持用户将数据下载至本地,以便用户进一步分析。在实践过程中我们发现,部分报表数据除了在查询后被即时下载外,还可能需要在未来的某个时间点被再次下载(如浏览器关闭后)。这种行为将触发二次查询,导致相同请求被反复执行以满足下载需求。
针对这种现象,我们在Download服务中同样引入了缓存机制,将下载的数据进行缓存,以便后续相同的下载请求直接从缓存中获取数据。
图5:Download 缓存
3.5 小结
如图6所示,通过引入多维度数据缓存机制,我们在平台的数据传输的全链路中,尽可能实现了对重复数据的复用。而为了进一步提升在计算过程中,数据和算力的使用效率,我们进一步在物化视图的使用上进行了探索。
图6:查询链路缓存总览
四、使用物化视图加速查询
物化视图(Materialized View, MV)作为一种预先计算的结果集,可以有效减少查询时的计算量,提升查询性能。面对计算型查询,MV可以将查询计算的部分结果进行固化存储,避免复杂计算逻辑的重复执行;面对I/O型查询,MV可以将查询所需数据进行预聚合,减少数据扫描的规模,或者更简单地,将需要频繁查询的数据底表保存为MV,作为优先级更高的Data Cache来使用,使得对Hive外表的查询达到和Starrocks内表一致的查询性能。不过,想要发挥MV的优势,需要解决如下几个问题:
1)视图定义:如何创建有效的MV;
2)视图利用:如何在查询时高效地利用MV;
3)视图维护:如何保证MV的新鲜度。
4.1 视图定义:为数据集构建MV
在Nova平台构建数据报表时,用户首先需要创建数据集,最终在数据集定义的范围内进行数据查询、生成可视化报表。这一特点使得在选择MV的目标时,将数据集作为构建对象是一个较为合适的选择,因为它代表着一系列特定业务逻辑的公共数据查询需求。
在用户自定义的数据集中,视构建MV的难度,可将数据集分为以下三类:
1)静态数据集:数据集的定义不随使用时间、用户及其他环境因素发生变化,在底表数据不发生变化的前提下,执行任意次查询都将返回相同结果。此类数据集在构建一次MV后便无需再次修改视图定义;
2)半静态数据集:数据集的定义按日(或更长的时间单位)进行更新,但其可变部分仅限于日期(配置了日期变参)。面对此种类型的数据集,需要构建模板,按日重新渲染视图;
3)动态数据集:即使数据集所涉底表的数据未有更新,但随查询的执行时间不同(如包含CURRENT_TIMESTAMP函数或RAND函数等)、执行查询的用户不同(数据集配置了用户变参)、查询的上下文参数不同(数据集配置了自定义变量)等,查询结果也会发生变化。此类数据集的MV构建较为困难,需要进一步拆解数据集的定义,分离得到其中静态的部分以构建MV,其极端情况为仅对数据集的底表进行物化。
通过将半静态或动态数据集转换为静态形式,并对静态数据集构建MV,可将各类查询的中间计算结果进行固化,在确保MV所涉数据底表中的数据没有更新的前提下,可将MV中的数据进行重复利用。
4.2 视图利用:MV自动改写
面对一个潜在的可利用已有MV数据的查询,Starrocks提供MV自动改写的能力,可将查询计划中的相关计算逻辑改写为直接从MV进行读取,从而减少查询的计算量。
Starrocks提供两类视图改写规则:SPJG改写和文本匹配改写。
4.2.1 SPJG模式改写
SPJG模式改写是一种基于逻辑计划的改写规则,其原理基于这篇论文:《Optimizing Queries Using Materialized Views: A Practical, Scalable Solution》。
这种改写规则的核心思想在于:首先确保查询(或某一逻辑子树构成的查询)所需的全部数据均可由MV查询得到,随后计算查询与MV间的谓词差异(称为补偿谓词),应用至可用于改写的视图上,构成一个新的查询计划。图7 是改写规则生效的一个示例。
图7:SPJG模式改写示例
SPJG模式改写的优势在于,面对建立在相同数据集上的各类不同查询,Starrocks可根据实际情况灵活复用已经在MV中完成预计算的数据,从而达成“一次计算,多次使用”的目的。
不过,由于这种改写的复杂性,目前仅支持对只包含Select、Project、Join和Group-By这四类算子(SPJG)的查询计划树(或逻辑子树)进行改写,而在涉及Union、Order-By、Limit等算子的查询改写时能力受限。面对复杂的数据集,更具实践性的做法是抽取出其中的SPJG模式子树用于创建MV,以扩展MV的适用范围,增大匹配改写成功率。
4.2.2 文本匹配改写
文本匹配改写是一种基于抽象语法树(AST)的改写规则,通过比对Query和MV规范化后的AST是否一致,可判断是否可以将查询改写至MV上。
这种改写的优势在于可支持更多类型算子的查询改写,但其改写的自由度和适应性不如SPJG模式改写,一旦数据集或查询的SQL结构在处理过程中发生变化,则文本匹配改写可能无法发挥作用。
4.2.3 改写数据一致性
通过合理利用上述两类MV改写规则,可做到以用户无感的方式,自动复用中间计算结果,在节约计算资源的同时大幅提升复杂查询的效率。
值得一提的是,在数据一致性方面,Starrocks在执行查询改写流程时,会自动检测MV中的数据是否过期,若是,则放弃改写,执行原有查询计划。这使得引入MV查询改写机制后,在达成查询加速效果的同时,依然能够保证查询结果的准确性。
而由于此机制的存在,截至目前,Starrocks在执行MV改写时,若发现MV本身的定义中包含非确定返回值(Non-deterministic)函数,例如CURRENT_DATE 和 RAND等,将弃用此MV,这是因为Starrocks无法保证MV返回的结果在当前时刻下依然可用。因此,在选择数据集进行物化时,必须先确保数据集的定义为静态,这也是“视图定义”小节对数据集分类的重要意义所在。
4.2.4 CURRENT_DATE函数改写问题
在构建MV时,我们发现部分数据集的定义中包含CURRENT_DATE函数,而并未使用平台提供的日期变参($EFFECTDATE)。这将导致即使数据集SQL的文本定义未发生变化,随着时间推进,数据集本身所指代的数据范围却按日发生变化。
与平台提供的日期变参不同的是,CURRENT_DATE函数在执行时不会被替换为具体日期,而是在查询时由执行引擎动态计算,这使得查询引擎在执行改写操作时,将由于涉及非确定返回值函数而弃用此类数据集对应的MV,使得这类本该满足半静态数据集条件的数据集需要被作为动态数据集处理。
经统计,报表平台中目前有相当数量的数据集定义,其可变部分仅为CURRENT_ DATE函数,为提升对此类数据集的物化能力,我们在平台侧引入了重渲机制,在将查询请求提交至引擎前,会将CURRENT_DATE函数以和EFFECTDATE变参类似的方式重渲为具体日期,从而保证查询引擎的改写机制能够正常生效。
4.3 视图维护:MV自动刷新
在确保MV SQL为静态的前提下,MV的数据新鲜度仅和底表数据是否更新有关。通过分析MV创建语句中SQL所涉及的底表追溯血缘依赖关系,并通过元数据服务获取底表的最近更新时间,可创建自动化应用实时监控MV是否过期,并根据实际情况选择是否刷新MV。
4.4 MV价值发掘
为数据集创建MV并不一定必然为平台的查询性能带来优化,原因可罗列为以下几点:
1)MV的创建和维护需要消耗额外的计算资源,每一次刷新都对应一次对数据集的查询操作。若MV的使用率较低,其带来的性能提升可能无法弥补其维护成本;
2)部分数据集在定义时所涉及的数据范围远大于真正使用所需,在直接执行查询操作时,可通过谓词下推等优化操作过滤掉不必要的扫描范围,而如果为此类数据集创建MV,将反而导致更大的全局计算开销;
3)Starrocks集群能够提供的磁盘空间有限,不可能为所有数据集都创建MV来优化。在有限的资源下,需保证“好钢用在刀刃上”,优先创建有较大查询性能提升潜力的MV进行创建。
为此,我们在MV的选择过程中,引入了MV价值评估机制,通过综合分析数据集的使用频率、数据集计算及相关查询的计算代价、数据集的数据规模等多个维度,为数据集的MV创建提供参考。以查询所消耗的CPU代价为例,建立MV前后的所节省的计算代价可使用如下公式计算:
为精确捕获查询执行或数据集刷新的计算代价,我们会将统计得到的待改写查询(高代价或高频查询)在独立环境下执行预跑,获取量化数据作为评估的依据,大致测试流程如图8所示。
图8:MV价值评估流程
4.5 小结
通过选取合适的数据集构建MV,并利用Starrocks的自动改写能力加速查询,可在一定程度上规避平台在计算过程中的出现的短板,补足全查询链路的数据复用能力,使得数据报表平台面对不同类型的高负载查询,有更加成熟、可靠、易维护的应对方案。
五、查询策略优化和SQL质量治理
目前,数据报表平台所承载的查询任务主要分为两类:一是即时报表查询,用户通过Web界面即时运行查询并获取数据报表;二是离线定时任务调度,用户通过配置定时任务执行查询,任务触发后用户可通过邮件等途径获取数据。
这两类查询任务分别存在以下特征:
1)即时报表查询:触发时间随机,由用户自主触发,查询请求所涉计算量相对较小,但对查询响应时间更为敏感,一般要求在数秒内返回结果;
2)离线定时任务调度:触发时间固定,由预注册的计划周期性自动触发,查询需要计算的数据量往往较大,但对查询响应时间要求相对较低,一般可容忍的查询响应延迟较宽。
立足于平台自身的业务场景思考,可发现以下痛点:
1)负载不均问题:平台执行查询的高峰和低谷期分明,需要调整定时任务调度策略,以平滑负载;
2)资源争用问题:相同时间段内,不同查询间存在资源争用,例如离线定时任务的执行可能影响同期即时报表查询的性能;
3)慢查询问题:部分查询请求自身所需的计算资源过大,或是配置的计算逻辑不佳,可能对平台造成过量负载。
对于慢查询问题,其成因可能有多种,包括但不限于以下几点:
1)平台查询策略问题:面对特定查询,原有的实现或查询策略在执行时性能较差,需要进行改进;
2)查询SQL实现欠优:查询SQL在实现上存在质量问题,在执行时消耗的计算量远大于业务逻辑所需,可能引入大量非必要的底表扫描和计算操作,需要整治优化;
3)业务需求:即使优化了查询策略和SQL质量,仍有部分查询请求所需计算量较大,对于其中涉及关键业务的部分,可能需要整合计算资源,以专门提升这类查询的性能。
在本节中,将分别对目前我们针对上述痛点所做的工作进行阐述。
5.1 整点调度问题治理
Nova平台所提供的离线任务定时调度功能,由用户根据所需自行配置任务的触发时间。但出于用户习惯,可发现大量查询都被设置在整点进行调度,这导致平台在整点出现查询负载高峰。
通过对此业务场景的痛点进行分析,我们采用了两种策略来解决整点调度问题。
一方面,通过与用户沟通,我们提出了“错峰调度”策略,即在原有配置的基础上,对部分任务进行时间错峰调度,以减少整点负载高峰。例如,原定于每日九点执行的调度任务,可能被延迟至九点十分执行。这种策略在一定程度上缓解了整点负载高峰的问题,而不至于为用户带来过多的使用不便。
另一方面,我们提供了“依赖调度”方案,即不再以时间,而是以数据更新为触发条件进行调度。报表元数据完成更新时间的随机性,使得这种调度方式可变相地起到“错峰调度”的作用,且通过数据更新作为触发条件,可使得报表数据新鲜度具有更加便捷的维护方式。
5.2 查询流量切分
资源隔离是避免查询请求相互干扰的有效手段。通过将平台的查询请求按照类型进行切分,能够有效提升查询请求的执行效率。
平台的原有的查询请求路由策略仅根据用户指定的查询引擎类型进行请求分流,而根据即时报表查询和离线调度查询的特征,我们选择进一步改进路由策略,将这两类查询请求分发至不同的Starrocks集群,防止离线调度过高的查询负载对即时报表查询的响应时间造成影响。
另外,通过对平台每日查询性能数据进行分析,可识别得到一系列慢查询请求,这些SQL需要我们根据实际情况细分,并逐步对其进行优化。为避免这些查询请求对平台中其他查询的性能造成影响,我们选择将这类查询分发至专门用于处理待优化查询的独立Starrocks集群,以减轻对主集群的压力。
图9:查询流量切分
5.3 Max-d查询专项治理
在各类数据集的查询中,有一类典型的SQL在实际查询时将引入极高的查询代价。这类查询在对数据条目的日期d进行筛选时,使用max(d)子查询作为谓词判断依据,以获取数据集中所包含的最新一日的数据进行计算,SQL示例如下所示:
此种查询结构将导致查询引擎在实际执行查询时,对数据表的每一行数据都触发一次全表扫描的子查询调用,随着数据表数据量的上升,这种查询的查询代价将呈二次函数倍率增长,产生难以忽视的性能开销。
面对这类查询,我们对其执行策略分两期进行了专项优化。
第一期,在Router侧,通过对查询SQL的AST进行模式匹配,可筛得存在Max-d问题的查询请求。在此基础上,我们将这类查询的执行策略拆分为两阶段:首先执行max(d)子查询,获取最新日期d的值,随后将此实值作为谓词条件对原查询进行改写后执行。
通过这种方式,不但可解决原有的嵌套查询问题,将查询扫表的时间复杂度由O(N²)降至O(N),还可进一步触发Starrocks的列分区裁剪行为,大幅减少这类查询的计算代价。
第二期,在Nova平台侧,直接通过配置变参方式替换Max-d SQL,然后获取MetaStore元数据最新分区替换变量。此举可提升这种执行机制的可维护性,且max(d)值在计算前即可获取,能够进一步提升优化后的查询性能。
5.4 SQL实现优化
查询效率优化的核心在于提升查询的执行速度、降低资源消耗,然而单从查询的执行侧进行优化,对于存在潜在质量问题的SQL,仍然难以解决根本病因。通过分析归纳用户提交的SQL中可能导致查询效率低下的原因,我们与用户协调,开展了以下几类SQL语句优化工作。
5.4.1 distinct * 语句删减
在撰写SQL时,为保证查询结果的唯一性,用户可能会习惯性地添加distinct * 子句进行去重,然而这种操作涉及到全表数据行、全字段域的扫描去重,当目标数据集合行数或列数较多时,将产生大量的cpu和内存资源开销。
为减少这类查询的资源消耗,我们向用户提出了两项修改建议:
1)检查在使用distinct * 前,计算结果是否已由上游数据源或计算操作去重,避免重复引入去重操作;
2)检查当前去重操作是否必要,是否必须对全部字段进行去重,尽可能减少对去重操作的依赖。
5.4.2 数据表拆分
通过对部分复杂的查询逻辑进行分析,可发现其主要原因是数据模型设计不合理,对应的数据表拆分不当,例如一些权限表和静态信息表存在严重耦合,致使计算逻辑复杂,查询数据量大。
对于这类问题,重新设计底表数据模型,将非必要的耦合部分进行子表拆分,重新定义数据集和报表计算流程,往往可以起到理想的性能优化效果。
5.4.3 限制查询分区
部分用户在创建SQL(主要是定义数据集)时,未对数据查询范围进行限制,或所做限制缺乏发展性考虑。例如,对一个按日分区的数据表,简单地将数据表查询范围设置为 d > 2022-01-01,而未配置动态参数。随着时间积累,此类数据集的查询资源开销将逐步上升,出现查询性能劣化现象。
在一个SQL被执行时,所涉底表分区的数量可能会对查询性能产生重要影响,因为它对应着查询涉及的目标点位数量。为提升此类数据集的查询性能,我们向用户提出了限制查询分区的建议,例如根据所需添加动态日期范围限制,保证报表数据计算的可持续发展性。
5.5 潜在慢查询阻断机制
为防止未经优化的SQL对平台的查询性能造成影响,我们在平台侧引入了慢查询阻断机制,对用户提交的SQL进行检查,判断其是否满足执行标准,目前主要涉及以下两个点位:
1)平台规范卡点:当用户在平台新增或修改数据集/报表时,平台将对用户提交的SQL进行检查,判断其是否满足规范。目前,上述规范将检查用户提交查询的扫描行数、查询耗时和占用内存,而后续将进一步补充对Join连接数、底表个数、Union操作个数、所涉分区数等指标的检查,以保证用户提交的SQL质量符合平台的执行标准。
2)Starrocks熔断机制:当Starrocks为查询请求生成执行计划,发现待扫描的文件数和分区数量过大时,将触发熔断机制,跳出此次查询的执行,并返回错误信息。
六、成效
通过以上一系列的查询性能治理措施,我们在数据报表平台的查询性能上达成了阶段性目标,平台查询性能逐步稳定,具体表现如下:
1)查询平均响应时间降低:在业务高峰期,平台的查询平均响应时间从原来的8秒降低至4秒;
2)查询超时数量显著降低:查询时间90线由原先的约18秒降低至约8秒,由超时导致的查询失败量由日均7000次缩减至日均1400次;
3)查询性能波动幅度减小:平台每日平均查询性能指标趋于平滑,全平台查询时间标准差由原来的约25秒缩短至14秒左右;
图10:查询响应时间统计
对平台所承载的各类大查询而言,本文所述的治理策略起到的优化作用尤为明显,由图11可见,长耗时查询数量在治理措施实施后呈明显下降趋势。
图11:长耗时查询数量变化趋势
七、总结
通过本文所述内容,我们采取了多项措施来对数据报表平台的查询效率进行治理。通过建立缓存机制、使用物化视图,可提升查询性能和算力使用效率;通过对查询策略进行优化,可裁剪非必要开销,解放平台查询瓶颈;通过切分流量和对SQL质量进行管控,可更好地实现资源隔离,提升平台查询质量。
未来,随着治理措施的不断演进,这些优化策略将被逐步规范化、集成化、自动化,以更好地服务于平台的查询需求。