PostgreSQL中的WAL:1.缓冲区缓存

之前的系列致力于PostgreSQL的隔离和多版本 ,今天我们开始了一个新的- 关于预写日志记录机制 。 让我提醒您,该材料基于Pavel pluzanov和我所做的行政培训课程 ,但不会逐字重复,并且旨在进行深思熟虑的阅读和独立的实验。

此周期将包括四个部分:

  • 缓冲区缓存(本文);
  • 预录日志 -在恢复过程中如何安排以及如何使用它;
  • 检查点和后台记录-为什么需要它们以及如何配置它们;
  • 日志调整 -要解决的级别和任务,可靠性和性能。

为什么需要日记?


在此过程中,DBMS处理的部分数据以延迟的方式存储在RAM中并写入磁盘(或其他非易失性介质)。 这种情况发生的次数越少,输入输出越少,系统运行越快。

但是,如果发生故障(例如,关闭电源或DBMS代码或操作系统中发生错误),将会发生什么? RAM中的所有内容都将丢失,仅保留写入磁盘的数据(在某些类型的故障中,磁盘也可能会受到影响,但在这种情况下,仅备份副本会有所帮助)。 原则上,I / O的组织方式可以使磁盘上的数据始终保持一致状态,但这很困难且效率不高(据我所知,只有Firebird才这样)。

通常,包括PostgreSQL在内,写入磁盘的数据不一致,并且从故障中恢复时,需要采取特殊措施来恢复一致性。 日记是使之成为可能的机制。

缓冲区缓存


奇怪的是,我们将开始讨论使用缓冲区高速缓存的日志记录。 缓冲区高速缓存不是存储在RAM中的唯一结构,而是最重要和最复杂的结构之一。 此外,了解其操作原理本身很重要,在本示例中,我们将熟悉如何在RAM和磁盘之间交换数据。

缓存在现代计算系统中无处不在;一个处理器可以单独计算三或四个缓存级别。 通常,需要任何高速缓存来消除两种类型的内存之间的性能差异,其中一种速度相对较快,但不足以满足所有人的需求,而另一种速度相对较慢,但容量却很大。 因此,缓冲区缓存可平滑对RAM(纳秒)和磁盘(毫秒)的访问时间之间的差异。

请注意,操作系统还具有解决相同问题的磁盘缓存。 因此,DBMS通常试图通过绕过OS缓存直接访问磁盘来避免双重缓存。 但是对于PostgreSQL,情况并非如此:所有数据都是使用普通文件操作读取和写入的。

此外,磁盘阵列,甚至磁盘本身也具有自己的缓存。 当我们谈到可靠性问题时,这一事实对我们仍然有用。

但是回到DBMS缓冲区缓存。

之所以这样称呼它,是因为它是一个缓冲区数组。 每个缓冲区是一个数据页(块)和一个标题的位置。 标题除其他外包括:

  • 页面在磁盘上的缓冲区中的位置(其中的文件和块号);
  • 页面上的数据已更改并且迟早应将其写入磁盘的迹象(这种缓冲区称为dirty );
  • 缓冲区的调用次数(使用计数);
  • 固定缓冲区的标志(引脚计数)。

缓冲区高速缓存位于服务器的共享内存中,所有进程均可访问。 要处理数据-读取或修改,-处理缓存中的读取页面。 当页面处于缓存中时,我们在RAM中使用它并保存对磁盘的访问。



最初,缓存包含空缓冲区,并且所有缓冲区都链接到空闲缓冲区列表中。 指向“下一个受害者”的指针的含义稍后将变得清楚。 为了在高速缓存中快速找到所需的页面,使用了哈希表。

缓存中的搜索页面


当进程需要读取页面时,它首先尝试使用哈希表在缓冲区高速缓存中找到它。 哈希键是文件号和文件内的页码。 在哈希表的相应篮子中,该过程找到缓冲区编号并检查其是否确实包含所需的页面。 与任何哈希表一样,此处可能发生冲突; 在这种情况下,该过程将不得不检查多个页面。

长期以来一直批评使用哈希表。 这种结构使您可以快速找到页面上的缓冲区,但是,例如,当您需要查找特定表占用的所有缓冲区时,它是完全没有用的。 但是还没有人提出好的替代方案。

如果在高速缓存中找到了所需的页面,则该过程应通过增加引脚数来“冻结”缓冲区(多个过程可以同时执行此操作)。 只要缓冲区是固定的(计数器值大于零),就认为缓冲区已被使用,并且其内容不应“激进”地更改。 例如,该行的新版本可能会出现在页面中-由于多版本和可见性规则,这不会打扰任何人。 但是无法将另一个页面读入固定缓冲区。

挤出来


可能会在缓存中找不到所需的页面。 在这种情况下,必须将其从磁盘读取到某个缓冲区。

如果缓存中仍然有可用缓冲区,则选择第一个可用缓冲区。 但是它们迟早会结束(通常数据库的大小大于为高速缓存分配的内存),然后您必须选择一个已占用的缓冲区,将页面强行移出并在空闲空间上读取一个新的页面。

抢占机制基于以下事实:每次访问缓冲区时,进程都会增加缓冲区头中的使用计数。 因此,那些不经常使用的缓冲区具有较低的计数器值,并且是排挤的良好候选者。

时钟扫描算法循环遍历所有缓冲区(使用指向“下一个受害者”的指针),从而将其访问计数减少一。 为了排挤,选择了第一个缓冲区,该缓冲区:

  1. 计数器为零(使用次数),
  2. 且不固定(零引脚数)。

您可以看到,如果所有缓冲区的命中计数器都为非零,则该算法将必须执行多个循环,以重置计数器,直到其中一个最终变为零为止。 为了避免“盘绕”,命中计数器的最大值限制为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) 

这提供了相同的自动预热主过程:读取文件,将页面拆分为数据库,对页面进行排序(以便从磁盘读取尽可能一致),并将自动预热工作程序传递给单独的工作流程进行处理。

待续

Source: https://habr.com/ru/post/zh-CN458186/


All Articles