PostgreSQL的WAL日志详解
PostgreSQL的WAL日志详解
1.WAL日志介绍
1.1 WAL介绍
wal日志即write ahead log预写式日志,简称wal日志。wal日志可以说是PostgreSQL中十分重要的部分,相当于oracle中的redo日志。
当数据库中数据发生变更时:
change发生时:先要将变更后内容计入wal buffer中,再将变更后的数据写入data buffer;
commit发生时:wal buffer中数据刷新到磁盘;
checkpoint发生时:将所有data buffer刷新到磁盘。
可以想象,如果没有wal日志,那么数据库中将会发生什么?
首先,当我们在数据库中更新数据时,如果没有wal日志,那么每次更新都会将数据刷到磁盘上,并且这个动作是随机i/o,性能可想而知。并且没有wal日志,关系型数据库中事务的ACID如何保证呢?
因此wal日志重要性可想而知。其中心思想就是:先写入日志文件,再写入数据。
说到checkpoint,我们再来看看哪些情况会触发数据库的checkpoing:
1.手动执行CHECKPOINT命令;
2.执行需要检查点的命令(例如pg_start_backup 或pg_ctl stop|restart等等);
3.达到检查点配置时间(checkpoint_timeout);
4.max_wal_size已满。
其中1和2两点都和数据库的配置无关,我们暂时先不看,这里先介绍下checkpoint_timeout和max_wal_size两个参数。
checkpoint_timeout:
自动 WAL 检查点之间的最长时间,以秒计。合理的范围在 30 秒到 1 天之间。默认是 5 分钟(5min)。增加这个参数的值会增加崩溃恢复所需的时间。
bill@bill=>show checkpoint_timeout ;
checkpoint_timeout
--------------------
30min
(1 row)
max_wal_size:
在自动 WAL检查点之间允许WAL 增长到的最大尺寸。这是一个软限制,在特殊的情况 下 WAL 尺寸可能会超过max_wal_size, 例如在重度负荷下、archive_command失败或者高的 wal_keep_segments设置。默认为 1 GB。增加这个参数可能导致崩溃恢复所需的时间。
bill@bill=>show max_wal_size ;
max_wal_size
--------------
2GB
(1 row)
和max_wal_size相对应的还有个min_wal_size,这里简单介绍下:
只要 WAL 磁盘用量保持在这个设置之下,在检查点时旧的 WAL文件总是被回收以便未来使用,而不是直接被删除。
可能对oracle熟悉的人会觉得wal日志和redo还是有些不同,没错,oracle中redo是固定几个redo日志文件,然后轮着切换去写入,因此我们常常会在io高的数据库中看到redo切换相关的等待事件。
而在pg中wal日志是动态切换,从pg9.6开始采用这种模式。和oracle不同的是,pg中这种动态wal切换步骤是这样的:单个wal日志写满(默认大小16MB,编译数据库时指定)继续写下一个wal日志,直到磁盘剩余空间不足min_wal_size时才会将旧的 WAL文件回收以便继续使用。
但是这种模式有一个弊端就是如果在checkpoint之前产生了大量的wal日志就会导致发生checkpoint时对性能的影响巨大,因此pg中还有一个参数checkpoint_completion_target来进行调整。
checkpoint_completion_target:
指定检查点完成的目标,作为检查点之间总时间的一部分。默认是 0.5。
什么意思呢,假如我的checkpoint_timeout设置是30分钟,而wal生成了10G,那么设置成0.5就允许我在15分钟内完成checkpoint,调大这个值就可以降低checkpoint对性能的影响,但是万一数据库出现故障,那么这个值设置越大数据就越危险。
总结:
大多数检查点应该是基于时间的,即由checkpoint_timeout触发。
性能(不频繁检查点)与恢复所需时间(频繁检查点)之间需要抉择:
值在15-30分钟之间是比例合适的,但到1小时不是什么坏事。
在决定checkpoint_timeout后,通过估计WAL的数量选择max_wal_size。
设置checkpoint_completion_target以便内核将数据刷新到磁盘的时间足够(但不是太多)。
1.2 WAL日志解读
对数据库操作会以record为单位首先记录到wal日志中,在checkpoint时才对数据进行刷
盘(background writer会定时刷脏数据,但最终还是都由checkpoint确认都刷盘成功)。
聊了这么久wal日志,我们都还不知道wal日志在哪?长啥样。。。
wal日志位置:
$PGDATA/pg_wal(pg10之前叫pg_xlog)
wal日志文件命名规则:
我们看到的wal日志是这样的:000000010000000100000092
其中前8位:00000001表示timeline;
中间8位:00000001表示logid;
最后8位:00000092表示logseg
wal日志LSN编号规则:
1/920001F8(高32位/低32位)
对照关系:
1、wal日志的logseg前6位始终是0,后两位是LSN低32位/16MB(2*24),即LSN的前两位。如上例中logseg最后两位是92,LSN低32前两位也是92。
2、LSN在wal日志中的偏移量即LSN低32位中后24位对应的十进制值。
例如当前wal日志偏移量为504
bill=# select pg_walfile_NAME_OFFSET(pg_current_wal_lsn());
pg_walfile_name_offset
--------------------------------
(000000010000000100000092,504)
(1 row)
bill=# select x'1F8'::int;
int4
------
504
(1 row)
1.3 wal日志内部详解
接下来我们来看看wal日志里面究竟记录的是些什么内容。如果你直接查看wal日志,可能会收到下面这样的提示:
因为wal日志是二进制格式的文件,不过我们可以使用pg_waldump这个工具来将其转换成可读的文件。
例1:
首先来看看insert数据时wal日志里面记录了些什么。
bill=# begin;
BEGIN
bill=# select pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
1/92021290
(1 row)
bill=# insert into tbl values(1,'bill');
INSERT 0 1
bill=# select pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
1/92021308
(1 row)
bill=# end;
COMMIT
接下来我们看看wal日志里面内容:可以看到wal日志里面记录了上面的insert操作。
例2:
我们再看看update时wal日志里面记录的内容:
bill=# select pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
1/92021450
(1 row)
bill=# update tbl set info = 'foucus' where id = 1;
UPDATE 1
这里简单介绍下这条记录的内容:
rmgr: Heap len (rec/tot): 65/ 177, tx: 717, lsn: 1/92021450, prev 1/92021418, desc: HOT_UPDATE off 1 xmax 717 flags 0x20 ; new off 2 xmax 0, blkref #0: rel 1663/16395/17623 blk 0 FPW
- rmgr: Heap :PostgreSQL内部将WAL日志归类到20多种不同的资源管理器。这条WAL记录所属资源管理器为 Heap,即堆表。除了Heap还有Btree,Transaction等。
- len (rec/tot): 65/ 177:wal记录的长度。
- tx: 717: 事务号。
- lsn: 1/92021450:本条wal记录的lsn。
- prev 1/92021418:上条wal记录的lsn。
- desc: HOT_UPDATE off 1 xmax 717 flags 0x20 ; new off 2 xmax 0: 这是一条热更新类型的记录,旧数据
- offset为1,xmax为717。旧tuple在page中的位置为1(即ctid的后半部分),新tuple在page中的位置为2。
- blkref #0: rel 1663/16395/17623 blk 0 :引用的第一个page(新tuple所在page)所属的堆表文件为1663/13543/16469,块号为0(即ctid的前半部分)。
2 流复制
3.1 日志提交过程
从上图我们可以看到流复制中日志提交的大致流程为:
1、事务commit后,日志在主库写入wal日志,还需要根据配置的日志同步级别,等待从库反馈的接收结果。
2、主库通过日志传输进程将日志块传给从库,从库接收进程收到日志开始回放,最终保证主从数据一致性。
2.2 流复制同步级别
PostgreSQL通过配置synchronous_commit (enum)参数来指定事务的同步级别。我们可以根据实际的业务需求,对不同的事务,设置不同的同步级别。
synchronous_commit = off # synchronization level;
# off, local, remote_write, or on
-
remote_apply:事务commit或rollback时,等待其redo在primary、以及同步standby(s)已持久化,并且其redo在同步
-
standby(s)已apply。
-
on:事务commit或rollback时,等待其redo在primary、以及同步standby(s)已持久化。
-
remote_write:事务commit或rollback时,等待其redo在primary已持久化; 其redo在同步standby(s)已调用write接口(写到 OS, 但是还没有调用持久化接口如fsync)。
-
local:事务commit或rollback时,等待其redo在primary已持久化;
-
off:事务commit或rollback时,等待其redo在primary已写入wal buffer,不需要等待其持久化;
不同的事务同步级别对应的数据安全级别越高,对应的对性能影响也就越大。上述从上至下安全级别越来越低。
详细的同步流复制原理见:
CommitTransaction @ src/backend/access/transam/xact.c
RecordTransactionCommit @ src/backend/access/transam/xact.c