PostgreSQL Internals之路Part II: Buffer Cache和WAL第9章 Buffer Cache

文摘   科技   2024-09-21 07:21   北京  

Part II: Buffer Cache和WAL

9. Buffer Cache

9.1 Caching

现代计算机系统中,缓存是无所不在的(omnipresent)---硬件和软件层面上都存在。处理器自身也拥有3级甚或4级缓存。RAID控制器和磁盘也都有自己的缓存。

缓存通常用于平滑快慢存储之间的性能差距。快速存储容量小,价格贵,而慢速存储容量大,价格便宜。因此,快速存储容纳不下存储在慢速存储里头的所有数据。但是在大多数情况下,只有一个小部分数据在特定时刻是处于使用状态的,因此将快速存储用于缓存,保留热数据,可以极大的降低由慢速存储访问引起的开销。

在PG中,buffer cache存放关系页,可以平衡磁盘(毫秒级)和RAM(纳秒级)的访问时间。

操作系统拥有自己的缓存,用于相同的目的。因为这个原因,数据库系统通常的设计,会尽量避免双缓存:存储在磁盘上的数据通常直接用于查询,绕过操作系统的缓存。但是PG使用了不同的方法:通过缓冲的文件操作来读取和写入所有数据。

使用direct I/O可以避免双缓存。它会降低开销,由于PG将使用直接内存访问(DMA),代替将缓存页复制到内存地址空间;另外,你会立即可以控制磁盘上的物理写操作。但是,在使用缓冲的情况下,direct I/O不支持数据的预取,这样,你必须基于异步I/O独立的去实现它,这需要在PG核心代码里进行大量的修改,当遇到要支持direct I/O和异步I/O的时候,还要处理操作系统的不兼容性。但是一旦异步通信设置好,你就可以享受到磁盘无等待访问的额外好处了。

