PostgreSQL是最优秀的对象关系数据库之一,其体系结构是基于进程的,而不是基于线程的。虽然目前几乎所有的数据库系统都使用线程来实现并行性,但是PostgreSQL的基于进程的体系结构是在POSIX线程之前实现的。PostgreSQL在启动时启动一个进程“postmaster”,之后每当一个新的客户端连接到PostgreSQL时,它就会跨越新的进程。
在版本10之前,单个连接中没有并行性。诚然,由于流程架构的原因,来自不同客户机的多个查询可以具有并行性,但它们无法从彼此获得任何性能好处。换句话说,单个查询是串行运行的,没有并行性。这是一个巨大的限制,因为单个查询不能利用多核。PostgreSQL中的并行性是从9.6版引入的。在某种意义上,并行是指一个进程可以有多个线程来查询系统并利用系统中的多核。这就提供了PostgreSQL内部查询并行性。
PostgreSQL中的并行性是作为多个功能的一部分实现的,这些功能包括顺序扫描、聚合和连接。
PostgreSQL中的并行组件
在PostgreSQL中,并行性有三个重要组成部分。这些是过程本身,聚集,和工人。如果没有并行,进程本身将处理所有数据,但是,当planner决定某个查询或其一部分可以并行时,它会在计划的可并行部分中添加一个Gather节点,并生成该子树的Gather根节点。查询执行从流程(leader)级别开始,计划的所有序列部分都由leader运行。但是,如果对查询的任何部分(或全部)启用并允许并行,则为其分配具有一组工作线程的gather节点。工作线程是与需要并行化的部分树(部分计划)并行运行的线程。关系的块在线程之间被划分,这样关系就保持顺序。线程数由PostgreSQL配置文件中设置的设置控制。工人使用共享内存进行协调/交流,一旦工人完成工作,结果将传递给领导进行积累。
并行顺序扫描
在PostgreSQL 9.6中,增加了对并行顺序扫描的支持。顺序扫描是对一个表的扫描,在这个表中,一个块序列一个接一个地被求值。这就其本质而言,允许并行性。所以这是第一个并行实现的自然候选。在这种情况下,整个表在多个工作线程中被顺序扫描。这里是一个简单的查询,我们在这里查询pgbench_accounts表行(63165),它有150000000个元组。总执行时间为4343080ms。由于没有定义索引,因此使用顺序扫描。整个表在一个没有线程的进程中被扫描。因此,无论有多少可用内核,都要使用CPU的单核。
db=# EXPLAIN ANALYZE SELECT *
FROM pgbench_accounts
WHERE abalance > 0;
QUERY PLAN
----------------------------------------------------------------------
Seq Scan on pgbench_accounts (cost=0.00..73708261.04 rows=1 width=97)
(actual time=6868.238..4343052.233 rows=63165 loops=1)
Filter: (abalance > 0)
Rows Removed by Filter: 1499936835
Planning Time: 1.155 ms
Execution Time: 4343080.557 ms
(5 rows)
如果这些150000000行在一个进程中使用“10”个工作线程并行扫描呢?它将大大缩短执行时间。
db=# EXPLAIN ANALYZE select * from pgbench_accounts where abalance > 0;
QUERY PLAN
----------------------------------------------------------------------
Gather (cost=1000.00..45010087.20 rows=1 width=97)
(actual time=14356.160..1628287.828 rows=63165 loops=1)
Workers Planned: 10
Workers Launched: 10
-> Parallel Seq Scan on pgbench_accounts
(cost=0.00..45009087.10 rows=1 width=97)
(actual time=43694.076..1628068.096 rows=5742 loops=11)
Filter: (abalance > 0)
Rows Removed by Filter: 136357894
Planning Time: 37.714 ms
Execution Time: 1628295.442 ms
(8 rows)
现在,总的执行时间是1628295ms;这是一个266%的改进,而使用10个工人线程用于扫描。
用于基准的查询:从abalance>0的pgbench_帐户中选择*;
表大小:426GB
表中总行:150000000
用于基准测试的系统:
CPU:2个Intel(R)Xeon(R)CPU E5-2643 v2@3.50GHz
内存:256GB DDR3 1600
磁盘:ST3000NM0033
上图清楚地显示了并行性如何提高顺序扫描的性能。添加单个工作进程时,由于没有获得并行性,性能下降是可以理解的,但是创建额外的聚集节点和单个工作会增加开销。但是,使用多个工作线程时,性能会显著提高。另外,需要注意的是,性能不会以线性或指数方式增加。它会逐渐改善,直到增加更多的工人不会给性能带来任何提升;有点像接近水平渐近线。这个基准测试是在一个64核的机器上执行的,很明显,拥有10个以上的工人不会显著提高性能。
平行聚合
在数据库中,计算聚合是非常昂贵的操作。当在单个过程中进行评估时,这些过程需要相当长的时间。在PostgreSQL 9.6中,通过简单地将它们分成块(一种分而治之的策略)来增加并行计算这些数据的能力。这允许多个工人在领导计算基于这些计算的最终值之前计算聚合部分。更严格地说,PartialAggregate节点被添加到一个计划树中,每个PartialAggregate节点从一个worker获取输出。这些输出随后发送到FinalizeAggregate节点,该节点组合来自多个(所有)PartialAggregate节点的聚合。因此,有效的并行部分计划包括一个FinalizeAggregate节点和一个Gather节点,后者将PartialAggregate节点作为子节点。
db=# EXPLAIN ANALYZE SELECT count(*) from pgbench_accounts;
QUERY PLAN
----------------------------------------------------------------------
Aggregate (cost=73708261.04..73708261.05 rows=1 width=8)
(actual time=2025408.357..2025408.358 rows=1 loops=1)
-> Seq Scan on pgbench_accounts (cost=0.00..67330666.83 rows=2551037683 width=0)
(actual time=8.162..1963979.618 rows=1500000000 loops=1)
Planning Time: 54.295 ms
Execution Time: 2025419.744 ms
(4 rows)
下面是并行计算聚合时计划的示例。在这里你可以清楚地看到性能的提高。
db=# EXPLAIN ANALYZE SELECT count(*) from pgbench_accounts;
QUERY PLAN
----------------------------------------------------------------------
Finalize Aggregate (cost=45010088.14..45010088.15 rows=1 width=8)
(actual time=1737802.625..1737802.625 rows=1 loops=1)
-> Gather (cost=45010087.10..45010088.11 rows=10 width=8)
(actual time=1737791.426..1737808.572 rows=11 loops=1)
Workers Planned: 10
Workers Launched: 10
-> Partial Aggregate
(cost=45009087.10..45009087.11 rows=1 width=8)
(actual time=1737752.333..1737752.334 rows=1 loops=11)
-> Parallel Seq Scan on pgbench_accounts
(cost=0.00..44371327.68 rows=255103768 width=0)
(actual time=7.037..1731083.005 rows=136363636 loops=11)
Planning Time: 46.031 ms
Execution Time: 1737817.346 ms
(8 rows)
对于并行聚合,在这种特殊情况下,当涉及10个并行工作线程时,执行时间2025419.744减少到1737817.346,我们的性能提升略高于16%。
用于基准的查询:从abalance>0的pgbench_帐户中选择count(*);
表大小:426GB
表中总行:150000000
用于基准测试的系统:
CPU:2个Intel(R)Xeon(R)CPU E5-2643 v2@3.50GHz
内存:256GB DDR3 1600
磁盘:ST3000NM0033
并行索引(B树)扫描
对B树索引的并行支持意味着索引页被并行扫描。B树索引是PostgreSQL中最常用的索引之一。在并行版本的B-Tree中,一个worker扫描B-Tree,当它到达它的叶节点时,它会扫描块并触发阻塞的等待worker扫描下一个块。
困惑的?我们来看一个例子。假设我们有一个具有id和name列的表foo,其中有18行数据。我们在表foo的id列上创建一个索引。系统列CTID附加在表的每一行,用于标识行的物理位置。CTID列中有两个值:块号和偏移量。
postgres=# <strong>SELECT</strong> ctid, id <strong>FROM</strong> foo;
ctid | id
--------+-----
(0,55) | 200
(0,56) | 300
(0,57) | 210
(0,58) | 220
(0,59) | 230
(0,60) | 203
(0,61) | 204
(0,62) | 300
(0,63) | 301
(0,64) | 302
(0,65) | 301
(0,66) | 302
(1,31) | 100
(1,32) | 101
(1,33) | 102
(1,34) | 103
(1,35) | 104
(1,36) | 105
(18 rows)
让我们在该表的id列上创建B树索引。
CREATE INDEX foo_idx ON foo(id)
假设我们要选择id<=200的值,其中有两个工人。Worker-0将从根节点开始扫描,直到叶节点200。它将把节点105下的下一个块移交给Worker-1,Worker-1处于阻塞等待状态。如果还有其他工人,就把街区分成工人区。重复类似的模式,直到扫描完成。
并行位图扫描
要并行化位图堆扫描,我们需要能够以非常类似于并行顺序扫描的方式在工作线程之间划分块。为此,将对一个或多个索引进行扫描,并创建指示要访问哪些块的位图。这是由一个引导进程完成的,即扫描的这一部分是按顺序运行的。然而,当识别出的块被传递给workers时,并行性就会启动,就像并行顺序扫描一样。
平行连接
合并联接支持中的并行性也是此版本中添加的最热门功能之一。在这种情况下,表与其他表的内部循环哈希或合并相连接。在任何情况下,内部循环中都不支持并行性。整个循环作为一个整体进行扫描,并行性在每个工作进程作为一个整体执行内部循环时发生。发送到gather的每个连接的结果都会累积并产生最终结果。
摘要
从我们在本博客中已经讨论过的内容中可以明显看出,并行性在某些情况下会显著提高性能,而在某些情况下会导致性能下降。确保已正确设置并行设置成本或并行元组成本,以使查询计划器能够选择并行计划。即使为这些gui设置了较低的值,如果没有生成并行计划,请参阅PostgreSQL并行性文档以了解详细信息。
对于并行计划,您可以获取每个计划节点的每个工作进程统计信息,以了解如何在工作进程之间分配负载。你可以通过解释(分析,详细)来做到这一点。与任何其他性能特性一样,没有一条规则适用于所有工作负载。无论需要什么,都应该仔细配置并行性,并且必须确保获得性能的概率明显高于性能下降的概率。
原文:https://www.percona.com/blog/2019/07/30/parallelism-in-postgresql/
本文:http://jiagoushi.pro/node/1049
讨论:请加入知识星球【首席架构师圈】或者小号【jiagoushi_pro】
最新内容
- 4 days 10 hours ago
- 4 days 10 hours ago
- 4 days 11 hours ago
- 4 days 11 hours ago
- 4 days 11 hours ago
- 1 week 3 days ago
- 1 week 4 days ago
- 1 week 6 days ago
- 1 week 6 days ago
- 1 week 6 days ago