暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

从一次提交聊聊饱受诟病的双缓存架构

原创 xiongcc 2024-04-16
329

前言

四月 6 号,由 Palak Chaturvedi 创作,Thomas Munro 提交了一个功能——驱逐缓冲,https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=13453eedd3f692f8dcf8e334396eee84f00fdde2,这个功能十分有用,尤其是要验证缓冲对于性能影响的时候,以往,我们需要使用 pg_dropcache 插件 (不再维护,兼容到 12),或者手动重启数据库,同时为了清空 page cache,还需要使用额外的 echo xx > /proc/sys/vm/drop_caches,比较麻烦。在 17 版本中,pg_buffercache 支持了 pg_buffercache_evict,顾名思义,排除指定缓冲区。

借此机会,也顺便聊聊 PostgreSQL 经常被人诟病的双缓存。

小试牛刀

让我们试一下

postgres=# \dx+ pg_buffercache 
 Objects in extension "pg_buffercache"
           Object description           
----------------------------------------
 function pg_buffercache_evict(integer)
 function pg_buffercache_pages()
 function pg_buffercache_summary()
 function pg_buffercache_usage_counts()
 type pg_buffercache
 type pg_buffercache[]
 view pg_buffercache
(7 rows)

postgres=# show shared_buffers ;
 shared_buffers 
----------------
 128MB
(1 row)

postgres=# create table test(id int,info text);
CREATE TABLE
postgres=# insert into test select n,'test' from generate_series(1,10000000) as n;
INSERT 0 10000000
postgres=# analyze test;
ANALYZE
postgres=# select reltuples,relpages from pg_class where relname = 'test';
   reltuples   | relpages 
---------------+----------
 1.0000175e+07 |    54055
(1 row)
复制

然后重启一下,释放 shared buffers

[postgres@mypg ~]$ psql
psql (17devel)
Type "help" for help.

postgres=# select count(*) from pg_buffercache where relfilenode = 'test'::regclass;
 count 
-------
     0
(1 row)

postgres=# select pg_relation_filepath('test');
 pg_relation_filepath 
----------------------
 base/5/16405
(1 row)
复制

现在已经为 0 了。但是由于 PostgreSQL 的 Double Buffering 问题,因此无法知晓是否真正发生了 IO。

图片

数据库系统区分逻辑读 (来自 RAM,即来自缓冲区缓存) 和物理读 (来自磁盘)。从 PostgreSQL 的角度来看,页面既可以从缓冲区缓存中读取,也可以从操作系统请求,但是在后一种情况下,无法判断它是在 RAM 中找到的,还是从磁盘读取的。

不过我们可以借助工具,比如 fincore,fincore 的工作原理是将指定文件的相应 Inode Data 与 Kernel 的 Page Cache Table 做对比,如果 Page Cache Table 有这个 Inode 信息,就找到该 Inode 对应的 Data Block 的大小。不过这个工具不再维护了,pcstat 与 fincore 类似,使用 go 开发。

让我们使用 pcstat 瞅瞅,可以看到,test 这个表全部都在 page cache 中。

[postgres@mypg ~]$ ./pcstat 17data/base/5/16405
|---------------------+----------------+------------+-----------+---------|
| Name                | Size           | Pages      | Cached    | Percent |
|---------------------+----------------+------------+-----------+---------|
| 17data/base/5/16405 | 442818560      | 108110     | 108110    | 100.000 |
|---------------------+----------------+------------+-----------+---------|
复制

为了更加可靠验证,让我们清空一下页缓存

[root@mypg ~]# echo 3 > /proc/sys/vm/drop_caches
[root@mypg ~]# su - postgres
Last login: Tue Apr 16 17:39:04 CST 2024 on pts/0
[postgres@mypg ~]$ ./pcstat 17data/base/5/16405
|---------------------+----------------+------------+-----------+---------|
| Name                | Size           | Pages      | Cached    | Percent |
|---------------------+----------------+------------+-----------+---------|
| 17data/base/5/16405 | 442818560      | 108110     | 0         | 000.000 |
|---------------------+----------------+------------+-----------+---------|
复制

然后查询一下总行数,计算一下耗时:

postgres=# explain (buffers,MEMORY,analyze) select count(*) from test;
                                                                 QUERY PLAN                                                                 
