PostgreSQL、GreenPlum、HAWQ 三者的关系
2018-11-16 11:32:55 阿炯

从单机版的关系型数据库(PostgreSQL),大规模并行处理(MPP)数据库(Greenplum)到SQL on Hadoop解决方案(Apache HAWQ),以及最新的SQL on Cloud数据仓库(HashData)。

PostgreSQL

所有的分布式数据库,包括Greenplum Database,Apache HAWQ以及HashData云端数据仓库,都是基于单机版关系型数据库PostgreSQL的。

每个PostgreSQL数据库的实例包含一个PostMaster的damon进程和多个子进程,包括负责写出脏数据的BG Writer进程,收集统计信息的Stats Collector进程,写事务日志的WAL Writer进程等等。

客户端应用通过libpq协议连接到PostMaster进程;PostMaster收到连接请求后,fork出一个子进程Postgres Server来处理来自这个连接的查询语句。Postgres Server进程的功能组件可以分成两大类:查询执行和存储管理。查询执行组件包括解析器、分析器、优化器以及执行器。在查询执行过程中,需要访问和更新系统状态和数据,包括缓存,锁,文件和页面等等。

Greenplum

作为一个单机版的关系型数据库,PostgreSQL更多地是作为联机事务处理(OLTP)系统使用的。由于其丰富的分析功能,很多企业也会基于PostgreSQL来构建数据仓库,特别是在数据量不大的情况下。但随着数据量的增大,基于单机PostgreSQL构建的数据仓库就无法满足企业用户对查询响应时间的要求:低延迟。

为了解决这个问题,MPP架构就被引入了。MPP数据库通过将数据切片分布到各个计算节点后并行处理来解决海量数据分析的难题。每个MPP数据库集群由一个主节点(为了提供高可用性,通常还会有一个从节点)和多个计算节点组成。主节点和每个计算节点都有自己独立的CPU,内存和外部存储。主节点负责接收客户端的请求,生成查询计划,并将计划下发到每个计算节点,协调查询计划的完成,最后汇总查询结果返回给客户端。计算节点负责数据的存储以及查询计划的执行。计算节点之间是没有任何共享依赖的(shared nothing)。查询在每个计算节点上面并行执行,大大提升了查询的效率。

MPP数据库的一个重要特征是,计算和存储是紧耦合的。每一张表的数据打散存储到每个计算节点上面。为了确保查询结果的正确性,每个计算节点都需要参与每条查询的执行中。在Greenplum Database的架构设计中,对于每个slice执行子树,在每个计算节点中会启动一个相应的Postgres Server进程(这里称为QE进程)来执行对应的操作。

从使用者的角度看,Greenplum Database跟PostgreSQL没有明显差别。主节点作为整个分布式系统集群的大脑,负责接收客户连接,处理请求。跟PostgreSQL一样,对于每一个连接请求,Greenplum Database都会在主节点上面fork一个Postgres Server进程(我们称之为QD)出来,负责处理这个连接提交的查询语句。对于每一条进来的查询语句,QD进程中的解析器执行语法分析和词法分析,生成解析树。

优化器根据解析器生成的解析树,生成查询计划。查询计划描述了如何执行查询,查询计划的优劣直接影响查询的执行效率。对于同样一条查询语句,一个好的查询执行效率比一个次好的查询计划快上100倍,也是一个很正常的事情。从PostgreSQL到MPP架构的Greenplum Database,优化器做了重大改动。虽然两者都是基于代价来生成最优的查询计划,但是Greenplum Database除了需要常规的表扫描代价、连接和聚合的执行方式外,还需要考虑数据的分布式状态、数据重分布的代价,以及集群计算节点数量对执行效率的影响,因为它最终是要生成一个分布式的查询计划。

调度器是Greenplum Database在PostgreSQL上新增的一个组件,负责分配处理查询需要的计算资源,将查询计划发送到每个计算节点。在Greenplum Database中,我们称计算节点为Segment节点。前面也提过,每一个Segment实例实际上就是一个PostgreSQL实例。调度器根据优化器生成的查询计划确定执行计划需要的计算资源,然后通过libpg(修改过的libpg协议)协议给每个Segment实例发送连接请求,通过Segment实例上的PostMaster进程fork出前面提到过的QE进程。调度器同时负责这些fork出来的QE进程的整个生命周期。

MPP数据库在执行查询语句的时候,跟单机数据库的一个重要差别在于,它会涉及到不同计算节点间的数据交换。在Greenplum Database系统架构中,我们引入了Interconnect组件负责数据交换,作用类似于MapReduce中的shuffling阶段。不过与MapReduce基于HTTP协议不一样,Greenplum Database出于数据传输效率和系统扩展性方面的考虑,实现了基于UDP协议的数据交换组件。前面在解析执行器的时候提到,Greenplum Database引入了一个叫Motion的操作节点,Motion操作节点就是通过Interconnect组件在不同的计算节点之间实现数据的重分布。

