返回 登录
0

PostgreSQL并行查询介绍

导语】2016年4月,PostgreSQL社区发布了PostgreSQL 9.6 Beta 1,迎来了并行查询(Parallel Query)这个新特性。在追求高性能计算和查询的大数据时代,能提升性能的特性都会成为一个新的热门话题。作为关注PostgreSQL发展的数据库开发者,本文作者将分享对于一些PostgreSQL并行查询特性相关话题的认识。

并行查询的背景

随着SSD等磁盘技术的平民化,以及动辄上百GB内存的普及,I/O层面的性能问题得到了有效缓解。提升数据库的扩展性能,可以追求Scale Out的方式,增加机器,往分布式方向发展,也可以追求Scale Up,增加硬件组件,充分利用各个硬件的资源,把单机的性能发挥到最大效果。相较而言,Scale Up通过软件加速性能,依赖软件层面的优化,是低成本的扩展方案。

现代服务器除了磁盘和内存资源的增强,多CPU的配置也足够强大。数据库的Join、聚合等操作内存耗费比较大,很多时间花在了数据的交换和缓存上,CPU的利用率并不高,所以面向CPU的加速策略中,并发执行是一种常见的方法。

查询的性能是评价OLAP型数据库产品好坏的核心指标,而并行查询可以聚焦在数据的读和计算上,通过把Join、聚合、排序等操作分解成多个操作实现并行。

并行查询的挑战在于,为了要做并行而加入的数据分片过程、进程或线程间的通信,以及并发控制方面带来的系统开销不但没有增加性能,反而降低了原有性能。实现上,如何在优化器里规划好并行计划也是很多数据库做不到的。

PostgreSQL的并行查询功能主要由PostgreSQL社区的核心开发者Robert Haas等人开发。从Robert Haas的个人博客了解到,社区开发PostgreSQL的并行查询特性时间表如下:

  • 2013年10月,执行框架上做了Dynamic Background Workers和Dynamic Shared Memory两个调整
  • 2014年12月,Amit Kapila提交了一个简单版的parallel sequential scan的patch;
  • 2015年3月,正式版的parallel sequential scan的patch被提交;
  • 2016年3月,支持parallel joins和parallel aggregation;
  • 2016年4月,作为9.6的新特性发布。

PostgreSQL的并行查询在大数据量(中间结果在GB以上)的Join、Merge场合,效果比较明显。效果上,因为系统开销,投入的资源跟性能提升并不是线性的,比如增加4个worker,性能则可能提升2倍左右,而不是4倍。通过TPCH的测试效果,表明在Ad-Hoc查询场景,普遍都有加速效果。

并行查询功能说明

现在支持的并行场景主要是以下3种:

  • parallel sequential scan
  • parallel join
  • parallel aggregation

鉴于安全考虑,以下4种场景不支持并行:

  • 公共表表达式(CTE)的扫描
  • 临时表的扫描
  • 外部表的扫描(除非外部数据包装器有一个IsForeignScanParallelSafeAPI)
  • 对InitPlan或SubPlan的访问

使用并行查询,还有以下限制:

  • 必须保证是严格的read only模式,不能改变database的状态
  • 查询执行过程中,不能被挂起
  • 隔离级别不能是SERIALIZABLE
  • 不能调用PARALLEL UNSAFE函数

并行查询有基于代价策略的判断,譬如小数据量时默认还是普通执行。在PostgreSQL的配置参数中,提供了一些跟并行查询相关的参数。我们想测试并行,一般设置下面两个参数:

  • force_parallel_mode:强制开启并行模式的开关
  • max_parallel_workers_per_gather:设定用于并行查询的worker进程数

一个简单的两表Join查询场景,使用并行查询模式的查询计划如下:

test=# select count(*) from t1;
  count   
----------
 10,000,000
(1 row)
test=# select count(*) from t2;
  count   
----------
 10,000,000
(1 row)
test=# explain analyze  select count(*) from t1,t2  where t1.id = t2.id ;
                           QUERY PLAN                                                
