- OLAP引擎底层原理与设计实践
- 高英举 许一腾
- 4006字
- 2025-05-07 12:39:47
2.3 Presto常见问题及应对策略
本节我们介绍6个企业在生产环境部署或应用Presto时遇到的典型问题及其应对策略。这些问题在不少OLAP引擎中同样存在。
2.3.1 查询协调节点单点问题
Presto的架构设计只允许一个集群协调节点存在,并且只允许集群协调节点接收用户的查询请求,如图2-1所示。这种单集群协调节点架构的主要问题如下。
❑ 集群协调节点是单点,存在因集群协调节点不可用而导致整个集群不可用的问题。
❑ 集群协调节点是处理用户查询请求的单点服务,极大地降低了Presto处理并发查询执行的能力。这个单点的集群协调节点也负责集群查询执行节点的发现(Discovery)与管理。

图2-1 Presto的协调节点单点架构
Presto的这部分设计确实有些简单粗暴了,实际上它应该做几个改造,如图2-2所示。
将集群协调节点的职责拆分为两个:一个是集群主节点(Master Node),只负责管理集群的所有节点,保证集群的可用性;另一个是查询请求处理节点(Search Node),只负责接收用户的查询请求,完成查询解析、规划、优化、调度并将分布式执行计划下发到查询执行节点。
❑ 集群主节点(Master Node)应该至少有3个。系统基于Paxos、Raft等分布式一致性协议来选主节点,并由主节点来负责维护集群的节点列表等元数据。
❑ 查询请求处理节点(Search Node)是客户端直接发出请求的节点,它的个数可以是查询执行节点个数的2~3倍,它可以承担查询分布式执行计划的最后一个查询执行阶段的执行工作,得到查询的最终计算结果后直接将数据返回给客户端。
如果你了解其他OLAP引擎的架构设计,例如ES、Doris,会感觉这种架构似曾相识。

