PostgreSQL 并行查询

490 阅读24分钟

并行查询概述

背景

大数据时代,人们使用数据库系统处理的数据量越来越大,请求越来越复杂,对数据库系统的大数据处理能力和混合负载能力提出更高的要求。PostgreSQL 作为世界上最先进的开源数据库,在大数据处理方面做了很多工作,如并行和分区。

PostgreSQL 从 2016 年发布的 9.6 开始支持并行,在此之前,PostgreSQL 仅能使用一个进程处理用户的请求,无法充分利用资源,亦无法很好地满足大数据量、复杂查询下的性能需求。2018 年 10 月发布的 PostgreSQL 11,在并行方面做了大量工作,支持了并行哈希连接,并行 Append 以及并行创建索引等特性,对于分区表,支持了 Partition-wise JOIN

本文从以下三方面介绍 PostgreSQL 的并行查询特性:

  • 并行查询基础组件,包括后台工作进程(Background Work Process),动态共享内存(Dynamic Shared Memory)以及后台工作进程间的通信机制和消息传递机制
  • 并行执行算子的实现,包括并行顺序扫描、并行索引扫描等并行扫描算子,三种连接方式的并行执行以及并行 Append
  • 并行查询优化,介绍并行查询引入的两种计划节点,基于规则计算后台工作进程数量以及代价估算

并行查询为什么会快?

  • 行查询不是因为并行读取,而是因为数据分散在许多CPU核心上。现代操作系统为PostgreSQL数据文件提供了良好的缓存。预读允许从存储中获取一个块,而不仅仅是PG守护进程请求的块。因此,查询性能不受磁盘IO的限制。它消耗CPU周期:
  • 从表数据页逐个读取行
  • 比较行值和where条件

并行查询工作原理和机制

如何工作?

  • process
    查询执行总是在“leader”进程中开始。一个leader执行所有非并行活动及其对并行处理的贡献。执行相同查询的其他进程称为“worker”进程。并行执行使用动态后台工作器基础结构(在9.4中添加)。由于PostgreSQL的其他部分使用进程,而不是线程,因此创建三个工作进程的查询可能比传统的执行速度快4倍.
  • Communication
    Workers使用消息队列(基于共享内存)与leader通信。每个进程有两个队列:一个用于错误,另一个用于元组。

image.png

并行基础组件

PostgreSQL 从 9.4 和 9.5 已经开始逐步支持并行查询的一些基础组件,如后台工作进程,动态共享内存和工作进程间的通信机制,本节对这些基础组件做简要介绍。

后台工作进程(Background Worker Process)

PostgreSQL 是多进程架构,主要包括以下几类进程:

  • 守护进程,通常称之为 postmaster 进程,接收用户的连接并 fork 一个子进程处理用户的请求;
  • backend 进程,即 postmaster 创建的用于处理用户请求的进程,每个连接对应一个 backend 进程;
  • 辅助进程,用于执行 checkpoint,后台刷脏等操作的进程;
  • 守护进程,通常称之为 postmaster 进程,接收用户的连接并 fork 一个子进程处理用户的请求
  • backend 进程,即 postmaster 创建的用于处理用户请求的进程,每个连接对应一个 backend 进程
  • 辅助进程,用于执行 checkpoint,后台刷脏等操作的进程:

image.png

上图中 server process 即 postmaster 进程,在内核中,postmaster 与 backend 进程都是 postgres 进程,只是角色不同。对于一个并行查询,其创建 worker 进程的大致流程如下:

  1. client 创建连接,postmaster 为其 fork 一个 backend 进程处理请求;
  2. backend 接收用户请求,并生成并行查询计划;
  3. 执行器向 backgroudworker 注册 worker 进程(并没有启动);
  4. 执行器通知(kill)postmaster 启动 worker 进程;
  5. worker 进程与 leader 进程协调执行,将结果返回 client;

动态共享内存(Dynamic Shared Memory)与 IPC

