之前的系列致力于PostgreSQL的
隔离和多版本 ,今天我们开始了一个新的-
关于预写日志记录
机制 。 让我提醒您,该材料基于Pavel
pluzanov和我
所做的行政
培训课程 ,但不会逐字重复,并且旨在进行深思熟虑的阅读和独立的实验。
此周期将包括四个部分:
- 缓冲区缓存(本文);
- 预录日志 -在恢复过程中如何安排以及如何使用它;
- 检查点和后台记录-为什么需要它们以及如何配置它们;
- 日志调整 -要解决的级别和任务,可靠性和性能。
为什么需要日记?
在此过程中,DBMS处理的部分数据以延迟的方式存储在RAM中并写入磁盘(或其他非易失性介质)。 这种情况发生的次数越少,输入输出越少,系统运行越快。
但是,如果发生故障(例如,关闭电源或DBMS代码或操作系统中发生错误),将会发生什么? RAM中的所有内容都将丢失,仅保留写入磁盘的数据(在某些类型的故障中,磁盘也可能会受到影响,但在这种情况下,仅备份副本会有所帮助)。 原则上,I / O的组织方式可以使磁盘上的数据始终保持一致状态,但这很困难且效率不高(据我所知,只有Firebird才这样)。
通常,包括PostgreSQL在内,写入磁盘的数据不一致,并且从故障中恢复时,需要采取特殊措施来恢复一致性。 日记是使之成为可能的机制。
缓冲区缓存
奇怪的是,我们将开始讨论使用缓冲区高速缓存的日志记录。 缓冲区高速缓存不是存储在RAM中的唯一结构,而是最重要和最复杂的结构之一。 此外,了解其操作原理本身很重要,在本示例中,我们将熟悉如何在RAM和磁盘之间交换数据。
缓存在现代计算系统中无处不在;一个处理器可以单独计算三或四个缓存级别。 通常,需要任何高速缓存来消除两种类型的内存之间的性能差异,其中一种速度相对较快,但不足以满足所有人的需求,而另一种速度相对较慢,但容量却很大。 因此,缓冲区缓存可平滑对RAM(纳秒)和磁盘(毫秒)的访问时间之间的差异。
请注意,操作系统还具有解决相同问题的磁盘缓存。 因此,DBMS通常试图通过绕过OS缓存直接访问磁盘来避免双重缓存。 但是对于PostgreSQL,情况并非如此:所有数据都是使用普通文件操作读取和写入的。
此外,磁盘阵列,甚至磁盘本身也具有自己的缓存。 当我们谈到可靠性问题时,这一事实对我们仍然有用。
但是回到DBMS缓冲区缓存。
之所以这样称呼它,是因为它是一个
缓冲区数组。 每个缓冲区是一个数据页(块)和一个标题的位置。 标题除其他外包括:
- 页面在磁盘上的缓冲区中的位置(其中的文件和块号);
- 页面上的数据已更改并且迟早应将其写入磁盘的迹象(这种缓冲区称为dirty );
- 缓冲区的调用次数(使用计数);
- 固定缓冲区的标志(引脚计数)。
缓冲区高速缓存位于服务器的共享内存中,所有进程均可访问。 要处理数据-读取或修改,-处理缓存中的读取页面。 当页面处于缓存中时,我们在RAM中使用它并保存对磁盘的访问。