--------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=107138.55..107138.56 rows=1 width=8) (actual time=3997.807..3998.373 rows=1 loops=1)
   Buffers: shared hit=256 read=53799
   ->  Gather  (cost=107138.33..107138.54 rows=2 width=8) (actual time=3996.857..3998.362 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         Buffers: shared hit=256 read=53799
         ->  Partial Aggregate  (cost=106138.33..106138.34 rows=1 width=8) (actual time=3837.345..3837.346 rows=1 loops=3)
               Buffers: shared hit=256 read=53799
               ->  Parallel Seq Scan on test  (cost=0.00..95721.67 rows=4166667 width=0) (actual time=1.151..3409.149 rows=3333333 loops=3)
                     Buffers: shared hit=256 read=53799
 Planning:
   Buffers: shared hit=14 read=5
   Memory: used=12368 bytes  allocated=16384 bytes
 Planning Time: 29.235 ms
 Execution Time: 3998.479 ms
(15 rows)
复制

说明在没有任何缓存的情况下,总共耗时约 4 秒。

让我们再次运行一下

postgres=# explain (buffers,MEMORY,analyze) select count(*) from test;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=107138.55..107138.56 rows=1 width=8) (actual time=1342.257..1342.324 rows=1 loops=1)
   Buffers: shared hit=352 read=53703
   ->  Gather  (cost=107138.33..107138.54 rows=2 width=8) (actual time=1341.114..1342.312 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         Buffers: shared hit=352 read=53703
         ->  Partial Aggregate  (cost=106138.33..106138.34 rows=1 width=8) (actual time=1331.432..1331.433 rows=1 loops=3)
               Buffers: shared hit=352 read=53703
               ->  Parallel Seq Scan on test  (cost=0.00..95721.67 rows=4166667 width=0) (actual time=0.038..727.401 rows=3333333 loops=3)
                     Buffers: shared hit=352 read=53703
 Planning:
   Memory: used=12368 bytes  allocated=16384 bytes
 Planning Time: 0.064 ms
 Execution Time: 1342.364 ms
(14 rows)

postgres=# select count(*) from pg_buffercache where relfilenode = 'test'::regclass;
 count 
-------
     0
(1 row)
复制

这次由于 Ring Buffer 的原因,可以看到,shared buffers 里面没有任何 test 的缓存,不难理解,Ring Buffer 就是为了避免缓冲区被污染,当大表的大小超过了缓冲区缓存的 1/4 时便会使用。环形缓冲区占用 256 kB。其他更多策略可以参照 Internal DB,或者 PostgreSQL 14 internal。

但是还是可以看到,时间快了接近 3 倍,这便是 page cache 的原因。

[postgres@mypg ~]$ ./pcstat 17data/base/5/16405
|---------------------+----------------+------------+-----------+---------|
| Name                | Size           | Pages      | Cached    | Percent |
|---------------------+----------------+------------+-----------+---------|
| 17data/base/5/16405 | 442818560      | 108110     | 108110    | 100.000 |
|---------------------+----------------+------------+-----------+---------|
复制

可以看到,这个表全部在操作系统页缓存里面。因此,生产中对于延迟极为敏感的应用,可以考虑使用 pgfincore/pg_prewarm 进行预热。

缓冲逐出

让我们验证一下逐出这个功能

postgres=# create table t1(id int);
CREATE TABLE
postgres=# insert into t1 values(generate_series(1,1000));
INSERT 0 1000
postgres=# analyze t1;
ANALYZE
postgres=# select bufferid from pg_buffercache where relfilenode = 't1'::regclass;
 bufferid 
----------
      990
      991
      992
      993
      994
      995
      996
      997
(8 rows)

postgres=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/5/16408
(1 row)

postgres=# \q
[postgres@mypg ~]$ ./pcstat 17data/base/5/16408
|---------------------+----------------+------------+-----------+---------|
| Name                | Size           | Pages      | Cached    | Percent |
|---------------------+----------------+------------+-----------+---------|
| 17data/base/5/16408 | 40960          | 10         | 10        | 100.000 |
|---------------------+----------------+------------+-----------+---------|
复制

老样子,这个表也全部在页缓存中。然后,让我们逐出 t1 表的所有 buffer

postgres=# select pg_buffercache_evict(bufferid) from pg_buffercache where relfilenode = 't1'::regclass;
 pg_buffercache_evict 
----------------------
 t
 t
 t
 t
 t
 t
 t
 t
(8 rows)

postgres=# select count(*) from pg_buffercache where relfilenode = 't1'::regclass;
 count 
-------
     0
(1 row)
复制

可以看到,虽然 shared buffers 里面没有了,但是页缓存中还在,所以,切忌双缓存的影响。

[postgres@mypg ~]$ ./pcstat 17data/base/5/16408
|---------------------+----------------+------------+-----------+---------|
| Name                | Size           | Pages      | Cached    | Percent |
|---------------------+----------------+------------+-----------+---------|
| 17data/base/5/16408 | 40960          | 10         | 10        | 100.000 |
|---------------------+----------------+------------+-----------+---------|
复制

小结

PostgreSQL 的双缓存被人诟病已久,从我这么久的经验来看,目前弊要大于利,时间一久,"直接可用"的内存就很少了,需要异步/直接回收,碰到业务高峰期的时候,会有明显延迟,尤其是 Direct Reclaim 的时候。

其次,由于双缓存的影响,查询的 RT 也不稳定,给人一种时快时慢的感觉,不过双缓存也并非完全一无是处,比如有 standby 的时候,亦或者数据库挂了重启的时候,由于页缓存,也可能不需要发生真正的 I/O,其次操作系统也可以合并一些 IO。

好在 16 已经提供了 Direct IO 的初步接口,相信在后面的大版本中会有改进。

另外,DBA Daily 5.0 已经正式定稿了,虽迟但到。目前已经在印刷中,第一期会小范围地赠送给一些朋友,后续赠送活动请关注后续消息。

图片

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论