PostgreSQL 是多进程架构,进程间通信往往通过共享内存和信号量来实现。对于并行查询而言,执行时创建的 worker 进程与 leader 进程同样通过共享内存实现数据交互。但这部分内存无法像普通的共享内存那样在系统启动时预先分配,毕竟直到真正执行时才知道有多少 worker 进程,以及需要分配多少内存。

PostgreSQL 实现了动态共享内存,即在执行时动态创建,用于 leader 与 worker 间通信,执行完成后释放。基于动态共享内存的队列用于进程间传递元组和错误消息。

如下图,每个 worker 在动态共享内存中都有对应的元组队列和错误队列,worker 将执行结果放入队列中,leader 会从对应队列获取元组返回给上层算子。动态共享内存的具体实现原理和细节在此不做展开。

image.png

实现过程

PostgreSQL 提供了一些简单的设施,使编写并行算法更容易。 使用名 ParallelContext 的数据结构,你可以安排启动后台工作进程,初始化它们的状态,使之与启动并行的后端相匹配。通过动态共享内存与它们通信,并编写相当复杂的共享内存与它们通信,并编写相当复杂的代码,这些代码既可在用户后台运行,也可在其中一个用户后端或某个并行工作进程中运行,而不需要知道它在哪里运行。

typedef struct ParallelContext
{
   dlist_node node;
   SubTransactionId subid;
   int          nworkers;     /* Maximum number of workers to launch */
   int          nworkers_to_launch; /* Actual number of workers to launch */
   int          nworkers_launched;
   char      *library_name;
   char      *function_name;
   ErrorContextCallback *error_context_stack;
   shm_toc_estimator estimator;
   dsm_segment *seg;
   void      *private_memory;
   shm_toc    *toc;
   ParallelWorkerInfo *worker;
   int          nknown_attached_workers;
   bool      *known_attached_workers;
} ParallelContext;

启动并行操作的后端(以下简称启动后端)首先要创建一个动态共享内存段,该段将在并行操作的生命周期内持续存在。 该动态共享内存段将包含:
(1) 一个 shm_mq,用于将错误(以及通过 elog/ereport 报告的其他消息)从 Worker 传回启动后端;
(2) 启动后端私有状态的序列化表示,以便 Worker 可以将其状态与启动后端同步;
(3) ParallelContext 数据结构的特定用户可能希望为其自身目的添加的任何其他数据结构。 一旦启动后端初始化了动态共享内存段,它就会要求后端管理员启动适当数量的并行 Worker。然后,这些 Worker 会连接到动态共享内存段,启动它们的状态,然后调用相应的入口点,详情如下。

  • Error Reporting
    启动时,每个并行 Worker 会首先附加动态共享内存段,并找到用于错误报告的 shm_mq;它会将所有协议消息重定向到该 shm_mq。 在此之前,后台 Worker 的任何故障都不会报告给启动后台;从启动后台的角度来看,该 Worker 只是启动失败。 无论如何,启动后台都必须做好准备,以应对比最初要求的并行工作者数量更少的并行工作者,因此应对这种情况不会造成额外负担。
    每当新消息(或部分消息;非常大的消息可能会换行)被发送到错误报告队列,PROCSIG_PARALLEL_MESSAGE被发送到启动后端。这将导致发起后端的下一个CHECK_FOR_INTERRUPTS()读取并重新抛出消息。在很大程度上,这使得并行模式下的错误报告可以正常工作。当然,为了正常工作,启动后端的代码定期CHECK_FOR_INTERRUPTS()并避免长时间阻塞中断处理是很重要的。