图2-2 Presto的协调节点高可用架构
你是否考虑过,虽然分布式架构看起来非常优秀,但实际上还有一些棘手的问题需要处理好,例如下面的问题。
❑ 改造前的Presto具备基于队列(Queue)的并发查询个数限制能力,改造后的Presto查询请求处理节点有多个,基于队列的并发查询限流要做全局级别的还是单节点级别的?如果是全局级别的,设计与实现会更复杂一些。
❑ 改造前的集群节点列表等元数据由集群协调节点维护,集群协调节点负责新增与删除查询执行节点,也负责将用户查询请求的任务调度到对应节点上。所有工作都在一个节点,设计与实现较简单。改造后就涉及一个集群节点列表的元数据需要由集群主节点以何种方式、何时同步到查询请求处理节点的问题。
上述问题的解决方案Presto社区一定想到了,只不过一直没有落地。相关的ISSUE在GitHub上有过多次讨论,但是最终也没有形成一个开源的设计实现交付给社区。这里列举了几个相关ISSUE。
❑ https://github.com/prestodb/presto/issues/13814
❑ https://github.com/prestodb/presto/issues/15453
❑ https://github.com/trinodb/trino/issues/391
社区的想法与前文描述的分布式架构实现逻辑类似,只是目前一直看不到明确的支持计划。我们可以先利用其他妥协的方案在一定程度上解决集群协调节点单点不可用的问题,例如:
❑ 搭建多个Presto集群,再搭建一个负载均衡(load balance)方案(如使用Nginx或HAProxy),只允许用户通过负载均衡方案访问这些Presto集群。
❑ 只搭建一个Presto集群,但是启动多个集群协调节点,再搭建一套集群协调节点的代理(Proxy)方案(如使用HAProxy),将Presto集群中的所有查询执行节点的discovery uri都设置为proxy的uri。这里要求查询执行节点请求代理时,代理能按照固定集群协调节点顺序将请求转发到第一个集群协调节点。如果第一个集群协调节点不可用,则转发给下一个。如果代理采用的是轮转(round robin)等方式转发请求,会导致查询执行节点被注册到不同的集群协调节点,从而形成多个集群。
2.3.2 查询执行过程没有容错机制
Presto为了简化查询执行流程,减少查询执行的耗时,没有在查询执行中加入容错机制,即某个查询执行过程中任何一个查询执行阶段的任何一个任务执行失败,都会导致整个查询失败,需要用户发起新查询重试。重试整个查询的计算开销代价,肯定比重试部分任务的代价要高。是不是重试部分任务一定就是最好的呢?像Spark那样实现更复杂推测执行(speculative execution)方式的重试是不是更合理呢?仍然像我们之前表达的观点一样,没有绝对的好坏,只有特定场景下的优劣,简单的重试机制对小查询(数据量少、低延迟)更友好,复杂的重试机制对大查询(数据量大、高延迟)更友好。重试机制不应该频繁触发,合理的重试机制可以保证维护重试上下文以及相关的并发同步不会成为查询执行的瓶颈。建议读者在使用Presto时,可以在请求发起侧判断出查询失败并发起重试。
2.3.3 查询执行时报错exceeding memory limits
报错exceeding memory limits(超过内存限制)并不是Presto独有的,而是所有OLAP引擎普遍存在的,各个引擎都在尝试对它做各种各样的优化,对于Presto来说主要优化手段如下。
❑ 设置好JVM Heap的大小,如果服务器上只部署了Presto查询执行节点,一般情况下可以将查询执行节点的堆(Heap)设置为80%的内存大小,这样可以有更多的堆内存来执行查询。
❑ 设置好查询执行相关的内存参数,主要是query.max-memory-per-node、query.max-total-memory-per-node、query.max-memory、query.max-total-memory、memory.heap-headroom-per-node这几个参数,适当调大它们的值可以使原来报错exceeding memory limits的大查询能够顺利执行完。详见https://trino.io/docs/current/admin/properties-memory-management.html。
❑ 设置好资源组(Resources Group)来控制多租户场景中各个租户的最大查询并发度。详见https://trino.io/docs/current/admin/resource-groups.html。
❑ 之前数据计算的过程全部在内存,新版本的Presto支持了将分类、关联、聚合的中间计算结果放到磁盘上(Spill To Disk),如果Presto集群执行的大查询比较多,可以开启此功能。详见https://trino.io/docs/current/admin/spill.html。
2.3.4 无法动态增删改或加载数据目录与UDF
在企业生产环境的联邦查询环境中,时常出现需要增删改catalog、schema、table的需求。Presto目前的方案是先更改配置目录,再重启整个集群。此方式过于简单粗暴,也直接影响集群的可用性。感兴趣的读者请查阅https://github.com/prestodb/presto/issues/2445。这个问题从2015年开始讨论,到本书出版之前,社区仍然没有给出具体的解决方案。不过不少公司做了自己的实现,例如京东在2016年出版的《Presto技术内幕》中针对此问题给出单独解决方案。再例如华为的Presto发行版Hetu也支持动态加载catalog,详见https://openlookeng.io/docs/docs/admin/dynamic-catalog.html。
与前面介绍的不能动态加载目录类似,UDF也不能动态加载,需要重启整个集群。对于Presto这种需要长时间稳定运行的服务,这种方式代价有点大了,大大增加了用户自定义UDF的烦恼。这个与整个插件的设计有关,所有插件都是静态加载的,而且Presto中大量应用了自动依赖注入,不支持动态加载UDF就不用考虑复杂的内存对象引用一致性的问题。
2.3.5 查询执行结果必须经集群协调节点返回
例如下面的SQL,它的分布式执行计划有两个查询执行阶段,数据是这么流动的:Stage1→Stage0→集群协调节点→presto sql client。Stage1的计算结果数据需要序列化后,再从Stage0反序列化继续计算,这个环节省不掉,其他的OLAP引擎也是这么做的。但是Presto没必要在Stage0计算得到最终结果后,再序列化给到集群协调节点,经由集群协调节点给到presto sql client,这增加了2次数据序列化与反序列化的开销,可能多增加几百毫秒甚至几秒的延迟。如果是在线服务的OLAP场景,这肯定是不能忍的。同时所有的查询结果的返回都要经过集群协调节点,伴随着网络IO、数据序列化/反序列化的CPU开销、JVM中大量对象创建销毁的GC压力,这些增加了集群协调节点的不稳定风险。

