购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第2章
Presto基本介绍

作为一个开源的分布式SQL查询引擎,Presto已经成为OLAP领域的重要工具,它以高性能、高易用性和高灵活性赢得了广泛的关注和应用。本章将带你深入了解Presto的架构、特性和应用场景。

本章首先会介绍Presto的基本知识,包括如何通过连接器与各种数据源进行交互,以及如何通过数据目录和数据表来组织和管理数据。接着,将探讨Presto的集群架构,包括集群协调节点和查询执行节点的角色和功能,以及它们如何协同工作以提供高效的查询服务。在介绍完Presto的工作原理后,本章将通过一系列实际案例,展示Presto在不同企业中的应用。同时,我们也会讨论Presto在实际部署中可能遇到的挑战,以及如何通过技术手段解决这些问题。

2.1 Presto概述:特性、原理、架构

Presto是一个开源的分布式SQL查询的执行引擎,在Facebook、Amazon、Uber、京东、美团、滴滴、阿里等企业满足了非常多的分析型需求,还有一些企业基于Presto搭建了商业化的服务。在易用性、灵活性和扩展性的设计方面,Presto还是做得非常不错的,这个笔者在多年的Presto使用经验中有深刻的感受。企业中常见的数据计算需求,如BI报表、即席查询,甚至是运行时间比较长的ETL任务,Presto都是支持的。不过目前来说,在笔者所知和所用的Presto应用场景中,还是查询时延要求在毫秒或者秒级别的SQL并发BI报表、即席查询以及多数据源关联查询居多,用Presto来做ETL的不多,毕竟Spark、Flink这些成熟的流批计算工具用作ETL工具更成熟一些。

Presto相关的论文中提到了一些概念,其中比较重要的几个如下。

数据源连接器(Connector): 如果想让Presto能够从某个存储系统、消息队列、文件拉取、API进行数据并发查询,需要实现对应的连接器。基本上任何可以提供数据的数据源都可以通过连接器的抽象接入Presto。

数据目录(Catalog): 数据目录是一个逻辑概念。在Presto中定义数据目录时,需要为其指定一种数据源连接器,并通过相关的配置指定如何连接上对应的数据源。数据目录与数据源连接器是多对一的关系。

数据库(Schema): 这个概念可以等同于传统关系型数据库的中的数据库的概念。一个数据目录可以包含多个数据库。

数据表(Table): 这个概念可以等同于传统关系型数据库中的数据表的概念。一个数据库可以包含多个数据表。

如图1-7所示为一个4节点的Presto集群架构,在Presto的架构中有两种节点。

集群协调节点 (Coordinator),负责接收SQL查询请求、解析SQL、生成和优化执行计划、生成和调度任务(Task)到查询执行节点上。集群协调节点将一个完整的查询拆分成多个查询执行阶段(Stage),每个阶段拆分出多个可以并行的任务。

集群执行节点 (Worker),负责执行集群协调节点发给它的任务,有部分任务负责到外部存储系统拉取数据,这部分任务会先执行,之后再执行那些负责计算的任务。从图1-7中可以看到,拉取数据的任务是在右侧两个集群执行节点上执行的,负责计算的任务是在左侧一个集群执行节点上执行的。

集群执行节点可以横向扩容为多个以支撑更多的计算需求。集群协调节点只能有一个。由此我们可以得出结论:集群协调节点是单点,在集群可用性上有一定的风险。至于这个问题如何解决,后文会详细讲解。

Presto的架构实际上就是一套分布式的SQL执行架构,它最大的特点是天然存储和计算分离,Presto只负责计算,存储的部分由数据源自身负责。这种存储与计算分离的架构很符合当今云计算发展的趋势——独立的存储云服务与计算云服务,这样做的好处是存储资源不够时可以独立扩容,计算资源不够时也可以独立扩容,而且计算和存储能够分别使用适合自己的机型。分布式存储系统的实现一般都比较复杂,涉及数据的分区、副本、容灾、文件格式、IO优化等,是一项非常大的工程。Presto直接放弃了存储,只做计算,而且它在计算方面也做了一些妥协,如不支持单个Query内部的执行容错,如果查询中的某个任务失败了,会导致整个查询失败。但是,这样的实现方案更简单,代价是需要用户侧来重试。

本质上,你可以认为Presto由以下几个部分组成。

2.1.1 一个高性能、分布式的SQL执行框架

SQL是一种声明式的编程语言,能够很清晰地表达用户想要什么,正是因为它学习难度比较低、易用性比较高,已经成为数据库和大数据计算领域最常用的业务计算逻辑编写方式。然而,在生产环境中,有很多系统没有对外暴露SQL执行接口,对内也没有SQL执行能力,如Elasticsearch和HBase;而有些系统虽然有SQL接口,但是没有海量数据计算能力,如MySQL;还有一些系统使用MapReduce完成SQL计算,时延太长不满足业务需求,如Hive。Presto包含的SQL执行框架可为数据源提供一种通用统一的SQL执行能力,在海量数据规模下,还具备高性能、分布式的计算能力。一个SQL查询进入Presto系统,SQL执行框架通过以下几个关键步骤得到最终结果。

1)接收SQL查询请求。

2)SQL解析、语义分析(生成AST,即抽象语法树)。

3)生成执行计划、优化执行计划。

4)划分查询阶段,生成和调度任务。

5)在Presto集群执行节点上执行任务(有从数据源拉取数据的任务,也有以计算为主的任务),生成结果。

6)分批给客户端返回查询结果。

Presto为用户屏蔽了SQL解析的底层细节,并尽它所能在查询延迟、并行度、数据本地性、根据规则或成本选择最优执行计划上做了非常多的优化。

2.1.2 一套插件化体系

Presto的插件包含数据源连接器和自定义执行逻辑的SQL函数。

简单地说,借助连接器机制,Presto可以将来自一切数据源的数据计算SQL化,无论用户的数据源是本地文件(File连接器)、内存(Memory连接器)、HTTP API(HTTP连接器),还是Hive(Hive连接器)、HBase(HBase连接器)、MySQL(MySQL连接器)。