(目前尚未解决的一个问题是,一些消息可能会被写入系统日志两次,一次是在最初生成报告的后端,另一次是在发起后端重新抛出消息时。如果我们决定取消这些报告中的一个,它很可能是第二个;否则,如果工作进程由于某种原因无法将消息传播回发起后端,消息将完全丢失。)

  • State Sharing
    并行计算是一种提高计算机程序性能的方法,通过将任务分配给多个处理器核心来并行执行。然而,并行计算可能会导致数据不一致、死锁等问题。在处理并行问题时,确保数据的一致性非常重要。 在C代码中,如果没有并行性,可能会出现以下问题:
    1)全局变量:当多个线程同时访问同一个全局变量时,可能会导致数据不一致。这是因为线程之间没有同步机制,每个线程都可以修改全局变量的值。
    2)伪随机数生成器:在处理随机数据时,确保生成的随机数序列是可预测的非常重要。但是,伪随机数生成器依赖于一些私有的状态,这可能会导致在并行计算环境中的数据不一致。
    为了确保并行计算中的数据一致性,可以采取以下方法:\
  1. 使用同步原语:同步原语是用于确保线程之间同步的机制。在C中,可以使用互斥锁(mutex)和信号量(sem)来实现同步原语.\
  2. 使用动态共享内存:为了在多线程之间共享数据,可以使用动态共享内存(DSM)。在Linux系统中,可以使用shm系统调用来实现动态共享内存。\
  3. 使用原子操作:在某些情况下,可以使用原子操作(如__sync_synchronize)来确保数据的同步。\
  4. 检查点:在并行计算过程中,定期检查计算结果是否正确。如果发现错误,可以回退并重新计算。
    总之,确保在并行计算中处理数据的一致性非常重要,这样可以减少错误并提高程序的性能。
    我们采取更加 pragmatic 的方法。首先,我们尝试将可以在并行模式下安全运行的操作工作正常。其次,我们尝试通过适当的错误检查来禁止常见的不安全操作。这些检查是为了捕获从SQL接口中进行的100%不安全的操作,但编写在C中的代码可能会做不安全的操作,这些操作不会触发这些检查。错误检查是通过调用EnterParallelMode()函数来启用并行模式,调用ExitParallelMode()函数来禁用并行模式来实现的。在并行模式下,所有操作必须严格只读;我们允许对数据库进行只读操作,但不允许进行写入操作和DDL。我们可能会在将来尝试放宽这些限制。
    为了在并行模式下尽可能多地工作,我们将重要状态从初始化后端复制到每个并行工作器。这包括:
    1)动态加载的库:dfmgr.c中动态加载的库。
    2)授权用户ID和当前数据库:每个并行工作器将使用与初始化后端相同的用户ID连接到相同的数据库。
    3)GUC值:根据需要,允许在工作区中临时更改GUC值。但请注意,永久更改GUC值是不允许的,因为这样可能会导致数据不一致。
    4)当前子事务XID:当在并行计算过程中更改事务状态时,需要确保每个工作器返回相同的结果,与初始化后端相同。有关事务集成的更多信息,请参阅以下部分。
    5)组合CID映射:在处理组合CID时,需要确保在不同工作器之间的一致性。这样,才能确保tuple可见性检查返回相同的结果。
    6)事务快照:在并行计算过程中,需要同步事务快照。
    7)活动快照:与事务快照不同,这可能与当前事务快照不同。
    8)活动用户ID和安全性上下文:在绑定到正确数据库时,还需要恢复授权用户ID和安全性上下文。当GUC值恢复时,这会自动设置SessionUserId、OuterUserId和CurrentUserId。
    9)等待重索引操作:防止访问正在构建的索引。
    10)活动relmapper.c映射状态:在处理映射关系时,需要确保一致的结果。
    为了防止在并行模式下运行时出现无原则的死锁,这段代码还安排了leader和worker参与组锁定。请参阅src/back end/storage/lmgr/README了解更多详细信息。
  • Transaction Integration
    在并行模式下,每个并行工作器都会最终具有一个深度为1的TransactionState堆栈。这个堆栈项具有特殊的事务块状态TBLOCK_PARALLEL_INPROGRESS,以便与其普通的顶级事务区分开来。XID设置为初始化后端的内部子事务的XID。此外,我们还存储了 initiating backend 的顶级XID和所有当前(正在处理或已提交)XID,以便在并行工作器中返回相同的结果,与初始化后端相同。我们可以在复制整个事务状态堆栈时,但这大多数是无用的,因为您无法从并行工作器中回滚到保存点,并且没有资源来关联内存上下文或资源所有者中间子事务。
    在并行模式下,不能对事务状态进行任何有意义的更改。不能分配xid,也不能开始或结束子事务,因为我们没有办法将这些状态变化传递给协作的后端,或者使它们同步。当并行性在所有并行worker退出之前开始时,发起后端退出任何正在进行的事务或子事务显然是不可行的;对于一个并行worker来说,试图子提交或分包当前的子事务,并在其他事务上下文中执行,而不是在启动后端中执行,这显然更加疯狂。允许在并行模式下使用内部子事务(例如,实现PL/pgSQL异常块)可能是可行的,前提是它们是无XID的,因为其他后端并不真正需要知道那些事务或因为它们而做任何不同的事情。现在,我们甚至不允许这样做。
    在并行操作结束时,与该操作相关联的并行workers会退出,这可能是因为该操作成功完成,也可能是因为它被错误中断。在错误情况下,并行leader中的事务中止处理杀死任何剩余的workers,然后并行leader等待它们死亡。在并行操作成功的情况下,并行leader不发送任何信号,而是必须等待worker完成并自行退出。在这两种情况下,在并行leader清理创建它们的(子)事务之前,所有worker实际上退出是非常重要的;否则,混乱就会接踵而至。例如,如果leader正在回滚创建由worker扫描的relation的事务,那么该relation可能会在worker仍忙于扫描它时消失。那不安全。
    通常,每个worker在这一点上执行的清理类似于顶级提交或中止。每个后端都有自己的资源所有者:buffer pins、catcache或relcache引用计数、元组描述符等由每个后端单独管理,并且必须在退出前释放它们。 然而,并行工作提交或中止与真正的顶级事务提交或中止之间有一些重要的区别。最重要的是:
    1)未写入提交或中止记录;发起后端对此负责。
    2)pg _ temp命名空间的清理未完成。并行工作者不能安全地访问启动后端的pg_temp名称空间,也不应该创建自己的名称空间。

  • Coding Conventions
    在开始任何并行操作之前,调用EnterParallelMode();所有并行操作完成后,调用ExitParallelMode()。要实际并行化一个特定的操作,请使用ParallelContext。基本编码模式如下所示:

