概念描述

pg_prewarm 是一个辅助数据库在重启后预热重启前的数据,防止在重启后,数据库内存中并没有数据进行数据预读,这样的情况下,系统在第一次査询数据的时候后会比较慢,等待数据LOAD 仅内存中。

pg_prewarm 模块提供了一种方便的方式将关系数据加载到操作系统缓冲区缓存或 PostgreSQL 缓冲区缓存中。预热可以使用 pg_prewarm 函数手动执行,也可以通过在 shared_preload_libraries 中包含 pg_prewarm 来自动执行。在自动执行情况下,系统将运行一个后台进程,定期将共享缓冲区的内容记录在一个名为 autoprewarm.blocks 的文件中。

当发起"select * from XXX"时,数据会加载到操作系统缓存然后才到shared buffer。PostgreSQL缓存读顺序share_buffers -> 操作系统缓存 -> 硬盘。同样当将脏页向磁盘刷写时,也是先到操作系统缓存,然后由操作系统调用fsync()将操作系统缓存中数据持久化到磁盘。这样PG实际上由两份数据,看起来有些浪费空间,但是操作系统缓存是一个简单的LRU而不是数据库优化的clock sweep algorithm。一旦在shared_buffers中命中,那么读就不会下沉到操作系统缓存。如果shared buffer和操作系统缓存有相同页,操作系统缓存中的页很快会被驱逐替换。

什么参数能影响操作系统的fsync将脏页刷回磁盘吗?

当然,通过postgresql.conf中参数bgwriter_flush_after,该参数整型,默认512KB。当后台写进程写了这么多数据时,会强制OS发起sync将cache中数据刷到底层存储。这样会限制内核页缓存中的脏数据数量,从而减小checkpoint时间或者后台大批量写回数据的时间。

不仅仅是bgwriter,即使checkpoint进程和用户进程也从shared buffer刷写脏页到OS cache。可以通过checkpoint_flush_after影响checkpoint进程的fsync,通过backend_flush_after影响后台进程的fsync。

pg_prewarm 它可以用于在系统重启时,手动加载经常访问的表到操作系统的cache或PG的shared buffer,从而减少检查系统重启对应用的影响。

auxdb=# \df pg_prewarm
List of functions
-[ RECORD 1 ]-------+------------------------------------------------------------------------------------------------------------------------------------------------------------
Schema              | public
Name                | pg_prewarm
Result data type    | bigint
Argument data types | regclass, mode text DEFAULT 'buffer'::text, fork text DEFAULT 'main'::text, first_block bigint DEFAULT NULL::bigint, last_block bigint DEFAULT NULL::bigint
Type                | normal
fencedmode          | f
propackage          | f
prokind             | f

备注: regclass 参数为数据库对像,通常情况为表名;

modex 参数指加载模式,可选项有 'prefetch', 'read','buffer', 默认为 'buffer' 具体稍后介绍;

fork 表示对像模式,可选项有 'main', 'fsm', 'vm', 默认为 'main', first_block 表示开始prewarm的数据块,last_block 表示最后 prewarm 的数据块.

pg_prewarm 加载模式

mode 参数指加载模式,可选项有 'prefetch', 'read','buffer', 默认为 'buffer'.
prefetch: 异步地将数据预加载到操作系统缓存;
read: 最终结果和 prefetch 一样,但它是同步方式,支持所有平台.
buffer: 将数据预加载到数据库缓存
postgres=# explain analyze select count(*) from tmp_t0;
                                QUERY PLAN                                 
-----------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate (cost=225497.55..225497.56 rows=1 width=8) (actual time=1291.463..1292.254 rows=1 loops=1)
 -> Gather (cost=225497.33..225497.54 rows=2 width=8) (actual time=1291.353..1292.247 rows=3 loops=1)
    Workers Planned: 2
    Workers Launched: 2
    -> Partial Aggregate (cost=224497.33..224497.34 rows=1 width=8) (actual time=1286.237..1286.238 rows=1 loops=3)
       -> Parallel Seq Scan on tmp_t0 (cost=0.00..214080.67 rows=4166667 width=0) (actual time=6.579..1137.312 rows=3333333 loops=3)
 Planning Time: 0.214 ms
 Execution Time: 1292.363 ms
(8 rows)
可以看到,近1G的表,全表扫描一遍,耗时1.2s
postgres=# select pg_prewarm('tmp_t0', 'read', 'main');
 pg_prewarm 
------------
  172414
(1 row)

postgres=# explain analyze select count(*) from tmp_t0;
                                QUERY PLAN                                 
