PostgreSQL中的WAL:4.日志设置

因此,我们熟悉了缓冲区高速缓存的设备,并以其示例为例,意识到在故障期间RAM的内容丢失时,需要恢复预写日志 。 由于定期执行检查点 ,因此所需日志文件的大小和恢复时间受到限制。

在先前的文章中,我们已经以一种或另一种方式查看了与日记相关的大量重要设置。 在本文(本系列的最后一篇)中,我们将考虑尚未讨论的调优问题:日志级别及其用途,以及日志记录的可靠性和性能。

日志级别


预记录日志的主要目的是提供从故障中恢复的能力。 但是,如果您仍然需要保留日记,则可以将其修改为其他任务,并向其中添加一定数量的其他信息。 有几个级别的日志记录。 它们由wal_level参数设置并进行组织,以使每个下一个级别的日志都包括进入上一个级别的日志的所有内容以及其他新内容。

最小的


最小可能级别由值wal_level = minimum设置,并保证仅在出现故障后才能恢复。 为了节省空间,不记录与海量数据处理有关的操作(例如CREATE TABLE AS SELECT或CREATE INDEX)。 而是将必要的数据立即写入磁盘,并将新对象添加到系统目录中,并在提交事务时显示该对象。 如果在操作过程中发生故障,则已经记录的数据将保持不可见并且不会违反一致性。 如果在操作完成之后发生故障,则所有必要的东西都已经存入磁盘,并且不需要进行记录。

让我们看看。 首先,设置所需的级别(为此,您还需要更改另一个参数-max_wal_senders )。

=> ALTER SYSTEM SET wal_level = minimal; => ALTER SYSTEM SET max_wal_senders = 0; 

 student$ sudo pg_ctlcluster 11 main restart 

请注意,更改级别需要重新启动服务器。

记住日志中的当前位置:

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/353927BC (1 row) 

现在,让我们创建表(CREATE TABLE AS SELECT)并再次在日志中写入位置。 在这种情况下,SELECT语句选择的数据量无关紧要,因此我们将自己限制为一行。

 => CREATE TABLE wallevel AS SELECT 1 AS n; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/353A7DFC (1 row) 

使用熟悉的pg_waldump实用程序,让我们看一下日志条目。

 postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353927BC -e 0/353A7DFC 

当然,某些细节可能因发射而异,但是在这种情况下,这就是发生的情况。 Heap2管理器条目涉及清洁,这是系统目录中表之一的页面清洁(用rel中的“ short”数字可以很容易地用裸眼区分系统对象):

 rmgr: Heap2 len (rec/tot): 59/ 7587, tx: 0, lsn: 0/353927BC, prev 0/35392788, desc: CLEAN remxid 101126, blkref #0: rel 1663/16386/1247 blk 8 FPW 

然后有一条记录,关于为我们将要创建的表获取下一个OID:

 rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/35394574, prev 0/353927BC, desc: NEXTOID 82295 

现在实际创建表:

 rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/35394594, prev 0/35394574, desc: CREATE base/16386/74103 

但是,未记录将数据插入表中。 然后有许多关于将行插入到不同的表和索引中的条目-这个PostgreSQL在系统目录中注册了创建的表(我以缩写形式给出):

 rmgr: Heap len (rec/tot): 203/ 203, tx: 101127, lsn: 0/353945C0, prev 0/35394594, desc: INSERT off 71, blkref #0: rel 1663/16386/1247 blk 8 rmgr: Btree len (rec/tot): 53/ 685, tx: 101127, lsn: 0/3539468C, prev 0/353945C0, desc: INSERT_LEAF off 37, blkref #0: rel 1663/16386/2703 blk 2 FPW ... rmgr: Btree len (rec/tot): 53/ 2393, tx: 101127, lsn: 0/353A747C, prev 0/353A6788, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW 

最后,交易固定:

 rmgr: Transaction len (rec/tot): 34/ 34, tx: 101127, lsn: 0/353A7DD8, prev 0/353A747C, desc: COMMIT 2019-07-23 18:59:34.923124 MSK 

复制品


当我们从备份还原系统时,我们从文件系统的某种状态开始,并逐渐将数据带到恢复点,从而播放存档的日记帐分录。 此类记录的数量可能非常大(例如几天),也就是说,恢复期将不会覆盖一个控制点,而是覆盖许多控制点。 因此,很明显,日志的最低级别是不够的-如果未记录某些操作,我们将根本不知道需要重复该操作。 要从备份还原,必须记录所有操作。