EnterParallelMode(); /* prohibit unsafe state changes */ 
pcxt = CreateParallelContext("library_name", "function_name", nworkers);  
/* Allow space for application-specific data here. */
shm_toc_estimate_chunk(&pcxt->estimator, size); 
shm_toc_estimate_keys(&pcxt->estimator, keys);

InitializeParallelDSM(pcxt);	/* create DSM and copy state to it */  	
/* Store the data for which we reserved space. */ 	
space = shm_toc_allocate(pcxt->toc, size); 	
shm_toc_insert(pcxt->toc, key, space);  	

LaunchParallelWorkers(pcxt);  	

/* do parallel stuff */  
WaitForParallelWorkersToFinish(pcxt);  	

/* read any final results from dynamic shared memory */  	
DestroyParallelContext(pcxt);  

ExitParallelMode();

如果需要,在调用WaitForParallelWorkersToFinish()之后,可以重置上下文,以便可以使用相同的并行上下文重新启动工作线程。为此,首先调用ReinitializeParallelDSM()来重新初始化由并行上下文机器本身管理的状态;然后,执行任何其他必要的状态重置;之后,您可以再次调用LaunchParallelWorkers。

使用要点

  • 如果所有CPU内核都已饱和,则不要启用并行执行。并行执行从其他查询中窃取CPU时 间,并增加响应时间。
  • 最重要的是,并行处理显著增加了具有高WORK-MEM值的内存使用量,因为每个hash 连接或排序操作占用一个WORk-MEM内存量。
  • 下一步,低延迟的OLTP查询在并行执行时不能再快了。特别是,当启用并行执行时, 返回单行的查询可能会执行得不好。
  • Pierian spring对于开发人员来说是一个TPC-H基准。检查是否有类似的查询以获得最佳 并行执行。
  • 并行执行只支持不带谓词锁的SELECT查询。
  • 正确的索引可能是并行顺序表扫描的更好选择
  • 不支持游标或挂起的查询。
  • 窗口函数和有序集聚合函数是非并行的。

