我是一个插槽,今天我做掉了数据库。

一. 什么是复制插槽及优缺点

在没有我的时代(9.4版本之前),管理WAL文件是一个巨大的挑战。在标准的流复制环境中,在主库执行增、删、改所有操做的变更都会记录到WAL文件中,如果主库的事务比较多,每分钟就会产生数GB的WAL文件,WAL文件的名字总是按照数字序号顺序递增,并且默认文件大小总是16MB。WAL的大小在11版本之前只能通过initdb建库指定--wal-segsize设置,而在11版本之后则可以使用pg_resetwal修改文件大小,参数wal_segment_size可以查看WAL大小。当主备流复制环境的备库需要停机维护的时候,主库上的WAL文件必须保留住,不然主库做完检查点可能就会回收或者覆盖WAL日志。这样一旦从库维护好了需要恢复,就会遭到巨大的打击,从库会报错requested WAL segment 0000000100000000000000xxx has already been removed。为了解决这个问题,工程师在9.4版本把我创造出来。我的主要的功能就是记录着备库需要的最老的WAL文件的地址(LSN),这样当主库回收或者覆盖WAL文件的时候,因为我的存在就会比较“克制”的清理。

而没有我的做法就比较low了。他们会选择将wal_keep_segment参数设置的足够大,以避免WAL日志回收覆盖写,这种方法的缺点就是很难确定设置的值是对的。比如停机维护需要2小时,根据需求设置了保留4小时的量,但是突发一个情况,维护时间变成了6小时,或者此时主库正好做了一个大事务,导致WAL短时间大量增长。这些巧合可能都会击垮设置的wal_keep_segment参数值。最终可能导致WAL文件再次被覆盖。如果设置的足够高,确实可以解决问题,但是就可能会耗尽pg_wal目录的空间,导致整个数据库hang住。还有一种做法就是设置archive_command把WAL归档到其他位置,这倒是个好注意。一旦主库的WAL被回收了,还有WAL备份可以进行恢复。

二、取代hot_standby_feedback和vacuum_defer_cleanup_age

而我还有一些作用,就是取代hot_standby_feedbackvacuum_defer_cleanup_age。这里我来一一科普。

首先说说hot_standby_feedback的故事。

有这样一种情况,开发人员或者是程序在备库执行一个较长的查询,此时在主库上有人频繁的做更新或者是删除操作,将会产生大量的dead tuple,当达到了自动auto vacumme设定的阈值,就会清理掉那些dead tuple。此时备库仍然在执行查询,而备库的回放进程应用了主库的vacumme操作时,就会检测出vacumm清理的元组仍然有事务在使用,此时就会认为有冲突错误,它会在等待max_standby_streaming_delay(默认30秒之后)终止备库上的查询操作,然后输出下列错误信息:

ERROR: canceling statement due to conflict with recovery
Detail: User query might have needed to see row versions that must be removed
复制代码

而设置了HOT_STANDBY_FEEDBACK参数为on时,备库就会定期向主库通知最小活跃事务id(xmin)值,这样主库做auto vacumme的时候就不会清理大于xmin的dead tuple。这个参数设置完后会减少备库执行查询时复制冲突的情况,但是它也会导致auto vacumme延迟清理,从而引起表膨胀。

可能大家会觉得hot_standby_feedback参数已经能很好的控制冲突的问题了,实际上则不然,不然我也不会取而代之。

例如一种情况:主备之间的网络出现异常,walreceiver连接断开,无法向主库正常发送xmin值,此时主库因为接收不到xmin值,理论上可以随意清理dead tuple,一旦网络恢复就又会造成冲突。而我则是把xmin直接记录了下来,就算网络异常,我也会提供保护。

再来说说vacuum_defer_cleanup_age的故事。

vacuum_defer_cleanup_age参数是推迟VACUUM的一种办法,设置它时候,最旧的xmin等于备用数据库上运行的最旧事务ID减去vacuum_defer_cleanup_age参数设置的值,由于该值是根据在主服务器上发生的写的事务数量来衡量的,因此很难预测将为备库查询提供多少额外的宽限时间。这个参数官方文档已经弃了,可能在早期的版本还能看到它的身影,现在已建议用hot_standby_feedback替代。

三、复制插槽的类型

我主要有2种类型,一种是用于标准的流复制环境的物理插槽,另外一种是逻辑复制插槽,逻辑复制插槽不仅仅具有物理复制插槽的作用,它还有逻辑解码的功能。逻辑解码可以直接从数据库的WAL日志中进行解码,并将数据库产生的变更抽取成多种文件格式。比较著名的复制软件Debezium就使用了以protobuf或者wal2json逻辑解码插件来进行抽取。其实大家也可以自己使用解码插件解码成json或者其他格式弄到mongodb或者es中解锁一些高级玩法。

四、使用物理复制插槽

接下来测试一下我的(物理复制插槽)功能,首先这是一套PG 13的流复制环境,我已经存在并保持着工作(pgstandby1),这里不动它,可以再创建一个做测试。

postgres=# select slot_name,slot_type,active,restart_lsn from pg_replication_slots;
slot_name  | slot_type | active | restart_lsn 
------------+-----------+--------+-------------
pgstandby1 | physical  | t      | 0/3001060
复制代码

使用pg_create_physical_replication_slot函数可以再次进行创建。

postgres=# select pg_create_physical_replication_slot('testslot');
pg_create_physical_replication_slot 
-------------------------------------
 (testslot,)
(1 row)
复制代码

