前言

相信大家学到这里,基础肯定很不错了。我们知道 wal 文件默认是 16M,wal_keep_size 默认是 0 ,表示不存放额外的 wal 日志,max_wal_size 表示 wal 目录的大小,如果超过了这个大小,有设置了归档的情况下,则会移动到归档目录里。在老版本里面这个参数是wal_keep_segments,表示保留 wal 文件的个数。如果你看到这里还是不太懂,表示这篇内容你可能还不太适合看,建议先补好基础,当然了你想看我也拦不了你。

如果现在从库异常宕机了,等恢复之后,向主库发送当前需要的 wal record,主库一看傻眼了,这个 wal 已经被归档或者已经被删除了,那怎么办呢?这种情况下只能pg_basebackup 了。

那么主库能不能把还没同步给从库的 wal 文件一直持有着,即使超过了max_wal_size 配置的大小也不删除呢?

有的,它就是我们今天要说的复制槽。

什么是复制槽

复制槽提供了一种办法确保主库不会“删除”还未发送到备库的WAL日志,也不会删除备库需要的多版本,即使备库掉线。

为什么要设置复制槽

复制槽是从 PostgreSQL9.4 引入的,主要是提供了一种自动化的方法来确保主库在所有的备库收到 wal 之前不会移除它们,并且主库也不会移除可能导致恢复冲突的行,即使备库断开也是如此。

在没有启用replication slots的环境中,如果碰到 ERROR: requested WAL segment xxxx has already been removed 的错误,解决办法是要么提前开启了归档,要么重做slave,另外还可以在主库上设置max_wal_size 为更大的值。当然,如果备库停机时间太长,可能主库的WAL日志目录会被撑满,如果设置了复制槽,建议将WAL日志目录放在大容量硬盘上。

工作原理

主库PostgreSQL实例会一直保留预写日志(WAL)文件,直到所有备库所需的插槽都确认已接收到特定段为止。只有完成此操作后,主库实例才会移除相应的WAL文件。

实验过程

#主库上创建一个复制槽
SELECT pg_create_physical_replication_slot('test_slot');

#从库的postgres.conf配置后重启
primary_slot_name = 'slot_name'

#此时查询已经有数据了
SELECT * FROM pg_replication_slots WHERE slot_name = 'test_slot';

接下里我们来做个实验

#关闭从库,该从库已经有配置了复制槽
docker-compose down postgres-01

#在主库上执行
insert into test values(generate_series(1,1000));
SELECT pg_switch_wal(); 
insert into test values(generate_series(1,1000));
SELECT pg_switch_wal(); 
insert into test values(generate_series(1,1000));
SELECT pg_switch_wal(); 
SELECT pg_switch_wal(); 

此时启动这从库,大家想想会发生什么事情?没错,此时启动从库还是正常的。因为一个 wal 文件大小是 16M,该目录下默认可以存 64 个文件。只是执行四五遍是没办法触发实验结果的。

SELECT pg_walfile_name(pg_current_wal_lsn()); 
00000002000000010000009D

#如果你乐意可以多执行几遍
DO $$
BEGIN
  FOR i IN 1..70 LOOP
    PERFORM pg_switch_wal();
    insert into test values(generate_series(1,1000));
    COMMIT;
  END LOOP;
END $$;

SELECT pg_walfile_name(pg_current_wal_lsn()); 
0000000200000001000000E4
#此时已经产生了70个文件了

[root@192 data]# du -sh pg_wal/
2.8G	pg_wal
我们可以看到,此时主库的pg_wal目录远远超过了默认的1G,因为此时从库一直没有反应,主库不敢删除从库没确认的wal

此时启动从库,可以看到复制状态是正常的,没有出现 ERROR: requested WAL segment xxxx has already been removed 的错误。

接下里我们把复制槽给删了,再来做个实验试试看

SELECT PID FROM pg_replication_slots WHERE slot_name = 'test_slot';
SELECT pg_terminate_backend(49);
SELECT pg_drop_replication_slot('test_slot');