举个栗子🌰

首先,通过一个例子,让我们对 PostgreSQL 的并行查询以及并行计划有一个较宏观的认识。如下查询:统计人员表 people 中参加 2018 PostgreSQL 大会的人数:

SELECT COUNT(*) FROM people WHERE  inpgconn2018 = 'Y';

没有开并行的情况下(max_parallel_workers_per_gather=0),查询计划如下:

Aggregate  (cost=169324.73..169324.74 rows=1 width=8) (actual time=983.729..983.730 rows=1 loops=1)
   ->  Seq Scan on people  (cost=0.00..169307.23 rows=7001 width=0) (actual time=981.723..983.051 rows=9999 loops=1)
         Filter: (atpgconn2018 = 'Y'::bpchar)
         Rows Removed by Filter: 9990001
 Planning Time: 0.066 ms
 Execution Time: 983.760 ms

开启并行的情况下(max_parallel_workers_per_gather=2),查询计划如下:

Finalize Aggregate  (cost=97389.77..97389.78 rows=1 width=8) (actual time=384.848..384.848 rows=1 loops=1)
   ->  Gather  (cost=97389.55..97389.76 rows=2 width=8) (actual time=384.708..386.486 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96389.55..96389.56 rows=1 width=8) (actual time=379.597..379.597 rows=1 loops=3)
               ->  Parallel Seq Scan on people  (cost=0.00..96382.26 rows=2917 width=0)
				 (actual time=378.831..379.341 rows=3333 loops=3)
                     Filter: (atpgconn2018 = 'Y'::bpchar)
                     Rows Removed by Filter: 3330000
 Planning Time: 0.063 ms
 Execution Time: 386.532 ms

max_parallel_workers_per_gather 参数控制执行节点的最大并行进程数,通过以上并行计划可知,开启并行后,会启动两个 worker 进程(即 Workers Launched: 2)并行执行,且执行时间(Execution Time)仅为不并行的40%。该并行计划可用下图表示:

image.png

并行查询计划中,我们将处理用户请求的 backend 进程称之为主进程(leader),将执行时动态生成的进程称之为工作进程(worker)。每个 worker 执行 Gather 节点以下计划的一个副本,leader 节点主要负责处理 Gather 及其以上节点的操作,根据 worker 数不同,leader 也可能会执行 Gather 以下计划的副本。

并行算子

以上简单介绍了并行查询依赖的两个重要基础组件:后台工作进程和动态共享内存。前者用于动态创建 worker,以并行执行子查询计划;后者用于 leader 和 worker 间通信和数据交互。本节介绍 PostgreSQL 目前支持并行执行的算子的实现原理,包括:

  • 并行扫描,如并行顺序扫描,并行索引扫描等;
  • 并行连接,如并行哈希连接,并行 NestLoop 连接等;
  • 并行 Append;

并行扫描

并行扫描的理念很朴素,即启动多个 worker 并行扫描表中的数据。以前一个进程做所有的事情,无人争抢,也无需配合,如今多个 worker 并行扫描,首先需要解决如何分工的问题。

PostgreSQL 中的并行扫描分配策略也很直观,即 block-by-block。多个进程间(leader 和 worker)维护一个全局指针 next,指向下一个需要扫描的 block,一旦某个进程需要获取一个 block,则访问该指针,获取 block 并将指针向前移动。

目前支持并行的常用扫描算子有:SeqScan,IndexScan,BitmapHeapScan 以及 IndexOnlyScan。

下图分别是并行 SeqScan(左)和 并行 IndexScan(右)的原理示意图,可见两者均维护一个 next 指针,不同的是 SeqScan 指向下一个需要扫描的 block,而 IndexScan 指向下一个索引叶子节点。

注意,目前并行 IndexScan 仅支持 B-tree 索引。

image.png