通过连接器机制,Presto实现了完全插件化的数据源的元数据获取、注册以及数据读取,不同的数据源对于Presto来说就是不同的连接器,用户操作数据源是通过连接器来实现的。Presto本身就自带了多个可以直接使用的数据源连接器,如Hive连接器、Kafka连接器、Elasticsearch连接器、MySQL连接器。举个例子,用户可以使用Presto的Hive连接器来查看Hive中都有哪些表,表结构是什么,也可以写SQL查询Hive中的数据,还可以把数据写到Hive中。在这个场景中,用Presto执行SQL与直接执行Hive的SQL的区别是:Presto的底层SQL执行引擎并不是HiveSQL,而是自行实现的一套MPP执行架构,实际执行SQL的时候,无论是查询的调度、执行计划的生成,还是执行任务的生成和执行,其速度都是HiveSQL的5~10倍(行业内有相关的性能对比测试报告)。众所周知,HiveSQL底层是使用Hadoop MapReduce执行模型实现的,而且查询的调度和执行都是通过启动新的JVM来完成的,所以Hive更适合去处理数据量超大而且对处理延迟要求比较低(一般是小时或天级别)的数据计算任务。如果是BI报表、即席查询类需求,显然Presto要优于Hive。

其实Presto连接器能做的远不止于此,它还有一个很实用的特性,即多数据源关联分析(有人称之为联邦查询)。假设用户有2个数据源,一个在Hive中(hive_table),另一个在HBase中(hbase_table),要执行的SQL如下。

传统的做法要么是把HBase的数据导入Hive中,创建Hive表执行Hive SQL,要么是把Hive数据导入HBase中,创建HBase表执行Phoenix SQL,这样带来了比较大的数据同步开销和数据一致性风险。如果用Presto来满足以上需求会简单很多——只需要配置好Presto的Hive连接器与HBase连接器,启动Presto SQL客户端,输入上面的SQL即可得到查询结果。这是怎么实现的呢?在Presto内部,SQL被解析后生成执行计划,并将Hive和HBase的数据读取任务调度到集群执行节点上来读取HDFS和HBase的数据,这些数据随后被传输到其他集群执行节点上完成JOIN以及其他SQL计算(如聚合操作)。由于计算过程中数据交换的中间存储介质都是内存,而且Presto也做了很多提高并发度的优化,故计算速度非常快。

在连接器体系中,Presto提供了一系列的Java接口,允许用户实现自定义的连接器。使用这些接口开发者能够自定义许多逻辑,下面列出了几个常见的逻辑。

❑ 获取数据表元数据及数据的位置。

❑ 获取数据表的统计数据,用于CBO(Cost-Based Optimization,基于代价的优化)。

❑ 控制分片(Split)的生成和划分,分片是Presto定义的数据分片基本单位。

❑ 实现计算下推接口,以决定哪些计算可以下推,常见的下推类型有Limit、Projection、Filter、Aggregation。连接器拉取数据时,通过此功能将必要的操作下推到数据源的存储引擎中,减少不必要的数据传输开销。

❑ 实现CreateTableAsSelect接口,实现通过Presto写数据到存储系统的目的。

与Hive类似,Presto也支持用户自定义SQL函数(俗称UDF),以实现那些直接用SQL不好表达但用Java代码比较容易实现的业务计算逻辑。

2.1.3 开箱即用的SQL内置函数和连接器

Presto支持的函数是非常丰富的,笔者时常感叹:使用SparkSQL和FlinkSQL时,如果能有这么多函数就好了。列举一些Presto中常见的函数。

❑ JSON系列:做JSON与Presto内置数据类型之间的转换和提取。

❑ 日期/时间系列:可以方便地操作时间字段,改变时间字段内容。

❑ 近似聚合系列:允许那些对准确度要求不是100%的用户,通过牺牲一定的准确性换来更高的执行性能。

❑ Array/Map系列:允许用户方便地操作Array、Map这种嵌套类型的字段,很多用户喜欢用Parquet文件存储嵌套类型的数据,而Presto为操作这种数据提供了便利。

如果在生产环境中,你发现Presto预先实现的连接器不能满足你对功能、性能的需求,或者Presto没有预先实现你需要的数据源,那么你可以大胆地去改进或实现,Presto源码的分层抽象做得比较好,有特殊需求只需要调整对应的连接器源码即可,编译打包也是比较方便的。

这里再举几个国内互联网公司改造Presto连接器的案例。

❑ 京东曾经改造Presto的MySQL连接器的源码,调整了分片生成的方式,大大提高了在利用Presto做Hive与MySQL关联分析时,MySQL拉取数据的并行能力。

❑ 阿里云因为有多个自研的存储系统,如对象存储(OSS),他们的工程师开发了Presto的OSS连接器,以允许Presto对接OSS中的数据查询。类似地,国外的云计算巨头AWS也开发了对象存储S3服务的Presto连接器,以为商业客户提供更好的服务。

❑ 笔者之前所在的某家公司在使用ES连接器时发现性能、功能都不满足需求,也做了诸多改进,如优先查询Keyword类型的字段,优化了Projection下推功能,通过scroll API拉取数据时默认从doc_values中拉取。

❑ Presto的HBase连接器在Facebook内部有实现,可惜的是没有开源出来,国内易观国际这家公司在GitHub上提供了开源实现,从其性能测试报告来看,该项目还是很优越的。如果大家需要在HBase上执行SQL,Presto的HBase连接器是一个不错的选择。

2.2 Presto的应用场景与企业案例

2.2.1 Presto的应用场景

Presto是定位用于数据仓库和数据分析领域的分布式SQL引擎,其中特别适合用于如下几个应用场景。

加速Hive查询 。Presto的执行模型是纯内存MPP模型,比Hive使用磁盘做数据交换的MapReduce模型快至少5倍。

统一SQL执行引擎 。Presto兼容ANSI SQL标准,能够连接多个RDBMS(关系型数据库管理系统)和数据仓库的数据源,在这些数据源上使用相同的SQL语法和SQL函数。

为那些不具备SQL执行功能的存储系统带来SQL执行能力 。Presto可以为HBase、Elasticsearch、Kafka带来SQL执行能力,甚至可以为本地文件、内存、JMX、HTTP接口带来SQL执行能力。

构建虚拟的统一数据仓库,实现多数据源联邦查询 。如果需要计算的数据源分散在不同的RDBMS、数据仓库,甚至其他RPC(远程过程调用)系统中,Presto可以直接把这些数据源关联(SQL Join)在一起分析,而不需要从数据源复制数据,统一集中到一起。

