我们已经熟悉了buffer cache的结构(共享内存的主要对象之一),并得出结论,要在所有RAM内容丢失后发生故障后恢复,必须保留预写日志(WAL)。
我们上次中断的地方尚未解决的问题是,我们不知道在恢复期间从哪里开始播放WAL记录。从头开始,这是不可行的:不可能从服务器启动时保留所有WAL记录-这可能既需要巨大的内存,又要很长的恢复时间。我们需要找到一个点,并且可以从该位置开始恢复(并相应地安全删除所有先前的WAL记录)。这就是我们要讲的检查点。
检查点
检查点必须具备哪些特点呢? 我们必须确保所有从检查点开始的WAL记录都将应用于刷新到磁盘的页面。如果不是这种情况,则在恢复期间,我们可能从磁盘上读取一个过旧的页面版本,对其应用WAL记录,这样做会不可逆转地损害数据。
我们如何获得检查点? 最简单的选择是不时暂停系统工作,并将缓冲区的所有脏页和其他高速缓存刷新到磁盘。(请注意,仅写入页面,而不从高速缓存中逐出页面)这些点将满足上述条件,但是时而连续"死亡"一段时间的系统,没有人会满意。
实际上这有点复杂:检查点从一个点变成一个间隔。首先,我们启动一个检查点。之后,我们悄悄地将脏缓冲区刷新到磁盘上,而不会中断工作或在任何可能的情况下导致峰值负载。
当所有在检查点开始时变脏的缓冲区都在磁盘上时,该检查点被视为已完成。现在(但不是更早),我们可以使用开始时间作为开始恢复的时间。而且我们不再需要到现在为止创建的WAL记录。
一个被称作检查点进程的后台进程执行检查点。
写入脏缓冲区的持续时间由checkpoint_completion_target参数定义。它显示了写入完成后两个相邻检查点之间的时间比例。默认值为0.5(如上图所示),即两次检查之间的写入时间占一半。通常,此值增加到1.0,以实现更高的均匀性。
让我们更详细地看看执行检查点时会发生什么。
首先,检查点将XACT缓冲区刷新到磁盘。由于它们很少(只有128个),因此它们会立即被写入。
然后,主要任务开始:从缓冲区高速缓存中刷新脏页。正如我们已经提到的,由于缓冲区高速缓存的大小可能很大,因此无法立即刷新所有页面。因此,buffer cache中所有当前脏的页面都用位于header中的特殊标志标记。
然后,检查点进程遍历所有缓冲区,并将标记的缓冲区刷新到磁盘。这里需要提醒你,页面不会从高速缓存中逐出,而只会写入磁盘。因此,你不必关注缓冲区的使用计数或是否被pin。
自然,在执行检查点时,buffer cache中的页面仍会继续被更新。但是不会标记新的脏缓冲区,并且检查点进程不会将它们写入磁盘。
在工作结束时,该过程将创建检查点末尾的WAL记录。该记录包含检查点开始时间的LSN。由于检查点启动时不会向WAL写入任何内容,因此任何日志记录都可以位于此LSN上。
此外,最后完成的检查点的指示在$PGDATA/global/pg_control文件中更新。在检查点完成之前,pg_control指向上一个检查点。
为了观看检查点的工作,让我们创建一个表。它的页面将进入buffer cache并称为脏页:
=> CREATE TABLE chkpt AS SELECT * FROM generate_series(1,10000) AS g(n); => CREATE EXTENSION pg_buffercache; => SELECT count(*) FROM pg_buffercache WHERE isdirty; count ------- 78 (1 row)
让我们记住当前的WAL位置:
=> SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3514A048 (1 row)
现在,让我们手动执行检查点,以确保高速缓存中不留任何脏页(正如我们已经提到的,可以出现新的脏页,但是在上述情况下,执行检查点时没有更改):
=> CHECKPOINT; => SELECT count(*) FROM pg_buffercache WHERE isdirty; count ------- 0 (1 row)
让我们看一下检查点在WAL中的体现:
=> SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3514A0E4 (1 row)
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/3514A048 -e 0/3514A0E4 rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/3514A048, prev 0/35149CEC, desc: RUNNING_XACTS nextXid 101105 latestCompletedXid 101104 oldestRunningXid 101105 rmgr: XLOG len (rec/tot): 102/ 102, tx: 0, lsn: 0/3514A07C, prev 0/3514A048, desc: CHECKPOINT_ONLINE redo 0/3514A048; tli 1; prev tli 1; fpw true; xid 0:101105; oid 74081; multi 1; offset 0; oldest xid 561 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 101105; online
在这里看到两个记录。最后一个是检查点完成的记录(CHECKPOINT_ONLINE)。在单词“ redo”之后输出检查点开始的LSN,此位置对应于在检查点开始时间的最后一个WAL记录。
我们将在控制文件中找到相同的信息:
postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | egrep 'Latest.*location' Latest checkpoint location: 0/3514A07C Latest checkpoint's REDO location: 0/3514A048
恢复
现在,我们准备更准确地陈述上一篇文章中提及的恢复算法。
如果postgresql server出现故障,则在随后的启动中,启动过程会通过查看pg_control文件来查找与“shutdown”状态不同的状态。在这种情况下,将执行自动恢复。
首先,恢复过程将从pg_control文件读取检查点开始位置。(要完成此操作,如果backup_label文件可用,那么将从那里读取检查点记录-从备份中还原这是必需的,但这是另一个系列的主题。)
然后,恢复过程将从找到的位置开始读取WAL,并将WAL记录逐一应用于页面(如果有需要,正如我们上次讨论的那样)。
最后,all unlogged tables are emptied by their initialization forks.。
这是启动过程完成的工作,检查点进程立即执行检查点以保护磁盘上已还原的状态。
我们可以通过强制在immediate模式下关闭来模拟故障。
student$ sudo pg_ctlcluster 11 main stop -m immediate --skip-systemctl-redirect
(这里需要--skip-systemctl-redirect选项,因为我们使用安装在Ubuntu上的PostgreSQL。它由pg_ctlcluster命令控制,该命令实际上调用systemctl,而后者又调用pg_ctl。但是--skip-systemctl-redirect选项使我们无需systemctl即可执行操作并保留重要信息。)
让我们检查集群的状态:
postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state Database cluster state: in production
启动时,PostgreSQL知道发生了故障,需要恢复。
student$ sudo pg_ctlcluster 11 main start postgres$ tail -n 7 /var/log/postgresql/postgresql-11-main.log 2019-07-17 15:27:49.441 MSK [8865] LOG: database system was interrupted; last known up at 2019-07-17 15:27:48 MSK 2019-07-17 15:27:49.801 MSK [8865] LOG: database system was not properly shut down; automatic recovery in progress 2019-07-17 15:27:49.804 MSK [8865] LOG: redo starts at 0/3514A048 2019-07-17 15:27:49.804 MSK [8865] LOG: invalid record length at 0/3514A0E4: wanted 24, got 0 2019-07-17 15:27:49.804 MSK [8865] LOG: redo done at 0/3514A07C 2019-07-17 15:27:49.824 MSK [8864] LOG: database system is ready to accept connections 2019-07-17 15:27:50.409 MSK [8872] [unknown]@[unknown] LOG: incomplete startup packet
日志中报告了需要恢复:数据库系统未正确关闭; 自动恢复正在进行中 然后在«redo starts at»位置开始播放WAL记录,并在可能获取下一个WAL记录的同时继续播放。这样就可以在«redo done at»位置完成恢复,并且DBMS开始与客户端一起工作(数据库系统已准备好接受连接)。
在服务器正常关闭时会发生什么? 要将脏页刷新到磁盘,PostgreSQL断开所有客户端的连接,然后执行最终检查点。
让我们记住当前的WAL位置:
=> SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/3514A14C (1 row)
现在,我们以常规方式关闭:
student$ sudo pg_ctlcluster 11 main stop
检查集群状态:
postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state Database cluster state: shut down
而且WAL具有最终检查点(CHECKPOINT_SHUTDOWN)的唯一记录:
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/3514A14C rmgr: XLOG len (rec/tot): 102/ 102, tx: 0, lsn: 0/3514A14C, prev 0/3514A0E4, desc: CHECKPOINT_SHUTDOWN redo 0/3514A14C; tli 1; prev tli 1; fpw true; xid 0:101105; oid 74081; multi 1; offset 0; oldest xid 561 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown pg_waldump: FATAL: error in WAL record at 0/3514A14C: invalid record length at 0/3514A1B4: wanted 24, got 0
重新启动实例:
student$ sudo pg_ctlcluster 11 main start
后台写
如我们所知,检查点是将脏页从buffer cache刷新到磁盘的过程之一。但这不是唯一的。
如果后端进程需要从缓冲区刷新页面,但是该页面含有脏数据,则该进程将不得不自行将页面写入磁盘。这种情况并不好,因为它需要等待-在后台异步完成写入会更好。
因此,除了检查点进程外,还存在后台写进程(也称为bgwriter或仅称为writer)。该进程使用与驱逐技术相同的算法来搜索缓冲区。他们有两个区别。
1.后台写进程使用自己的指针,而不是指向«next victim»的指针。自己的指正可以在«next victim»的指针之前,但是永远不能在它之后。 2.遍历缓冲区时,使用计数不会减少。
被写出的buffer要满足以下条件:
·包含脏的数据
·没有被pin住(pin计数为0)
·使用计数为0
因此,后台写过程先于eviction,找到很可能很快被逐出的缓冲区。理想的情况是,后台写必须能够检测到他们选择的缓冲区可以被使用,而不会浪费写入时间。
调优
通常根据以下推理来设置检查点。
首先,我们需要确定在两个检查点之间可以负担多少数量的WAL记录(以及我们可以接受的恢复时间)。越多越好,但是出于明显的原因,该值是有限的。
然后,我们可以计算出在正常负载下生成此数量的wal所需的时间。我们已经讨论了如何执行此操作(我们需要记住WAL中的位置,并从另一个位置中减去一个)。
接着是检查点之间的通常间隔。设置checkpoint_timeout参数。默认值为5分钟,显然太短;通常会增加到半个小时。
但是有可能(甚至可能)有时负载会比平时更高,并且在参数指定的时间内会生成过多的WAL记录。在这种情况下,希望更频繁地执行检查点。为此,我们在max_wal_size参数中指定允许的WAL文件大小。如果实际量更多,则服务器将启动计划外的检查点。
服务器需要保留从最后一个完成的检查点开始的WAL文件以及当前检查点期间累积的文件。因此,可以将总数量估算为一个检查点周期中的数量乘以(1 + checkpoint_completion_target)。在版本11之前,我们应该乘以(2 + checkpoint_completion_target),因为PostgreSQL还保留了最后一个检查点中的文件。
因此,大多数检查点都按计划执行:每个checkpoint_timeout时间单位一次。但是在负载增加时,达到max_wal_size的数量时,检查点执行的频率会更高。
重要的是要理解可以超过max_wal_size参数的值:
·max_wal_size参数的值仅是理想值,而不是严格的限制。实际可能超过该值。
·server不能擦除尚未通过复制槽传递、或尚未归档的的wal文件,
可以通过min_wal_size参数指定最小值。
仅在调整检查点时才调整后台写才有意义。
后台写一次最多写bgwriter_lru_maxpages个页,在下一次写之前会根据bgwriter_delay的值sleep一段时间。
默认值为:bgwriter_delay = 200毫秒,bgwriter_lru_maxpages = 100。
如果根本找不到脏缓冲区(也就是说,系统中什么也没有发生),则它“进入休眠状态”。
监控
你需要根据监控结果来调优检查点进程和后台写。
如果wal数量太多,参数checkpoint_warning会输出警告提醒,默认值是30秒,我们需要将其调整到checkpoint_timeout的值。
参数log_checkpoints可以将检查点信息写入log。默认是不开启
=> ALTER SYSTEM SET log_checkpoints = on; => SELECT pg_reload_conf();
现在,让我们更改数据中的某些内容并执行检查点。
=> UPDATE chkpt SET n = n + 1; => CHECKPOINT;
在看看log文件的内容:
postgres$ tail -n 2 /var/log/postgresql/postgresql-11-main.log 2019-07-17 15:27:55.248 MSK [8962] LOG: checkpoint starting: immediate force wait 2019-07-17 15:27:55.274 MSK [8962] LOG: checkpoint complete: wrote 79 buffers (0.5%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.013 s, total=0.025 s; sync files=2, longest=0.011 s, average=0.006 s; distance=1645 kB, estimate=1645 kB
我们可以在此处看到写入了多少缓冲区,在检查点之后更改了WAL文件集,执行检查点花费了多长时间以及相邻检查点之间的距离(以字节为单位)。
但是,最有用的信息可能是pg_stat_bgwriter视图中检查点和后台写进程的统计信息。
=> SELECT * FROM pg_stat_bgwriter \gx -[ RECORD 1 ]---------+------------------------------ checkpoints_timed | 0 checkpoints_req | 1 checkpoint_write_time | 1 checkpoint_sync_time | 13 buffers_checkpoint | 79 buffers_clean | 0 maxwritten_clean | 0 buffers_backend | 42 buffers_backend_fsync | 0 buffers_alloc | 363 stats_reset | 2019-07-17 15:27:49.826414+03
其中:
·checkpoints_timed--按计划(到达checkpoint_timeout时)。
·checkpoints_req--按需(包括在达到max_wal_size时执行的检查)。该值越大,表明检查点发生的越频繁。
以下是有关写入页数的重要信息:
·buffers_checkpoint--通过检查点。
·buffers_backend--通过后端进程。
·buffers_clean--通过后台写进程。
在一个经过良好调整的系统中,buffers_backend的值必须小于buffers_checkpoint和buffers_clean的总和。
参数maxwrite_clean的值也将有助于调整后台写。它显示由于超出bgwriter_lru_maxpages的值而使进程停止了多少次。
可以在重置收集的统计信息:
=> SELECT pg_stat_reset_shared('bgwriter');
原文地址:https://habr.com/en/company/postgrespro/blog/494464/