前面讲到的解析器、优化器、调度器、执行器和Interconnect都是跟计算相关的组件,属于无状态组件。下面我们再看一下跟系统状态相关的组件。首先是系统表。系统表负责存储和管理数据库、表、字段等元数据。主节点上面的系统表是全局数据库对象的元数据,称为全局系统表;每个Segment实例上也有一份本地数据库对象的元数据,称为本地系统表。解析器、优化器、调度器、执行器和Interconenct等无状态组件在运行过程中需要访问系统表信息,决定执行的逻辑。由于系统表分布式地存储在不同的节点中,如何保持系统表中信息的一致性是极具挑战的任务。一旦出现系统表不一致的情况,整个分布式数据库系统是无法正常工作的。

跟很多分布式系统一样,Greenplum Database是通过分布式事务来确保系统信息一致的,更确切地说,通过两阶段提交来确保系统元数据的一致性。主节点上的分布式事务管理器协调Segment节点上的提交和回滚操作。每个Segment实例有自己的事务日志,确定何时提交和回滚自己的事务。本地事务状态保存在本地的事务日志中。

介绍完Greenplum Database的查询组件和系统状态组件后,我们再看看它是如何提供高可用性的。首先是管理节点的高可用。我们采取的方式是,启动一个称为Standby的从主节点作为主节点的备份,通过同步进程同步主节点和Standby节点两者的事务日志,在Standby节点上重做系统表的更新操作,从而实现两者在全局系统表上面的信息同步。当主节点出故障的时候,我们能够切换到Standby节点,系统继续正常工作,从而实现管理节点的高可用。

计算节点高可用性的实现类似于管理节点,但是细节上有些小不同。每个Segment实例都会有另外一个Segment实例作为备份。处于正常工作状态的Segment实例我们称为Primary,它的备份称为Mirror。不同于管理节点日志重放方式,计算节点的高可用是通过文件复制。对于每一个Segment实例,它的状态以文件的形式保存在本地存储介质中。这些本地状态可以分成三大类:本地系统表、本地事务日志和本地表分区数据。通过以文件复制的方式保证Primary和Mirror之间的状态一致,我们能够实现计算节点的高可用。


HAWQ

Hadoop出现之前,MPP数据库是为数不多的大数据处理技术之一。随着Hadoop的兴起,特别是HDFS的成熟,越来越多的数据被保存在HDFS上面。一个自然的问题出现了:我们怎样才能高效地分析保存在HDFS上面的数据,挖掘其中的价值。4、5年前,SQL-on-Hadoop远没有现在这么火,市场上的解决方案也只有耶鲁大学团队做的Hadapt和Facebook做的Hive,像Impala,Drill,Presto,SparkSQL等都是后来才出现的。而Hadapt和Hive两个产品,在当时无论是易用性还是查询性能方面都差强人意。

我们当时的想法是将Greenplum Database跟HDFS结合起来。与其他基于connector连接器的方式不同,我们希望让HDFS,而不是本地存储,成为MPP数据库的数据持久层,这就是后来的Apache HAWQ项目。但在当时,我们把它叫做Greenplum on Hadoop,其实更准确的说法应该是,Greenplum on HDFS。当时的想法非常简单,就是将Greenplum Database和HDFS部署在同一个物理机器集群中,同时将Greenplum Database中的Append-only表的数据放到HDFS上面。Append-only表指的是只能追加,不能更新和删除的表,这是因为HDFS本身只能Append的属性决定的。

除了Append-only表之外,Greenplum Database还支持Heap表,这是一种能够支持增删改查的表类型。结合前面提到的Segment实例的本地状态,我们可以将本地存储分成四大类:系统表、日志、Append-only表分区数据和非Append-only表分区数据。我们将其中的Append-only表分区数据放到了HDFS上面。每个Segment实例对应一个HDFS的目录,非常直观。其它三类数据还是保存在本地的磁盘中。

总体上说,相对于传统的Greenplum Database,Greenplum on HDFS架构上并没有太多的改动,只是将一部分数据从本地存储放到了HDFS上面,但是每个Segment实例还是需要通过本地存储保存本地状态数据。所以从高可用性的角度看,我们还是需要为每个实例提供备份,只是需要备份的数据少了,因为Append-only表的数据现在我们是通过HDFS本身的高可用性提供的。

Greenplum on HDFS作为一个原型系统,验证了MPP数据库和HDFS是可以很好地整合起来工作的。基于这个原型系统,我们开始将它当成一个真正的产品来打造,也就是后来的HAWQ。

从Greenplum on HDFS到HAWQ,我们主要针对本地存储做了系统架构上的调整。我们希望将计算节点的本地状态彻底去掉。本地状态除了前面提到的系统表(系统表又可以细分成只读系统表(系统完成初始化后不会再发生更改的元数据,主要是数据库内置的数据类型和函数)和可写系统表(主要是通过DDL语句对元数据的修改,如创建新的数据库和表))、事务日志、Append-only表分区数据和非Append-only表分区数据,同时还有系统在执行查询过程中产生的临时数据,如外部排序时用到的临时文件。其中临时数据和本地只读系统表的数据都是不需要持久化的。我们需要考虑的是如何在Segment节点上面移除另外四类状态数据。