2.3.6 不支持低延迟、高并发
在Facebook 2018年发布的论文“Presto on Everything”中提到在Facebook的广告、A/B测试、报表等场景,Presto可以支撑数百的查询QPS。然而随着数据驱动的业务越来越多,体量越来越大,要求越来越高,一部分原本是离线分析型的需求(OLAP需求)慢慢演变为在线服务型(Servering)分析需求,企业对OLAP引擎能够支撑的查询QPS量级的要求也越来越高,动辄是几万甚至几十万级别的QPS,企业对OLAP引擎的查询延迟的要求也越来越高,从几秒级别一直降低到百毫秒级别。Presto对在线服务场景的支撑能力存在一定的限制,这主要体现在如下几点。
❑ 只能有一个集群协调节点接收用户查询请求。因为它是单点,不符合在线服务场景下对服务高可用的要求,加之单个节点很难承载几千到几万的查询QPS,会带来线程频繁切换、内存不足、CPU利用率过高等问题,所以我们需要调整Presto的架构,如前文所述,将集群划分为集群协调节点(包括集群主节点和查询请求处理节点)以求让查询执行节点达到更好的优化效果。
❑ Presto的执行模型是面向中大型查询的多查询执行阶段的MPP流水线执行模型,支持计算全流程在内存中以流水线的方式执行,其查询速度能够比Hive的MapReduce执行方式快5~10倍,部分查询能够在百毫秒级别计算完成,但是它仍然做不到查询延迟Pct99达到百毫秒级别,对于在线服务场景下的中小型查询支持不好。这些中小型查询用Scatter-Gather执行模型再加上一些极致的优化,是可以做到查询延迟Pct99在百毫秒级别的,例如Elasticsearch。有些与Presto类似的引擎(如Apache Doris)也可以做到,Doris起初是在Impala引擎的代码上修改而来的,Impala的执行模型与Presto类似。因此我们需要引入Scatter-Gather执行模型,并引入一定的CBO(Cost-Based Optimizatioin,基于代价的优化)能力,让中小查询执行得更快。前文多次提到Scatter-Gather执行模型可以算作MPP模型的查询执行阶段的特例,因此我们可以知道在Presto中引入Scatter-Gather执行模型并不太难,之前的大部分设计实现可以复用。
❑ 工程师们对Presto的执行速度的印象大部分来自于Presto on Hive,这是一种典型的计算存储分离的架构,Presto只负责计算,Hive只负责管理元数据,HDFS只负责提供存储。由于Hive构建在HDFS之上,HDFS的open、seek、read都比较慢且表现不稳定,导致了从用户视角来看,Presto的查询速度不够快,动辄是10s以上的延迟。实际上这种场景下瓶颈不在Presto。因此我们需要引入更快的存储,相关的案例有很多,包括Facebook实现的RapatorX,还有Alluxio统一缓存系统等,能够大大加快查询的执行速度。有些企业甚至直接在Presto查询执行节点上构建了本地存储服务,使用的存储介质可能是本地磁盘,也有可能是直接基于内存,相当于做了存储与计算不分离的方案。只要存储服务的数据拉取效率高一点,对Presto不做任何改造,其查询速度也将有5~10倍的提升。
Presto并不是无法具备针对在线服务场景的服务能力,它的技术底子还是很优秀的,只要稍加改造即可实现。主要是Presto社区还没有意识到这个场景的价值,没有在社区发展路线中加入这个特性,这是场景需求问题,而不是技术问题。如果只是技术问题,那么优化了上述几点即可满足在线服务场景的需求。Cloudera公司维护了另一个知名的OLAP引擎——Impala,它也存在类似的情况,后来百度将Impala改造为Doris并开源出来才支持了在线服务能力。这个也是由于国内诸如百度、阿里等公司维护着大量的广告主、淘宝店家等企业客户,这些客户对广告投放数据、店铺数据的分析要求是实时和快速,因此催生了更高的在线服务场景需求。相对来说,国外的企业虽然用户的体量也大,但是在这方面的要求没这么高。