数据迁移和ETL工具 。Presto可以连接多个数据源,再加上它有丰富的SQL函数和自定义函数的能力,可以帮助数据工程师完成从一个数据源拉取(E)、转换(T)、装载(L)数据到另一个数据源。

2.2.2 Presto的企业案例

本节介绍Presto在几个典型企业中的落地,让大家直观感受Presto的强大和实用。

1.Facebook

Facebook在全球有超过10亿的用户,它数据仓库中数据的规模非常大,在2013年就已经超过30PB。这些数据的用途非常广泛,包括离线批处理、图计算、机器学习、交互式查询等。

2008年,Facebook开源了Hive,执行模型基于MapReduce设计,使用SQL来表达计算需求,算是海量数据计算的一次非常大的进步。Hive在Facebook内部也有大规模应用,Hive的优势是能够应对超海量数据、运行稳定、吞吐量大。

然而,对于数据分析师、产品经理、工程师来说,查询的速度越快(不要等一杯茶的时间),能够处理的数据越多,交互式能力越强,他们的数据分析效率也就更高。基于海量数据的快速交互式查询的需求,越来越迫切。

2013年,Facebook完成了Presto的研发及生产环境的落地,搭建了几十个Presto集群,总节点数超过10000,为300PB的数据赋予了快速交互式查询的能力。

据Facebook官方公开披露的Presto论文的描述,Presto在Facebook中的几个主要使用场景如下。

1) 交互式分析: Facebook工程师和数据分析师经常需要对一些数据集(一般压缩后大小为50GB到3TB)进行分析、验证猜想、绘制图表等。单个Presto集群需要支持50~100的并发查询,支持秒级的查询时延。这些有交互式分析需求的用户更关心查询执行的快慢,而不是占用资源的多少。

2) ETL: 数据仓库经常需要定时根据SQL逻辑从上游表生成下游表(例如数据仓库的分层设计,从ODS表到DW表,或从DW表到DM表),在这种场景下,Presto可以用来执行长时间运行的SQL ETL任务,任务的计算逻辑需要完全用SQL来表达。例如,类似下面的SQL。

当你用上述方式使用Presto的时候,其实与使用SparkSQL、FlinkSQL没有太大区别。这里需要由一个任务调度系统来定时调起Presto的SQL。类似的ETL任务在Facebook很常见,它们经常处理TB级别的数据,占用比较多的CPU和内存资源,任务执行耗时不像交互式查询那样重要,更重要的是提高数据处理的吞吐量。

3) A/B测试: A/B测试是企业用来量化产品中不同功能带来不同影响的方法。在Facebook,大部分A/B测试的基础设施都构建在Presto之上。分析师和产品经理需要在A/B测试结果上做多种分析,这些分析很难通过数据预先聚合的方式来提高查询速度。

4) 开发者/广告主分析: 有很多面向Facebook外部开发者和广告主的报表工具是基于Presto建设的。例如Facebook Analytics 3,它是给那些使用Facebook Platform构建应用的开发者分析数据用的。类似的应用,暴露出的是特定的WebUI查询入口,查询的模式基本上是固定的,大部分是关联、聚合、窗口函数中的一种。虽然整体数据量非常大,但是经过数据过滤后,实际参与计算的数据不多,如广告主只能查看他自己的广告(大广告主除外)。为这些开发者或者广告主提供的数据分析服务,查询时延要求一般都是在50毫秒到5秒之间,Presto集群必须要保证5个9的可用性,并且要支持同时处理几百个不同用户的请求。

2.Amazon Athena

Amazon Athena是基于Presto搭建的一种交互式查询服务,用户可使用标准SQL分析Amazon S3中的数据,具备SQL技能的任何人都可以轻松快速地分析大规模数据集,查询结果一般都在数秒内返回。在AWS上Athena作为一个大数据商业服务提供给商业付费客户。

3.京东

在即席查询的需求中,京东调研过SparkSQL、Impala和Presto,最终选择了Presto,并持续改进源码,迭代出了自己的JD-Presto版本,后续也有部分功能回馈了社区。JD-Presto团队是国内首批Presto源码的贡献者。在京东内部,有20多个系统在使用JD-Presto版本,尤其是在精准营销平台中,JD-Presto作为大数据即席查询计算平台起到了关键性作用,极大地提升了采销部门进行精准营销活动的效果和效率。JD-Presto团队出版过一本专门介绍Presto原理和源码的书籍《Presto技术内幕》。

4.美团

2014年美团在Hadoop集群上搭建了Presto来服务于公司内部的分析师、产品经理、工程师。美团曾经选取了5000个平时的Hive查询,通过Presto查询对比发现,Presto的总查询时间消耗是Hive的1/5左右,这个效果还是很明显的。

5.乐视云计算

笔者曾经在乐视的云计算公司负责搭建、维护生产环境中由1500多台机器组成的Hadoop集群,上面的YARN节点上运行着几十个Presto集群(集群规模从5个节点到400个节点都有)。不同的Presto集群为不同的业务服务,如CDN运维质量分析、风控安全、视频服务的数据分析等。Presto用于支撑这些业务的Ad-Hoc查询以及报表类查询,查询响应Pct99在3s以内。

这里要说明一下,Presto本身是不支持直接部署在YARN上的,需要使用Slider来部署。为了在同一台YARN宿主机上部署多个Presto节点,笔者修改了Slider工具以生成Presto配置,从而支持了多端口部署。

6.其他公司

其实国内国外还有很多公司在生产环境中使用Presto,如Twitter、Airbnb、滴滴、小米等。因为公司信息安全问题,它们大多没有对外纰漏详情,如果你感兴趣,可以参加InfoQ等大型技术会议获取最新动态。

2.2.3 Presto不适合哪些场景

Presto虽然很强大,但它不是万能的,在如下场景中就不适合使用Presto。

1.完全替代Hive(MapReduce执行模型)

