PostgreSQL的WAL(4)--WAL创建和调优
2020-09-21 21:43 abce 阅读(4003) 评论(0) 编辑 收藏 举报
在前面的文章中,我们已经回顾了许多与WAL相关的重要设置。在本文(本系列的最后一篇)中,我们将讨论尚未解决的WAL设置问题:WAL的不同级别及其用途、以及WAL的可靠性和性能。
WAL级别
1.minimal
最小可能的级别由wal_level=minimal设置,只能保证在出现故障后可以执行恢复。为了节省空间,不会将与批量数据处理有关的操作(例如CREATE TABLE AS SELECT或CREATE INDEX)写入WAL。而是将所需的数据立即写入磁盘,并将新对象添加到系统目录中,并在事务提交时可见。如果在执行操作时发生故障,则已写入的数据将保持不可见并且不会违反一致性规则。并且,如果在完成操作后发生故障,则所需的所有内容都已经在磁盘上,不需要进行日志记录。
=> ALTER SYSTEM SET wal_level = minimal; => ALTER SYSTEM SET max_wal_senders = 0;
student$ sudo pg_ctlcluster 11 main restart
这些修改需要重启postgresql server。
=> SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353927BC (1 row)
现在,让我们创建一个表(CREATE TABLE AS SELECT),并再次记下WAL位置。在这种情况下,由SELECT运算符检索的数据量完全无关紧要,因此一行就足够了。
=> CREATE TABLE wallevel AS SELECT 1 AS n; => SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- 0/353A7DFC (1 row)
来看看wal记录:
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353927BC -e 0/353A7DFC
当然,可能有一些细节上的不同,但在本例中,我们将得到以下内容。Heap2 manager的记录与vacuum有关,这里是系统目录表的in-page vacuum。
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
但是数据插入到表中并没有写入wal。在不同的表和索引中插入多个记录——通过这种方式,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
复制也是如此:没有记录的所有内容都不会发送给副本,也不会重新播放。如果希望在副本上运行查询,则会使情况更加复杂。
首先,我们需要主服务器上出现的exclusive advisory locks的信息,因为它们可能会与副本上的查询发生冲突。这些锁将被写入wal,然后启动过程将它们应用到副本上。
其次,我们需要创建数据快照,要做到这一点,需要关于正在执行的事务的信息。对于副本,不仅意味着本地事务,还意味着主服务器上的事务。提供这些信息的唯一方法是不时地写wal日志记录(这种情况每15秒发生一次)。
确保从备份恢复和物理复制可能性的WAL级别由wal_level = replica设置。(在版本9.6之前,有两个独立的级别——archive和hot_standby——但后来它们被合并了)
=> 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)
再来检查一下wal记录:
postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353AF21C -e 0/353BE51C
vacuum、获取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
出现了新的内容。与`Standby` manager相关的排他锁记录——这里是事务ID上的锁:
rmgr: Standby len (rec/tot): 42/ 42, tx: 101129, lsn: 0/353BE2D8, prev 0/353BD954, desc: LOCK xid 101129 db 16386 rel 74106
这是我们表中插入行的记录(比较文件号rel和创建记录中的记录):
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指定,提供了逻辑解码和逻辑复制。逻辑订阅时必须打开。
从WAL记录的角度来看,此级别实际上与replica级别相同:添加了与复制源相关的记录,以及应用程序可以添加到WAL的任意逻辑记录。但是逻辑解码主要取决于有关正在执行的事务的信息,因为需要创建数据快照来跟踪对系统目录的更改。
写的可靠性
日志记录技术必须是可靠的,并确保在任何情况下都可以恢复。许多因素都会影响可靠性,我们将讨论缓存、数据损坏和写入原子性。
将数据存储到非易失性存储的过程存在着多级缓存。
如果某个程序要求操作系统(OS)在磁盘上写入内容,则OS会将数据传输至其RAM缓存。写入实际上是异步发生的,具体取决于操作系统的I/O调度程序的设置。
当操作系统决定写入数据时,它们会进入存储(硬盘)的缓存中。存储器的电子设备还可以例如通过将更有效地写入在一起的数据分组来推迟写入。而且,如果使用RAID控制器,则在操作系统和磁盘之间添加一个更高的缓存级别。
因此,如果不采取特殊措施,就绝对不清楚何时以可靠的方式实际保存数据。但是PostgreSQL在某些关键区域必须确保以适当的可靠性写入数据。这里主要是日志(如果WAL记录未到达磁盘,则它将与其余RAM内容一起丢失)和检查点(我们必须确保脏页确实已写入磁盘)。但是还有其他情况,例如以最小级别执行未记录的操作等。
操作系统提供了确保立即将数据写入非易失性存储器的功能。有几个选项,但是它们减少到两个主要选项:在写操作之后,执行同步调用(fsync,fdatasync),或者在打开文件(或写入文件)之后,设置一个特殊标志来指示需要同步,甚至直接写绕过OS缓存。
对于WAL,pg_test_fsync实用程序允许我们选择最适合特定操作系统和特定文件系统的方法,并且该方法在wal_sync_method参数中指定。普通文件使用fsync同步。
选择方法时,我们需要考虑硬件特性。例如:如果使用的控制器使用了电池备份单元(BBU),则没有理由避免使用控制器的缓存,因为BBU可以在断电的情况下保存数据。
在任何情况下,同步都是昂贵的。
通常,你可以关闭同步(fsync参数负责此操作),但是在这种情况下,你必须忘记存储的可靠性。通过关闭fsync,你同意可以随时丢失数据。 当可以轻松地从其他来源恢复数据时(例如在初始迁移时),此参数唯一合理的使用案例是性能的暂时提高。
2.数据损坏
硬件并不完美,通过接口电缆等传输时,存储中的数据可能会损坏。 其中一些错误是在硬件级别处理的,而其他则没有。
为了快速检测问题,WAL记录中提供了校验和。
数据页也可以由校验和保护。 之前只能在集群初始化时执行此操作,但是在PostgreSQL 12中,可以通过pg_checksums实用程序打开和关闭校验和。
在生产环境中,必须强制启用校验和,无论计算和验证它们的开销成本如何。 这降低了无法及时检测到损坏的可能性。
校验和只是减少了但没有消除数据损坏的可能性: 首先,仅在访问页面时才校验和。因此,损坏可以逃脱检测,直到它进入全备份为止。出于这个原因,pg_probackup在数据备份期间验证所有集群页面的校验和。 其次,用零填充的页面被认为是正确的,因此,如果文件系统错误地使文件为«nullifies» ,则可以逃避检测。 第三,校验和仅保护数据的主分支。其他派生文件和其余文件(例如,事务状态XACT)完全不受保护。
让我们看看它是如何工作的。 首先,我们确保打开校验和(请注意,在类似Debian的系统上安装的软件包中,默认情况下并非如此):
postgres=# show fsync; fsync ------- on (1 row) postgres=# show wal_sync_method; wal_sync_method ----------------- fdatasync (1 row) postgres=#
这是我们表所在的文件:
=> SELECT pg_relation_filepath('wallevel'); pg_relation_filepath ---------------------- base/16386/24890 (1 row)
现在我们关闭postgresql server并在零页上更改一些字节,例如:从header中删除最后一个WAL记录的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)
还有一点需要注意。启用校验和后,提示位(hint bits)将被WAL记录,因为对任何位(甚至是非必需位)的更改都会导致对校验和的更改。关闭校验和时,wal_log_hints参数负责WAL记录提示位。
提示位的更改始终记录为FPI(full page image),这会增加了WAL大小。 在这种情况下,使用wal_compression参数打开FPI压缩是有意义的(此参数已在9.5版中添加)。
最后,看看写的原子性存在的问题。数据库页占用不少于8 KB(可能为16或32KB),在底层,写操作以块形式进行,通常较小(通常为512字节或4KB)。因此,在断电的情况下,可能会将部分写入数据页。显然,在恢复期间,将常规的WAL记录应用于这样的页面是没有意义的。
为了避免这种情况,PostgreSQL在检查点周期开始以来的页面的第一次更改中启用了WAL记录整个页面的镜像(当提示位更改时,也会记录同一图像)。full_page_writes参数对此进行控制,并且默认情况下处于启用状态。
如果恢复过程遇到WAL中的FPI,它将无条件地将镜像写入磁盘(无需LSN检查):FPI更受信任,因为它受校验和保护,就像每个WAL记录一样。
尽管在PostgreSQL中,FPI不包括可用空间(我们之前讨论了块结构),但FPI大大增加了WAL记录的生成量。如前所述,可以通过压缩FPI(使用wal_compression参数)来改善这种情况。
为了深入了解FPI会如何更改WAL的大小,让我们使用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: 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)
看看wal记录大小:
=> 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: 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)
查看生成的wal的大小:
=> SELECT pg_size_pretty('0/3BE87658'::pg_lsn - '0/3A69C530'::pg_lsn); pg_size_pretty ---------------- 24 MB (1 row)
wal大小减小了,但没有我们期望的那么大。
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%]
其中值为0的被移除了,这样可以看得更紧凑一点。注意看汇总那一行(total),比较FPI和常规的WAL记录的大小。
仅当文件系统和硬件本身确保写入的原子性时,才能关闭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: 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)
查看wal记录的大小:
=> SELECT pg_size_pretty('0/3CBD3EA8'::pg_lsn - '0/3BE87710'::pg_lsn); pg_size_pretty ---------------- 13 MB (1 row)
在正常工作期间,WAL文件按照一个接一个的顺序被连续写入。由于没有随机访问,即使是HDD磁盘也能正常工作。但是,这种负载与访问数据文件时的负载有很大不同。
因此,将WAL存储在单独的物理磁盘(或磁盘阵列)上通常是有益的。必须创建指向相应目录的符号链接,而不是$PGDATA/pg_wal目录。
在某些情况下,不仅需要写入WAL文件,还需要读取WAL文件。第一个是发生故障后恢复的明确案例。第二个不那么琐碎。如果使用流复制,WAL记录仍在主服务器的OS缓冲区中时,副本延迟接收WAL记录。在这种情况下,walsender进程必须从磁盘读取必要的数据。进行复制时,我们将对此进行更详细的讨论。
WAL以以下两种方式之一写入:
·同步-在事务提交时,直到该事务的所有WAL记录都进入磁盘后,才能继续工作。 ·异步-事务立即完成,并且WAL在后台写入。
缺省情况下,synchronous_commit参数处于打开状态,用于设置同步模式。
由于同步与实际的(即较慢的)输入/输出有关,因此尽可能少地进行同步是有好处的。为此,完成事务并写入WAL的后端进程会稍作暂停,这由commit_delay参数定义。但这仅在系统具有不少于commit_siblings活动事务的情况下才会发生。此行为依赖于这样的期望:在等待时间内,一些事务将完成,并且可以一次性同步它们。这类似于你摁住电梯门的方式,以便有人有时间进入。
默认情况下,commit_siblings = 5,commit_delay = 0,因此实际上没有等待。仅对于执行大量OLTP事务的系统,更改commit_delay的值才有意义。
然后,将WAL的一部分刷新到所需的LSN位置(如果在等待时间内添加了新记录,则刷新更多)。刷新之后,事务被视为完成。
同步写入可确保持久性(ACID缩写中的字母D):如果提交了事务,则其所有WAL记录都已经在磁盘上,并且不会丢失。但是缺点是同步写入会增加响应时间(COMMIT命令直到同步结束才返回控制权),并且会降低系统性能。
您可以通过设置sync_commit = off(或local)来使写入变成异步。
当异步写入时,WAL记录将由wal writer进程刷新,该进程将交替工作并等待(等待时间由wal_writer_delay参数指定,默认值为200 ms)。
该进程被唤醒后,它将检查自上次以来是否出现了完全填充的WAL页面。如果确实出现了,则该进程将忽略当前未填充到最后的页面,仅写入完全填充的页面。(但是,并非一次完成:写入到达高速缓存末尾时会停止,而下一次从高速缓存起始点开始。)
但是,如果没有任何页面被填满,则该进程将刷新当前的WAL页面(未填充到末尾)—要不然,唤醒后干嘛呢?
该算法旨在尽可能避免多次同步同一页面,这对于大量更新至关重要。
异步写入比同步写入更有效,因为更改的提交不等待WAL页面的写入。但是可靠性降低了:如果在提交和失败之间经过的时间少于3×wal_writer_delay单位,则提交失败的数据可能会在失败的情况下丢失(使用默认设置,该时间稍长于半秒)。
在效率和可靠性之间并非一个简单的选择,要取决于系统管理员。
请注意:与关闭同步(fsync = off)不同,异步模式不会使恢复变得不可能。万一发生故障,系统将恢复一致状态,但也许某些最后的事务将不存在。
可以为单独的事务设置synchronous_commit 参数。这样可以通过仅牺牲某些事务的可靠性来提高性能。例如金融交易必须同步进行,而聊天消息却可能使用异步。
实际上,两种模式可以协同工作。即使使用同步提交,长事务的WAL记录也将被异步写入,以释放WAL缓冲区。而且,如果在从缓冲区高速缓存中刷新页面期间,似乎还没有在磁盘上存储相应的WAL记录,那么它将以同步模式立即被刷新。
为了深入了解异步提交的好处,让我们尝试在此模式下重复pgbench测试。
=> ALTER SYSTEM SET synchronous_commit = off; => SELECT pg_reload_conf(); student$ pgbench -T 30 test starting vacuum...end. transaction type: 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 tps。 不用说,在实际系统中的实际负载下,所占的比例会有所不同,但是很明显,对于短期交易而言,其影响可能是相当大的。
原文地址:https://habr.com/en/company/postgrespro/blog/496150/