最初,缓存包含空缓冲区,并且所有缓冲区都链接到空闲缓冲区列表中。 指向“下一个受害者”的指针的含义稍后将变得清楚。 为了在高速缓存中快速找到所需的页面,使用了哈希表。
缓存中的搜索页面
当进程需要读取页面时,它首先尝试使用哈希表在缓冲区高速缓存中找到它。 哈希键是文件号和文件内的页码。 在哈希表的相应篮子中,该过程找到缓冲区编号并检查其是否确实包含所需的页面。 与任何哈希表一样,此处可能发生冲突; 在这种情况下,该过程将不得不检查多个页面。
长期以来一直批评使用哈希表。 这种结构使您可以快速找到页面上的缓冲区,但是,例如,当您需要查找特定表占用的所有缓冲区时,它是完全没有用的。 但是还没有人提出好的替代方案。
如果在高速缓存中找到了所需的页面,则该过程应通过增加引脚数来“冻结”缓冲区(多个过程可以同时执行此操作)。 只要缓冲区是固定的(计数器值大于零),就认为缓冲区已被使用,并且其内容不应“激进”地更改。 例如,该行的新版本可能会出现在页面中-由于多版本和可见性规则,这不会打扰任何人。 但是无法将另一个页面读入固定缓冲区。
挤出来
可能会在缓存中找不到所需的页面。 在这种情况下,必须将其从磁盘读取到某个缓冲区。
如果缓存中仍然有可用缓冲区,则选择第一个可用缓冲区。 但是它们迟早会结束(通常数据库的大小大于为高速缓存分配的内存),然后您必须选择一个已占用的缓冲区,将页面强行移出并在空闲空间上读取一个新的页面。
抢占机制基于以下事实:每次访问缓冲区时,进程都会增加缓冲区头中的使用计数。 因此,那些不经常使用的缓冲区具有较低的计数器值,并且是排挤的良好候选者。
时钟扫描算法循环遍历所有缓冲区(使用指向“下一个受害者”的指针),从而将其访问计数减少一。 为了排挤,选择了第一个缓冲区,该缓冲区:
- 计数器为零(使用次数),
- 且不固定(零引脚数)。
您可以看到,如果所有缓冲区的命中计数器都为非零,则该算法将必须执行多个循环,以重置计数器,直到其中一个最终变为零为止。 为了避免“盘绕”,命中计数器的最大值限制为5。但是,即使缓冲区高速缓存大小较大,该算法也会导致大量开销。
找到缓冲区后,将发生以下情况。
缓冲区被固定以显示正在使用的其他进程。 除了修复之外,还使用其他阻塞方法,但是我们将单独讨论更多。
如果缓冲区原来是脏的,也就是说,它包含已更改的数据,则不能简单地丢弃该页面-首先需要将其保存到磁盘。 这不是一个好情况,因为将要读取页面的过程必须等待“外来”数据的记录,但是这种影响可以通过检查点和后台记录过程来解决,这将在后面进行讨论。
接下来,从磁盘将新页面读取到所选缓冲区中。 呼叫次数计数器设置为1。 此外,到加载页面的链接必须在哈希表中注册,以便将来可以找到它。
现在,指向“下一个受害者”的链接指向下一个缓冲区,刚加载的缓冲区有时间增加命中计数器,直到指针绕过整个缓冲区高速缓存并再次返回为止。
用我自己的眼睛
按照PostgreSQL的惯例,有一个扩展允许您查看缓冲区缓存内部。
=> CREATE EXTENSION pg_buffercache;
创建一个表并将一行插入其中。
=> CREATE TABLE cacheme( id integer ) WITH (autovacuum_enabled = off); => INSERT INTO cacheme VALUES (1);
缓冲区缓存中将包含什么? 至少应在其中显示一个页面,并添加一行。 我们将通过以下查询对此进行验证,在该查询中,我们仅选择属于我们表的缓冲区(通过文件号relfilenode),并解码层号(relforknumber):
=> SELECT bufferid, CASE relforknumber WHEN 0 THEN 'main' WHEN 1 THEN 'fsm' WHEN 2 THEN 'vm' END relfork, relblocknumber, isdirty, usagecount, pinning_backends FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('cacheme'::regclass);
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 1 | 0 (1 row)
就是这样-缓冲区中只有一页。 它是脏的(脏),命中计数器等于1(使用计数),并且不受任何进程固定(pinning_backends)。
现在添加另一行并重复查询。 为了保存字母,我们在另一个会话中插入一行,然后使用
\g
命令重复长的请求。
| => INSERT INTO cacheme VALUES (2);
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 2 | 0 (1 row)
没有添加新的缓冲区-第二行适合同一页面。 请注意,使用情况计数器已增加。
| => SELECT * FROM cacheme;
| id | ---- | 1 | 2 | (2 rows)
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 3 | 0 (1 row)
在访问要阅读的页面后,计数器也会增加。
如果你打扫?
| => VACUUM cacheme;
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15731 | fsm | 1 | t | 1 | 0 15732 | fsm | 0 | t | 1 | 0 15733 | fsm | 2 | t | 2 | 0 15734 | vm | 0 | t | 2 | 0 15735 | main | 0 | t | 3 | 0 (5 rows)
清洗创建了一个可见性图(一页)和一个可用空间图(三页-此图的最小尺寸)。
等等。
尺寸设定
缓存大小由
shared_buffers参数设置。 默认值为荒谬的128 MB。 这是在安装PostgreSQL之后立即增加的有意义的参数之一。
=> SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers';
setting | unit ---------+------ 16384 | 8kB (1 row)
请记住,更改参数需要重新启动服务器,因为所有必需的缓存内存都是在服务器启动时分配的。
由于什么原因选择合适的值?
即使最大的数据库也只有有限的“热”数据集,利用这些数据可以在每个时刻进行有效的工作。 理想情况下,应将此集放置在缓冲区高速缓存中(以及一些用于“一次性”数据的空间)。 如果缓存大小较小,则频繁使用的页面将不断相互挤压,从而产生过多的输入输出。 但是,盲目地增加缓存也是错误的。 大尺寸的情况下,维护它的开销会增加,此外,RAM还需要用于其他需求。
因此,最佳缓冲区高速缓存大小在不同系统中会有所不同:它取决于数据,应用程序和负载。 不幸的是,没有任何一种神奇的含义能同样适合所有人。
标准建议是将RAM的1/4作为第一近似值(对于PostgreSQL 10之前的Windows,建议选择较小的大小)。
然后,您需要查看情况。 最好做一个实验:增加或减少缓存大小并比较系统性能。 当然,为此,必须有一个测试台并能够再现典型的负载-在生产环境中,这样的实验看起来令人怀疑。
请务必查看Nikolay Samokhvalov在PgConf-2019上的报告:“ PostgreSQL调整的工业方法:数据库实验 ”
但是,可以使用相同的pg_buffercache扩展在实时系统上直接收集有关正在发生的事情的一些信息-最重要的是,以正确的角度查看。
例如,您可以根据缓冲区的使用程度来研究它们的分布:
=> SELECT usagecount, count(*) FROM pg_buffercache GROUP BY usagecount ORDER BY usagecount;
usagecount | count ------------+------- 1 | 221 2 | 869 3 | 29 4 | 12 5 | 564 | 14689 (6 rows)
在这种情况下,许多空计数器值都是空闲缓冲区。 对于没有任何反应的系统,这不足为奇。
您可以看到数据库中有多少表被缓存以及这些数据的使用有多活跃(在此查询中,活跃使用是指使用计数器大于3的缓冲区):
=> SELECT c.relname, count(*) blocks, round( 100.0 * 8192 * count(*) / pg_table_size(c.oid) ) "% of rel", round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 3) / pg_table_size(c.oid) ) "% hot" FROM pg_buffercache b JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode WHERE b.reldatabase IN ( 0, (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 ---------------------------+--------+----------+------- vac | 833 | 100 | 0 pg_proc | 71 | 85 | 37 pg_depend | 57 | 98 | 19 pg_attribute | 55 | 100 | 64 vac_s | 32 | 4 | 0 pg_statistic | 27 | 71 | 63 autovac | 22 | 100 | 95 pg_depend_reference_index | 19 | 48 | 35 pg_rewrite | 17 | 23 | 8 pg_class | 16 | 100 | 100 (10 rows)
例如,在这里可以看到vac表占据了最大的位置(我们在前面的主题之一中使用了它),但是很长一段时间没有人解决过这个问题,并且仅仅由于可用缓冲区还没有用完而还没有被挤出。
您可以提出其他章节,以提供有用的思想信息。 只需要考虑这样的请求:
- 必须重复几次:数字将在一定范围内变化;
- 由于扩展程序会在短时间内阻止使用缓冲区高速缓存进行操作,因此无需经常执行此操作(作为监视的一部分)。
还有一件事。 我们不应忘记PostgreSQL是通过定期调用操作系统来处理文件的,因此,存在双重缓存:页面同时进入DBMS缓冲区缓存和OS缓存。 因此,缓冲区高速缓存中的“未命中”并不总是导致对实际I / O的需求。 但是排挤操作系统的策略与DBMS策略不同:操作系统对读取的数据的含义一无所知。
质量排量
在执行批量读取或写入数据的操作中,存在使用“一次性”数据快速将缓冲区高速缓存中的有用页面替换的危险。
为了防止这种情况发生,所谓的
缓冲区环用于此类操作-为每个操作分配一小部分缓冲区高速缓存。 挤出仅在环内起作用,因此其余缓冲区高速缓存数据不会受到影响。
为了顺序读取大表(其大小超过缓冲区高速缓存的四分之一),分配了32页。 如果另一个进程在读取表时也需要此数据,则它不会首先开始读取表,而是连接到现有的缓冲环。 扫描后,他读取表的“缺失”开头。
让我们来看看。 为此,请创建一个表格,以便一行占据整个页面-计数起来更加方便。 默认的缓冲区高速缓存大小为128 MB = 16384页8 KB。 因此,您需要在表中插入4096个以上的行。
=> CREATE TABLE big( id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, s char(1000) ) WITH (fillfactor=10); => INSERT INTO big(s) SELECT 'FOO' FROM generate_series(1,4096+1);
让我们分析一下表格。
=> ANALYZE big; => SELECT relpages FROM pg_class WHERE oid = 'big'::regclass;
relpages ---------- 4097 (1 row)
现在,我们必须重新启动服务器,以清除分析读取的表数据的缓存。
student$ sudo pg_ctlcluster 11 main restart
重新启动后,请阅读整个表:
=> EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
QUERY PLAN --------------------------------------------------------------------- Aggregate (actual time=14.472..14.473 rows=1 loops=1) -> Seq Scan on big (actual time=0.031..13.022 rows=4097 loops=1) Planning Time: 0.528 ms Execution Time: 14.590 ms (4 rows)
并确保缓冲区高速缓存中的表格页面仅占用32个缓冲区:
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 32 (1 row)
如果禁止顺序扫描,则将按索引读取表:
=> SET enable_seqscan = off; => EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
QUERY PLAN ------------------------------------------------------------------------------------------- Aggregate (actual time=50.300..50.301 rows=1 loops=1) -> Index Only Scan using big_pkey on big (actual time=0.098..48.547 rows=4097 loops=1) Heap Fetches: 4097 Planning Time: 0.067 ms Execution Time: 50.340 ms (5 rows)
在这种情况下,不使用缓冲区环,并且整个表都出现在缓冲区高速缓存中(几乎也包括整个索引):
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)
以类似的方式,缓冲区环用于清理过程(也为32页)和批量写入操作COPY IN和CREATE TABLE AS SELECT(通常为2048页,但不超过总缓冲区高速缓存的1/8)。
临时表
一般规则的一个例外是临时表。 由于临时数据仅对一个进程可见,因此它们与共享缓冲区高速缓存无关。 此外,临时数据仅存在于单个会话中,因此不需要对其进行保护以免发生故障。
对于临时数据,在拥有该表的进程的本地内存中使用高速缓存。 由于此类数据仅可用于一个进程,因此不需要使用锁进行保护。 本地缓存使用通常的抢先算法。
与常规缓冲区高速缓存不同,本地高速缓存的内存是根据需要分配的,因为并非在所有会话中都使用临时表。 一个会话中临时表的最大内存量受
temp_buffers参数限制。
预热缓存
重新启动服务器后,缓存“预热”之前应该经过一段时间-累积实际的活动使用数据。 有时立即将某些表的数据读入缓存可能很有用,为此专门设计了一个扩展名:
=> CREATE EXTENSION pg_prewarm;
以前,扩展只能读取缓冲区缓存中的某些表(或仅读取OS缓存中的表)。 但是在PostgreSQL 11中,它能够将当前缓存状态保存到磁盘上,并在服务器重启后将其还原。 要利用此优势,您需要将库添加到
shared_preload_libraries并重新启动服务器。
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
student$ sudo pg_ctlcluster 11 main restart
如果未更改
pg_prewarm.autoprewarm参数,则重新启动字段将自动启动autoprewarm主后台进程,该进程一旦进入
pg_prewarm.autoprewarm_interval,便会将高速缓存的页面列表
刷新到磁盘(设置
max_parallel_processes时不要忘记考虑新进程)。
=> SELECT name, setting, unit FROM pg_settings WHERE name LIKE 'pg_prewarm%';
name | setting | unit ---------------------------------+---------+------ pg_prewarm.autoprewarm | on | pg_prewarm.autoprewarm_interval | 300 | s (2 rows)
postgres$ ps -o pid,command --ppid `head -n 1 /var/lib/postgresql/11/main/postmaster.pid` | grep prewarm
10436 postgres: 11/main: autoprewarm master
现在缓存中没有大表:
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 0 (1 row)
如果我们假设其所有内容都很重要,则可以通过调用以下函数将其读入缓冲区高速缓存:
=> 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)
页面列表将转储到autoprewarm.blocks文件中。 要查看它,您可以等到自动预热主进程第一次运行时,但是我们手动启动它:
=> SELECT autoprewarm_dump_now();
autoprewarm_dump_now ---------------------- 4340 (1 row)
丢弃的页面数超过4097,其中包括服务器已读取的系统目录对象的页面。 这是文件:
postgres$ ls -l /var/lib/postgresql/11/main/autoprewarm.blocks
-rw------- 1 postgres postgres 102078 29 15:51 /var/lib/postgresql/11/main/autoprewarm.blocks
现在再次重新启动服务器。
student$ sudo pg_ctlcluster 11 main restart
启动后,我们的表立即再次出现在缓存中。
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)
这提供了相同的自动预热主过程:读取文件,将页面拆分为数据库,对页面进行排序(以便从磁盘读取尽可能一致),并将自动预热工作程序传递给单独的工作流程进行处理。
待续 。