Presto在提供更高的并发查询能力和更低的查询延迟上,确实比Hive强很多,大部分测试都显示Presto执行SQL的速度是Hive的5倍以上。然而,这并不意味着Hive就应该退出历史,毕竟Presto的计算主要依靠内存,当数据量非常大时,超过了整个Presto集群中单个查询允许的内存大小,Presto容易出现OOM(内存溢出),相比之下Hive更稳定。虽然Presto官方正在做中间计算结果溢出到磁盘(Spill To Disk)的功能,但是如果在数据计算过程中有大量的Spill To Disk操作,磁盘IO势必会成为瓶颈,进而大大影响查询的执行速度。Presto要想足够快,需要给到足够的CPU和内存资源,对于那些对时延要求不高的查询,Hive可以使用非常小的资源持续稳定地运行数小时甚至数天并最终给出结果,而这是Presto做不到的。Hive仍然是数据计算高吞吐、低成本、高稳定性的“代言”,所以建议在生产环境中让Presto和Hive形成合理分工,优势互补,而不是由谁来淘汰谁。

2.分析型的在线服务

分析型的在线服务指的是某类数据统计服务,但是它查询模式相对固定(虽然是多维度多指标,但是维度指标相对固定),这个服务的用户基数比较大(一般到100000以上),如广告主的广告投放分析、比特币交易平台的C端用户交易统计、淘宝店主的生意参谋或销售数据分析等。在这些系统中,用户并发查询的QPS还是非常高的。

分析型的在线服务的特点是需要查询引擎能够做到高并发、低延迟。高并发指的是单集群QPS能够支撑1000以上,查询延迟Pct99一般在800ms以下(如果查询超过800ms,再加上系统的其他时间开销,用户看到的页面加载会很慢,体验不好)。

这些在线服务的查询特征是:具有相对简单的多维度条件过滤、多指标聚合,没有特别复杂的逻辑(如复杂的Join操作)。在这种场景下,使用Druid、ES更靠谱,使用Presto不合适。理由是Druid、ES的查询执行模型是Scatter-Gather(相当于一次Map操作或一次Reduce操作),比较简单,也没有复杂的执行计划生成和优化逻辑,任务的调度很简单,整体花在查询调度上的CPU和线程开销较小。Presto是基于SQL的MPP(大规模并行处理)模型实现的,查询执行模型相对复杂。

3.OLTP

Presto是OLAP引擎,它的设计决定了它不能用于OLTP,不能当作MySQL来用。首先Presto要操作的连接器实现了相关接口,它是可以支持插入和删除的,但是不支持更新。其次当数据在Presto查询执行节点的内存中被传输和处理时,它是以列式存储的方式存在的,这不便于执行OLTP系统中对整行进行CRUD(增加、读取、更新和删除)操作。最后,Presto对事务的支持并不好,而这是MySQL的基本能力。

有的技术方案,如TiDB和阿里的AnalyticDB,尝试融合OLTP与OLAP系统,形成HTAP,即兼备了两种系统的功能,两边好处都占上,但是其本质仍然是分别实现了OLTP和OLAP。在大部分生产环境中,很少有必须用HTAP的理由。

4.替代Spark、Flink

Spark和Flink是很难被Presto替代的,反过来,Spark和Flink也很难替代Presto。归根结底,它们不是同类型的技术,解决的不是完全相同的问题,虽然确实是有重叠的部分,例如三者都可以在各种数据源上执行SQL。

Spark、Flink擅长的是提供比SQL更丰富的编程API完成业务计算逻辑,它们有一个突出的强项是流式计算。你也可以启动长期运行的Spark或者Flink集群,接入交互式的SQL客户端。到目前为止,它们调度和执行SQL的时延都比Presto要长,而且能够支撑的QPS也比Presto更少,还没有听说哪家企业把BI报表应用直接运行在Spark和Flink集群上。在2019年的Flink Forward Asia大会上,阿里的Flink官方宣布在尝试参考Presto来增加OLAP的能力,但是短期内必定不会有大的成效。

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并开源出来才支持了在线服务能力。这个也是由于国内诸如百度、阿里等公司维护着大量的广告主、淘宝店家等企业客户,这些客户对广告投放数据、店铺数据的分析要求是实时和快速,因此催生了更高的在线服务场景需求。相对来说,国外的企业虽然用户的体量也大,但是在这方面的要求没这么高。

2.4 Presto与Trino的项目与版本选择

2.4.1 Trino与Presto选择哪个

如果你在互联网上搜索Presto,会发现两个Presto项目:

❑ Presto——https://prestodb.io/,github项目源码地址:https://github.com/prestodb/presto。

❑ Trino(曾用名PrestoSQL)——https://trino.io/,github项目源码地址:https://github.com/trinodb/trino。

Presto是2013年Facebook的三个核心工程师(Martin Traverso、Dain Sundsrom、David Phillips)创造和开源出来的,在Facebook内部,它的应用规模是很庞大的(部署了多个集群,集群节点数规模较大)。这三个工程师一直想把Presto发扬光大,但是一直到了2019年,同时期的三个开源大数据技术Spark、Flink、Kafka都已经创建了自己的商业化公司进行推广,Presto却没有得到对应的支持。

2019年Presto的三位核心工程师离职并加入刚成立两年的Starburst公司,这家公司基于Presto的项目源码开发了PrestoSQL(后改名为Trino),创建了自己的代码仓库和官方网站,并进行商业化运营。

如果你问笔者该选哪个,笔者更倾向于选择Trino,因为它近两年的源码迭代速度更快,而且还有三位创始人的支持,Trino的发展前景可能更好,所以在本书中如果有涉及源码讲解,我们也会使用Trino的源码作为学习示例。不过事情也不是绝对的,Presto与Trino也在互相学习,并且会把对方比较好的实现合并到自己的项目里,所以同时关注这两个项目的动态没有坏处。由于这两个项目的大部分核心代码是完全相同的,所以我们以Trino来举例并不会妨碍你学习Presto,也就是说参照本书你也可以把Presto学明白。为了方便描述,本书不会过多区分Trino与Presto,将使用统一的名词“Presto”来阐述。另外,本书编撰的初衷实际上也不是为了带领读者学习Presto源码,我们的目标是让读者通过学习Presto来理解通用大数据OLAP引擎。因为对通用大数据OLAP引擎的讲解要有对标项目,要结合某个具体的开源项目的设计实现来讲解,所以本书才会讲解Presto的源码。如果想知道关于Presto分裂为两个项目的来龙去脉,请参考https://zhuanlan.zhihu.com/p/55628236;如果想知道两个项目有什么不同,请参考https://zhuanlan.zhihu.com/p/87621360或者https://github.com/prestosql/presto/issues/380。

