目录
3.4.1 Greenplum还是SQL-on-Hadoop
Greenplum是一个分布式大规模并行处理数据库,在大多数情况下适合做大数据的存储引擎、计算引擎和分析引擎,尤其适合构建数据仓库。本篇重点介绍Greenplum的系统架构和主要功能。我们先从历史演进和所采用的MPP框架对Greenplum做一个概要说明,然后描述其顶层架构,之后详细介绍存储模式、事务支持、并行查询与数据装载、容错与故障转移、数据库统计、过程化语言扩展等方面的功能特性,正是它们支撑Greenplum成为一款理想的分析型数据库产品。本篇最后简单对比Greenplum与另一个流行的大数据处理框架Hadoop,进而阐述可以选择前者的理由。
希望读者通过阅读本篇的内容,对Greenplum的概念有一个基本的认识,最重要的是理解为什么要使用它建立数据仓库。
3.1 Greenplum简介
Greenplum是一个大规模并行SQL分析引擎,针对的是分析型应用。与其它关系型数据库类似,接收SQL,返回结果集。
3.1.1 历史与现状
Greenplum最早出现在2002年,比大名鼎鼎的Hadoop(约2004年前后面世)还要早一些。当时正值互联网行业经过近10年的由慢到快的发展,累积了大量数据。传统主机的向上扩展(Scale-up)模式在海量数据面前遇到了瓶颈,除造价昂贵外,在技术上也难于满足数据计算的性能需求。这种情况下急需一种新的计算方式处理数据,于是分布式存储和分布式计算理论被提出来,Google公司著名的GFS和MapReduce也从此引起业界的关注,可以支持向外扩展(Scale-out)的分布式并行数据计算技术登场了。Greenplum正是在这一背景下产生,它借助于分布式计算思想,在流行的开源数据库PostgreSQL之上开发,实现了基于数据库的分布式数据存储和并行计算。
Greenplum的名字据说源自创始人家门口的一棵青梅。初创公司召集了十几位业界大咖花了一年多的时间完成最初的版本设计和开发,用软件实现了在开放X86平台上的分布式并行计算,不依赖于任何专有硬件,达到的性能却远远超过传统高昂的专有系统。2006年,当时的Sun微系统公司与Greenplum开始联手打造即时数据仓库。2010年EMC收购了Greenplum,2012年EMC、VMWare和Greenplum又联手建新公司Pivotal,之后由Pivotal公司商业运营。
Greenplum于2015年10月开源,社区具有很高的知名度和热度,至今依然保持着几周发版的更新速度。在2020年Pivotal被兄弟公司VMWare收购,由VMWare继续运营商业产品,形成了商业VMware Tanzu Greenplum和开源Greenplum两条产品线。商业产品提供了比开源产品更多的功能,如与EMC DD Boost、Symantec NetBackup的整合,QuickLZ压缩算法,替代过时gpcheck的gpsupport实用程序等。
3.1.2 MPP——一切皆并行
Greenplum采用无共享(Shared-Nothing)的大规模并行处理架构(Massively Parallel Processing ,MPP),将实际的数据存储设备分成一个个段服务器上的小存储单元,每个单元都有一个连接本地磁盘的专用独立的、高带宽通道。段服务器可以通过完全并行的方式处理每个查询,同时使用所有磁盘连接,并按照查询计划的要求在各段间实现高效数据流动。Greenplum基于这种架构可以帮助客户创建数据仓库(Greenplum从开始设计的时候就被定义成数据仓库),充分利用低成本的商用服务器、存储和联网设备,通过经济的方式进行PB级数据运算,并且在处理OLAP、BI和数据挖掘等任务时性能远超通用数据库系统。
并行工作方式贯穿了Greenplum功能设计的方方面面:外部表数据装载是并行的,查询计划执行是并行的,索引的建立和使用是并行的,统计信息收集是并行的,表关联(包括其中的重分布或广播及关联计算)是并行的,排序和分组聚合都是并行的,备份恢复也是并行的,甚而数据库启停和元数据检查等维护工具也按照并行方式来设计。得益于这种无所不在的并行,Greenplum在数据装载和数据计算中表现出强悍的性能。
Greenplum建立在无共享架构上,让每一颗CPU和每一块磁盘I/O都运转起来,无共享架构将这种并行处理发挥到极致。试想一台内置16块SAS盘的X86服务器,磁盘扫描性能约在2000MB/s左右,20台这样的服务器构成的集群I/O性能是40GB/s,这样超大的I/O吞吐量是传统存储难以达到的。另外,Greenplum还是建立在PostgreSQL数据库实例级别上并行计算,可在一次SQL请求中利用到每个节点上多个CPU核的计算能力,对X86的CPU超线程有很好的支持,提供更好的请求响应速度。
3.2 Greenplum系统架构
Greenplum是一个纯软件的MPP数据库服务器,其体系结构专门用于管理大规模分析型数据仓库或商业智能工作负载。
技术上讲,MPP(也称为无共享体系结构)是指具有多个节点的系统,每个节点都有自己的内存、操作系统和磁盘,它们协作执行一项操作。Greenplum使用这种高性能系统架构分配PB级别的数据,并行使用系统的所有资源来处理查询。
Greenplum 6版本基于PostgreSQL 9.4开源数据库,本质上是若干面向磁盘的PostgreSQL数据库实例,共同作为一个内聚的数据库管理系统(database management system,DBMS)。大多数情况下,在SQL支持、功能、配置选项和最终用户功能方面与PostgreSQL非常相似。用户操作Greenplum数据库就像与常规PostgreSQL交互一样。
Greenplum与PostgreSQL的主要区别为:
- 除了支持Postgres优化器外,还有自己的GPORCA优化器。
- Greenplum数据库可以使用Append-Optimized存储格式。
- Greenplum支持列存储,即逻辑上组织为表的数据,物理上以面向列的格式存储的行和列。列存储只能与Append-Optimized表一起使用。
Greenplum对PostgreSQL的内部结构进行了修改或补充,以支持数据库的并行结构。例如,系统目录、优化器、查询执行器和事务管理器组件做过修改和增强,能够在所有并行PostgreSQL数据库实例上同时运行查询。Greenplum依赖Interconnect内部互连在网络层支持不同PostgreSQL实例之间的通信,使得系统作为一个逻辑数据库运行。
较之标准PostgreSQL,Greenplum还增加了并行数据装载(外部表)、资源管理、查询优化和存储增强功能。反之Greenplum开发的许多功能和优化也进入了PostgreSQL社区,促进了PostgreSQL的发展。例如,表分区是Greenplum首先开发的一个特性,现在已成为标准PostgreSQL的一部分。
Greenplum顶层系统架构如图3-1所示。Master是Greenplum数据库系统的入口,是客户端连接并提交SQL语句的数据库实例。Master将其工作与系统中其它叫做Segment的数据库实例进行协调,这些数据库实例负责实际存储和处理用户数据。
图3-1。Greenplum顶层系统架构
3.2.1 Master
Master是Greenplum的系统入口,它接收客户端连接和SQL查询,并将工作分配给Segment实例。最终用户通过Master与Greenplum数据库交互,就像与典型PostgreSQL数据库交互一样。用户使用诸如psql之类的客户端程序或JDBC、ODBC或libpq(PostgreSQL C API)之类的应用程序编程接口(application programming interface,API)连接到数据库。
Master数据库实例中存储全局系统目录(global system catalog)。全局系统目录是一组系统表,其中包含关于Greenplum本身的元数据。Master实例中不包含任何用户数据,用户数据仅驻留在Segment实例中。Master验证客户端连接,处理传入的SQL命令,在Segment之间分配工作负载,协调每个Segment返回的结果,并将最终结果显示给客户端程序。
Greenplum数据库使用写前日志(Write-Ahead Logging,WAL)进行主/备用Master镜像。在基于WAL的日志记录中,所有修改都会在应用之前写入日志,以确保任何进程内操作的数据完整性。
3.2.2 Segment
Greenplum的Segment实例是独立的PostgreSQL数据库,每个数据库存储一部分数据并执行一部分查询处理。当用户通过Master连接到数据库并发出查询时,将在每个Segment数据库中创建进程以处理该查询的工作。有关查询过程的更多信息,参见3.3.3小节。
用户定义的表及其索引分布在所有可用的Segment中,每个Segment都包含互斥的部分数据(复制表除外,这种表会在每个Segment实例上存储一份完整的数据拷贝)。提供服务的数据库服务器进程在相应的Segment实例下运行。
Segment在称为段主机的服务器上运行。段主机通常运行2到8个Segment实例,具体数量取决于CPU核、内存、磁盘、网卡和工作负载。所有段主机的配置应该相同,以避免木桶效应。从Greenplum获得最佳性能的关键是将数据和负载均匀分布到多个能力相同的Segment上,以便所有Segment同时处理任务并同时完成其工作。
3.2.3 Interconnect
Interconnect内部互连是Greenplum数据库体系结构中的核心组件,互连指的是Segment在网络间的进程间通信。Interconnect使用标准以太网交换数据,出于性能原因,建议使用万兆网或更快的系统。
默认情况下,Interconnect使用带有流量控制的用户数据报协议(User Datagram Protocol with flow control,UDPIFC)进行通信,通过网络发送消息。Greenplum软件执行超出UDP提供的数据包验证,这意味着可靠性相当于传输控制协议(Transmission Control Protocol,TCP),性能和可扩展性超过TCP。如果将Interconnect改为TCP,Greenplum数据库的可伸缩性限制为1000个Segment实例,UDPIFC作为Interconnect的默认协议不受此限制。
Interconnect实现了对同一个集群中多个PostgreSQL实例的高效协同和并行计算,承载了并行查询计划生产和派遣分发(Query Dispatch,QD)、协调节点上查询执行器(Query Executor,QE)的并行工作,负责数据分布、Pipeline计算、镜像复制、健康探测等等诸多任务。
3.3 Greenplum功能特性
Greenplum绝不仅仅只是“PostgreSQL + Interconnect并行调度 + 分布式事务”这么简单,它还提供了许多高级数据分析管理功能和企业级管理模块。本节介绍其中几个重要的特色功能。
3.3.1 存储模式
Greenplum提供了几种灵活的存储模式,创建表时可以选择如何存储其数据。建表时通过定义本小节介绍的存储选项,为工作负载选择最佳存储模式。为了简化建表时定义存储模式,可以通过gp_default_storage_options参数设置缺省的存储选项。
1. Heap存储
Greenplum默认使用与PostgreSQL相同的堆(Heap)存储模型。堆表适用于OLTP类型的工作负载,在这种工作负载中,数据通常在最初装载后进行修改。update和delete操作需要存储行级别的版本控制信息以确保数据库事务处理的可靠性。堆存储适合小表,例如维度表,这些表通常在初始装载后更新。
行存对表是缺省的存储模式,建表时不需要额外语法:
-- 建表
create table foo (a int, b text) distributed by (a);
-- 查看表信息
\d foo
Table "public.foo"
Column | Type | Modifiers
--------+---------+-----------
a | integer |
b | text |
Distributed by: (a)
Greenplum在6版本中引入了全局死锁检测的新概念,以降低update和delete的锁级别。6以前的版本中,update和delete操作使用表级排它锁,也就是说,在6之前的版本,一张表上同时只能有一个update或者delete语句被执行,其它的update或delete语句需要等待前面的语句执行完成之后才获得所需要的锁。
从6版本开始,打开全局死锁检后,堆存储表update和delete操作的锁将降低为行级排它锁。允许并发更新。全局死锁检测确定是否存在死锁,并通过取消一个或多个与最年轻事务相关联的后端进程来消除死锁。全局死锁检测由gp_enable_global_deadlock_detector参数控制,缺省为off:
$ gpconfig -s gp_enable_global_deadlock_detector
Values on all segments are consistent
GUC : gp_enable_global_deadlock_detector
Master value: off
Segment value: off
$ gpconfig -c gp_enable_global_deadlock_detector -v on
$ gpstop -arf
$ gpconfig -s gp_enable_global_deadlock_detector
Values on all segments are consistent
GUC : gp_enable_global_deadlock_detector
Master value: on
Segment value: on
另外,如果要进行高并发insert、update、delete操作,建议关闭log_statement参数(缺省为all),因为过多的日志输出也会影响这种操作的性能。
2. Append-Optimized存储
Append-Optimized存储表(后简称AO表)适合于数据仓库环境中非规范化的事实表。事实表通常分批加载,并通过只读查询进行访问,是系统中最大的表。将大型事实表采用AO存储可消除维护行级更新的多版本控制存储开销,每行可节省约20字节,这使得存储页面结构更精简、更易于优化。而且AO表一般还会选择压缩选项,可以大大节省存储空间。AO存储模型针对批量数据装载进行了优化,不建议使用单行insert语句。 通过create table的with子句定义存储选项,缺省不指定with子句时,创建的是行存堆表(如果设置了gp_default_storage_options参数,存储模式与该参数的设置一致)。下面是一个创建不带压缩选项的AO表的例子:
-- 建表
create table bar (a int, b text) with (appendoptimized=true) distributed by (a);
-- 查看表信息
\d bar
Append-Only Table "public.bar"
Column | Type | Modifiers
--------+---------+-----------
a | integer |
b | text |
Compression Type: None
Compression Level: 0
Block Size: 32768
Checksum: t
Distributed by: (a)
appendoptimized是以前appendonly的别称,在系统表中仍然存储appendonly关键字,显示存储信息时也将显示appendonly。在可重复读或串行化隔离级事务中,不允许对AO表进行update或delete。cluster、declare ... for update不适用于AO表。
3. 选择行存或列存
Greenplum支持在create table时选择行存或列存,或者在分区表中为不同分区做不同选择,具体情况需要根据业务场景进行确切评估。建议绝大部分情况下选择行存,因为现在的列存技术容易导致文件数严重膨胀,后果更为严重。
从一般角度说,行存具有更广泛的适用性,列存对于一些特定的业务场景可以节省大量I/O资源以提升性能,也可以提供更好的压缩效果。在考虑行存还是列存时可参考如下几点:
- 数据更新:如果一张表在数据装载后有频繁的更新操作,则选择行存堆表。列存表必须是AO表,所以没有别的选择。
- insert频率:如果有频繁的insert操作,那么就选择行存表。列存表不擅长频繁地insert操作,因为列存表在物理存储上每一个字段都对应一个文件,频繁地insert操作将需要每次都写很多个文件。
- 查询涉及的列数:如果在select列表或where条件中经常涉及很多字段,选择行存表。列存表对于大数据量的单字段聚合查询表现更好,如:
select sum(salary) ...
select avg(salary) where salary > 10000
或者在where条件中使用单独字段进行条件过滤且返回相对少量的记录数,如:
select salary, dept ... where state='ca'
- 表中列数:当需要同时查询许多列,或者当表的行大小相对较小时,行存效率更高。对于列很多,但只查询很少列时,列存表提供更好的查询性能。
- 压缩:列存表将具有相同的数据类型列数据连续存储在一起,因此对于相同的数据和压缩选项,往往列存的压缩效果更好,而行存无法具备这种优势。当然,越好的压缩效果意味着越困难的随机访问,因为数据读取都需要解压缩。不过6版本引入的ZSTD压缩算法具有非常优秀的压缩/解压缩效率。
在create table时使用with子句指定表的存储模式,缺省使用行存堆表。列存表必须是AO表。下面语句创建一个不带压缩的列存表:
-- 建表
create table bar (a int, b text) with (appendoptimized=true, orientation=column) distributed by (a);
-- 查看表信息
\d bar
Append-Only Columnar Table "public.bar"
Column | Type | Modifiers
--------+---------+-----------
a | integer |
b | text |
Checksum: t
Distributed by: (a)
4. 使用压缩(必须是AO表)
AO表的压缩可以作用于整个表,也可以压缩特定列,可以对不同的列应用不同的压缩算法。表3-1总结了可用的压缩算法。
行或列 | 可用压缩类型 | 支持的压缩算法 |
---|---|---|
行 | 表级 | ZLIB, ZSTD, and QUICKLZ(开源版本不可用) |
列 | 列级或表级 | RLE_TYPE, ZLIB, ZSTD, and QUICKLZ(开源版本不可用) |
表3-1 AO表压缩算法
选择AO表的压缩类型和级别时,需要考虑以下因素:
- CPU性能:Segment主机需要有足够的CPU资源进行压缩和解压缩。
- 压缩比/磁盘大小:最小化磁盘大小是一个因素,但也要考虑压缩和扫描数据所需的时间和CPU资源。我们需要找到有效压缩数据的最佳设置,而不会导致过长的压缩时间或较慢的扫描速度。
- 压缩速度:QuickLZ压缩通常使用较少的CPU资源,比zlib压缩速度快,但压缩率低。zlib压缩率高,但压缩速度慢。例如,在压缩级别1(compresslevel=1)下,QuickLZ和zlib具有相当的压缩率,尽管速度不同。与QuickLZ相比,使用压缩级别为6的zlib可以显著提高压缩率,但压缩速度较低。Zstandard compression则可以提供良好的压缩率或速度。
- 解压缩/扫描速度。压缩AO表的性能不仅取决于压缩选项,还与硬件、查询优化设置等因素有关。应该进行比较测试以确定合适的压缩选项。
不要在使用压缩的文件系统上创建压缩AO表,这样做只会来带额外的CPU开销。下面语句创建压缩级别为5的zlib压缩的AO表:
-- 建表
create table foo (a int, b text) with (appendoptimized=true, compresstype=zlib, compresslevel=5);
-- 查看表信息
\d foo
Append-Only Table "public.foo"
Column | Type | Modifiers
--------+---------+-----------
a | integer |
b | text |
Compression Type: zlib
Compression Level: 5
Block Size: 32768
Checksum: t
Distributed by: (a)
5. 检查AO表的压缩与分布情况
Greenplum提供了内置函数用以检查AO表的压缩率和分布情况。这两个函数可以使用对象ID或表名作为参数,表名可能需要带模式名,如表3-2所示。
函数 | 返回类型 | 描述 |
---|---|---|
get_ao_distribution(name) get_ao_distribution(oid) | 集合类型(dbid, tuplecount) | 展示AO表的分布情况,每行对应segid和记录数。 |
get_ao_compression_ratio(name) get_ao_compression_ratio(oid) | float8 | 计算AO表的压缩率。如果该信息未得到,将返回-1。 |
表3-2 压缩AO表元数据函数
压缩率得到的是一个常见的比值类型。例如,返回值3.19或3.19:1表示未压缩表的大小略大于压缩表大小的三倍。表的分布作为一组行返回,指示每个Segment上存储该表的记录数。例如,在一个有着四个Segment的系统上,dbid范围为0-3,函数返回类似下面的结果集:
=# select get_ao_distribution('lineitem_comp');
get_ao_distribution
---------------------
(0,7500721)
(1,7501365)
(2,7499978)
(3,7497731)
(4 rows)
3.3.2 事务与并发控制
数据库管理系统中的并发控制机制使并发查询都返回正确的结果,同时确保数据完整性。传统数据库的分布式事务使用两阶段锁协议,防止一个事务修改另一个并发事务读取的数据,并防止任何并发事务读取或写入另一个事务更新的数据,即读写相互阻塞。协调事务所需的锁会增加数据库争用,因而降低总体事务吞吐量。
Greenplum沿用PostgreSQL多版本并发控制(Multiversion Concurrency Control,MVCC)模型来管理堆表的并发分布式事务。使用MVCC,每个查询都会取得一个查询启动时的数据库快照。查询在运行时无法看到其它并发事务所做的更改。这可以确保查询所看到的是数据库一致性视图。读取行的查询永远不会阻塞写入行的事务,写入行的查询不会被读取行的事务阻塞。与传统的使用锁来协调读写数据事务之间的访问相比,MVCC允许更大的并发性。AO表使用的并发控制模型与这里讨论的MVCC模型不同,它们适用于“一次写入,多次读取”的应用程序,这类应用从不或很少执行行级更新。
1. 快照
快照是在语句或事务开始时可见的一个行集,可确保查询在执行期间具有一致且有效的数据视图。一个新事务开始时被分配一个唯一的事务ID(XID),它是一个递增的32位整数。未包含在事务中的SQL语句被视为单语句事务,BEGIN和COMMIT被隐式添加,效果类似于某些数据库系统(如MySQL)中的自动提交。Greenplum仅将XID值分配给涉及DDL或DML操作的事务,这些事务通常是唯一需要XID的事务。
当事务插入一行时,XID与该行一起保存在xmin系统列中。当事务删除一行时,XID保存在xmax 系统列中。更新一行被视为先删除再插入,因此XID保存到已删除行的xmax和新插入行的xmin。xmin和xmax列以及事务完成状态所确定的一系列事务,其中的行版本对当前事务是可见的。一个事务可以看到小于xmin的所有事务的执行结果(保证已提交),但不能看到任何大于或等于xmax的事务结果(未提交)。
对于多语句事务,还必须标识事务中插入行或删除行的命令,以便可以看到当前事务中前面语句所做的更改。cmin系统列标识事务中的插入命令,cmax系统列标识事务中的删除命令。命令标识仅在事务期间起作用,因此在事务开始时将该值将重新从0开始累加。cmin和cmax用于判断同一个事务内的其它命令导致的行版本变更是否可见。
XID是数据库实例的一个属性。每个Segment实例都有自己的XID序列,无法与其它Segment的XID进行比较。主机使用集群范围的会话ID号(称为gp_session_ID)与Segment协调分布式事务,Segment维护分布式事务ID与其本地XID的映射。Master使用两阶段提交协议在所有Segment之间协调分布式事务。如果事务在任何一个Segment上执行失败,它将在所有Segment上回滚。
可以通过select语句查看任意行的xmin、xmax、cmin和cmax系统列:
select xmin, xmax, cmin, cmax, * from tablename;
在Master上执行的查询返回的XID是分布式事务ID。如果在单个Segment实例中运行该查询,那么xmin和xmax值将是该Segment的本地事务ID。
Greenplum将复制表(replicated table)的所有行分布到每个Segment,因此每一行在每个Segment上都是重复的。每个Segment实例维护自己的xmin、xmax、cmin和cmax以及gp_segment_id和ctid系统列值。Greenplum不允许用户查询从Master访问复制表的这些系统列(将会得到一个字段不存在的错误信息),因为它们没有明确的单一值。
2. 事务ID回卷
如前所述,MVCC模型使用事务ID(XID)来确定在查询或事务开始时哪些行可见。XID是一个32位的整数,因此理论上Greenplum最大可以运行大约42亿个事务,之后XID将回卷重置。Greenplum对XID使用模2的32次方的计算方式,这允许事务ID循环使用。对于任何给定的XID,过去的XID大约有20亿,未来的XID大约有20亿。这里有个问题,当一行的版本持续存在了大约20亿个事务后,再循环使用时,该行的XID又从头开始计数,使它突然看似为一个新行。为了防止这种情况,Greenplum有一种称为Frozen XID的特殊XID,它比任何常规XID都要老。某一行的xmin必须要在20亿次事务内替换为Frozen XID,这也是VACUUM命令执行的功能之一。
每隔20亿个事务对数据库进行至少一次清理,就可以防止XID回卷。Greenplum数据库监视事务ID,并且在需要一次VACUUM操作时发出警告。当事务ID大部分不再可用,且在事务ID发生回卷之前,将发出警告:
WARNING: database "database_name" must be vacuumed within number_of_transactions transactions
发出警告时就需要一次VACUUM操作。如果没有执行所需的VACUUM操作,Greenplum在事务ID发生回卷前且达到一个限度时,会停止创建新事务以避免可能的数据丢失,并发出以下错误:
FATAL: database is not accepting commands to avoid wraparound data loss in database "database_name"
有关从此错误中恢复的过程,参阅Routine System Maintenance Tasks | Greenplum Docs。
服务器配置参数xid_warn_limit和 xid_stop_limit控制何时显示这些警告和错误。xid_warn_limit参数指定在xid_stop_limit之前多少个事务ID时发出警告。xid_stop_limit参数指定在回卷发生之前多少个事务ID时发出错误并且不再允许创建新事务。
3. 事务隔离模式
SQL标准描述了数据库事务并发运行时可能出现的三种现象:
- 脏读:一个事务可以从另一个并发事务中读取未提交的数据。
- 不可重复读:一个事务两次读取同一行得到不同的结果,因为另一个并发事务在这个事务开始后提交了更改。
- 幻读:在同一事务中执行两次查询可以返回两组不同的行,因为另一个并发事务添加了行。
SQL标准定义了数据库系统可以支持的四个事务隔离级别,以及每个级别下并发执行事务时所允许的现象,如表3-3所示。
隔离级 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommitted | 可能 | 可能 | 可能 |
Read Committed | 不可能 | 可能 | 可能 |
Repeatable Read | 不可能 | 不可能 | 可能 |
Serializable | 不可能 | 不可能 | 不可能 |
表3-3 SQL事务隔离模式
Greenplum缺省的事务隔离级为read committed,由default_transaction_isolation参数指定。Greenplum的read uncommitted和read committed隔离模式的行为类似于SQL标准的read committed模式,serializable和repeatable read隔离模式的行为类似于SQL标准的repeatable read模式,只是还防止了幻读。
read committed和repeatable read之间的区别在于,前者事务中的每个语句只能看到在语句启动之前提交的行,而后者事务中的语句只能看到在事务启动之前提交的行。
在read committed隔离模式下,如果另一个并发事务自事务开始以来已提交更改,则在事务中检索两次的行中的值可能不同。read committed模式还允许幻读,在同一事务中运行两次查询可以返回两组不同的行。
Greenplum的repeatable read隔离模式可避免不可重复读和幻读。试图修改由另一个并发事务修改的数据的事务将被回滚。如果应用程序不需要repeatable read隔离模式,则最好使用read committed模式以提高并发。
Greenplum不保证并发运行的一组事务产生与串行化顺序执行相同的结果。若指定了serializable隔离级,Greenplum数据库将返回到可重复读。
对于Greenplum的并发事务,应检查并识别可能并发更新相同数据的事务。对识别出来的问题,可以通过使用显式的表锁,或要求冲突的事务更新一个虚行(该虚行表示冲突),来防止该问题发生。SQL语句SET TRANSACTION ISOLATION LEVEL可以设置当前事务的隔离模式。必须要在执行SELECT、INSERT、DELETE、UPDATE或COPY语句前设置:
begin;
set transaction isolation level repeatable read;
...
commit;
隔离模式也可以指定为BEGIN语句的一部分:
begin transaction isolation level repeatable read;
4. 删除过期行
更新或删除行会在表中保留该行的过期版本,当过期的行不再被任何活动事务引用时,可以删除该行并重新使用它占用的空间。VACUUM命令将过期行使用的空间标记为可重用。
当表中过期的行累积后,必须扩展磁盘文件以容纳新行。由于运行查询所需的磁盘I/O增加,性能会下降,这种情况称为膨胀(bloat),应该通过定期清理表来进行管理。
不带FULL的VACUUM 命令可以与其它查询同时运行。它会标记之前被过期行所占用的空间为空闲,并更新空闲空间映射。当Greenplum之后需要空间分配给新行时,它首先会查询该表的空闲空间映射,寻找有可用空间的页面。如果没有找到这样的页面,会为该文件追加新的页面。
不带FULL的VACUUM不会合并页面或者减小表在磁盘上的尺寸。它回收的空间只是放在空闲空间映射中表示可用。为了防止磁盘文件大小增长,经常运行VACUUM非常重要。运行VACUUM的频率取决于表中更新和删除(插入只会增加新行)的频率。大量更新的表可能每天需要运行几次VACUUM,以确保通过空闲空间映射能找到可用的空闲空间。在一个更新或者删除大量行的事务之后运行VACUUM也非常重要。
VACUUM FULL命令会把表重写为没有过期行,并且将表减小到其最小尺寸。表中的每一页面都会被检查,其中的可见行被移动到前面还没有完全填满的页面中,空页面会被丢弃。该表会被一直锁住直到VACUUM FULL完成。相对于常规的VACUUM命令来说,它是一种非常昂贵的操作,可以用定期的清理来避免或者推迟这种操作。最好是在一个维护期来运行VACUUM FULL。VACUUM FULL的一种替代方案是,用一个CREATE TABLE AS语句重新创建表并且删除掉旧表。
可以运行VACUUM VERBOSE tablename来得到一份Segment上已移除的过期行数量、受影响页面数以及可用空闲空间页面数的报告。查询pg_class系统表可以找出一个表在所有Segment上使用了多少页面。注意应先对该表执行ANALYZE确保得到的是准确的数据。
select relname, relpages, reltuples from pg_class where relname='tablename';
另一个有用的工具是gp_toolkit模式中的gp_bloat_diag视图,它通过比较表使用的实际页数与预期页数来鉴别表膨胀。
5. 管理事务ID示例
下面看一个Greenplum官方文档中提供的示例。这个简单的例子说明了MVCC的概念以及它如何使用事务ID管理数据和事务,展示的概念如下:
- 如何使用事务ID管理表上的多个并发事务。
- 如何使用Frozen XID管理事务ID。
- 模计算如何根据事务ID确定事务的顺序。
示例表假设如下:
- 该表是一个包含2列和4行数据的简单表。
- 有效的事务ID(XID)值从0到9,9之后,XID将在0处重新启动。
- Frozen XID为-2(与Greenplum数据库不同)。
- 事务在一行上执行。
- 仅执行插入和更新操作。
- 所有更新的行都保留在磁盘上,不执行删除过期行的操作。
表的初始数据如表3-4所示,xmin的顺序即为行插入的顺序。
item | amount | xmin | xmax |
---|---|---|---|
widget | 100 | 0 | null |
giblet | 200 | 1 | null |
sprocket | 300 | 2 | null |
gizmo | 400 | 3 | null |
表3-4 初始示例表
表3-5显示了对金额列执行如下更新后的表数据。
- xid = 4: update tbl set amount=208 where item = 'widget'
- xid = 5: update tbl set amount=133 where item = 'sprocket'
- xid = 6: update tbl set amount=16 where item = 'widget'
粗体表示当前行,其它是过期行。可以通过xmax为null值条件确定表的当前行(Greenplum使用了稍微不同的方法来确定当前表行)。
item | amount | xmin | xmax |
---|---|---|---|
widget | 100 | 0 | 4 |
giblet | 200 | 1 | null |
sprocket | 300 | 2 | 5 |
gizmo | 400 | 3 | null |
widget | 208 | 4 | 6 |
sprocket | 133 | 5 | null |
widget | 16 | 6 | null |
表3-5 更新示例表
MVCC使用XID值确定表的状态。例如下面两个独立事务同时运行。在UPDATE事务期间,查询返回300,直到UPDATE事务完成。
- UPDATE命令将sprocket数量值更改为133(xmin值5)。
- SELECT命令返回sprocket的值。
对于这个简单的示例,数据库的可用XID值即将用完。当Greenplum即将用完可用的XID值时将执行以下操作:
- 发出警告,指出数据库的XID值即将用完。
- 在分配最后一个XID之前,Greenplum停止接收事务,以防止两个事务分配同一XID值,并发出严重告警。
为了管理存储在磁盘上的事务ID和表数据,Greenplum提供了VACUUM命令。
- VACUUM操作释放XID值,以便通过将xmin值更改为Frozen XID,使表可以有10行以上。
- VACUUM命令更改XID值为obsolete以指示过期行。Greenplum中不带FULL的VACUUM操作会适时删除磁盘上的行,并且对性能和数据可用性影响最小。
表3-6显示在示例表上执行VACUUM操作后的情况,该命令更新了磁盘上的表数据。这里显示执行方式与Greenplum中的VACUUM命令略有不同,但概念相同。
- 对于磁盘上不再是当前的widget行和sprocket行标记为过时。
- 对于当前的giblet和gizmo行,xmin已更改为Frozen XID,这些值仍然是当前表值(行的xmax值为null)。这些表行对所有事务都可见,因为当执行模计算时,xmin值是Frozen XID,比所有其它XID值都小。
VACUUM操作后,XID值0、1、2和3可供使用。
item | amount | xmin | xmax |
---|---|---|---|
widget | 100 | obsolete | obsolete |
giblet | 200 | -2 | null |
sprocket | 300 | obsolete | obsolete |
gizmo | 400 | -2 | null |
widget | 208 | 4 | 6 |
sprocket | 133 | 5 | null |
widget | 16 | 6 | null |
表3-6 VACUUM后的示例表
当更新xmin值为-2的磁盘行时,xmax值会像往常一样替换为事务XID,并且在访问该行的任何并发事务完成后,磁盘上的行将被视为过期,可以从磁盘删除过期行。对于Greenplum数据库,带有FULL选项的VACUUM命令执行更广泛的处理以回收磁盘空间。
表3-7显示了更多更新事务后磁盘上的表数据。XID值已回卷并在0处重新开始,没有进行额外的VACUUM操作。
item | amount | xmin | xmax |
---|---|---|---|
widget | 100 | obsolete | obsolete |
giblet | 200 | -2 | 1 |
sprocket | 300 | obsolete | obsolete |
gizmo | 400 | -2 | 9 |
widget | 208 | 4 | 6 |
sprocket | 133 | 5 | null |
widget | 16 | 6 | 7 |
widget | 222 | 7 | null |
giblet | 233 | 8 | 0 |
gizmo | 18 | 9 | null |
giblet | 88 | 0 | 1 |
giblet | 44 | 1 | null |
表3-7 回卷XID的示例表
3.3.3 并行查询
理解Greenplum的查询处理过程有助于写出更加优化的查询语句。与任何其它数据库管理系统类似,Greenplum有如下查询执行步骤:
- 用户使用客户端程序(如psql)连接到Greenplum Master主机上的数据库实例,并向系统提交SQL语句。
- Master接收到查询后,由查询编译器解析提交的SQL语句,并将生成的查询解析树递交给查询优化器。
- 查询优化器根据查询所需的磁盘I/O、网络流量等成本信息,生成它认为最优的执行计划,并将查询计划交给查询分发器。
- 查询分发器向Segment分发查询计划。
- 查询执行器并行执行查询,将结果传回至Master,最后Master向客户端返回查询结果。
1. 计划分发
Master负责接收、解析和优化查询,它将并行查询计划分发给所有Segment,如图3-2所示。
图3-2 分发并行查询计划
每个Segment负责在其自己的数据集上执行本地数据库操作。大多数数据库操作(如表扫描、连接、聚合和排序等)在所有Segment上并行,每个操作都在一个Segment数据库实例上执行,与存储在其它Segment中的数据无关。
某些查询可能仅访问单个数据Segment上的数据,例如单行插入、更新、删除或对表分布键列进行过滤的查询。在此类查询中,查询计划不会分发给所有Segment,而是只发给包含受影响行的Segment,如图3-3所示。
图3-3 分发目标查询计划
2. 查询计划
一个查询计划是Greenplum为了产生查询结果而要执行的一系列操作。查询计划中的每个节点或步骤,表示一个数据库操作。查询计划被由底向上读取和执行。
除了通常的扫描、连接、聚合、排序等数据库操作,Greenplum还有一种叫做motion的操作类型。查询处理期间,motion操作通过内部互联网络在Segment实例间移动数据。并不是每个查询都需要motion操作。为了实现查询执行的最大并行度,Greenplum将查询计划分成多个slice,每个slice可以在Segment上独立执行。查询计划中的motion操作总是分片的,迁移数据的源和目标上各有一个slice。
下面的查询连接两个数据库表:
select customer, amount
from sales join customer using (cust_id)
where datecol = '04-30-2016';
图3-4显示了为该查询生成的三个slice。每个Segment接收一份查询计划的拷贝,查询计划在多个Segment上并行工作。
图3-4 查询计划分片
注意slice 1中的redistribute motion操作,它在Segment间移动数据以完成表连接。假设customer表通过cust_id字段在Segment上分布,而sales表通过sale_id字段分布。为了连接两个表,sales的数据必须通过cust_id重新分布。因此查询计划在每个分片上各有一个redistribute motion操作。
在这个执行计划中还有一种叫做gather motion的motion操作。当Segment将查询结果发送回Master,用于向客户端展示时,会使用gather motion。因为查询计划中发生motion的部分总是被分片,所以在图3-4的顶部还有一个隐含的slice 3。并不是所有查询计划都包含gather motion,例如,CREATE TABLE x AS SELECT ... 语句就没有gather motion操作,因为结果数据被发送到新表而不是Master。
3. 并行查询
Greenplum会创建许多数据库进程处理一个查询。Master和Segment上的查询工作进程分别被称为查询分发器(query dispatcher,QD)和查询执行器(query executor,QE)。QD负责创建和分发查询计划,并返回最终的查询结果。QE在Segment中完成实际的查询工作,并与其它其它工作进程互通中间结果。
查询计划的每个slice至少需要一个工作进程。工作进程独立完成被赋予的部分查询计划。一个查询执行时,每个Segment中有多个并行执行的工作进程。工作在不同Segment中的相同slice构成一个gang。查询计划被从下往上执行,一个gang的中间结果数据向上流向下一个gang。不同Segment的进程间通信是由Greenplum的内部互联组件Interconnect完成的。
图3-5显示了示例查询中Master和Segment上的工作进程,查询计划分成了三个slice,两个Segment上的相同slice构成了gang。
图3-5 查询工作进程
4. GPORCA查询优化器
Greenplum缺省使用的查询优化器是GPORCA,但遗留的老优化器与GPORCA并存。GPORCA在处理分区表查询、子查询、通用表表达式、INSERT语句、去重聚合等方面做了增强和改进。Greenplum尽可能使用GPORCA生成查询的执行计划,当GPORCA没有启用或无法使用时,Greenplum用老的查询优化器生成执行计划。可以通过EXPLAIN命令的输出确定查询使用的是哪种优化器。GPORCA会忽略与老优化器相关的服务器配置参数,但当查询使用老优化器时,这些参数仍然影响查询计划的生成。相对于老优化器,GPORCA在多核环境中的优化能力更强,并且在分区表查询、子查询、连接、排序等操作上提升了性能。图3-6显示了Greenplum查询优化器。
图3-6 Greenplum查询优化器
3.3.4 并行数据装载
在大型数据仓库中,必须在相对较小的维护窗口内装载大量数据。Greenplum通过其外部表功能支持快速并行数据装载。用户还可以在单行错误隔离模式下装载外部表,以便在继续装载格式正确的行的同时将坏行过滤到单独的错误日志中。可以为装载操作指定错误阈值,以控制导致Greenplum取消装载操作的错误行数。通过将外部表与Greenplum的并行文件服务器(gpfdist)结合使用,可以从Greenplum系统获得最大的并行性和吞吐量,如图3-7所示。
图3-7 使用gpfdist的外部表
gpfdist是Greenplum提供的一种文件服务器,提供了良好的性能并且非常容易运行。gpfdist利用Greenplum系统中的所有Segment读写外部表。gp_external_max_segs服务器配置参数控制可被单一gpfdist实例同时使用Segment的数量,缺省值为64。
另一个Greenplum实用程序gpload运行在YAML格式的控制文件中指定的装载任务,可以在控制文件中描述源数据位置、格式、所需转换、参与主机、目标数据库以及其它详细信息。可通过这种方式定义一个复杂的任务,并以可控、可重复的方式运行。
本专栏后面的“Greenplum运维与监控”一篇中将详细介绍gpfdist和gpload技术,并给出具体示例。
3.3.5 冗余与故障转移
Greenplum可以配置高可用,以使得数据库集群更可靠地运行。如果不能接受数据丢失,Greenplum要求Mater和Segment实例都必须开启高可用配置。也就是说,高可用配置不仅仅是可靠性的保证,也是数据安全的保证。Greenplum支持为Master配置Standby,为Segment配置Mirror,以确保数据库中的每个角色备份,而不会出现单点故障。
1. Segment镜像
部署Greenplum系统时,可以选择配置Mirror Segment实例,Mirror允许数据库查询在Primary Segment不可用时自动切换到Mirror上。强烈建议在生产系统上配置Mirror。Mirror由事务日志复制过程保持最新,该过程同步Primary实例和Mirror实例之间的数据。Primary向Mirror同步数据的时候,Primary对于每一次写数据页都会通过消息发送到Mirror。如果Primary无法向其Mirror发送数据,Primary将数据放入队列,超过gp_segment_connect_timeout(缺省10分钟)后认为Mirror故障,这将导致将对应的Mirror标记为down,对应的Primary则变为更改跟踪模式。
Mirror和Primary实例必须始终位于不同的主机上,以防止单个主机出现故障。在Greenplum初始化或扩容时,有两种可用的标准Mirror配置。默认配置为组镜像(group mirroring),将一台主机上所有Primary Segment的Mirror放置在群集中的另一台主机上,如图3-8所示。
图3-8 组镜像
另一种标准配置是扩展镜像(spread mirroring),将每个主机Primary Segment的Mirror扩展到其余主机上。这显然要求群集中的主机数多于每个主机的Primary Segment数。图3-9显示了配置扩展镜像时如何分布Segment数据。
图3-9 扩展镜像
2. Segment故障切换与恢复
在Greenplum系统中启用Mirror Segment时,如果Primary实例或所在主机宕掉,系统将自动切换到相应的Mirror实例,只要剩余活动Segment实例上的所有数据可用,Greenplum数据库系统即可保持运行。
当无法连接到Primary Segment时,会在Greenplum系统目录中将该Primary实例标记为down,并自动用其Mirror替换失效的Primary以继续提供服务。发生故障的Segment将停止运行,直到采取人为步骤使它重新联机。可以在系统启动和运行时恢复故障Segment,恢复过程仅复制Segment停止运行期间丢失的增量差异。如果发生故障时有正在执行的事务,则该事务将回滚并在新的Segment上自动重新启动。gpstate程序可用于识别故障Segment,该程序显示目录表gp_segment_configuration中的信息。
如果整个Greenplum系统由于Segment故障而无法运行(例如未启用Mirror或没有足够的Segment联机以访问所有用户数据),系统将自动关闭。客户端在尝试连接到数据库时会看到类似下面的错误,此时必须先恢复所有失败的Segment,然后才能继续操作。
ERROR: All segment databases are unavailable
3. Master镜像
如同Primary需要Mirror一样,可以在另一台主机上为Master配置一个镜像,按照惯例将其称为Standby。Standby是一个纯粹的容错节点,只作为Master出现问题时的热备,并要求与Master配置相同端口。在Master健康时,客户端只能通过Master来建立连接并执行SQL命令。在Master发生故障,无法继续提供服务时,需要在Standby主机上执行gpactivatestandby命令来激活Standby作为新的Master。到目前为止,Greenplum没有实现Standby的自动激活。
可以为Mater和Standby配置同一个虚IP,用于在Master和Standby之间漂移。当Standby被激活时,将虚IP漂移到Standby所在主机,这样客户端不需要修改连接信息,可以继续访问数据库。
Greenplum中的Master节点镜像架构如图3-10所示。
图3-10 Master镜像
Standby通过WAL同步保持与Master的实时一致。由于Master不存储用户数据,在Master和Standby之间仅同步系统表数据。这些表的数据量与用户数据相比很小,并且较少发生变化。当这些系统表数据被更新时(如DDL所引起),就会自动同步到Standby从而保证与Master的一致性。所以,Standby与Master可以保持实时同步。
Master失效时,WAL同步进程会自动停止。在Standby被激活时,冗余的WAL日志会被用来将数据库状态恢复到最后成功提交时的状态。激活的Standby实际上会成为Greenplum的新Master,接收客户端的连接访问。一旦Standby被激活,旧的失败Master将脱离集群,要将其重新加入集群,需要使用gpinitstandby命令将其添加为Standby的角色。
3.3.6 数据库统计
统计信息指的是数据库中所存储数据的元信息描述。查询优化器需要依据最新的统计信息,为查询生成最佳执行计划。例如查询连接了两个表,一个表必须被广播到所有Segment,那么优化器会选择广播其中的小表,使网络流量最小化。
ANALYZE命令计算优化器所需的统计信息,并将结果保存到系统目录中。有三种方式启动分析操作:
- 直接运行ANALYZE命令。
- 在数据库外运行analyzedb命令行实用程序。
- 执行DML操作的表上没有统计信息,或者DML操作影响的行数超过了指定的阈值时,系统自动执行分析操作。
计算统计信息会消耗时间和资源,因此Greenplum会在大表上进行采样,通过计算部分数据产生统计信息的估算值。大多数情况下,缺省设置能够提供生成正确查询执行计划的信息。如果产生的统计不能生成优化的查询执行计划,管理员可以调整配置参数,通过增加样本数据量,产生更加精确的统计。统计信息越精确,所消耗的CPU和内存资源越多,因此可能由于资源的限制,无法生成更好的计划。此时就需要查看执行计划并测试查询性能,目标是要通过增加的统计成本达到更好的查询性能。
1. 系统统计
(1)表大小
查询优化器使用查询必须处理的数据行数和必须访问的磁盘页数等统计信息,寻找查询所需的最小磁盘I/O和网络流量的执行计划。用于估算行数和页数的数据分别保存在pg_class系统表的reltuples和relpages列中,其中的值是最后运行VACUUM或ANALYZE命令时生成的数据。随着行的添加或删除,估算的准确性会降低。但操作系统始终可以提供磁盘页面的准确计数,因此只要reltuple与relpages的比例没有显著变化,优化器就可以生成足够准确的行数估计值,以选择正确的查询执行计划。
如果reltuples列的值与SELECT COUNT(*)的返回值差很多,应该执行分析以更新统计信息。REINDEX命令完成重新创建索引后,relpages和reltuples列将重置为0,应该在表上运行ANALYZE命令以更新这些列。
(2)pg_statistic系统表与pg_stats视图
pg_statistic系统表保存每个数据库表上最后执行ANALYZE操作的结果。pg_stats视图以一种更友好的方式表示pg_statistic的内容。
为一列收集的统计信息因不同的数据类型而异,因此pg_statistic表将适合该数据类型的统计信息存储在四个槽位中,每个槽位由四列组成。例如,第一个槽位通常包含列的最常用值,由列stakind1、staop1、stanumbers1和stavalues1组成。
stakindN列中的每一列都包含一个数字代码,用于描述存储在其槽位中的统计信息的类型。从1到99的stakind代码是为PostgreSQL数据类型保留的。Greenplum数据库使用代码1、2、3、4、5和99。值为0表示槽位未使用。
新创建的表和索引没有统计信息。可以使用gp_stats_missing视图检查缺少统计信息的表,该视图位于gp_toolkit模式中:
select * from gp_toolkit.gp_stats_missing;
(3)采样
为大表计算统计信息时,Greenplum通过对基表采样数据的方式建立一个小表。如果基表是分区表,从全部分区中采样。
(4)统计更新
运行不带参数的ANALYZE会更新数据库中所有表的统计信息,这可能需要执行很长时间,因此最好在数据更改后有选择地分析表。也可以选择分析一个表列的子集,例如只分析join、where、order by、group by、having等子句中用到的列。
如果样本包含空页,则分析严重膨胀的表可能会生成较差的统计信息。因此在分析膨胀表前,最好先执行VACUUM操作。
(5)分析分区表和AO表
在分区表上运行ANALYZE命令时,它逐个分析每个叶级别的子分区。也可以只在新增或修改的分区文件上运行ANALYZE,避免分析没有变化的分区。analyzedb命令行应用自动跳过无变化的分区,并且它是多会话并行的,可以同时分析几个分区。缺省运行5个会话,会话数可以通过命令行的-p选项设置,值域为1 ~ 10。每次运行analyzedb,它都会将AO表和分区的状态信息保存在Master节点数据目录中的db_analyze目录下,如/data/master/gpseg-1/db_analyze。下次运行时,analyzedb比较每个表的当前状态与上次保存的状态,不分析没有变化的表或分区。Heap表总是会进行被分析。
GPORCA查询优化器需要分区表根级别的统计信息,而老的优化器不使用该统计。如果启用了(默认启用)GPORCA优化器,则还需要运行ANALYZE或ANALYZE ROOTPARTITION来刷新根分区统计信息。
分析分区表的时间与分析具有相同数据的非分区表的时间类似,因为ANALYZE ROOTPARTITION不收集叶分区的统计信息(仅对数据进行采样)。
Greenplum服务器配置参数optimizer_analyze_root_partition会影响在分区表的根分区上收集统计信息的时间。如果参数为on(默认),则在运行ANALYZE时,不需要ROOTPARTITION关键字来收集根分区的统计信息。在根分区上运行ANALYZE时,或者在分区表的子叶分区上运行ANALYZE,并且其它子叶分区具有统计信息时,将收集根分区统计信息。如果所有子分区的统计信息都已经更新,ROOTPARTITION选项可用于只收集分区表的全局状态信息,这可以节省分析时间。
如果该参数处于禁用状态,则必须运行ANALYZE ROOTPARTITION以收集根分区统计信息。如果不使用GPORCA对分区表运行查询(将服务器配置参数optimizer设置为off),可以将服务器配置参数optimizer_analyze_root_partition也设置为off,以限制analyze更新根分区统计信息。
2. 统计配置
(1)统计目标
统计目标指的是一个列的most_common_vals、most_common_freqs和histogram_bounds数组的大小。这些数组的含义可以从pg_stats视图的定义得到。缺省目标值为25,可以通过设置服务器配置参数修改全局目标值,也可以使用ALTER TABLE命令设置任何表列的目标值。目标值越大,优化器评估质量越高,但ANALYZE需要的时间也越长。
default_statistics_target服务器配置参数设置系统缺省的统计目标。缺省值100通常已经足够,只有经过测试确定要定义一个新目标时,才考虑提高或降低该值。例如,要将默认统计信息目标从100提高到150,可以使用gpconfig程序:
gpconfig -c default_statistics_target -v 150
单个列的统计目标可以用ALTER TABLE命令设置。例如,某些查询可以通过为特定列,尤其是分布不规则的列增加目标值以提高性能。如果将一列的目标值设置为0,ANALYZE将忽略该列。下面的命令将notes列的统计目标设置为0,因为该列对于查询优化没有任何作用。
alter table emp alter column notes set statistics 0;
统计目标可以设置为0 ~ 1000之间的值,或者设置成-1,此时恢复使用系统缺省的统计目标值。父分区表上设置的统计目标影响子分区。如果父表上某列的目标设置为0,所有子分区上的该列统计目标也为0。但是,如果以后增加或者交换了其它子分区,新增的子分区将使用缺省目标值,交换的子分区使用以前的统计目标。因此如果增加或交换了子分区,应该在新的子分区上设置统计目标。
(2)自动收集统计信息
如果一个表没有统计信息,或者在表上执行的特定操作改变了大量数据时,Greenplum可以在表上自动运行ANALYZE。对于分区表,自动统计收集仅当直接操作叶表时被触发,它仅分析叶表。自动收集统计信息有三种模式:
- none:禁用自动收集。
- on_no_stats:在一个没有统计信息的表上执行CREATE TABLE AS SELECT、INSERT、COPY命令时触发分析操作。
- on_change:在表上执行CREATE TABLE AS SELECT、UPDATE、DELETE、INSERT或COPY命令,并且影响的行数超过了gp_autostats_on_change_threshold配置参数设定的阈值时触发分析操作。
依据命令是单独执行,还是在函数中执行,自动收集统计信息模式的设置方法也不一样。如果是在函数外单独执行,gp_autostats_mode配置参数控制统计模式,缺省值为on_no_stats。gp_autostats_mode_in_functions参数控制在过程语言函数中执行表操作时的行为,默认情况下设置为none。
在on_change模式下,仅当受影响的行数超过gp_autostats_on_change_threshold配置参数定义的阈值时,才会触发分析。此参数的默认值是一个非常高的值2147483647,能有效禁用自动统计数据收集,必须将阈值设置为较低的数字才能启用它。on_change模式可能触发意外中断系统的大型分析操作,因此不建议全局设置。它在会话中可能很有用,例如在加载后自动分析表。
要禁用函数外的自动统计信息收集,将gp_autostats_mode参数设置为none:
gpconfigure -c gp_autostats_mode -v none
要在函数中为没有统计信息的表启用自动统计信息收集,将gp_autostats_mode_in_functions更改为on_no_stats:
gpconfigure -c gp_autostats_mode_in_functions -v on_no_stats
如果要记录自动统计信息收集操作,将log_autostats系统配置参数设置为on。
3.4 为什么选择Greenplum
现在的SQL、NoSQL、NewSQL、Hadoop等等,都能在不同层面或不同应用上处理大数据的某些问题,其中Hadoop是较早用来处理大数据集合的分布式存储计算基础架构。那么作为用户,面对这么多技术选型,我们何时以及为什么要选择Greenplum构建数据仓库?近年来我也尝试过一些Hadoop产品,从最初的Hive,到Spark SQL,再到Impala、HAWQ,在这些产品上进行了一系列ETL、CDC、多维数据仓库、OLAP实验。从数据库的角度看,我的总体感觉是这些产品与传统的DBMS相比,功能不够完善,性能差距较大,甚至很难找到一个相对完备的数据仓库解决方案。本节就以我个人的实践体验对比一下Greenplum与SQL-on-Hadoop,并简述Greenplum的可行性和局限性。
3.4.1 Greenplum还是SQL-on-Hadoop
Greenplum和Hadoop都是为了解决大数据并行计算而出现的技术,两种技术的相似点在于:
- 分布式存储数据在多个节点上。
- 采用分布式并行计算框架。
- 支持向外扩展来提高整体的计算能力和存储容量。
- 支持X86开放集群架构。
但两种技术在数据存储和计算方法上,也存在很多显而易见的差异:
- Greenplum按照关系数据库行列表方式存储数据(有模式);Hadoop按照文件切片方式分布式存储(无模式)。
- 两者采用的数据分布机制不同,Greenplum采用Hash分布,计算节点和存储紧密耦合,数据分布在记录级的更小粒度,一般在1KB以下;Hadoop FS按照文件切块后随机分配,节点和数据无耦合,数据分布粒度在文件块级,缺省64MB。
- Greenplum采用SQL并行查询计划;Hadoop采用MapReduce框架。
基于以上不同,体现在效率、功能特性等方面也大不相同。Greenplum数据库在计算并行度、计算算法上比Hadoop更加优雅,效率更高。图3-11由Pivotal提供,显示相同硬件环境下,基于MapReduce的Hive和Greenplum在TPCH 22个SQL测试的性能比较,可以看到两者的执行速度相去甚远。
图3-11 Hive、Greenplum TPCH性能比较
为了取得第一手数据,我做了以下Greenplum与MySQL查询的性能对比测试。也许你会觉得拿分布式集群数据库与单机集中式数据库做比较有失公允,没错!我只想说明的是,最初考虑上Greenplum就是为了解决大数据量MySQL查不动的问题,而且这也并不是严格的对等测试,Greenplum只是由三台测试机组成的集群,而MySQL使用的是线上高配服务器。
moments_dynamic表79309341行,relation表499194521行。
-- 查询1:
select userid, target, relation_type, update_time
from relation
where userid = 717600270
and relation_type in (1, 2)
order by update_time desc
limit 30;
Greenplum:44 ms;MySQL:9210 ms
-- 查询2:
select a.*
from moments_dynamic a -- force index (idx_user_all)
join (select target
from relation r
where r.userid = 918046590
and (r.relation_type = 1 or r.relation_type = 2)
union all
select 918046590) b
on a.userid=b.target
where dynamic_status = 0
and dynamic_type in (1, 6, 8, 11, 13)
order by id desc
limit 80;
Greenplum:75ms;MySQL:170 ms(force index (idx_user_all))
在功能上Greenplum数据库采用SQL作为主要交互式语言。SQL语言简单易学,具有很强数据操纵能力和过程语言的流程控制能力,是专门为统计和数据分析开发的语言,各种功能和函数琳琅满目,大幅简化了数据的操作和交互过程。
而对MapReduce编程明显是困难的,在原生的MapReduce开发框架基础上的开发,需要技术人员谙熟于JAVA开发和并行原理,即便技术人员也难以学习和操控。为了解决易用性的问题,近年来SQL-on-Hadoop技术大量涌现出来,几乎成为当前Hadoop开发使用的一个技术热点。其中,Hive支持MapReduce、Spark、Tez三种计算框架,SparkSQL采用内存中的MapReduce技术,Impala、HAWQ则借鉴MPP计算思想来做查询优化和内存数据管道计算,以此来提高性能。
虽然SQL-On-Hadoop比原始的MapReduce在易用上有所提高,但在SQL成熟度和复杂分析上目前还与Greenplum数据库有较大差距,这在本人使用过程中深有体会。
- 上述系统,除了HAWQ外(HAWQ从代码级别上可以简单理解成是数据存储在HDFS上的Greenplum数据库),对SQL的支持都非常有限,特别是分析型复杂SQL,如SQL 2003 OLAP WINDOW函数,几乎都不支持,更不用说存储过程等数据库常规功能。以Impala为例,不支持Date数据类型,不支持XML和JSON相关函数,不支持covar_pop、covar_samp、corr、percentile、 percentile_approx、histogram_numeric、collect_set等聚合函数,不支持rollup、cube、grouping set等操作,不支持数据抽样(Sampling)等数据分析中的常用操作。在TPC-DS测试中,包括SparkSQL、Impala、Hive只支持其中1/3左右。TPC-DS是专门用于评测决策支持系统(大数据或数据仓库)的标准SQL测试集,包含99个SQL。
- 由于HDFS本身只能追加数据的特性(Append-only),SQL-On-Hadoop大多不支持行级数据更新(update)和删除(delete)功能;而像Hive虽然通过配置可以支持事务和行级更新,但实现极为别扭,性能更是无法接受,基本不具实用价值。
- SQL-On-Hadoop不擅长于交互式的即席查询(ad-hoc query),多通过预关联的方式来规避这个问题。另外,在并发处理方面能力较弱,高并发大查询场景下,需要控制计算请求的并发度,避免资源过载导致的稳定性问题和性能下降问题。我就曾多次遇到多个并发SparkSQL任务占用大量内存,最终出现OOM错误。
反观专为大数据存储、计算、挖掘而设计Greenplum,它所拥有的丰富特性使其成为构建数据仓库等分析型应用的理想选择。
- 完善的标准支持:Greenplum完全支持ANSI SQL 2008标准和SQL OLAP 2003 扩展。例如支持内连接、外连接、全连接、笛卡尔连接、相关子查询等所有表连接方式,支持并集、交集、差集等集合操作,并支持递归函数调用。作为一个数据库系统,提供这些功能很好理解。
- 除了包含诸多字符串、数字、日期时间、类型转换等常规标量函数以外,Greenplum还包含丰富的窗口函数和高级聚合函数,这些函数经常被用于分析型数据查询。窗口函数包括cume_dist、dense_rank、first_value、lag、last_valueexpr、lead、ntile、percent_rank、rank、row_number等。高级聚合函数包括median、percentile_cont (expr) within group (order by expr [desc/asc])、percentile_disc (expr) within group (order by expr [desc/asc])、sum(array[])、pivot_sum (label[], label, expr)等。
- 得益于PostgreSQL良好的扩展性(这里是extension,不是scalability),Greenplum 可以采用各种开发语言来开发用户自定义函数(User Defined Function,UDF)。自定义函数部署到Greenplum后能充分享受到实例级别的并行性能优势。建议把库外的处理逻辑,部署到用MPP数据库的UDF这种库内方式来处理,这将获得意想不到的性能和方便性。出于纯粹的个人兴趣,我尝试在PostgreSQL上创建C语言的UDF,解决“世界最难数独”问题,用时不到1毫秒,重点是比原始的C程序执行还快3-4倍。详见“用PLSQL解决世界最难数独”。
- 支持分布式事务,支持ACID,保证数据的强一致性。
- Greenplum支持用“Hadoop外部表”方式来访问、加载HDFS的数据。虽然Greenplum的Hadoop外部表性能大幅低于MPP内部表,但比Hadoop自身的Hive要快很多。Greenplum还提供了gpfdist文件服务器,可并行读写本地文件系统中的文件,最大化数据装载性能。
- Greenplum有完善的生态系统,可以与很多企业级产品集成,如SAS、Cognos、Informatic、Tableau等,也可以与很多种开源软件集成,如Pentaho、Talend等。
3.4.2 适合DBA的解决方案
Greenplum最吸引人的地方是它支持SQL过程化编程,这是通过UDF实现的。编写UDF的语言可以是SQL、C、Java、Perl、Python、R和pgSQL。数据库应用开发人员常用的自然是SQL和pgSQL,PL/pgSQL函数可以为SQL语言增加控制结构,执行复杂计算任务,并继承所有PostgreSQL的数据类型(包括用户自定义类型)、函数和操作符。Greenplum是我所使用过的分布式数据库解决方案中唯一支持SQL过程化编程的。对于习惯了编写存储过程的DBA来说,这无疑大大提高了Greenplum的易用性。Greenplum的UDF提供以下特性:
- 给内部函数起别名
- 返回结果集的表函数
- 参数个数可变的函数
- 多态数据类型
1. 给内部函数起别名
许多Greenplum的内部函数是用C语言编写的。这些函数在集群初始化时声明,并静态连接到Greenplum服务器。用户不能自己定义新的内部函数,但可以给已存在的内部函数起别名。下面的例子创建了一个新的函数fn_all_caps,它是内部函数upper的别名。
create function fn_all_caps (text) returns text as 'upper' language internal strict;
2. 返回结果集的表函数
表函数返回多行结果集,调用方法就像查询一个FROM子句中的表、视图或子查询。如果表函数返回单列,那么返回的列名就是函数名。下面是一个表函数的例子,该函数返回channel表中给定ID值的数据。
create function fn_getchannel(int) returns setof channel as $$
select * from channel where id = $1;
$$ language sql;
3. 参数个数可变的函数
Greenplum从PostgreSQL继承了一个非常好的特性,即函数参数的个数可变。在某些数据库系统中,想实现这个功能很麻烦。参数个数可变是通过一个动态数组实现的,因此所有参数都应该具有相同的数据类型。这种函数将最后一个参数标识为VARIADIC,并且参数必须声明为数组类型。下面是一个例子,实现类似原生函数greatest的功能。
create or replace function fn_mgreatest(variadic numeric[]) returns numeric as $$
declare
l_i numeric:=-99999999999999;
l_x numeric;
array1 alias for $1;
begin
for i in array_lower(array1, 1) .. array_upper(array1, 1)
loop
l_x:=array1[i];
if l_x > l_i then
l_i := l_x;
end if;
end loop;
return l_i;
end;
$$ language 'plpgsql';
执行函数结果如下:
db1=# select fn_mgreatest(variadic array[10, -1, 5, 4.4]);
fn_mgreatest
--------------
10
(1 row)
db1=# select fn_mgreatest(variadic array[10, -1, 5, 4.4, 100]);
fn_mgreatest
--------------
100
(1 row)
4. 多态数据类型
PostgreSQL中的anyelement、anyarray、anynonarray和anyenum四种伪类型被称为多态类型。使用这些类型声明的函数叫做多态函数。多态函数的同一参数在每次调用函数时可以有不同数据类型,实际使用的数据类型由调用函数时传入的参数所确定。当一个查询调用多态函数时,特定的数据类型在运行时解析。
如果一个函数的返回值被声明为多态类型,那么它的参数中至少应该有一个是多态的,并且参数与返回结果的实际数据类型必须匹配。例如,函数声明为assubscript(anyarray, integer) returns anyelement。此函数的第一个参数为数组类型,而且返回值必须是实际数组元素的数据类型。再比如一个函数的声明为asf(anyarray) returns anyenum,那么参数只能是枚举类型的数组。参数个数可变的函数也可以使用多态类型,实现方式是声明函数的最后一个参数为VARIADIC anyarray。
下面看几个多态类型函数的列子。
例1:判断两个入参是否相等,每次调用的参数类型可以不同,但两个入参的类型必须相同。
create or replace function fn_equal (anyelement,anyelement)
returns boolean as $$
begin
if $1 = $2 then return true;
else return false;
end if;
end; $$
language 'plpgsql';
函数调用:
db1=# select fn_equal(1,1);
fn_equal
----------
t
(1 row)
db1=# select fn_equal(1,'a');
ERROR: invalid input syntax for integer: "a"
LINE 1: select fn_equal(1,'a');
^
db1=# select fn_equal('a','A');
ERROR: could not determine polymorphic type because input has type "unknown"
db1=# select fn_equal(text 'a',text 'A');
fn_equal
----------
f
(1 row)
postgres=# select fn_equal(text 'a',text 'a');
fn_equal
----------
t
(1 row)
例2:遍历任意类型的数组,数组元素以行的形式返回。
create or replace function fn_unnest(anyarray)
returns setof anyelement
language 'sql' as
$$
select $1[i] from generate_series(array_lower($1,1),array_upper($1,1)) i;
$$;
函数调用:
db1=# select fn_unnest(array[1,2,3,4]);
fn_unnest
-----------
1
2
3
4
(4 rows)
db1=# select fn_unnest(array['a','b','c']);
fn_unnest
-----------
a
b
c
(3 rows)
例3:新建fn_mgreatest1函数,使它能返回任意数组类型中的最大元素。
create or replace function fn_mgreatest1(v anyelement, variadic anyarray)
returns anyelement as $$
declare
l_i v%type;
l_x v%type;
array1 alias for $2;
begin
l_i := array1[1];
for i in array_lower(array1, 1) .. array_upper(array1, 1) loop
l_x:=array1[i];
if l_x > l_i then
l_i := l_x;
end if;
end loop;
return l_i;
end;
$$ language 'plpgsql';
说明:
- 变量不能定义成伪类型,但可以通过参数进行引用,如上面函数中的l_i v%type。
- 动态数组必须是函数的最后一个参数。
- 第一个参数的作用仅是为变量定义数据类型,所以在调用函数时传空即可。
函数调用:
db1=# select fn_mgreatest1(null, variadic array[10, -1, 5, 4.4]);
fn_mgreatest1
---------------
10
(1 row)
db1=# select fn_mgreatest1(null, variadic array['a', 'b', 'c']);
fn_mgreatest1
---------------
c
(1 row)
3.4.3 Greenplum的局限
Greenplum最大的特点总结就一句话:在低成本开放平台基础上提供强大的并行数据计算性能和海量数据管理能力。并行计算能力是对大任务、复杂任务的快速高效计算,但如果你指望MPP并行数据库能够像OLTP数据库一样,在极短的时间处理大量的高并发小任务,这个并非MPP数据库所长。请牢记,并行和并发是两个完全不同的概念,MPP数据库是为了解决大数据问题而设计的并行计算技术,而不是大量的小数据问题的高并发请求。
再通俗点说,Greenplum主要定位在OLAP领域,利用Greenplum MPP数据库做大数据计算或分析平台非常适合,例如数据仓库系统、ODS系统、历史数据管理系统、分析系统、数据集市等等。而MPP数据库都不擅长做OLTP交易系统,所谓交易系统,就是高频的事务型小规模数据插入、修改、删除,每次事务处理的数据量不大,但每秒钟都会发生几十次甚至几百次以上交易型事务。这类系统的衡量指标是TPS,适用的系统是OLTP数据库,如MySQL。在本专题后面介绍实时数据同步时,会看到作为主打AP的Greenplum,在同步TP的MySQL数据时,所表现出来的量化的性能差异。
除了OLTP、OLAP之分,近来还出现了所谓HTAP(Hybrid Transactional/Analytical Processing),即混合事务/分析处理数据库。在我个人看来,HTAP作为概念的意义大于实用意义,至少目前如此,就像概念车,外观上无比炫酷,但就是不会量产。从原理上讲,TP与AP在需求、应用场景、性能衡量指标、建模与设计方法、优化策略等方面都截然不同(参见“Greenplum 实时数据仓库实践(1)——数据仓库简介”中的表1-1),结果必然是在实现技术上分道扬镳。正所谓鱼和熊掌不可兼得,虽然也有一些打上HTAP标签的产品,但终究还是在AP与TP之间做权衡,侧重其中之一,而在另一方面表现则差强人意。
3.5 小结
- Greenplum是MPP架构的分布式数据库,针对于分析型应用,尤其适合用于数据仓库。
- Greenplum建立在无共享架构上,其并行工作方式可以最大限度地发挥硬件能力。
- Master、Segment和Interconnect是构成Greenplum的顶层组件。每个Master和Segment都是一个单独的PostgreSQL数据库实例。Master是Greenplum的系统入口,它接收客户端连接和SQL查询,并将工作分配给segment实例。Master实例只存储系统元数据表,不存储任何用户数据。每个Segment存储部分用户数据,处理部分查询工作。Interconnect内部互连是Greenplum数据库体系结构中的核心组件。实现了对同一个集群中多个PostgreSQL实例的高效协同和并行计算,承载了并行查询计划生产和派遣分发、协调节点上查询执行器的并行工作,负责数据分布、Pipeline计算、镜像复制、健康探测等等诸多任务。
- Greenplum支持Heap表和AO表,Heap表支持行存,AO表支持行存和列存,并可以使用压缩。
- Greenplum继承了PostgreSQL的MVCC模型进行并发控制,支持SQL标准中的全部四种事务隔离级,缺省隔离级为read committed。
- 每个Segment中有多个并行执行的工作进程协同处理一个查询。
- 利用gpfdist外部表或gpload程序,可以向Greenplum并行装载外部数据,最大化数据装载性能。
- Primary/Mirror提供Segment自动检测和故障切换,Mirror有group和spread两种标准分布方式,缺省为group。Standby提供Master热备,需要时手工激活。这两种机制共同为生产环境提供高可用、容错的Greenplum环境。
- Greenplum查询优化器依赖表列的统计信息生成执行计划,可以通过配置自动收集这些统计信息。
- 有别于SQL-on-Hadoop技术,Greenplum更符合数据库特性,支持行级更新,通过UDF实现过程化编程,编写UDF的语言可以是SQL、C、Java、Perl、Python、R和pgSQL。这些功能特性在提高易用性的同时提供了更高的性能,是更适合DBA的解决方案。
- Greenplum不适合OLTP类型的应用场景。