并行 IndexOnlyScan 的原理类似,只是无需根据索引页去查询数据页,从索引页中即可获取到需要的数据;并行 BitmapHeapScan 同样维护一个 next 指针,从下层 BitmapIndexScan 节点构成的位图中依次分配需要扫描的 block。

并行连接

PostgreSQL 支持三种连接算法:NestLoop,MergeJoin 以及 HashJoin。其中 NestLoop 和 MergeJoin 仅支持左表并行扫描,右表无法使用并行;PostgreSQL 11 之前 HashJoin 也仅支持左表并行,PostgreSQL 11 支持了真正的并行 HashJoin,即左右表均可以并行扫描。

以下图左侧的 NestLoop 查询计划为例,NestLoop 左表是并行 Seq Scan,右表是普通的 Index Scan,三个进程(1 个 leader,2 个 worker)分别从左表中并行获取部分数据与右表全量数据做 JOIN。Gather 算子则将子计划的结果向上层算子输出。图中右表是索引扫描,其效率可能还不错,如果右表是全表扫描,则每个进程均需要全表扫描右表。

同理,MergeJoin 也是类似的,左表可以并行扫描,右表不能并行。由于 MergeJoin 要求输入有序,如果右侧计划需要显式排序,则每个进程都需要执行 sort 操作,代价较高,效率较低。

PostgreSQL 10 中的并行 HashJoin 如下图所示,每个子进程都需要扫描右表并构建各自的 HashTable 用于做 HashJoin。

image.png

PostgreSQL 11 实现了真正的并行 HashJoin,所有进程并行扫描右表并构建共享的 HashTable,然后各进程并行扫描左表并执行 HashJoin,避免了 PostgreSQL 10 中各自构建一个私有 HashTable 的开销。

image.png

并行 Append

PostgreSQL 中用 Append 算子表示将多个输入汇聚成一个的操作,往往对应 SQL 语法中的 UNION ALL。在 PostgreSQL 11 中实现了 partition-wise join,如果多个分区表的查询满足特定连接条件(如拆分键上的等值连接),则可将其转换为多个子分区的局部 JOIN,然后再将局部 JOIN 的结果 UNION ALL 起来。具体转换细节以及实现在此不展开,读者可以参考 Ashutosh Bapat 的这篇文章。以下给出一个转换后的示例图:

image.png 在实现并行 Append 之前,Append 算子下的多个孩子节点均只能通过一个进程依次执行,并行 Append 则分配多个 worker 进程,并发执行多个孩子节点,其孩子节点可以是以上介绍的并行执行算子,也可以是普通的算子。

并行查询优化

PostgreSQL 实现了基于代价的优化器,大致流程如下:

  • 考虑执行查询的可能路径(Path);
  • 估算代价,并选择代价最小的路径;
  • 将路径转换为计划供执行器执行;

在并行查询优化中,将路径节点分为两类:

  • parallel-aware 节点,即节点本身可以感知自己在并行执行的节点,如 Parallel Seq Scan;
  • parallel-oblivious 节点,即节点本身意识不到自己在并行执行,但也可能出现在并行执行的子计划中(Gather 以下),如以上提到的并行 NestLoop 计划中的 NestLoop 节点;

并行查询引入了两个新的节点:Gather 和 GatherMerge,前者将并行执行子计划的结果向上层节点输出,不保证有序,后者能够保证输出的顺序。

并行查询优化在生成路径时,会生成部分路径(Partial Paths),所谓 partial,即说明该路径仅处理部分数据,而非全部数据。Partial Paths 从最底层的扫描节点开始,比如 Parallel Seq Scan,就是一个 partial path;包含 partial path 的路径(Gather/GatherMerge 以下)同样也是 partial path,比如我们在 Partial Seq Scan 节点上做聚合操作(Aggregate),此时的聚合操作是对局部数据的聚合,即 Partial Aggregate。随后,优化器会在 partial path 之上添加对应的 Gather/GatherMerge 节点,Gather/GatherMerge 相当于把 partial path 封装成一个整体,对上屏蔽并行的细节。

并行度