2.4.2 本书为什么用Trino的v350版本来做介绍

v350版本实际上是笔者精心选择的一个代码版本,它是2020年12月28日发行的版本,也是Trino与Presto项目模块结构完全相同的最后一个版本。在这之后,Trino对项目进行了结构调整,不过仅是改变了Maven模块的位置,代码的核心设计实现没什么变化。Presto核心代码在2019年之前基本上就定型了,之后未出现特别大的变化。不停变化的数据连接器实现都不是核心代码。因此即使到了2024年,我们在本书中拿v350做介绍也没问题,并不存在v350代码过时的问题。另外,现在整个OLAP领域的设计实现,还是基于20年前的VLDB、SIGMOID的基础理论。

下面可以看看v350版本后,到2024年的第一个版本,Trino主要变化有哪些。

聚合下推: 支持了聚合下推给存储,非核心功能变化。

模式匹配: 支持了SQL的模式匹配语法,非核心功能变化,详见https://trino.io/blog/2021/05/19/row_pattern_matching.html。

表函数: 支持SQL的Table Value Function(表值函数)语法,非核心功能变化,详见https://trino.io/docs/current/develop/table-functions.html和https://trino.io/blog/2022/07/22/polymorphic-table-functions.html。

物化视图: 支持物化视图的定义和管理,接近核心功能变化,详见https://trino.io/docs/current/language/sql-support.xhtml#materialized-view-management。

Presto在2020年到2024年的主要变化如下:

底层执行引擎支持了Velox: Velox是用C++开发的单机执行引擎,起初它仅是为了加速Presto的查询执行节点的执行部分,后面逐渐发展成独立的开源项目,具备了以底层执行引擎加速Spark、Flink计算的能力。这部分建议读者重点关注。

非中心化协调节点(Disaggregated Coordinator): 支持了多集群协调节点的架构,这个功能在社区的呼声很大,早应该支持了,详见https://prestodb.io/blog/2022/04/15/disggregated-coordinator和https://www.youtube.com/watch?v=slwPm-mROZ0。

其他变化都是非核心功能的调整,具体可以看看两个项目的发行版本记录(Release Notes):

❑ Trino发行版本记录:https://trino.io/docs/current/release.html。

❑ Presto发行版本记录:https://prestodb.io/docs/current/release.html。

如果你看发行版本记录就会发现大部分变更都是增加了更多的连接器,要么就是某个连接器的代码又修复了漏洞或者又优化了某个功能。从整个OLAP的发展趋势来看,一个OLAP引擎支持多个数据源的场景不是主流趋势,Presto与Trino过分强调这个,笔者认为偏离了主航道,它们应该多去做内核部分的演进,比如优化器的设计实现、查询调度、节点间RPC、聚合或关联查询的计算效率、计算过程中的数据倾斜处理、集群协调节点或单点瓶颈、与存储配合以减少拉取数据的IO等。

2.4.3 Presto项目源码结构

1.v350版本及以前的源码结构

像Presto这种来自一线大厂的大型开源项目,抽象和模块化做得都非常好,虽然模块非常多,但源码结构是非常清晰的。Presto源码结构如下。

以上模块从整体上可以分为4类。

❑ 核心流程控制类模块,包括presto-main和presto-parser。

❑ 核心API定义类模块,即presto-spi。

❑ 各种连接器实现类模块,比如Hive连接器presto-hive、MySQL连接器presto-mysql、Kafka连接器presto-kafka、Cassandra连接器presto-cassandra、Elasticsearch连接器presto-elasticsearch、TPC连接器presto-tpcds和presto-tpch等。

❑ 周边工具类模块,包括SQL客户端工具presto-cli、打包工具presto-server和presto-server-rpm、列式存储读取工具presto-parquet和presto-orc、模式匹配工具presto-matching、性能对比跑分工具presto-benchmark和presto-benchmark-driver等。

上述第一种与第二种是Presto源码中的核心,看懂了这些源码就相当于看懂了Presto,本书后面的内容会对其继续深入拆解。

2.v351版本及以后的源码结构

在v351以后,PrestoSQL项目更名为Trino,彻彻底底与Presto分离。同时Trino项目也修改了项目源码的结构,主要变化如下。

❑ 将所有的Maven模块进行分类,与我们上面描述的分类方式类似,区分了client、core、docs、lib、plugin、serivce、testing,形成了项目的根目录,如图2-3所示。

❑ 在根目录中放置了对应的模块,例如原来的presto-main、presto-spi被放到了core目录中,presto-tpcds被放到了plugin目录中。

❑ 将presto-*模块名称全部修改为trino-*,并将代码中包名、类名中的带presto的全部修改为Trino,但是Trino的核心代码与Presto差不多。

图2-3 Trino项目v351版本及以后的源码结构

2.5 编译与运行Presto源码

2.5.1 环境准备

环境准备主要涉及如下5个方面。

1) 操作系统: 编译运行Presto源码推荐的操作系统是Mac或者Linux。由于笔者已经10年没有用过Windows,因此不知道本书所介绍的内容是否适用于Windows,有条件的还是尽量使用Mac或者Linux编译运行Presto源码。常见的Linux系统(如Ubuntu、CentOS)都是可以的。

2) IDEA: 笔者在日常阅读或调试源码时使用的是IntelliJ IDEA作为Java开发的IDE,这款软件使用非常方便,需要的读者可自行到https://www.jetbrains.com.cn/idea/download处下载安装。我们在编译源码前需要将其导入IDEA中。

3) JDK: 在新版本的Presto源码中,已经对JDK的最低版本有了明确要求,这里笔者建议JDK版本选择11到13,版本太高或者太低都会在编译时报JDK相关的错误。

4) Maven Repo加速: 在编译源码之前,建议国内的读者先将Maven依赖源设置为阿里提供的Maven仓库,否则可能会非常慢(大概需要几小时)。如果读者的操作系统是Mac或Linux(其他操作系统读者自行搜索并查阅设置方法),则需要编辑~/.m2/settings.xml(如果没有可以创建),具体如下。