复制也是如此-任何未记录的内容都不会转移到副本中,也不会被复制。 但是,如果我们想在副本上执行请求,它仍然很复杂。

首先,我们需要有关主服务器上发生的排他锁的信息,因为它们可能与副本服务器上的请求冲突。 这样的锁被记录并应用于副本(代表启动过程)。

其次,您需要能够构建数据快照 ,为此,正如我们回想的那样,需要有关正在进行的事务的信息。 对于副本服务器,我们不仅在谈论本地事务,还在谈论主服务器上的事务。 传输此信息的唯一方法是定期将其写入日志(这种情况每15秒发生一次)。

日志级别由值wal_level = 复制副本设置,该日志级别既保证了从备份中恢复的能力又保证了物理复制的可能性。 (在9.6版之前,有两个单独的级别archive和hot_standby,但随后将它们合并为一个普通级别。)

从PostgreSQL 10开始,默认情况下会设置此级别(在此之前是最小级别)。 因此,只需将参数重置为默认值即可:

 => ALTER SYSTEM RESET wal_level; => ALTER SYSTEM RESET max_wal_senders; 

 student$ sudo pg_ctlcluster 11 main restart 

我们删除该表并重复与上次完全相同的操作顺序:

 => DROP TABLE wallevel; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/353AF21C (1 row) 
 => CREATE TABLE wallevel AS SELECT 1 AS n; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/353BE51C (1 row) 

现在检查日记帐分录。

 postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353AF21C -e 0/353BE51C 

清理,获取OID,创建表并在系统目录中注册-现在,一切都保持原样:

 rmgr: Heap2 len (rec/tot): 58/ 58, tx: 0, lsn: 0/353AF21C, prev 0/353AF044, desc: CLEAN remxid 101128, blkref #0: rel 1663/16386/1247 blk 8 rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/353AF258, prev 0/353AF21C, desc: NEXTOID 82298 rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/353AF278, prev 0/353AF258, desc: CREATE base/16386/74106 rmgr: Heap len (rec/tot): 203/ 203, tx: 101129, lsn: 0/353AF2A4, prev 0/353AF278, desc: INSERT off 73, blkref #0: rel 1663/16386/1247 blk 8 rmgr: Btree len (rec/tot): 53/ 717, tx: 101129, lsn: 0/353AF370, prev 0/353AF2A4, … rmgr: Btree len (rec/tot): 53/ 2413, tx: 101129, lsn: 0/353BD954, prev 0/353BCC44, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW 

但是有些新东西。 与备用管理器相关的排他锁的记录-在这种情况下,它阻止了事务号(为什么需要该事务号,我们将在下一系列文章中详细讨论):

 rmgr: Standby len (rec/tot): 42/ 42, tx: 101129, lsn: 0/353BE2D8, prev 0/353BD954, desc: LOCK xid 101129 db 16386 rel 74106 

这是一条有关在表中插入行的记录(将文件编号rel与CREATE记录中上面指示的编号进行比较):

 rmgr: Heap len (rec/tot): 59/ 59, tx: 101129, lsn: 0/353BE304, prev 0/353BE2D8, desc: INSERT+INIT off 1, blkref #0: rel 1663/16386/74106 blk 0 

提交记录:

 rmgr: Transaction len (rec/tot): 421/ 421, tx: 101129, lsn: 0/353BE340, prev 0/353BE304, desc: COMMIT 2019-07-23 18:59:37.870333 MSK; inval msgs: catcache 74 catcache 73 catcache 74 catcache 73 catcache 50 catcache 49 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 snapshot 2608 relcache 74106 snapshot 1214 

另一个周期性发生的记录与已完成的事务无关,它与备用管理器有关,并报告当前正在进行的事务:

 rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/353BE4E8, prev 0/353BE340, desc: RUNNING_XACTS nextXid 101130 latestCompletedXid 101129 oldestRunningXid 101130 

逻辑上


最后,最后一个级别由参数wal_level = logical的值设置,并提供了逻辑解码和逻辑复制的可能性。 必须在发布服务器上启用它。

从日志条目的角度来看,此级别实际上与副本没有什么不同-与复制源相关的记录以及可以添加到应用程序日志中的任意逻辑条目都已添加。 基本上,逻辑解码取决于有关正在进行的事务的信息,因为您需要构建数据快照以跟踪系统目录中的更改。

现在,我们将不再详细介绍备份和复制的操作-这是单独系列文章中的重要话题。