------------------------------------------------------------------------------
 Finalize Aggregate  (cost=596009.38..596009.39 rows=1 width=8) (actual time=17129.158..17129.158 rows=1 loops=1)
   ->  Gather  (cost=596009.17..596009.38 rows=2 width=8) (actual time=16907.462..17129.132 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=595009.17..595009.18 rows=1 width=8) (actual time=17038.230..17038.231 rows=1 loops=3)
               ->  Hash Join  (cost=308310.48..570009.22 rows=9999977 width=0) (actual time=8483.284..16703.813 rows=3333333 loops=3)
                     Hash Cond: (t1.id = t2.id)
                     ->  Parallel Seq Scan on t1  (cost=0.00..85914.87 rows=4166687 width=4) (actual time=0.575..741.057 rows=3333333 loops=3)
                     ->  Hash  (cost=144247.77..144247.77 rows=9999977 width=4) (actual time=8449.743..8449.743 rows=10000000 loops=3)
                           Buckets: 131072  Batches: 256  Memory Usage: 2400kB
                           ->  Seq Scan on t2  (cost=0.00..144247.77 rows=9999977 width=4) (actual time=0.294..2177.531 rows=10000000 loops=3)

并行查询开启后,解析器会生成一份Gather…Partial风格的执行计划,这意味着到Executor层,会将Partial部分的计划并行执行。

执行计划里可以看到,在做并行查询时,额外创建了2个worker进程,加上原来的master进程,总共3个进程。Join的驱动表数据被平均分配了3份,通过并行scan分散了I/O操作,之后跟大表数据分别做Join。

并行查询的实现

PostgreSQL的并行由多个进程的机制完成。每个进程在内部称之为1个worker,这些worker可以动态地创建、销毁。PostgreSQL在SQL语句解析和生成查询计划阶段并没有并行。在执行器(Executor)模块,由多个worker并发执行被分片过的子任务。即使在查询计划被并行执行的环节,一直存在的进程也会充当一个worker来完成并行的子任务,我们可以称之为主进程。同时,根据配置参数指定的worker数,再启动n个worker进程来执行其他子计划。

PostgreSQL内延续了共享内存的机制,在每个worker初始化时就为每个worker分配共享内存,用于worker各自获取计划数据和缓存中间结果。这些worker间没有复杂的通信机制,而是都由主进程做简单的通信,来启动和执行计划。

PostgreSQL中并行的执行模型如图1所示。

  图1  PostgreSQL并行查询的框架

图1 PostgreSQL并行查询的框架

以上文的Hash Join的场景为例,在执行器层面,并行查询的执行流程如图2所示。

图2  并行查询的执行流程

图2 并行查询的执行流程

各worker按照以下方式协同完成执行任务:

  • 首先,每个worker节点做的任务相同。因为是Hash Join,worker节点使用一个数据量小的表作为驱动表,做Hash表。每个worker节点都会维护这样一个Hash表,而大表被平均分之后跟Hash表做数据Join。

  • 最底层的并行是磁盘的并行scan,worker进程可以从磁盘block里获取自己要scan的block。

  • Hash Join后的数据是全部数据的子集。对于count()这种聚合函数,数据子集上可以分别做计算,最后再合并,结果上可以保证正确。

  • 数据整合后,做一次总的聚合操作。

worker进程又是如何创建和运行的?首先来看worker的创建逻辑(参见图3)。

图3  PostgreSQL的worker创建

图3 PostgreSQL的worker创建

PostgreSQL的并行处理,以worker动态创建为前提。worker可以由主进程初始化出来,并且在上下文中,先指定好入口函数。

并行查询中,入口函数被指定为ParallelWorkerMain。而ParallelWorkerMain函数里,在完成一系列信号代理设定后,会调用ParallelQueryMain来执行查询。ParallelQueryMain创建了一个新的执行器上下文,递归执行并行子查询计划。

用来并行查询的worker进程接收主进程的信号,比如一旦发送创建进程的信号,worker进程就会启动,紧接着执行ParallelWorkerMain函数。进而,ParallelQueryMain也会执行,各个worker进程独立执行子计划,执行结果会存在共享内存里。所有进程执行结束后,master进程会去搜集共享内存里的结果数据(tuple),做数据整合。

并行查询的改进

并行查询的特性公布后,不乏对并行的评价和之后的改进计划。社区并行查询的开发者在博客中提到准备做一个大的共享Hash Table,这样Hash Join操作的并行度会进一步提升。

图4  创建大的Hash表共享数据

图4 创建大的Hash表共享数据