因为postgresql不允许直接删除活跃的复制槽,所以需要终止后再进行删除
执行70次的日志切换后启动就能看到报错了,从库所需的wal日志已经是被清除了
2025-07-07 16:08:51.718 UTC [34] FATAL:  could not receive data from WAL stream: ERROR:  requested WAL segment 000000020000000300000044 has already been removed
2025-07-07 16:08:51.719 UTC [30] LOG:  waiting for WAL to become available at 3/44000018

相关配置参数和监控

wal_keep_size:当 pg_wal 目录大小超过配置参数max_wal_size 时,提供额外的空间存储 wal 日志。默认是 0 表示不额外存储。pg_wal 目录大小=wal_keep_size+max_wal_size

max_wal_size(12 之后)/wal_keep_segments(12 之前):设置为较大值,保证pg_wal目录下保留较多的wal日志,主库上wal日志留存越多,允许备库宕机的时间越长,设置此参数需要注意不要将pg_wal目录撑满。或者在主库上开启归档,如果没有足够的硬盘空间保留wal归档,至少在备库停机维护时临时开启主库归档。如果备库落后主库wal_keep_segments数量的wal,则主库可能会删除备用服务器仍需要的wal,这种情况下,流复制就会中断。

hot_standby_feedback:备库定时将最小活跃事务ID(xmin)告诉master,使得 master在执行vacuum 时对备库还需要的tuple手下留情,但这样可能会导致主库膨胀,在每个wal_receiver_status_interval定义的周期内发送的频率不超过一次,并且此设置不会覆盖在主数据库上的old_snapshot_threshold行为。

max_standby_streaming_delay:通常会将一些执行时间较长的分析任务、统计SQL跑在备库上。在备库上执行长时间的查询,由于涉及的记录有可能在主库上被更新或删除,主库上对更新或删除数据的老版本进行vacuum后,从库上也会执行这个操作,从而与从库上的当前查询产生冲突。此参数默认为30s,当备库执行SQL时,有可能与正在应用的WAL发生冲突,此查询如果30s没有执行完就被中止,注意30s不是备库上单个查询允许的最大执行时间,是指当备库上应用WAL时允许的最大WAL延迟应用时间,因此备库上查询的执行时间有可能不到这个值就被中止了,此参数可以设置为-1,表示当从库上的WAL应用进程与从库上执行的查询冲突时,WAL应用进程一直等待直到从库查询执行完成。

old_snapshot_threshold:单位为min,最大可以设置为60天,当vacuum回收垃圾时,遇到垃圾记录的xmax大于数据库中现存的最早未提交事务xmin时,不会对其进行回收。因此当数据库中存在很久为结束的事务时,可能会导致数据库膨胀。此参数代表强制删除为过老的事务快照保留的死元组。这会导致长事务读取已被删除的tuple时出错。

vacuum_defer_cleanup_age:指定vacuum延迟清理死亡元组的事务数,vacuum会延迟清除无效的记录,延迟的事务个数通过vacuum_defer_cleanup_age进行设置。默认为0,在主库上设置一个稍大的值也可以减少冲突的发生,但是并不太好计量。

max_standby_archive_delay:备机因为处理归档的wal日志产生查询冲突而取消查询之前的等待时间,和上面的参数类似。

recovery_min_apply_delay:延迟备库设置备库延迟重做WAL的时间,而备库依然及时接收主库发送的WAL日志流,只是不是一接收到WAL后就立即应用,而是等待此参数设置的值再应用。使用此功能将延迟hot_standby_feedback,当synchronous_commit设置为remote_apply时,同步复制也会受此设置的影响,每个commit都需要等待。

#创建复制槽
SELECT pg_create_physical_replication_slot('test_slot');
#删除复制槽
SELECT pg_drop_replication_slot('test_slot');
#查看复制槽情况
SELECT * FROM pg_replication_slots WHERE slot_name = 'test_slot';

总结

我们把之前学的 wal 日志进行了总结,再加上做了实验加深影响,想必大伙都能理解透了,现在就差没有看源码去分析。

我们只需要记住一点,主库会一直保留从库没接收到的 wal 日志。这样子会导致磁盘被撑爆!!!!!以及到时从库起来之后,会有大量的 IO 写,为了赶上主库的进度。

我们这一篇主要讲的是物理复制槽,还有逻辑逻辑流备份和逻辑复制槽我们留着以后再讲。

参考来源:https://www.modb.pro/db/29737