记录可靠性


显然,日志记录机制必须是可靠的,并且可以保证在任何情况下都可以恢复(当然,与数据载体的损坏无关)。 可靠性受许多因素影响,我们将考虑其中的缓存,数据损坏和记录的原子性。

快取


非易失性存储(例如硬盘驱动器)的数据路径上有许多高速缓存。

当某个程序(任何,但在我们的情况下为PostgreSQL)要求操作系统将某些内容写入磁盘时,操作系统会将数据传输到RAM中的缓存中。 实际记录异步发生,具体取决于操作系统的I / O调度程序的设置。

当操作系统决定写入数据时,它们将落入驱动器(硬盘)的缓存中。 驱动电子设备还可以延迟记录,例如,以更有利可图的方式在同一时间记录的组中收集数据。 而且,如果使用RAID控制器,则操作系统和驱动器之间会出现另一级缓存。

因此,如果不采取特殊措施,则完全不清楚何时可以真正安全地存储数据。 通常这并不重要,但是在某些关键地方,PostgreSQL需要确保安全地写入数据。 首先,这是日记(如果日记条目未到达磁盘,它将与RAM的其余内容一起消失)和一个检查点(必须确保脏页实际上已写入磁盘)。 但是还有其他情况,例如,在最低级别执行非新闻化操作等。

操作系统提供的工具必须保证立即将数据写入非易失性存储器。 有几个选项,但主要分为两个选项:要么在记录后给出同步命令(fsync,fdatasync),要么在打开文件(或对其进行写入)时显示一个特殊标志,用于同步甚至直接记录,绕过OS缓存。

对于日志,使用pg_test_fsync实用程序可以选择最适合特定OS和特定文件系统的方法,并将其安装在wal_sync_method配置参数中。 常规文件始终使用fsync进行同步。

微妙之处在于,选择一种方法时,必须考虑设备的特性。 例如,如果您使用备用电池支持的控制器,则没有理由不使用其缓存,因为在断电时电池将保存数据。

该文档包含有关此主题的许多详细信息

在任何情况下,同步都是昂贵的,并且发生的频率不超过绝对必要的频率(谈论性能时,我们将回到这个问题的位置要低一些)。

一般来说,可以关闭同步( fsync参数负责此操作),但是在这种情况下,您应该忘记存储可靠性。 通过禁用fsync ,您同意随时可能丢失数据。 当可以轻松地从其他来源恢复数据时(例如,在初始迁移期间),使用此选项的唯一合理选择可能是暂时提高生产率。

资料损坏


设备不完美,通过接口电缆等传输数据时,介质上的数据可能会损坏。其中一些错误是在硬件级别处理的,而有些则不是。

为了及时发现问题,日志条目始终提供有校验和。

数据页也可以用校验和保护。 目前,这只能在初始化集群时完成,但是在PostgreSQL 12中,可以使用pg_checksums实用程序将其打开和关闭(尽管尚未运行,但仅在服务器停止时才打开)。

在生产环境中,尽管计算和控制开销很大,但必须包括校验和。 这降低了无法及时检测到故障的可能性。

减少但不消除。
首先,仅在访问页面时才检查校验和-因此,损坏可能不会引起注意,直到它进入所有备份为止。 这就是为什么pg_probackup在备份期间检查集群所有页面的校验和的原因。
其次,用零填充的页面被认为是正确的-如果文件系统错误地使文件“无效”,则可能不会引起注意。
第三,校验和仅保护数据文件的主要层。 其余层和其他文件(例如XACT事务状态)不受任何保护。
las

让我们看看它是如何工作的。 首先,请确保已启用校验和(请注意,在类似Debian的系统上安装软件包时并非如此):

 => SHOW data_checksums; 
  data_checksums ---------------- on (1 row) 

data_checksums参数为只读。

这是我们的表所在的文件:

 => SELECT pg_relation_filepath('wallevel'); 
  pg_relation_filepath ---------------------- base/16386/24890 (1 row) 

停止服务器并在零页面中更改一些字节,例如,从LSN标头中删除最后一个日志条目。

 student$ sudo pg_ctlcluster 11 main stop 

 postgres$ dd if=/dev/zero of=/var/lib/postgresql/11/main/base/16386/24890 oflag=dsync conv=notrunc bs=1 count=8 
 8+0 records in 8+0 records out 8 bytes copied, 0,0083022 s, 1,0 kB/s 