另外,对PostgreSQL而言,反倒是基于其folk出来的一些数据库产品先于它做了并行查询的特性,可以学习参考:

  • Postgres-XC的分布式框架
  • GreenPlum的MPP架构
  • CitusDB的分布式
  • VitesseDB基于多线程的并行
  • Fujitsu的Fujitsu Enterprise PostgreSQL的并行

其中开源数据库GreenPlum并行架构很有借鉴意义。GreenPlum的并行查询设计了一个专门的调度器来协调查询任务的分配,而PostgreSQL没有这样的设计。关于GreenPlum的执行框架,简单讲是以下三层结构:

  • 调度器(QD):调度器发送优化后的查询计划给所有数据节点(Segments)上的执行器(QE)。调度器负责任务的执行,包括执行器的创建、销毁、错误处理、任务取消、状态更新等。

  • 执行器(QE):执行器收到调度器发送的查询计划后,开始执行自己负责的那部分计划。典型的操作包括数据扫描、哈希关联、排序、聚集等。

  • Interconnect:负责集群中各个节点间的数据传输。

GreenPlum会根据数据分布情况做数据的广播和重分布,这是PostgreSQL的并行模型可以借鉴的。

仅仅是一个大的Hash Table,在数据访问上有串行的开销,worker的并行仍然受限。如图5所示,大表和小表Join的场景参考GreenPlum的数据广播机制,驱动表的数据可以给每个worker进程准备一个拷贝,相当于广播了一份数据。这样数据被高度共享,并行的效果会更好。

除了PostgreSQL生态的数据库,关系型数据库老大哥Oracle在并行查询上已经积累了30年的经验,也需要借鉴。在Oracle的官方手册中,有对其并行查询机制做出的说明。

图5  借鉴GreenPlum的广播机制提升并行效果

图5 借鉴GreenPlum的广播机制提升并行效果

Oracle在每个操作环节,都能把数据高度分片,可以参考图6所示的Hash Join的并行。

图6  Oracle的Hash Join操作的并行流程

图6 Oracle的Hash Join操作的并行流程

而在内部并行控制上,数据被分组后,不管是scan还是排序,几组worker对分组的数据都能分治。

也就是说Oracle做到了操作符(Operator)Level的并行。在每个操作中,把数据分片后动态的并行运算。可以看到Oracle的并行查询在做Operator级别的并行,每个操作环节,都能把数据分片后分而治之,并行程度非常高。这对数据的流转要求也很高,数据和操作既能水平分治也能垂直分治。

PostgreSQL目前是任务级别的并行,将原先的执行计划垂直拆分成几个可以分离的子任务,并行实现简单,但在大数据量时并行度不够,而且共享内存的访问负荷加重,性能提升不明显。

图7  Oracle内部动态的并行操作

图7 Oracle内部动态的并行操作

参考Oracle的方式,按上图改进后,worker不再是单独执行1个任务,而是随时被调用执行操作。数据根据操作分层、分片、广播,worker进程为数据操作服务,而不是数据为worker服务。这样在超大规模数据的场景,驱动表作为producer做数据partition,外表作为consumer做operator运算。多组这样的操作产生的并行计算更自由,性能也更有想象空间,也是我们团队目前在尝试的方向。

图8  通过数据分组和worker分组提升PostgreSQL的并行

图8 通过数据分组和worker分组提升PostgreSQL的并行

笔者对数据库实现的理解深度有限,立足自己的经验分享了关于并行查询的以上认识。关注社区邮件,可以看到PostgreSQL社区非常积极地加入更多并行查询的特性,比如parallel bitmap index等,相信并行查询的特性会更丰富。期待后面越来越强大的并行计算,以及随之而来性能加速的无限可能。

作者:赵志强,南京SkyData(天数科技)数据库团队的负责人,2008年开始从事数据库的开发工作。2016年加入SkyData初创团队,研发数据平台产品。目前主要关注数据库等基础软件的加速技术,以及时序数据库的开发与应用。
责编:仲培艺,关注数据库领域,纠错、寻求报道或投稿请致邮zhongpy@csdn.net。
本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅2017年《程序员》


瞄准业界实时新闻资讯,精准定位,深度挖掘技术前沿一手实践,爆点话题开放讨论,技术无边界,尽在CSDN资讯——一键关注,IT资讯,掌上纵横。

图片描述

评论