由于国内的各个Maven仓库都是各个公司维护的,可能会发生变化,如果读者发现以上配置已经失效,可自行搜索寻找其他公司的Maven仓库。

5) Docker运行环境 :Docker运行环境主要用来编译presto-docs模块,这里面是Presto的文档,不过我们学习源码直接看官网(https://trino.io/)的文档就好,没必要自己编译生成文档了,因此Docker运行环境可以没有。

2.5.2 下载源码并载入IDEA

本书主要介绍的是Trino,其项目地址为https://trino.io/,github源码地址为https://github.com/trinodb/trino。将源码导入IDEA中有如下两种方法。

1)先手动输入git clone命令,再导入IDEA中,具体如下:

git clone完成后,打开IDEA,单击open选项,找到Trino项目的根目录,打开即可导入源码,如图2-4所示。

图2-4 IDEA open project-1

2)直接通过IDEA从github导入,如图2-4所示,仍然是在此页面上单击Get from VCS(Version Control System,版本控制系统)选项,在弹出的Get from Version Control(从版本控制获取)页面选择Repository URL(存储库URL),填入Git上的代码地址,单击Clone按钮即可将源码导入IDEA中,如图2-5所示。

图2-5 IDEA open project-2

2.5.3 编译Presto源码

在继续后面的步骤之前,可以先粗略阅读一下项目根目录中的README.md,里面有编译运行方法的大致介绍,再结合着本节内容来完成Presto源码的编译运行。之后我们就可以执行以下命令开始编译Presto源代码(Presto代码比较多,并且外部依赖较多需要一一下载,编译时间可能会长达数十分钟)。

以上编译命令是Presto项目根目录中README.md给出的编译方法,它会编译所有的Maven模块,包括presto-docs等实际上在我们学习Presto源码时用不到的模块。你可以用下面的命令只编译运行Presto必须有的模块。

使用上述命令的好处是:

❑ 只编译学习Presto源码必须要用到的Maven模块,同时也不需要提前安装Docker等不需要的依赖。

❑ 编译过程中不运行单元测试。

2.5.4 标记Antlr4自动生成的代码为generated source

将Antlr4自动生成的代码标记为generated source。Antlr4是一种能够自动生成解析代码的词法语法解析器。Presto将Antlr4作为SQL的语法解析器,主要实现在presto-parser模块中。Antlr4自动生成的代码不在src/main目录下,而是在target/generated-sources/antlr4中。在使用IDEA学习Presto源码时,默认IDEA没有识别出来这些自动生成的Java class源码,导致我们在IDEA中查看或导入(import)这些class的Java源码时,显示找不到源码,无法跳转到这些class的源码文件中,如图2-6所示。

实际上,我们可以浏览目录树找到这些类的源码文件,如图2-7所示。

如果想在IDEA中识别Antlr4自动生成的Java class源码,需要将presto-parser/target/generated-sources/antlr4标记为Generated Sources Root(生成的源根),具体的操作方法是在此目录上右击,然后在弹出的快捷菜单中依次选择Mark Directory as(将目录标记为)→Generated Sources Root,如图2-8所示。

图2-6 无法找到源码的提示

图2-7 源码所在目录

图2-8 引入Generated Source源码

2.5.5 在IDEA中运行3个节点的Presto集群

我们可以在IDEA中启动一个有三个节点的Presto集群,用于模拟出一个分布式的集群环境,调试Presto的分布式执行代码。

第一个节点(PrestoServer)是集群协调节点角色,没有查询执行节点角色,有助于调试查询的执行计划,生成优化相关代码。

第二个节点(PrestoServer)是查询执行节点角色,我们称之为worker1,有助于调试查询的某个查询执行阶段的任务执行的代码。

第三个节点(PrestoServer)是查询执行节点角色,我们称之为worker2,有助于调试查询的其他查询执行阶段的任务执行的代码。

1.为什么要在IDEA中运行一个Presto集群

对于查询执行阶段比较多的某个查询,调试时如果都只在一个既是集群协调节点角色,又是查询执行节点角色的单节点打断点运行,会比较混乱。一般情况下,笔者调试时都会在本地IDEA中启动一个集群协调节点和多个查询执行节点,根据需要,一般有多少查询执行阶段就会启动多少查询执行节点,让每个查询执行节点对应一个查询执行阶段的一个任务。这样调试起来时,集群协调节点、不同查询执行阶段的任务的处理逻辑都不会在同一个节点上执行。

但是这里有一点需要注意:需要有一种机制能够做到在调试时,让Presto的集群协调节点成功将不同查询执行阶段的任务分配到不同的查询执行节点,这个涉及一个查询中各个查询执行阶段的各个任务如何调度的问题。笔者一般是临时实现一个自定义的NodeScheduler来实现该目的,或者是在集群协调节点的调度代码上打断点,运行到此处时直接修改任务的目标调度节点。总之这些都是为了方便分别调试每个查询执行阶段,避免相互干扰。

2.准备启动Presto集群的配置文件

请在对应的目录中创建3个配置文件,并填充以下内容。

1)集群协调节点配置文件:presto-server-main/etc/coordinator.properties。

2)Worker1配置文件:presto-server-main/etc/worker1.properties。

3)Worker2配置文件:presto-server-main/etc/worker2.properties。

3.在IDEA中启动Presto集群

在IDEA的顶部菜单中找到Run-> Edit Configurations(见图2-9),可配置程序的启动命令,3个节点需要分别配置并启动3个程序(Application),分别如图2-10所示。

1)集群协调节点的IDEA运行配置(run configuration)如图2-11所示。

其中VM启动参数(VM options)的内容如下。

图2-9 IDEA的Edit Configurations选项

图2-10 配置并启动程序

图2-11 集群协调节点的IDEA运行配置

2)Worker1的IDEA运行配置如图2-12所示。

其中VM启动参数的内容如下。

图2-12 Worker1的IDEA运行配置

3)Worker2的IDEA运行配置(Run Configuration)如图2-13所示。

图2-13 Worker2的IDEA运行配置

其中VM启动参数的内容如下。

配置好后,即可通过普通运行模式或调试模式(Debug)启动3个节点,它们的启动顺序是coordinator、worker1、worker2。运行效果如图2-14所示。

图2-14 运行效果

在浏览器打开集群协调节点的WebUI:

即可看到Presto WebUI,如图2-15所示,有2个查询执行节点。

图2-15 Presto WebUI

4.补充说明

如果想在IDEA中启动只有一个节点的Presto集群,这个节点既是集群协调节点角色,又是查询执行节点角色,只需要将集群协调节点的配置文件中node-scheduler.include-coordinator设置为true,并只启动coordinator节点即可。配置文件为presto-server-main/etc/coordinator.properties,具体如下。

2.5.6 运行Presto命令行工具

Presto命令行工具presto-cli允许我们在命令行中输入SQL,执行查询并获得计算结果。Presto集群运行成功后,再运行presto-cli的方法,具体如下。

启动presto-cli后,我们就可以在这里输入期望执行的SQL了。

2.5.7 调试Presto源码常见问题

问题1:修改了某个数据源连接器模块的源码,重新启动集群协调节点、查询执行节点,但是程序没有执行最新的代码。

修改了某个连接器模块的源码后,需要手动重新编译对应的Maven模块后修改才会生效。假设我们修改了presto-tpcds模块的源码,那么要执行如下命令重新编译。

问题2:调试Presto代码时,查询总是自动退出执行,导致调试无法继续。

Presto查询的执行过程维护了查询级别、查询执行阶段级别、任务级别的状态机。执行中的查询处于运行状态,如果超时了,将有异步线程将查询的状态迁移到Abort/Canceled状态。我们在调试代码时,如果长时间停留在某个断点就有可能出现超时。要解决这类问题,除了将一些超时参数值设置得更大之外(在集群协调节点、查询执行节点的配置文件中我们已经设置),还可以在QueryStateMachine、StageStateMachine、TaskStateMachine的transitionToXXX处打断点,当程序执行到这里时,触发断点并禁止向后运行,防止调试过程被异常终止。

问题3:以调试模式启动Presto后,程序执行异常缓慢。

笔者在调试Presto源码时也遇到过类似问题,当时主要的原因是把断点直接设置到了Java方法(method)级别上,严重影响了调试性能。这是使用IDEA经常遇到的问题,注意这里不是说不能在Java方法的代码实现上设置断点,而是不要在IDEA中设置方法(method)级别的断点(参见IDEA官方介绍:https://intellij-support.jetbrains.com/hc/en-us/articles/206544799)。解决方案是把断点打到方法实现代码的某行上。

问题4:Presto有许多运行时自动codegen出来的代码,这部分代码既无法看到也无法调试。

在包含聚合、关联查询、表达式等计算语义的查询执行过程中,Presto使用airlift bytecode来自动生成代码,其底层实际上依赖的是ASM。这些代码的确无法调试,但是可以使用阿里巴巴开源的JVM程序诊断工具Arthas(https://arthas.aliyun.com/doc/)将其导出。大致步骤如下。

1)由于生成的代码的Java类名(class name)是不固定的,所以需要在某个引用这些类的实例化对象的代码上打断点,以便通过IDEA看到具体的Java类名。

2)启动Arthas,并将其绑定到Presto JVM进程上,执行如下命令即可导出源码到指定文件。

问题5:整个查询流程太繁杂了,而且跨节点,如何让调试轻松一点?

即使是熟悉Presto源码的朋友们,想完整地调试一个查询的执行过程也是非常难的,主要是因为Presto代码量大,执行过程是多节点多线程异步的,数据库领域设计实现有理解难度。即使如此,我们仍然要去多多调试源码,因为实践是检验真理的唯一标准。如果只是不痛不痒地看看本书是没有意义的,需要你去实践。对于初学者来说,如果调试整个查询太难,可以换一种降低难度的方式——采用分而治之的手法:先阅读与调试那些单元测试代码,再调试整个流程。Presto的单元测试代码写得非常清晰完整,是我们学习Presto的重要资料。例如我们不太理解聚合实现原理,可以分别调试TestAggregationOperator、TestHashAggregationOperator、TestGroupByHash。

2.6 基于Presto的数据仓库及本书常用SQL

2.6.1 数据仓库介绍

本书接下来要介绍的所有数据集与查询集都基于一个独立、完整、易于理解的数据仓库模型——TPC-DS Benchmark标准中的零售商数据仓库模型,我们在这里称之为TPC-DS数据模型。TPC-DS是OLAP的一个Benchmark标准,它除提供了标准规范文档外,还附带了用于生成数据仓库中表结构、数据集合、查询集合(99个典型OLAP SQL)、执行压测的若干工具。通过本节对TPC-DS数据模型及对应SQL的介绍,能够让我们对本书将要介绍的SQL有一个总体性的认知,使学习的过程更加体系化。

数据仓库,简称数仓,广泛存在于现代企业的技术架构中,我们可以通过它来驱动业务发展。数仓的主要职能是集中存储数据,提供数据分析能力,建立维护数仓数据模型,作为数据导入、计算、取数的集散地。从数仓的实时性上来讲,数仓又分为实时数仓、离线数仓。

实时数仓: 数据写入是实时的,能够做到秒级别;数据查询也可以做到实时,海量数据查询能够在几秒、几分钟返回。(很多人认为只要数据写入是实时的就是实时数仓,这是一种误解。试想一下,如果数仓的查询结果需要2小时才能得到,就算数据实时写入了又有什么用呢?)

离线数仓: 数据写入是离线的,如小时级别;数据查询也是离线的,如小时级别。

技术架构与数据模型是我们谈论数仓的两大主题。

数仓的技术架构有多种,离线数仓一般是“远古”组合(如Hive、HDFS、YARN),大多数公司都嫌弃这种离线数仓太慢,会用“现代”组合(如Hive Metastore、HDFS、YARN、Spark、Presto、Alluxio)来做加速。

远古组合: 通过各种数据集成工具,如sqoop,将数据写入Hive,查询时使用Hive MapReduce。

现代组合: 使用Spark Streaming或Flink Streaming将数据写入Hive;只用Hive Metastore存储库、表、字段这些元数据,不使用Hive MapReduce做查询;对于有低延迟要求的查询使用Presto作为计算引擎,延迟要求不高的查询使用Spark SQL作为离线计算引擎,部分公司还利用Alluxio做HDFS cache来加快Presto查询速度。