原则上,服务器无法停止。 将页面写入磁盘并被迫退出缓存就足够了(否则服务器将使用缓存中的页面)。 但是,这种情况很难重现。

现在,我们启动服务器并尝试读取该表。

 student$ sudo pg_ctlcluster 11 main start 

 => SELECT * FROM wallevel; 
 WARNING: page verification failed, calculated checksum 23222 but expected 50884 ERROR: invalid page in block 0 of relation base/16386/24890 

但是,如果无法从备份还原数据怎么办? ignore_checksum_failure参数使可以尝试读取表,这自然会导致数据失真。

 => SET ignore_checksum_failure = on; => SELECT * FROM wallevel; 
 WARNING: page verification failed, calculated checksum 23222 but expected 50884 n --- 1 (1 row) 

当然,在这种情况下,一切都会顺利进行,因为我们仅弄乱了页面标题,而不弄乱了数据本身。

还有一件事。 启用校验和后,提示位会写入日志(我们之前已对其进行了检查 ),因为任何位(甚至是非必需位)的更改也会导致校验和的更改。 关闭校验和后, wal_log_hints参数负责将提示位写入日志

对工具提示位的更改始终记录为整页图像 (FPI,整页图像),这将按顺序增加日志的大小。 在这种情况下,使用wal_compression参数(此参数出现在9.5版中)启用完整图像的压缩是有意义的。 下面我们看一些具体的数字。

原子度记录


最后,还有记录的原子性问题。 数据库页面至少占用8 KB(可以为16或32 KB),而在较低级别上,记录以通常较小的块(通常为512字节或4 KB)进行。 因此,在电源故障的情况下,数据页可能会被部分记录。 显然,在恢复过程中,将普通日记帐分录应用于此类页面没有任何意义。

为了保护起见,PostgreSQL允许您在检查点开始后第一次更改页面将页面完整图像写入日志(当工具提示位更改时,会记录同一图像)。 full_page_writes参数控制此 ,默认情况下启用。

如果在日志还原过程中遇到页面图像,则会无条件地(无需LSN检查)将其写入磁盘:对它有更多的信任,因为与任何日志记录一样,它受校验和保护。 并且已经将常规日记帐分录应用于此保证正确的图像。

尽管PostgreSQL从整个页面图像中排除了未分配的空间(我们之前看过块结构),但是生成的日记帐分录的数量却显着增加。 如前所述,可以通过压缩完整图像( wal_compression参数)来改善这种情况。

为了以某种方式感觉到日志大小的变化,我们将使用pgbench实用工具进行一个简单的实验。 让我们初始化:

 student$ pgbench -i test 
 dropping old tables... creating tables... generating data... 100000 of 100000 tuples (100%) done (elapsed 0.15 s, remaining 0.00 s) vacuuming... creating primary keys... done. 

启用了full_page_writes选项

 => SHOW full_page_writes; 
  full_page_writes ------------------ on (1 row) 

运行断点并立即运行测试30秒钟。

 => CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/38E04A08 (1 row) 

 student$ pgbench -T 30 test 
 starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 26851 latency average = 1.117 ms tps = 895.006720 (including connections establishing) tps = 895.095229 (excluding connections establishing) 

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/3A69C478 (1 row) 

日志大小:

 => SELECT pg_size_pretty('0/3A69C478'::pg_lsn - '0/38E04A08'::pg_lsn); 
  pg_size_pretty ---------------- 25 MB (1 row) 

现在关闭full_page_writes参数:

 => ALTER SYSTEM SET full_page_writes = off; => SELECT pg_reload_conf(); 

并重复实验。

 => CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/3A69C530 (1 row) 

 student$ pgbench -T 30 test 
 starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 27234 latency average = 1.102 ms tps = 907.783080 (including connections establishing) tps = 907.895326 (excluding connections establishing) 

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/3BE87658 (1 row) 

日志大小:

 => SELECT pg_size_pretty('0/3BE87658'::pg_lsn - '0/3A69C530'::pg_lsn); 
  pg_size_pretty ---------------- 24 MB (1 row) 

是的,大小已减小,但没有像人们预期的那么重要。