PG社区已经启动了这方面的工作,但是它可能要花很长的时间才能最终实现。(参考:[PostgreSQL: Asynchronous and "direct" IO support for PostgreSQL.](https://www.postgresql.org/message-id/flat/20210223100344.llw5an2aklengrmn%40alap3.anarazel.de "PostgreSQL: Asynchronous and "direct" IO support for PostgreSQL."))

9.2 Buffer Cache设计

Buffer cache位于服务器实例的共享内存区,能被所有进程访问。它是共享内存的主要部分,也是PG中最重要和最复杂的数据结构。理解缓存如何工作很重要,但其它结构(如子事务,CLOG事务状态,WAL记录)使用了类似的缓存机制,只不过稍简单一点儿。

这个缓存的名字也可以从内部结构里体现出来,它由一些缓冲数组组成。每个Buffer保留一个内存块,用以容纳带有页头的单个数据页。

页头包含有关缓冲和页的信息,如:

  • 页的物理位置(文件ID, 分叉[fork],分叉中的块号)
  • 显示数据已改或即将要写回磁盘(这样的页也称为脏页)的属性
  • 缓冲已用的数量
  • 引用的数量(pin count或reference count)

要访问一个关系的数据页,进程会从缓冲管理器请求,接收到包含该页缓冲的ID。接着它读取缓存的数据,按照需求在缓存里对它进行修改。如果该页正在被用,它的缓冲就被认为是被引用(pinned)。被引用(pin)禁止缓存页被踢出,也允许与其它锁一起使用。每个pin同时也对已用数量加1.

只要页被缓存,它的使用不会导致任何文件操作。

我们可以使用pg_buffercache插件来浏览buffer cache:

CREATE EXTENSION pg_buffercache;

我们创建一张表,并插入一行值:

CREATE TABLE cacheme(
id integer
WITH (autovacuum_enabled = off);

INSERT INTO cacheme VALUES (1);
INSERT 0 1

现在buffer cache包含了新插入行的一个堆页。你可以通过查询指定表所有的缓冲看到它。我们需要多次查询它,因此将它包装成一个函数:

CREATE FUNCTION buffercache(rel regclass)
RETURNS TABLE(
bufferid integer, relfork text, relblk bigint,
isdirty boolean, usagecount smallint, pins integer
AS $$
SELECT bufferid,
CASE relforknumber
WHEN 0 THEN 'main'
WHEN 1 THEN 'fsm'
WHEN 2 THEN 'vm'
END,
relblocknumber,
isdirty,
usagecount,
pinning_backends
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode(rel)
ORDER BY relforknumber, relblocknumber;
$$ LANGUAGE sql; 
SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
    15674 | main    |      0 | f       |          1 |    0
(1 row)

页是脏页:它已经被修改了,但是还没有写回磁盘。使用量是1。

9.3 缓存命中率

当缓冲管理器必须读页的时候,它要检查buffer cache。

所有缓冲的ID存储在一个哈希表里,用于加速查找。

很多现代编程语言将hash表用作基本的数据类型。Hash表经常被称作是关联数组,或者从用户的角度来看,他们确实看起来像个数组;但是它们的索引可以是任意数据类型,例如,一个文本串而不是整数用作索引。

然而键值的范围可以非常大,hash表从来不会同一时刻包含许多不同的值。hash的思想是运用hash函数将一个键值转化成一个整数。这个数(或一些2进制位)用作常规数组的索引。数组的元素称为hash表的桶。

好的hash函数会把桶间的hash键分布的比较统一,但是,它仍然可能把相同的数赋给不同的键,最后会放到相同的桶里;这也被称为”冲突“。因为这个原因, 将值存储在桶里与对应的hash键存到了一起;要通过键访问到hash值,PG必须检查桶里所有的键。

hash表有很多实现;在所有可能的选项中,buffer cache使用了可扩展的表,使用链表来解决hash碰撞。

一个hash键由关系文件的ID、分叉类型、分叉文件的页的ID来组成。这样,知道了页,PG可以很快找到包含此页的缓冲,或者确定该页并没有被缓存。

buffer cache的实现长期以来,一直被批评,说是依赖于hash表:在查的特定关系的页用了哪些buffer,这个结构没有用。运行DROP和TRUNCATE或者在清理时对表进行截断,需要将页从缓存中移除。但是到目前为止,也没有人建议使用合适的替代方案。

如果hash表包含所需要的buffer ID,缓冲管理器会引用这个缓冲,并返回进程的ID。然后进程就可以开始使用缓存页,不会有任何的I/O流量。

要引用这个缓冲,PG必须增加在页头pin的计数;一个缓冲可以同时被多个进程引用。当Pin计数器大于0时,缓问就被认为是正在使用,它的内容如果没有太大的变化,也是被允许的。例如,一人元组可以出现(在可见性规则下它可能不可见),但是这个页自身是不能被替换的。

当运行analyze和缓冲选项时,EXPLAIN命令执行显示的查询计划,并且显示已用的缓冲的数量:

EXPLAIN (analyze, buffers, costs off, timing off, summary off)
mydb-# SELECT * FROM cacheme;
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Seq Scan on cacheme (actual rows=1 loops=1)
Buffers: shared hit=1
Planning:
Buffers: shared hit=12 read=7
(4 rows)

这里hit=1意味着要读取的页在缓存里头被发现。

缓冲的引用会将使用计数器加1:

SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
    15674 | main    |      0 | t       |          2 |    0
(1 row)

要观察执行的时候pin的行为,我们可以打开一个游标 --- 它会持有缓冲引用,它还得提供在结果集中下一行的快速访问:

mydb=# BEGIN;
BEGIN
mydb=*# DECLARE c CURSOR FOR SELECT * FROM cacheme;
DECLARE CURSOR
mydb=*# FETCH c;
 id
----
  1
(1 row)

mydb=*# SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
    15674 | main    |      0 | t       |          3 |    1
(1 row)

如果进程不能使用引用的缓冲,它就跳过它,简单的选择另一个。我们可以在表的清理过程中看到它:

VACUUM VERBOSE cacheme;
INFO:  vacuuming "public.cacheme"
INFO:  table "cacheme": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages
DETAIL:  0 dead row versions cannot be removed yet, oldest xmin: 889
Skipped 1 page due to buffer pins, 0 frozen pages.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM

页被跳过去,因为它的元组还没有从引用的缓冲里头物理的移除。

但如果确实需要这个缓冲,进程就会加到相应的队列,直到排它性的访问这个缓冲。带冻结的清理就是这样的例子。

一旦游标关闭或者移到另一页,缓冲的引用就会解除。在这个例子中,它发生在事务的最后:

mydb=*# COMMIT;
COMMIT
mydb=# SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
    15674 | main    |      0 | t       |          3 |    0
    15653 | vm      |      0 | f       |          2 |    0
(2 rows)

页修改也被相同的pinning机制所保护。例如,我们插入另一行到表中(它会进入到同一页):

INSERT INTO cacheme VALUES (2);
INSERT 0 1
mydb=# SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
    15674 | main    |      0 | t       |          4 |    0
    15653 | vm      |      0 | f       |          2 |    0
(2 rows)

PG并不立即执行任何写操作到磁盘:buffer cache中的脏页仍然会持续一段时间,这对于读和写操作而言会提供更好的性能。

9.4 缓存丢失

如果hash表没有查询页的对应项,这意味着这页并没有被缓存。这种情况下,新的缓冲被分配(并立即被引用),页被读进缓冲,hash表引用也相应做修改。

我们重启一下实例,清除它的buffer cache:

postgres$ pg_ctl restart -l /home/postgres/logfile

试图读取一页将会出现缓存丢失的结果,该页会被读进新的缓冲:

EXPLAIN (analyze, buffers, costs off, timing off, summary off)
mydb-# SELECT * FROM cacheme;
                 QUERY PLAN
---------------------------------------------
 Seq Scan on cacheme (actual rows=2 loops=1)
   Buffers: shared read=1 dirtied=1
 Planning:
   Buffers: shared hit=15 read=7
(4 rows)

不只是命中,查询计划现在显示了读状态,它的值也反映了缓存丢失。另外,该页变脏了,因为查询修改了某些提示位。

buffer cache查询显示:新加入的页面的使用计数被设置为1:

SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      166 | main    |      0 | t       |          1 |    0
(1 row) 

视图pg_statio_all_tables包含了所有表里buffer cache使用的完整的统计信息:

SELECT heap_blks_read, heap_blks_hit
mydb-# FROM pg_statio_all_tables
mydb-# WHERE relname = 'cacheme';
 heap_blks_read | heap_blks_hit
----------------+---------------
              3 |             5
(1 row)

PG为索引和序列也提供了类似的视图。它们可以显示I/O操作的统计信息,但必须在track_io_timing被开启的情况下。

缓冲搜索和丢弃

为一页选择缓冲并不常见。有两种可能的场景:

  1. 在服务器启动以后,所有的缓冲都是空的,并且跟列表绑在一起。

    当某些缓冲仍然是空闲的情况下,磁盘下一页读操作会占据第一个缓冲,它最终会从list中移除。

    一个缓冲只有在它对应的页消失,没有被的页所替换时,可以返还给列表。在调用DROP或者TRUNCATE命令或者表在清理的时候执行了TRUNCATE的时候,会出现这样的情况。

  2. 最终会出现没有空间缓冲的情况(因为数据库的大小通常比缓存中分配的内存快要大)。缓冲管理器会挑选其中一个正在使用的缓冲,将其中缓存的页刷出缓冲。它采用了时钟扫描算法,这个可以通过时钟比喻来解释。指向其中一个缓冲,时钟指针会绕着buffer cache走,路过的时候,每个缓存页的计数减1。第一个解除引用计数为0的缓冲,如果被发现,则会被清除。

    这样,计数在每次访问缓冲的时候会加1(pinned),当缓冲管理器查找要被刷出的页面时,会减小。结果,最近最少使用的页会先被替换出去,而那些经常被访问的页面在缓存中的时间会保留的长一些。

    你也能猜得到,如果所有的缓冲使用计数都不是0,时钟指针在最终找到0值之前,就要扫描多个周期。为避免多个周期,PG将使用计数限制到5。

    一旦要被踢出的缓冲发现了,仍在该缓冲里对该页的引用必须从hash表里头移除。

    但是如果这个缓冲是脏的,也就是说,它包含了一些修改了的数据,旧的页不能简单的扔掉---缓冲管理器必须先要写磁盘。

接着缓冲管理器会读新页到已发现的缓冲---不管它是否被清理或是否是空闲。它使用了缓冲I/O,这样只有在操作系统在自己的缓存里找不到该页时,它才会从磁盘中读取。

使用了direct I/O并且不依赖于操作系统缓存的那些数据库系统,主要是通过逻辑读(从RAM或者从buffer cache)和物理读(从磁盘)来区分的。从PG的立场来看,页要么从buffer cache中读取,要么从操作系统中请求得到,但没有办法区分它是在RAM中找到的,还是从磁盘中读取的(从操作系统中请求时)。

hash表更新以后,会指向新的页,对应的缓冲也被绑定。使用计数增加,变为1,在时钟指针遍历buffer cache时,这也给了缓冲一些时间来增加此值。

9.5 批量替换(Bulk Eviction)

如果批量读取或写入被执行,有一种风险:一次性数据可以很快将buffer cache中的有用的页给替换掉。

作为预防,批量操作会使用稍小的缓冲环,替换操作只在它们的边界内执行,而不影响别的缓冲。

伴随着”缓冲环“,代码里头也使用术语”环缓冲“。但是,这个同义词相当模糊,因为环缓冲自己也包含着一些缓冲(属于buffer cache)。术语”buffer cache"在这方面显得更准确。

特定大小的缓冲环由一个接着一个的缓冲数组构成。首先,缓冲环是空的,各个缓冲从buffer cache中被一个个挑选并加进来。缓冲的替换也会参与,但是必须在环的限制下。(参考 :[git.postgresql.org Git - postgresql.git/blob - src/backend/storage/buffer/freelist.c[1]](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_14_STABLE)

加到环里的缓冲不会被buffer cache排除在外,它仍然可以用于其它操作。因此,如果被重用的缓冲最后被绑定或者使用计数比1大,它会简单的从环中分享,并被另一个缓冲替换。

PG支持三种替换策略。

批量读策略 用于大表的顺序扫描,在表的大小超过了buffer cache的1/4时。环缓冲占用256kB (32个标准页)。

该策略不允许写脏页到磁盘来释放一个缓冲;相反,缓冲被环排除在外,并由另一个缓冲替代。结果,读操作不需要等写操作完成,它可以执行的更快。

如果发现该表已经扫描过了,启动另一个扫描的进程会加入现存的buffer ring,来访问当前的数据,而不需要额外的I/O操作(src/backend/access/common/syncscan.c[2] ) 当第一个进程完成扫描时,第二个进程会回到表中已经跳过的部分。

批量写策略 主要被COPY FROM, CREATE TABLE AS SELECT以及CREATE MATERIALIZED VIEW命令所使用,跟ALTER TABLE类似,会引起表的重写。分配的环会很大,默认大小是16MB(2048个标准页),但是它不会超过整个buffer cache的1/8大小。

清理策略  主要用于清理时执行全表扫描而不用考虑可见性映射的处理。ring buffer 会分配256kB的RAM (32个标准页)。

缓冲环(Buffer ring)并不总是阻止非预期的替换。如果UPDATE或DELETE命令影响了很多行,执行的表扫描会应用批量读策略,但是由于页是不断地被修改,缓冲环最后就变得没有用。

另一个值得一提的例子是在TOAST表里存储过量的数据。不管潜在的大容量数据是否需要读,toast值总是要通过索引进行访问,它们就绕过了缓冲环。

我们再细看下批量读策略,简化一下,我们创建一张表,让插入的行刚好占满一页,默认情况下,buffer cache应该是16384页,每页是8kB。这样表使用缓冲环,它必须有4096页用于扫描。

CREATE TABLE big(
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
char(1000)
WITH (fillfactor = 10);
INSERT INTO big(s)
mydb-# SELECT 'FOO' FROM generate_series(1,4096+1);
INSERT 0 4097

我们来分析下该表:

mydb=# ANALYZE big;
ANALYZE
mydb=# SELECT relname, relfilenode, relpages
mydb-# FROM pg_class
mydb-# WHERE relname IN ('big', 'big_pkey');
 relname  | relfilenode | relpages
----------+-------------+----------
 big      |       24857 |     4097
 big_pkey |       24862 |       14
(2 rows)

重启服务器清掉缓存,因为它会包含一些在分析阶段读取的堆页。

postgres$ pg_ctl restart -l /home/postgres/logfile

一旦服务器重启,我们再读取整张表:

EXPLAIN (analyze, costs off, timing off, summary off, summary off)
SELECT id FROM big;
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Seq Scan on big (actual rows=4097 loops=1)
(1 row)

堆页占用了32个缓冲,组成了这个操作的缓冲环:

SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
−−−−−−−
32
(1 row)

但是在索引扫描中,缓冲环就用不上了:

EXPLAIN (analyze, costs off, timing off, summary off, summary off)
SELECT * FROM big ORDER BY id;
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Index Scan using big_pkey on big (actual rows=4097 loops=1)
(1 row)

结果,buffer cache最后会包含全表和全部索引的数据:

SELECT relfilenode, count(*)
FROM pg_buffercache
WHERE relfilenode IN (
pg_relation_filenode('big'),
pg_relation_filenode('big_pkey')
)
GROUP BY relfilenode;
 relfilenode | count
-------------+-------
       24857 |  4098
       24862 |    14
(2 rows)

9.6 选择Buffer Cache的大小

buffer cache的大小由参数:shared_buffers来定义。默认值是128MB。从所周知,它比较小,因此在安装完PG以后大多需要将它调大。你必须重新加载服务器,因为共享内存是在服务器启动的时候分配给缓存用的。

但是我们如何确定一个合适的值呢?

即使一个非常大的数据库拥有有限大小的同时使用的热数据。在完美世界当中,最好是这个设置与buffer cache相匹配(一次性数据保留一些空间)。如果缓存空间太小,活动的使用的页将会互相被替换,导致过多的I/O操作。但是轻率地增加缓存的大小也不是一个好主意:RAM是稀缺资源,另外,大的缓存会导致更高的维护成本。

优化的buffer cache大小因系统而不同:它所依赖的因素包括总可用内存的大小、数据的配置文件、工作负载类型。不幸的是,没有一个魔幻的值或者公式来非常好的适用于每个人。

你应该记住缓存丢失在PG不一定会触发一个物理I/O操作。如果buffer cache非常小,操作系统缓存使用剩余的空闲内存,也能在某种程度上将问题缓解。但不像数据库,操作系统对于已经读取的数据毫不知情,因此它应用的是不同的替换策略。

一个典型的推荐值是RAM的1/4,接着再对这个值进行调整 。

最好的方法是进行试验:你可以增加或减少缓存大小,来比较下系统性能。当然,它需要一个完全类似于生产系统的测试系统,你必须能重现典型的工作负载。

你也可以使用扩展pg_buffercache运行某些分析。例如,基于缓存的使用浏览缓存的分布情况:

SELECT usagecount, count(*)
FROM pg_buffercache
GROUP BY usagecount
ORDER BY usagecount;
usagecount | count
−−−−−−−−−−−−+−−−−−−−
1 | 4128
2 | 50
3 | 4
4 | 4
5 | 73
| 12125
(6 rows)

NULL计数器值对应于空间缓冲。在此例中,它非常符合预期。因为服务器重启了,并且在大多数时间都是空闲的。使用的缓冲大多数都包含的是系统表中的页数据,它们主要由后端进程读取,填充到自己的系统catalog缓存里,并执行查询。

我们可以检查每个关系有多大比例被缓存,并且缓存的是否是热数据(一个页如果使用计数器大于1,就被认为是热数据):

SELECT c.relname,
count(*) blocks,
round100.0 * 8192 * count(*) /
pg_table_size(c.oid) ) AS "% of rel",
round100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 1) /
pg_table_size(c.oid) ) AS "% hot"
FROM pg_buffercache b
JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode
WHERE b.reldatabase IN (
0-- cluster-wide objects
(SELECT oid FROM pg_database WHERE datname = current_database())
)
AND b.usagecount IS NOT NULL
GROUP BY c.relname, c.oid
ORDER BY 2 DESC
LIMIT 10;
             relname             | blocks | % of rel | % hot
---------------------------------+--------+----------+-------
 big                             |   4098 |      100 |     0
 pg_attribute                    |     30 |       49 |    48
 big_pkey                        |     14 |      100 |    93
 pg_class                        |     13 |       76 |    76
 pg_proc                         |     13 |       12 |     6
 pg_operator                     |     11 |       61 |    50
 pg_proc_oid_index               |      9 |       82 |    45
 pg_attribute_relid_attnum_index |      8 |       73 |    73
 pg_proc_proname_args_nsp_index  |      6 |       17 |     6
 pg_statistic                    |      5 |       14 |     5
(10 rows)

这个例子显示出,该大表和它的索引完全被缓存起来了,但是它们的页并没有被有效的使用。

分析数据会从不同的角度,你可以得到一些有用的结论,但是,当使用pg_buffercache查询时,需要遵守一些简单的规则:

  • 重复多次执行该查询,因为返回的结果在某种程度上可能不太相同。
  • 不要运行这样的查询不停(non-stop),因为pg_buffercache扩展会锁定要看的缓冲,即使只是简单的查询。

9.7 缓存的加热(Cache Warming)

服务器重启以后,缓存需要一些时间来预热,也就是说,来积累那些有效使用的数据。立即缓存特定的表可能有帮助,pg_prewarm扩展就可以达成此目的:

CREATE EXTENSION pg_prewarm;

除了将表数据装载到buffer cache(或者到os cache), 这个扩展可以将当前缓存状态写到磁盘,然后在服务器重启以后再恢复回来。为了启用此功能,你必须将扩展的库添加到shared_preload_libraries并重启服务器:

ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
postgres$ pg_ctl restart -l /home/postgres/logfile

如果pg_prewarm.autoprewarm的设置没有变,一个叫auto_prewarm的进程将会在服务器重新加载的时候自动启起来;该进程会每隔pg_prewarm.autoprewarm_interval秒一次,将缓存的页列表刷回磁盘(使用一个max_parallel_processes槽)。

postgres$ ps -o pid,command \
--ppid `head -n 1 /usr/local/pgsql/data/postmaster.pid` | \
grep prewarm
23129 postgres: autoprewarm leader

既然服务器已经重启,那张大表就不再被缓存:

SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
−−−−−−−
0
(1 row)

如果你能假定整个表会被使用,并且磁盘访问会导致响应时间变的不可接受的长,你就可以将表提前装载到buffer cache:

SELECT pg_prewarm('big');
pg_prewarm
−−−−−−−−−−−−
4097
(1 row)
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
−−−−−−−
4097
(1 row)

这个页列表会被导出到文件:PGDATA/autoprewarm.blocks当中。你也可以等autoprewarm leader进程第一次完成,但是我们可以手动触发这个导出操作:

SELECT autoprewarm_dump_now();
 autoprewarm_dump_now
----------------------
                 4301
(1 row)

刷出的页数量大于4097,是因为所有的可用缓冲都考虑进去了。该文件写成了文本文件格式。它包含数据库、表空间、文件的ID以及分叉和段号:

[20:55:23-postgres@centos1:/var/lib/pgsql/14/data]$ head -n 10 autoprewarm.blocks
<<4301>>
0,1664,1262,0,0
16384,1663,1259,0,0
16384,1663,1259,0,1
16384,1663,1259,0,2
16384,1663,1259,0,3
16384,1663,1249,0,0
16384,1663,1249,0,1
16384,1663,1249,0,2
16384,1663,1249,0,3

我们再重启一下服务器。

postgres$ pg_ctl restart -l /home/postgres/logfile

表立即出现在缓存里头:

SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
count
−−−−−−−
4097
(1 row)

进程autoprewarm leader又一次做了所有的预备工作:读取文件,按数据库将页排序,重新排序(这样磁盘可以按顺序读),接着将它们传给autoprewarm workder进行处理。

9.8 Local Cache(本地缓存)

临时表并不遵循上边描述的工作流程 。因为临时表数据只对单个进程可见,将它装载到共享的缓存里没有意义。因此,临时数据使用的是拥有该表的进程的本地缓存 (1[3])。

通常情况下,本地buffer cache与共享缓存工作类似:

  • 页查找是通过hash表进行
  • 替换策略采用的是标准算法(除了没有使用buffer ring以外)
  • 页可以被绑定,以避免被替换

但是,本地缓存实现要简单的多,因为它不需要处理内存结构上的锁(单进程访问),也不需要容错处理(临时数据最多保存到会话结束 的时候)。

因为只有很少的会话会用到临时表,本地缓存内存可以按需进行分配。单个会话本地缓存的最大大小由参数temp_buffers确定。

仅管名字相似,temp_file_limit参数与临时表没有任何关系;它只与查询执行期间临时存储中间数据产生的文件相关。

在EXPLAIN命令的输出结果里,对本地buffer cache的调用都会冠以local,而不是shared:

CREATE TEMPORARY TABLE tmp AS SELECT 1;

EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT * FROM tmp;
               QUERY PLAN
-----------------------------------------
 Seq Scan on tmp (actual rows=1 loops=1)
   Buffers: local hit=1
 Planning:
   Buffers: shared hit=12 read=7
(4 rows)

参考资料

[1]

[git.postgresql.org Git - postgresql.git/blob - src/backend/storage/buffer/freelist.c: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_14_STABLE

[2]

src/backend/access/common/syncscan.c: src/backend/access/common/syncscan.c

[3]

1: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/localbuf.c;hb=REL_14_STABLE


数据库杂记
数据库技术专家,PostgreSQL ACE,SAP HANA,Sybase ASE/ASA,Oracle,MySQL,SQLite各类数据库, SAP BTP云计算技术, 以及陈式太极拳教学倾情分享。出版过三本技术图书,武术6段。
 最新文章