快手基于 Hive 构建数据仓库,并把 Hive 的元数据信息存储在 MySql 中,随着业务发展和数据增长,一方面对于计算引擎提出了更高的要求,同时也给 Hive 元数据库的服务稳定性带来了巨大的挑战。本文将主要介绍 Hive MetaStore 服务在快手的挑战与优化,包括:
Apache Hive 是由 Facebook 开源的数据仓库系统,提供 SQL 查询能力,快手基于 Hive 搭建数据仓库,随着业务迅速发展和数据规模增长,Hive 的性能开始成为瓶颈,无法满足业务需求。
Hive 把用户 SQL 通过解释器转换为一系列 MR 作业提交到 hadoop 环境中运行,MR 存在作业启动、调度开销大、落盘多磁盘 IO 重的问题,这导致其性能注定无法太好,针对 Hive 查询速度慢的问题,业界先后推出了包括 presto/impala/spark 等查询引擎,在实现和适用场景上各有优缺点。
在计算引擎层面我们所面临的几个挑战是:
基于上述考虑,我们最终基于 HiveServer 本身的 Hook 架构,实现一个 BeaconServer。所有的查询仍然以 HiveServer 作为统一入口,从而解决易用性和低成本的问题。
BeaconServer 作为后端 Hook Server 服务,配合 HS2 中的 Hook,在 HS2 服务之外实现了所需的功能,包括根据一定规则路由 SQL 到适当的引擎,从而起到查询加速的效果。当前支持的模块包括路由、审计、SQL 重写、错误分析、优化建议等。
BeaconServer 本身是一个无状态服务,我们可以很方便进行水平扩容,并且 BeaconServer 服务调整升级不影响 HiveServer 服务本身。
基于上述架构,我们很好的应对了前面所提到的四大挑战,引入了更高效的计算引擎,在业务无感知的前提下大幅提升了查询效率。
这里特别提一下,除了引入 presto、spark 等高效计算引擎并对齐进行优化之外,我们针对 Hive 本身的 FetchTask 机制(本地读取 hdfs 文件返回结果,不存在作业提交开销)也进行了系列改进,使其适用场景更广,查询效率更高,在日常查询中也占了很大比重。
在介绍完我们智能引擎架构之后,接下来进入今天重点的主题 Hive MetaStore 在快手的挑战。
首先,我们基于整体服务架构简单说明一下 Hive MetaStore 服务的作用以及其重要性:Hive Metastore 是 hive 用来管理元数据的服务,包含 database、table、partition 等元信息,presto/spark 也都以 Hive Metastore 作为统一的元数据中心。
除了计算引擎本身,数据血缘、数据地图、数据依赖等上层服务也重度依赖 Hive Metatstore。
接下来介绍一下 Hive MetaStore 服务当前所面临的挑战。
由于快手业务使用场景需要大面积使用动态分区,同时数据量和查询量也在随着业务快速增长,这对 Hive MetaStore 服务的性能和稳定性带来挑战:
针对上述问题,我们考虑从几个方面进行优化:
首先,访问量多的问题,在大数据场景下,存在写少读多的特定,对于元数据主库,当前压力也主要来自于大量的读操作导致 QPS 过高,因此第一个优化方向是通过读写分离来降低主库压力;
其次,在 HIVE 的元数据查询上,存在大量的多表联合查询,尤其存储分区信息的两个大表(PARTITONS 和 PARTITION_KEY_VALS)之间的联合查询,会对服务带来很大压力,可能导致查询超时以及慢查询等问题,因此第二个优化方向是优化元数据 API 调用;
最后,从长远考虑,随着业务发展,数据量和访问量还会持续上涨,我们需要具备在极端情况下对于不同优先级的访问进行流量控制的能力,满足服务分级保障的需求,同时具备在服务容量不足时对服务进行水平扩容的能力。
接下来我们从四个优化方向分别进行介绍:
首先,我们介绍一下 MetaStore 读写分离架构设计。
根据业务应用场景和需求不同,例如数据血缘、数据地图、数据依赖等服务只有读请求,我们可以直接把 MetaStore 服务拆分为读写和只读,只读服务链接从库来承接这部分读请求。从库本身可水平扩展,能够很好的降低服务 QPS 压力,把服务访问延迟控制在较好的水平,满足业务需求。
在查询场景中,既有读请求也有写请求,没有办法直接从服务层面进行拆分。由于大数据场景下普遍写少读多,大量读请求直接发送到主库会导致 QPS 峰值高,服务抖动引发慢查询,进而影响服务稳定性。对此我们的优化方案是在查询层面实现 HiveMetaStore API 粒度的读写分离,通过把对主库的读请求尽量路由到从库,从而有效降低主库的 QPS 压力。这个方案要解决的一个主要问题是如何保证数据一致性,避免由于主从同步延迟,导致读请求在从库中漏读数据或者读取到错误的过期数据。
整体解决思路也很简单,我们在把读请求路由到从库之前,先确保当前服务所连接的从库已经完成数据同步即可。
具体流程为:在 HiveServer 或者 Spark 提交 SQL 创建会话链接时,会首先从主库获取并保存当前最新的 GTID,在同一个会话中,每次写请求操作完成后,都会更新当前会话所持有的 GTID;对于读请求,会首先获取从库当前的 GTID,通过比较 GTID 来判断从库是否已经完成了数据同步,只有当从库 GTID 大于等于当前会话持有的 GTID 时,这次读操作才会被真正路由到从库。
通过上述读写分离方案,我们主库的 QPS 负载下降 70%+,并且由于压力下降,主库的慢查询问题也同步大幅减少,有效提升了服务稳定性。
我们通过读写分离手段卸载了主库压力,把大量访问请求转移到从库,一方面我们可以通过水平扩容进行负载均衡来缓解从库压力,另一方面通过优化 MetaStore 接口调用,也能够有效提升服务性能和稳定性。
根据我们分析定位,MetaStore API 调用当前主要面临的问题包括:
第一,查询层面存在大量的 API 调用,造成底层服务 QPS 过高;
第二,在 Hive MetaStore 层,单次 API 调用访问的数据量过大,容易导致服务瞬时压力大;
第三,对于存储分区信息的两个大表(单表记录超 10 亿)查询时延过高。
第二个单次访问数据量大,造成服务瞬时压力高,改成分批方式返回,就能起到削峰作用;第三个分析对两张大表的查询性能瓶颈,针对具体问题采用合适的优化方案。
首先针对 API 调用量大的问题,我们需要查一下为什么有这么多的调用,是不是都是正常必要的调用以及如何减少冗余 API 的调用。这里我们主要进行了两点优化:
HIVE 的 DDL DESC TABLE 命令,社区默认行为除了返回表相关元信息外,还会遍历获取这个表所有的分区信息,对于一个包含大量分区的表来说,这个操作会非常耗时同时也是不必要的。对此我们做的优化是默认跳过这个遍历获取分区元信息的操作。通过测试对比,对于一个包含十万分区的表执行 DESC 命令,优化前需要两百多秒,优化之后只需要 0.2 秒。
然后通过对于 MetaStore API 调用占比进一步分析发现,get_functions/get_function 这两个接口被大量调用,这个不太符合预期。经过排查发现这个调用行为是 Spark SQL 在初始化 Hive MetaStore 的时候所触发。社区 Spark 在 3.0 版本之前底层所依赖的 Hive 版本一直是 1.2,在这个版本中的初始化实现会先通过 get_database 获取所有的 HIVE 库,然后针对每个 HIVE 库再逐个调用 get_functions 接口,接口调用次数和总的 HIVE 库数量成正比,导致了大量冗余调用。在 Hive2.3 版本中这块行为已经得到了优化,我们通过升级 Spark 所依赖的 HIVE 包到 2.3 版本解决了该问题。根据我们的统计,优化后整体 API 调用次数减少近 30%。
其次针对单次访问数据量大,造成服务瞬时压力高的问题,我们可以考虑改成分批方式完成大数据量的扫描,从而起到削峰作用。
例如查询一个大表某个时间范围内的所有分区,涉及分区数 11W,优化前由于需要一次性扫描大量数据并返回,导致元数据服务压力过大,接口调用超时,任务查询失败;我们通过把一次大查询拆分成一系列小查询,分批轮次返回需要的数据,就能有效规避服务层面瞬时压力过大造成的一系列不良后果,优化后这次查询总共耗时 17115 毫秒。通过上图元数据服务测试时的网络压力等指标变化,也可以间接反映优化效果。
然后我们再分析一下存储分区信息的两个大表查询时延过高的问题,看看性能瓶颈究竟在什么地方以及如何进行针对性优化。
对于 select * from table where p_date=‘20200101’ and p_product=‘a’这样一条 Hive 查询,在进行分区下推时发送给元数据服务的查询表达式为:where ((( “FILTER0”.“PART_KEY_VAL”= ?) and (“FILTER2”.“PART_KEY_VAL”= ?)))。
这个查询表达式使用 PARTITION_KEY_VAL 表中的 PART_KEY_VAL 字段来进行匹配过滤,存在的问题是:PARTITION_KEY_VAL 表中没有 TBL_ID 字段,导致会扫描到无关表的同名分区;PARTITION_KEY_VAL 表中没有索引列,无法通过索引加速。
针对上述问题,我们的优化方案是应用 PARTITONS 表中的分区名索引加速查询,并且 PARTITIONS 表中包含 TBL_ID 字段,也能够有效避免对无关表的分区扫描。
通过分析 expresssionTree,解析时间范围子树,获取最长子串前缀:‘20200101’,从而得到优化后的查询表达式为:where ((( “FILTER0”.“PART_KEY_VAL”= ?) and (“FILTER2”.“PART_KEY_VAL”= ?))) and(“PARTITIONS”.“PART_NAME”like ? )。
这个查询优化前后的耗时对比为 2662ms VS 786ms,取得了很大提升。
接下来我们再看另一种可优化的场景,对于 select * from table where p_date=20200101 and p_hourmin=1000 这样一条 Hive 查询,由于其分区字段类型是 string 类型,但是 Hive 查询中给的是整型值,导致无法通过分区名进行过滤,会命中该表的全部子分区。
优化方案也很简单,在 SQL 解析时,如果 filter 字段为分区字段,并且类型为 string,强制转换 constantValue 到 string 类型。
优化前耗时:32288ms,优化后耗时:586ms。
我们接着聊一下 MetaStore 流量控制架构设计:
Hive MetaStore 作为核心的底层依赖服务,需要具备服务分级保障能力,当服务压力过高响应能力出现瓶颈时,要能够优先满足高优先级任务请求、限制或者阻断低优先级请求的能力,防止元数据库出现雪崩状况。
整体流量控制架构设计如上图所示,核心点在于引入 BeaconServer 服务作为中控层。Beacon Server 作为中控层,支持动态更新设置流控策略,以及实时获取当前元数据服务压力状况。Hive MetaStore 作为客户端会周期性去中控层获取当前最新的元数据服务压力状况和流控策略,并针对不同优先级的 API 调用请求采取对应的流控措施。
基于上述架构,我们可以实现在服务流量高峰期出现性能瓶颈时,能够按比例延迟或阻断部分低优先级的访问请求,保证高优先级请求继续得到正常响应,当服务压力缓解状态恢复正常后,再自动恢复对低优先级访问请求的响应。
流量控制架构在上线后,有效缓解了生命周期管理服务定期清理大批量分区时对于元数据服务造成 TPS 过高的压力。
最后介绍一下 MetaStore Federation 架构设计,长远看,随着业务量持续增加,MySQL 单机依然会存在性能及存储瓶颈的风险。解决 MySQL 单机瓶颈和压力,业界通用方案是分库分表,由于 Hive 元数据信息存储采用三范式设计,表关联较多,直接在 MySQL 层面进行拆库拆表会存在改造成本大、风险高且不利于未来扩容的问题,因此我们考虑采取 HiveMetaStore 层面的 Federation 方案,实现元数据水平扩展能力。
首先看一下 Hive MetaStore 内部实现逻辑,持久化元数据层被抽象成了 RawStore,比如 MySQL 对应的实现时 ObjectStore,HBase 对应的实现则是 HBaseStore。
基于上述原理,我们首先想到的方案 1 是基于 ObjectStore 已有功能和代码实现 KwaiStore,在 KwaiStore 中实现 Hive DB 路由数据源的功能,配置不同 Hive DB 到对应 MySQL 数据源的映射关系。
方案 1 的优点在于可以保持包和配置的统一,降低韵味成本;缺点在于对 Hive 的侵入性较大,并且上线时如果要做到数据完全一致,需要暂停服务。
方案 2 是通过引入路由层,使用代理转发请求的方式来实现。这个方案下 Metastore 代码不用做任何改动,新增 Router 层,根据请求数据的 Hive DBName 来决定路由到哪个 Metastore 上去。Router 层可以水平扩容,可以在 Router 层做很多扩展功能,白名单、多数据源支持(统一元数据)、Hive DB 禁用、元数据权限等操作。在扩容时,可以在 Router 层添加规则,指定某个 Hive DB 暂时不可访问,待数据源完全准备好后,再添加路由规则到 Router 层,同时取消 DB 不可用限制;可以做到只影响部分 Hive DB 的使用。
总结一下,方案 2 的优点在于对 Hive 没有侵入性,升级版本比较容易,可以灵活定制 Router 层策略,HA 水平扩容,扩容 MySQL 时相对影响较小,上线风险较小,统一元数据入口,方便审计和溯源;不足之处在于新引入服务层,增加运维成本,Metastore 被划分标签,配置不完全统一。
综合考虑方案 1 和方案 2 的优缺点,我们最终选择了在 HMHandler 层来实现路由功能,在 HMSHandler 中维护一组 HiveDB 与 RawStore 的映射关系,在 getMS()时传入 Hive DB,路由根据 db 判断应该使用那个 RawStore,不修改 RawStore 中 API 的实现,不涉及到持久化层的侵入改造。
这个方案的优点是配置统一,运维成本低;不修改持久化层,改造难度小;缺点是需要调整大量 Thrift API,在调用时传入 DB。
以上是本次关于 HiveMetaStore 服务在快手遇到的挑战与优化的全部内容。今天的分享就到这里,谢谢大家。
分享嘉宾:
王磊,任职于快手数据平台部,担任离线计算引擎方向负责人,负责 SQL 引擎研发和平台架构建设,技术栈领域包括资源调度、分布式离线计算。
本文转载自:DataFunTalk(ID:dataFunTalk)
领取专属 10元无门槛券
私享最新 技术干货