常见的数仓数据模型有两种,即Inmon的关系数据模型与Kimball的维度数据模型。关系数据模型强调数据模型设计者对企业的业务有较为全面的认知,能够站在全局的角度设计出遵循范式的数据模型。维度数据模型强调将业务过程映射为维度与事实,并分为维度表与事实表,不要求对企业业务有整体认识,追求快速迭代数据模型。下面对上面涉及的几个关键术语进行解读。

业务过程: 企业开展业务时发生的业务流程,如电商领域中的下单、支付、送货、确认收货等。

维度: 维度表达的是业务过程的上下文环境信息,如下单时间、下单APP、用户ID等。维度表中存储的是维度信息,并且会存在多个维度表,如用户维度信息表(包含用户ID、用户昵称、用户年龄等)、商品维度信息表(包含商品ID、商品名称、商品其他属性等)。

事实: 事实表达的是业务过程中的具体度量,如购买数量、订单金额等。事实表中存储的是各个维度ID以及各个事实的度量值。

2.6.2 TPC-DS Data Model数据模型介绍

TPC-DS数据模型模拟的是一个零售产品供应商,包含线上销售与线下销售,这是我们对此模型从业务上的直观理解,也很符合现实生活中的实际情况。

TPC-DS Data Model采用的是业界常用的Kimball维度建模方式。本节会对其简要介绍。对这部分感兴趣的读者,请参考《数据仓库工具箱》和《阿里巴巴大数据之路》。

如图2-16所示,customer、store、item是维度表,包含了客户、商店、商品的各个维度的属性。store_sales、store_returns是两张核心的事实表,表达的是线下商店的订单销售数据与退货订单数据。

图2-16 TPC-DS的Kimball模型的事实表与维度数据模型

下面对主要的维度表和事实表进行简要介绍。

1.维度表

(1)store表

商店的维度表,包含商店名称(s_store_name)、店长(s_manager)、地址(s_country,s_state,s_city,s_county)等维度属性,每行记录的主键是s_store_sk,在事实表中会作为其维度属性键来引用,如表2-1所示。

表2-1 商店的维度属性表

(2)item表

商品的维度表,包含商品名称(i_product_name)、商品类目(i_category)、品牌信息(i_brand)、当前售价(i_current_price)等维度属性,每行记录的主键是i_item_sk,在事实表中会作为其维度属性键来引用,如表2-2所示。

表2-2 商品的维度属性表

(续)

2.事实表

store_sales表是线下商店销售订单的事实表。如表2-3所示,外键(Foreign Key)的字段是维度属性的键,其他字段是事实度量值,如销售价格(ss_sales_price)。

表2-3 线下商店销售订单的事实表

2.6.3 本书常用SQL

本书中用到的所有SQL,都是基于TPC-DS数据模型的SQL,所以请读者务必先阅读并理解前面的内容。下面所列SQL示例,背后的执行原理将在后文分别讲解。需要特别注意的是,这里的SQL内容与编号,并不是TPC-DS自带的99个SQL的内容与编号,而是本书根据分布式查询原理的讲解需要由浅入深地设计出来的,希望读者阅读时不要混淆。之所以重新设计出一组SQL,是因为TPC-DS自带的99个SQL都是较复杂的查询SQL,不完全适用于本书对分布式查询原理的讲解。

1.简单查询

简单查询指的是直接拉取数据,可能会附带一些过滤条件。本书通过以下SQL来讲解简单查询SQL的设计实现原理。

2.ORDER BY、LIMIT

本书通过以下SQL来讲解包含ORDER BY、LIMIT的SQL的设计实现原理。

3.聚合

本书通过以下SQL来讲解包含聚合操作的SQL的设计实现原理。

4.聚合+ORDER BY、LIMIT

本书通过以下SQL来讲解既包含聚合操作又包含ORDER BY、LIMIT的SQL的设计实现原理。

5.JOIN查询

本书通过以下SQL来讲解包含连接(JOIN)的SQL的设计实现原理。

6.聚合+JOIN+ORDER BY、LIMIT

本书通过以下SQL来讲解包含多种计算的SQL设计实现原理。

7.窗口聚合

本书通过以下SQL来讲解包含窗口聚合的SQL的设计实现原理。

8.DDL

本书通过以下SQL来讲解包含定义表(DDL)的SQL的设计实现原理。

注意:以上列出的几个SQL涉及的不是TPC-DS数据模型中的表。Presto的tpcds连接器没有实现CREATE Table相关功能。

9.管理类SQL

本书通过以下SQL来讲解管理类SQL的设计实现原理。

2.6.4 在哪里执行本节介绍的SQL

Presto项目中内置了tpcds连接器,大家只需要在配置文件中包含tpcds连接器相关的配置并启动Presto Server,即可通过presto-cli命令行客户端来执行本节介绍的各个SQL。关于如何配置Presto服务器以及如何启动presto-cli,我们在本章前面几节已经做过详细的介绍,这里不再赘述。

2.7 总结、思考、实践

本章是关于Presto的详细介绍,包括基本认知、特性、原理、架构以及应用场景。首先介绍了Presto的核心概念,如数据源连接器、数据目录、数据库和数据表。然后详细阐述了Presto的集群架构,包括集群协调节点和查询执行节点的角色和功能。接着,本章探讨了Presto的应用场景,如加速Hive查询、统一SQL执行引擎、为不具备SQL执行功能的存储系统提供SQL能力等。此外,还提到了Presto在企业中的案例,如Facebook、Amazon Athena、京东等公司的应用实例。本章还讨论了Presto不适用的场景,以及在生产环境中遇到的常见问题及其解决方案。

思考与实践:

❑ 在OLAP领域中,Presto与其他OLAP引擎相比有哪些优势和局限性?

❑ 如何根据企业的具体需求选择合适的OLAP解决方案?

❑ 针对Presto在生产环境中可能遇到的问题,如集群协调节点单点问题、查询执行过程中的容错机制缺失等,你有哪些优化建议?

❑ 考虑到OLAP引擎的发展趋势,你认为Presto在未来的发展中需要重点关注哪些方面? cYqJ4P93ipyq9RxuDMkzzH0Q3GGD8XXg1Gdd8SGJyGFEUedNW9Z4z6VOVoTD9baZ

点击中间区域
呼出菜单
上一章
目录
下一章
×