PgSQL · 特性分析· pg_prewarm
最后更新于:2022-04-01 10:41:27
PostgreSQL内核中引入了一个很有意思的插件,pg_prewarm。它可以用于在系统重启时,手动加载经常访问的表到操作系统的cache或PG的shared buffer,从而减少检查系统重启对应用的影响。这个插件是这个通过这个[patch](http://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=c32afe53c2e87a56e2ff930798a5588db0f7a516)加入PG内核的。 pg_prewarm的开发者在设计pg_prewarm时,把它设计成一个执行单一任务的工具,尽求简单,所以我们看到的pg_prearm功能和实现都非常简单。下面我们对它进行性能实测并分析一下它的实现。
**基本信息**
利用下面的语句可以创建此插件:
~~~
create EXTENSION pg_prewarm;
~~~
实际上,创建插件的过程只是用下面的语句创建了pg_prewarm函数。这个函数是此插件提供的唯一函数:
~~~
CREATE FUNCTION pg_prewarm(regclass,
mode text default 'buffer',
fork text default 'main',
first_block int8 default null,
last_block int8 default null)
RETURNS int8
AS 'MODULE_PATHNAME', 'pg_prewarm'
LANGUAGE C
~~~
函数的第一个参数是要做prewarm的表名,第二个参数是prewarm的模式(prefetch模式表示异步预取到操作系统cache;read表示同步预取;buffer则表示同步读入到PG的shared buffer),第三个参数是relation fork的类型(一般用main,其他类型有visibilitymap和fsm,参见[[1]](https://github.com/postgres/postgres/blob/4baaf863eca5412e07a8441b3b7e7482b7a8b21a/src/include/common/relpath.h)[[2]](https://github.com/postgres/postgres/blob/b819dd7cb55aed1d607cef36b0ecd1a0642872b2/src/backend/storage/smgr/README)),最后两个参数是开始和结束的block number(一个表的block number从0开始,block总数可以通过pg_class系统表的relpages字段获得)。
**性能实测**
再来看看,这个prewarm性能上能达到多大效果。我们先将PG的shared buffer设为2G,OS总的memory有7G。然后创建下面的大小近1G的表test:
~~~
pgbench=# \d test
Table "public.test"
Column | Type | Modifiers
--------+---------------+-----------
name | character(20) |
~~~
~~~
pgbench=# SELECT pg_size_pretty(pg_total_relation_size('test'));
pg_size_pretty
----------------
995 MB
~~~
在每次都清掉操作系统cache和PG的shared buffer的情况下,分别测试下面几种场景:
1)不进行pg_prewarm的情况:
~~~
pgbench=# explain analyze select count(*) from test;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=377389.91..377389.92 rows=1 width=0) (actual time=22270.304..22270.304 rows=1 loops=1)
-> Seq Scan on test (cost=0.00..327389.73 rows=20000073 width=0) (actual time=0.699..18287.199 rows=20000002 loops=1)
Planning time: 0.134 ms
Execution time: 22270.383 ms
~~~
可以看到,近1G的表,全表扫描一遍,耗时22秒多。
2)下面我们先做read这种模式的prewarm,test表的数据被同步读入操作系统cache(pg_prewarm返回的是处理的block数目,此处我们没指定block number,也就是读入test的所有block),然后再做全表扫:
~~~
pgbench=# select pg_prewarm('test', 'read', 'main');
pg_prewarm
------------
127389
~~~
~~~
pgbench=# explain analyze select count(*) from test;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=377389.90..377389.91 rows=1 width=0) (actual time=8577.767..8577.767 rows=1 loops=1)
-> Seq Scan on test (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.086..4716.444 rows=20000002 loops=1)
Planning time: 0.049 ms
Execution time: 8577.831 ms
~~~
时间降至8秒多!这时反复执行全表扫描,时间稳定在8秒多。
3)再尝试buffer模式:
~~~
pgbench=# select pg_prewarm('test', 'buffer', 'main');
pg_prewarm
------------
127389
~~~
~~~
pgbench=# explain analyze select count(*) from test;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=377389.90..377389.91 rows=1 width=0) (actual time=8214.277..8214.277 rows=1 loops=1)
-> Seq Scan on test (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.015..4250.300 rows=20000002 loops=1)
Planning time: 0.049 ms
Execution time: 8214.340 ms
~~~
比read模式时间略少,但相差不大。可见,如果操作系统的cache够大,数据取到OS cache还是shared buffer对执行时间影响不大(在不考虑其他应用影响PG的情况下)。
4)最后尝试prefetch模式,即异步预取。这里,我们有意在pg_prewarm返回后,立即执行全表查询。这样在执行全表查询时,可能之前的预取还没完成,从而使全表查询和预取并发进行,缩短了总的响应时间:
~~~
explain analyze select pg_prewarm('test', 'prefetch', 'main');
QUERY PLAN
------------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0) (actual time=1011.338..1011.339 rows=1 loops=1)
Planning time: 0.124 ms
Execution time: 1011.402 ms
~~~
~~~
explain analyze select count(*) from test;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=377389.90..377389.91 rows=1 width=0) (actual time=8420.652..8420.652 rows=1 loops=1)
-> Seq Scan on test (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.065..4583.200 rows=20000002 loops=1)
Planning time: 0.344 ms
Execution time: 8420.723 ms
~~~
可以看到,总的完成时间是9秒多,使用pg_prewarm做预取大大缩短了总时间。因此在进行全表扫描前,做一次异步的prewarm,不失为一种优化全表查询的方法。
**实现**
pg_prewarm的代码只有一个pg_prewarm.c文件。可以看出,prefetch模式下,对于表的每个block,调用一次PrefetchBuffer,后面的调用为:
~~~
PrefetchBuffer -> smgrprefetch -> mdprefetch -> FilePrefetch -> posix_fadvise(POSIX_FADV_WILLNEED)
~~~
可见,它是最终调用posix_fadvise,把读请求交给操作系统,然后返回,实现的异步读取。
而在read和buffer模式(调用逻辑分别如下)中,最终都调用了系统调用read,来实现同步读入OS cache和shared buffer的(注意buffer模式实际上是先读入OS cache,再拷贝到shared buffer):
~~~
read模式:smgrread -> mdread -> FileRead -> read
~~~
~~~
buffer模式:ReadBufferExtended -> ReadBuffer_common -> smgrread -> mdread -> FileRead -> read
~~~
**问题**
可能有人比较疑惑:执行1次select * from 不就可以将表的数据读入shared buffer和OS cache而实现预热了吗?岂不是比做这样一个插件更简单?实际上,对于较大的表(大小超过shared buff的1/4),进行全表扫描时,PG认为没必要为这种操作使用所有shared buffer,只会让其使用很少的一部分buffer,一般只有几百K,详细描述可以参见[关于BAS_BULKREAD策略的代码](https://github.com/postgres/postgres/blob/4baaf863eca5412e07a8441b3b7e7482b7a8b21a/src/include/storage/bufmgr.h)和[README](https://github.com/postgres/postgres/tree/17792bfc5b62f42a9dfbd2ac408e7e71c239330a/src/backend/storage/buffer))。所以,预热大表是不能用一个查询直接实现的,而pg_prewarm正是在这方面大大方便了用户。