Append-only表分区数据前面已经提到过,交给HDFS处理。为了提高访问HDFS的效率,我们没有采用Hadoop自动的HDFS访问接口,而是用C++实现了原生的HDFS访问库,libhdfs3。针对非Append-only表数据的问题,我们的解决方案就比较简单粗暴了:通过修改DDL,我们彻底禁止用户创建Heap表,因为Heap表支持更新和删除。所以,从那时起到现在最新的Apache HAWQ,都只支持表数据的追加,不支持更新和删除。没有了表数据的更新和删除,分布式事务就变得非常简单了。通过为每个Append-only表文件对应的元数据增加一列,逻辑EoF,即有效的文件结尾。只要能够保证EoF的正确性,我们就能够保证事务的正确性。而且Append-only表文件的逻辑EoF信息是保存在主节点的全局系统表中的,它的正确性通过主节点的本地事务保证。为了清理Append-only表文件在追加新数据时事务abort造成的脏数据,我们实现了HDFS Truncate功能。

对于本地可写系统表,我们的做法是将Segment实例上面的本地可写系统表放到主节点的全局系统表中。这样主节点就拥有了全局唯一的一份系统表数据。查询执行过程中需要用到的系统元数据,我们通过Metadata Dispatch的方式和查询计划一起分发给每个Segment实例。

通过上述的一系列策略,我们彻底摆脱了Segment节点的本地状态,也就是实现了无状态Segment。整个系统的高可用性策略就简单了很多,而且也不需要再为Segment节点提供Mirror了,系统的利用率大大提升。

数据的高可用交给了HDFS来保证。当一个Segment节点出故障后,我们可以在任意一台有空闲资源的机器上重新创始化一个新的Segment节点,加入到集群中替代原来出故障的节点,整个集群就能够恢复正常工作。

我们也做到了计算和存储物理上的解耦合,往彻底摆脱传统MPP数据库(例如Greenplum Database)计算和存储紧耦合的目标迈出了有着实质意义的一步。

虽然在HAWQ 1.x的阶段,我们做到了计算和存储物理上的分离,但是逻辑上两者还是集成的。原因是,在将本地表分区数据往HDFS上面迁移的时候,为了不改变原来Segment实例的执行逻辑流程,我们为每个Segment指定了一个其专有的HDFS目录,以便跟原来本地数据目录一一对应。每个Segment负责存储和管理的数据都放在其对应的目录的底下,而且该目录底下的文件,也只有它自身能够访问。这种HDFS数据跟计算节点逻辑上的集成关系,使得HAWQ 1.x版本依然没有摆脱传统MPP数据库刚性的并发执行策略:无论查询的复杂度如何,所有的计算节点都需要参与到每条查询的执行中。这意味着,系统执行一条单行插入语句所使用的计算资源,和执行一条对几TB数据进行复杂多表连接和聚合的语句所使用的资源是一样的。这种刚性的并行执行策略,极大地约束了系统的扩展性和吞吐量,同时与Hadoop基于查询复杂度来调度计算资源的弹性策略也是相违背的。

我们决心对HAWQ的系统架构做一次大的调整,使其更加地Hadoop Native,Hadoop原生,而不仅仅是简单地将数据放到HDFS上面。当时,我们内部成为HAWQ 2.0,也就是大家现在在github上面看到的Apache HAWQ。

其中最重要的一步是,我们希望计算和存储不仅物理上分离,逻辑上也是分离。数据库中的用户表数据在HDFS上不再按照每个Segment单独来组织,而是按照全局的数据库对象来组织。举个例子,我们将一张用户表对应的多个数据文件(因为当往该表插入数据的时候,为了提高数据插入的速度,系统会启动了多个QE进程同时往HDFS写数据,每个QE写一个单独文件)放到同一个目录底下,而不是像原来那样,每个QE进程将文件写到自己对应的Segment目录底下。这种改变带来的一个直观结果就是,由于所有文件的数据文件都放一起了,查询执行的时候,根据需要扫描的数据量不同,我们既可以使用一个Segment实例去完成表扫描操作,也可以使用多个Segment实例去做,彻底摆脱了原来只能使用固定个Segment实例来执行查询的刚性并行执行策略。

当然,HDFS数据目录组织的改变只是实现HAWQ 2.0弹性执行引擎的一步,但是却是最重要的一步。计算和存储的彻底分离,使得HAWQ可以像MapReduce一样根据查询的复杂度灵活地调度计算资源,极大地提升了系统的扩展性和吞吐量。

本文节选自:PostgreSQL GreenPlum HAWQ三者的关系及演变过程
---------------------
作者:YYDU_666
原文:https://blog.csdn.net/yydu_666/article/details/80827202