----------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate (cost=225497.55..225497.56 rows=1 width=8) (actual time=437.033..437.697 rows=1 loops=1)
 -> Gather (cost=225497.33..225497.54 rows=2 width=8) (actual time=436.866..437.689 rows=3 loops=1)
    Workers Planned: 2
    Workers Launched: 2
    -> Partial Aggregate (cost=224497.33..224497.34 rows=1 width=8) (actual time=434.013..434.014 rows=1 loops=3)
       -> Parallel Seq Scan on tmp_t0 (cost=0.00..214080.67 rows=4166667 width=0) (actual time=0.039..305.793 rows=3333333 loops=3)
 Planning Time: 0.083 ms
 Execution Time: 437.726 ms
(8 rows)
时间降至4秒多!这时反复执行全表扫描,时间稳定在0.4秒多。

尝试buffer模式:
postgres=# select pg_prewarm('tmp_t0', 'buffer', 'main');
 pg_prewarm 
------------
  172414
(1 row)

postgres=# explain analyze select count(*) from tmp_t0;
                                QUERY PLAN                                 
----------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate (cost=225497.55..225497.56 rows=1 width=8) (actual time=480.060..482.189 rows=1 loops=1)
 -> Gather (cost=225497.33..225497.54 rows=2 width=8) (actual time=479.862..482.180 rows=3 loops=1)
    Workers Planned: 2
    Workers Launched: 2
    -> Partial Aggregate (cost=224497.33..224497.34 rows=1 width=8) (actual time=475.604..475.605 rows=1 loops=3)
       -> Parallel Seq Scan on tmp_t0 (cost=0.00..214080.67 rows=4166667 width=0) (actual time=0.035..336.157 rows=3333333 loops=3)
 Planning Time: 0.068 ms
 Execution Time: 482.220 ms
(8 rows)
比read模式时间略少,但相差不大。可见,如果操作系统的cache够大,数据取到OS cache还是shared buffer对执行时间影响不大(在不考虑其他应用影响PG的情况下)

尝试prefetch模式,即异步预取。这里,有意在pg_prewarm返回后,立即执行全表查询。这样在执行全表查询时,可能之前的预取还没完成,从而使全表查询和预取并发进行,缩短了总的响应时间:
postgres=# select pg_prewarm('tmp_t0', 'prefetch', 'main');
 pg_prewarm 
------------
  172414
(1 row)

postgres=# explain analyze select count(*) from tmp_t0;
                                QUERY PLAN                                 
----------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate (cost=225497.55..225497.56 rows=1 width=8) (actual time=434.313..436.782 rows=1 loops=1)
 -> Gather (cost=225497.33..225497.54 rows=2 width=8) (actual time=434.155..436.774 rows=3 loops=1)
    Workers Planned: 2
    Workers Launched: 2
    -> Partial Aggregate (cost=224497.33..224497.34 rows=1 width=8) (actual time=431.066..431.067 rows=1 loops=3)
       -> Parallel Seq Scan on tmp_t0 (cost=0.00..214080.67 rows=4166667 width=0) (actual time=0.034..307.687 rows=3333333 loops=3)
 Planning Time: 0.061 ms
 Execution Time: 436.816 ms
(8 rows)
可以看到,总的完成时间是0.4秒多,使用pg_prewarm做预取大大缩短了总时间。因此在进行全表扫描前,做一次异步的prewarm,不失为一种优化全表查询的方法。
问题点:
执行1次select * from 不就可以将表的数据读入shared buffer和OS cache而实现预热了吗?岂不是比做这样一个插件更简单?实际上,对于较大的表(大小超过shared buff的1/4),进行全表扫描时,PG认为没必要为这种操作使用所有shared buffer,只会让其使用很
少的一部分buffer,一般只有几百K,所以,预热大表是不能用一个查询直接实现的


测试验证

1.安装 pg_prewarm 扩展

[postgres@test01 ~]$ cd postgresql-15.3/contrib/pg_prewarm
[postgres@test01 pg_prewarm]$ make
[postgres@test01 pg_prewarm]$ make install

2.修改 postgresql.conf

[postgres@test01 ~]$ cat pgsql/data/postgresql.conf | grep shared_preload_libraries
shared_preload_libraries = 'pg_prewarm' # (change requires restart)

重启postgres服务

[postgres@test01 ~]$ pg_ctl stop
waiting for server to shut down.... done
server stopped
[postgres@test01 ~]$ pg_ctl -D /home/postgres/pgsql/data -l logfile start
waiting for server to start.... done
server started

3.创建 pg_prewarm 扩展