原因是群集是用数据页中的校验和初始化的,因此,在更改工具提示位时,仍然必须将整页图像写入日志。 这些数据(在我们的示例中)约占总数据量的一半,可以通过查看统计数据来查看:

 postgres$ /usr/lib/postgresql/11/bin/pg_waldump --stats -p /var/lib/postgresql/11/main/pg_wal -s 0/3A69C530 -e 0/3BE87658 
 Type N (%) Record size (%) FPI size (%) ---- - --- ----------- --- -------- --- XLOG 1721 ( 1,03) 84329 ( 0,77) 13916104 (100,00) Transaction 27235 ( 16,32) 926070 ( 8,46) 0 ( 0,00) Storage 1 ( 0,00) 42 ( 0,00) 0 ( 0,00) CLOG 1 ( 0,00) 30 ( 0,00) 0 ( 0,00) Standby 4 ( 0,00) 240 ( 0,00) 0 ( 0,00) Heap2 27522 ( 16,49) 1726352 ( 15,76) 0 ( 0,00) Heap 109691 ( 65,71) 8169121 ( 74,59) 0 ( 0,00) Btree 756 ( 0,45) 45380 ( 0,41) 0 ( 0,00) -------- -------- -------- Total 166931 10951564 [44,04%] 13916104 [55,96%] 

为了紧凑起见,我从表中删除了零行。 注意总行(总计),并将完整图像的大小(FPI大小)与普通记录的大小(记录大小)进行比较。

仅当单独使用的文件系统和硬件保证原子记录时,才能禁用full_page_writes参数。 但是,正如我们所看到的,没有什么充分的理由(假设包括校验和)。

现在,让我们看看压缩是如何帮助的。

 => ALTER SYSTEM SET full_page_writes = on; => ALTER SYSTEM SET wal_compression = on; => SELECT pg_reload_conf(); 

重复相同的实验。

 => CHECKPOINT; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/3BE87710 (1 row) 

 student$ pgbench -T 30 test 
 starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 26833 latency average = 1.118 ms tps = 894.405027 (including connections establishing) tps = 894.516845 (excluding connections establishing) 

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/3CBD3EA8 (1 row) 

日志大小:

 => SELECT pg_size_pretty('0/3CBD3EA8'::pg_lsn - '0/3BE87710'::pg_lsn); 
  pg_size_pretty ---------------- 13 MB (1 row) 

结论:在存在大量的全页图像(由于校验和或full_page_writes ,几乎总是如此)的情况下,尽管压缩会加载处理器,但使用压缩还是很有可能的。

性能表现


在正常服务器操作期间,会连续顺序记录日志文件。 由于没有随机访问权限,因此常规硬盘也可以完成此任务。 但是,加载的这种性质与访问数据文件的方式显着不同。

因此,通常将日志放在服务器文件系统上安装的单独的物理磁盘(或磁盘阵列)上是有利的。 代替$ PGDATA / pg_wal目录,您需要创建一个指向相应目录的符号链接。

在某些情况下,不仅需要写入日志文件,还需要读取日志文件。 第一个是发生故障后可以恢复的情况。 第二个不那么琐碎。 如果使用流复制,则会发生这种情况,并且副本仍旧在主服务器RAM缓冲区中时,副本将无法接收日记条目。 然后,walsender进程必须从磁盘读取必要的数据。 复制时,我们将详细讨论。

记录以两种模式之一进行:

  • 同步-提交事务后,直到有关该事务的所有日志条目都在磁盘上,才能继续工作;
  • 异步-事务立即完成,并且日志写入后台。

同步模式由synchronous_commit参数确定,默认情况下启用。

由于同步与实际(即慢速)I / O相关联,因此尽可能少地执行同步是有益的。 为此,完成事务并写入日志的服务过程需要短暂的暂停,这由commit_delay参数确定。 , commit_siblings . , . , , - .

commit_siblings = 5, commit_delay = 0, . commit_delay , OLTP-.

LSN ( , ). .

( D ACID) — , . , ( COMMIT ) .

, synchronous_commit = off ( local).

wal writer, ( wal_writer_delay = 200ms ).

, , WAL. , , , , . (, : , , .)

, ( ) — ?

, , .

— . : , 3 × wal_writer_delay ( ).

— — .

: ( fsync = off), . , , , .

synchronous_commit . , . , , .

. , WAL. , , .

- , , pgbench.

 => ALTER SYSTEM SET synchronous_commit = off; => SELECT pg_reload_conf(); 

 student$ pgbench -T 30 test 
 starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 30 s number of transactions actually processed: 45439 latency average = 0.660 ms tps = 1514.561710 (including connections establishing) tps = 1514.710558 (excluding connections establishing) 

900 (tps), — 1500. , , , .

. - , . !

, .

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


All Articles