接下来我们建立一个目录,使用本机的pg_receivewal命令,指定插槽名称来接收wal日志。

[postgres@centos8 ~]$ pg_receivewal -D testwals/ -S testslot -v 
pg_receivewal: starting log streaming at 0/3000000 (timeline 1)
复制代码

查看文件夹下的文件,发现当前正在接收03文件。

[postgres@centos8 testwals]$ ls -lrt
total 16384
-rw-------. 1 postgres postgres 16777216 Nov 15 15:24 000000010000000000000003.partial
复制代码

在主库执行pgbench,造点数据压一波。

[postgres@centos8 ~]$ pgbench -i -s 10 test
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
1000000 of 1000000 tuples (100%) done (elapsed 1.41 s, remaining 0.00 s)
vacuuming...
creating primary keys...

done in 2.27 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 1.46 s, vacuum 0.29 s, primary keys 0.50 s).
复制代码

此时可以看到我们的pg_receivewal程序接收了一批wal日志。

[postgres@centos8 ~]$ pg_receivewal -D testwals/ -S testslot -v 
pg_receivewal: starting log streaming at 0/3000000 (timeline 1)
pg_receivewal: finished segment at 0/4000000 (timeline 1)
pg_receivewal: finished segment at 0/5000000 (timeline 1)
pg_receivewal: finished segment at 0/6000000 (timeline 1)
pg_receivewal: finished segment at 0/7000000 (timeline 1)
pg_receivewal: finished segment at 0/8000000 (timeline 1)
pg_receivewal: finished segment at 0/9000000 (timeline 1)
pg_receivewal: finished segment at 0/A000000 (timeline 1)
		
[postgres@centos8 testwals]$ ls -lrt
total 131072
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000003
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000004
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000005
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000006
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000007
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000008
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 000000010000000000000009
-rw-------. 1 postgres postgres 16777216 Nov 15 15:42 00000001000000000000000A.partial
	
	
postgres=# select slot_name,slot_type,active,restart_lsn from pg_replication_slots where slot_name='testslot';
 slot_name | slot_type | active | restart_lsn 
-----------+-----------+--------+-------------
 testslot  | physical  | t      | 0/A000000
(1 row)
复制代码

这里可以看到,我一直在修改restart_lsn的值,即使磁盘空间快满了,PostgreSQL也不会回收restart_lsn后面的WAL文件。

四、做掉数据库

最后给大家说说,我是怎么做掉数据库的。大家可以看到如果备库异常宕机,短时间无法拉起,那么我记录的位置就一直会停滞不前。这样如果系统短时间产生大量的WAL文件会把服务器空间撑爆,空间一旦满了,就无法处理新事务,数据库就会hang住,甚至可能会直接crash。

那么有没有办法阻止这一切?

有,在13版本之前,可以通过监控pg_replication_slots视图及时发现,以下sql可以监控插槽保留的lsn位置到最新的redo lsn落后多少,如果落后的量徒增或者特别的大,就应该引起注意了。

postgres=# SELECT redo_lsn, slot_name,restart_lsn, round((redo_lsn-restart_lsn)/1024/1024,2) AS  MB_behind  FROM pg_control_checkpoint(), pg_replication_slots;
 redo_lsn  | slot_name  | restart_lsn | mb_behind 
-----------+------------+-------------+-----------
 0/AAFF2D0 | pgstandby1 | 0/AAFF3B8   |      0.00
(1 row)

复制代码

而在13版本的时候,推出了一个新参数max_slot_wal_keep_size。参数含义非常简单,允许复制插槽保留的WAL的最大磁盘空间。如果复制插槽的restart_lsn落后于当前redo LSN超过给定参数大小,则该插槽将将被标记为无效,同时之前的WAL文件可能会被删除。如果walsender进程正在使用该插槽,则将发出终止该进程的信号。如果walsender重新启动,发现要传输的WAL文件已经被删,则使用插槽的备库必须重建。如果max_slot_wal_keep_size为零(这是默认值),则和以前工作模式一样没有限制。

而13版本的pg_replication_slots的视图得到了进一步的增强,增加了wal_statussafe_wal_size字段。

postgres=# select wal_status,safe_wal_size  from pg_replication_slots;
 wal_status | safe_wal_size 
------------+---------------
 reserved   |    1078987728
(1 row)
复制代码

wal_status字段代表要求保护的WAL文件的可用性。一共可能有4种情况,如果是reserved,表示声明的文件在max_wal_size内。max_wal_size是设置自动检查点之间WAL日志可以增长的最大值,默认为1GB。如果是extended,表示此时超出了max_wal_size的限制,但复制插槽仍然保留了文件。此时受(wal_keep_sizemax_slot_wal_keep_size参数保护)。此时前面两种状态暂且正常。但是,当插槽超出限制时,这里就会变成unreserved,这意味着它处于迫在眉睫的危险,如果足够快,它仍然可以恢复。最后是lost状态,代表了WAL文件已被删除且无法恢复。

仅当`max_slot_wal_keep_size为非负数时,才能看到最后两个状态。如果restart_lsn为NULL,则此字段为null。

另一列safe_wal_size,需要设置max_slot_wal_keep_size才会出现数值,意思是到在被删除WAL文件之前,可以写入的WAL字节数。如果这个值是负数或者是0,意味着已经超过了max_slot_wal_keep_size设置的值,只要做检查点,就会删除wal文件,这样使用插槽的备库就必须重建了。

最后希望大家好好关注我,不要因为我而带来重大的灾难!