postgres=# create extension pg_prewarm;
CREATE EXTENSION
postgres=# \dx+ pg_prewarm
           Objects in extension "pg_prewarm"
                  Object description                   
-------------------------------------------------------
 function autoprewarm_dump_now()
 function autoprewarm_start_worker()
 function pg_prewarm(regclass,text,text,bigint,bigint)
(3 rows)

从上面的输出可知,pg_prewarm 扩展自动创建了3个与数据预热相关的函数。
数据的预热主要是通过函数 pg_prewarm 来完成。

是否启用由参数 pg_prewarm.autoprewarm 控制,默认这个参数为on,这个参数只能在服务器启动时设置。

postgres=# show pg_prewarm.autoprewarm;
 pg_prewarm.autoprewarm 
------------------------
 on
(1 row)

4.创建测试表,并插入300万条数据

postgres=# create table t_prewarm (id int, name varchar(20));
CREATE TABLE
postgres=# insert into t_prewarm select i,'name'||i from generate_series(1,3000000) i;
INSERT 0 3000000

5.执行查询语句,并查看执行计划

postgres=# explain (analyze,buffers,timing) select * from t_prewarm;
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Seq Scan on t_prewarm  (cost=0.00..46217.00 rows=3000000 width=15) (actual time=0.021..158.558 rows=3000000 loops=1)
   Buffers: shared read=16217
 Planning:
   Buffers: shared hit=14 read=9 dirtied=2
 Planning Time: 1.137 ms
 Execution Time: 244.942 ms
(6 rows)

postgres=# explain (analyze,buffers,timing) select * from t_prewarm;
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Seq Scan on t_prewarm  (cost=0.00..46217.00 rows=3000000 width=15) (actual time=0.038..151.646 rows=3000000 loops=1)
   Buffers: shared hit=32 read=16185
 Planning Time: 0.034 ms
 Execution Time: 237.989 ms
(4 rows)

postgres=# explain (analyze,buffers,timing) select * from t_prewarm;
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Seq Scan on t_prewarm  (cost=0.00..46217.00 rows=3000000 width=15) (actual time=0.033..342.701 rows=3000000 loops=1)
   Buffers: shared hit=64 read=16153
 Planning Time: 0.031 ms
 Execution Time: 553.615 ms
(4 rows)

总共执行3次,第1次从磁盘上读取了16217个块,第2次16185个,第3次16153个。

6.重启pg实例,手工预热数据后再次查看执行计划

[postgres@test01 ~]$ pg_ctl stop
waiting for server to shut down.... done
server stopped
[postgres@test01 ~]$ pg_ctl -D /home/postgres/pgsql/data -l logfile start
waiting for server to start.... done
server started
[postgres@test01 ~]$ psql
psql (15.3)
Type "help" for help.

postgres=# select pg_prewarm('t_prewarm');
 pg_prewarm 
------------
      16217
(1 row)

postgres=# explain (analyze,buffers,timing) select * from t_prewarm;
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Seq Scan on t_prewarm  (cost=0.00..46217.00 rows=3000000 width=15) (actual time=0.017..250.165 rows=3000000 loops=1)
   Buffers: shared hit=16179 read=38
 Planning:
   Buffers: shared hit=14 read=9
 Planning Time: 0.701 ms
 Execution Time: 419.241 ms
(6 rows)

可以看到有16179个块在内存中命中。

7.数据的自动预热

当 pg_prewarm 扩展安装且配置成功后,pg会周期性的把共享内存中的内容记录在 autoprewarm.blocks 文件中,并在实例重启启动时读取该文件,以达到数据预热的目的。该文件位于$PGDATA目录下。

[postgres@test01 ~]$ ls -l $PGDATA | grep autoprewarm
-rw------- 1 postgres postgres 332179 Apr 27 12:21 autoprewarm.blocks

在默认情况下,pg_prewarm 扩展每间隔5分钟就将内存中的数据写入 autoprewarm.blocks 文件,这是由参数 pg_prewarm.autoprewarm_interval 控制。

postgres=# show pg_prewarm.autoprewarm_interval;
 pg_prewarm.autoprewarm_interval 
---------------------------------
 5min
(1 row)

在重启pg实例时,通过后台进程 autoprewarm leader 将预热数据文件 autoprewarm.blocks 重新加载到内存中。

[postgres@test01 ~]$ ps -ef | grep prewarm | grep -v grep
postgres 25708 25702  0 12:23 ?        00:00:00 postgres: autoprewarm leader

参考文档

https://www.postgresql.org/docs/15/pgprewarm.html