既然要并行执行,就需要解决并行度的问题,即评估需要几个 worker。目前,PostgreSQL 仅实现了基于规则的并行度计算,大体包括两部分:

  • 通过 GUC 参数获取并行度
  • 基于表大小评估并行度

并行度相关的 GUC 参数如下:

  • max_parallel_workers_per_gather 每个 Gather/GatherMerge 最大的并行 worker 数(不包含 leader)
  • force_parallel_mode 是否强制使用并行
  • min_parallel_table_scan_size 使用并行扫描的最小表大小,默认 8MB
  • min_parallel_index_scan_size 使用并行扫描的最小索引大小,默认 512KB

image.png

根据表大小计算并行度的公式如下:

log(x / min_parallel_table_scan_size) / log(3) + 1 workers

以 min_parallel_table_scan_size 为默认值 8MB 来计算,表大小在 [8MB, 24MB) 区间时为 1 个 worker,在 [24MB, 72MB) 区间时为 2 个 worker,以此类推。

需要注意的是,尽管在查询优化阶段已经计算了并行度,但最终执行的时候是否会启动对应数量的进程还取决于其他的因素,如最大允许的后台工作进程数(max_worker_processes),最大允许的并行进程数(max_parallel_workers),以及事务隔离级别是否为 serializable(事务隔离级别可能在查询优化以后,真正执行之前发生改变)。一旦无法启动后台工作进程,则由 leader 进程负责运行,即退化为单进程模式。

代价估算

并行查询优化需要估算 Partial Path 的代价以及新加节点 Gather/GatherMerge 的代价.

Partial Path

对于 Partial Path 中的 parallel-aware 节点,比如 Partial Seq Scan,由于多个 worker 并行扫描,每个 worker 处理的数据量减少,CPU 消耗也减少,通过如下方法评估 parallel-aware 的 CPU 代价和处理行数。

  1. 计算并行除数(parallel_divisor)
static double get_parallel_divisor(Path *path) 
{ 	
    double		parallel_divisor = path->parallel_workers;
    if (parallel_leader_participation) 	
    { 		
        double		leader_contribution;  
        leader_contribution = 1.0 - (0.3 * path->parallel_workers);
        if (leader_contribution > 0) 
            parallel_divisor += leader_contribution; 	
     }  	
     return parallel_divisor; 
} 

以上算法说明,worker 越多,leader 就越少参与执行 Gather/GatherMerge 以下的子计划,一旦 worker 数超过 3 个,则 leader 就完全不执行子计划。其中 parallel_leader_participation 是一个 GUC 参数,用户可以显式控制是否需要 leader 参与子计划的执行。

  • 估算 CPU 代价,即 cpu_run_cost /= parallel_divisor ;
  • 估算行数,path->rows / parallel_divisor;

对于 Partial Path 中的 parallel-oblivious 节点,则无需额处理,由于其并不感知自身是否并行,其代价只需要根据下层节点的输入评估即可。

Gather/GatherMerge

查询中引入了两个新的代价值:parallel_tuple_cost 和 parallel_setup_cost。

  • rallel_tuple_cost 每个 Tuple 从 worker 传递给 master 的代价,即 worker 将一个 tuple 放入共享内存队列,然后 master 从中读取的代价,默认值为 0.1
  • parallel_setup_cost 启动并行查询 worker 进程的代价,默认值为 1000

在此不具体介绍这两个节点的代价计算方式,感兴趣的读者可以参考 cost_gather 和 cost_gather_merge 的实现。

并行限制

PostgreSQL 并行查询功能日趋完善,但仍然有很多情况不支持使用并行,这也是未来社区需要解决的问题,主要包括以下场景:

  • 写数据或者锁行的查询均不支持并行,CREATE TABLE ... AS,SELECT INTO,和 CREATE MATERIALIZED VIEW 等创建新表的命令可以并行;
  • 包含 CTE(with…)语句的查询不支持并行;
  • DECLARE CURSOR 不支持并行;
  • 包含 PARALLEL UNSAFE 函数的查询不支持并行;
  • 事务隔离级别为 serializable 时不支持并行;

参考

PostgreSQL 并行查询概述