PG之流复制冲突
PostgreSQL 的基于流复制的物理备库是基于redo的物理块复制备库,允许开放只读的功能,但是需要注意,由于主库会源源不断的产生redo,这些redo可能会与备库的QUERY产生冲突。
1、什么是流复制冲突?
每当恢复进程无法在备用服务器上应用从主服务器传递过来的WAL信息时,就会发生流复制冲突,因为变更会中断正在执行的查询。这些冲突不会在主服务器上的查询中发生,但会在流复制中的备用服务器上发生,因为主服务器对备用服务器上发生的事情知之甚少。
流复制冲突发生在从库允许开放只读的情况下,因为主库可能不断产生wal,这些wal在apply时可能会与备库的query产生冲突。注意流复制冲突不会发生在primary服务器上。
简单点来说,就是备库查询(备库query)与恢复(备库apply)冲突。
2、常见的复制冲突类型?
即在什么情况下query会堵塞、或与恢复冲突?
当以下操作产生的REDO被复制到备库,并且备库准备拿这些REDO来恢复时会产生复制冲突。
快照复制冲突
这是最常见的复制冲突。如果VACUUM处理一个表并且删除了死亡元组,可能会发生快照冲突。该删除将在备用服务器上重播,这一删除的动作也会在备用服务器上重放。备用服务器上的查询可能已经在主服务器上的VACUUM发生之前启动(它有一个较早的快照),因此它仍然可以看到应该删除的元组。这就构成了快照冲突。
Application of a vacuum cleanup record from WAL conflicts with standby transactions whose snapshots can still "see" any of the rows to be removed.
主库回收dead tuple的REDO,同事备库当前的query snapshot需要看到这些记录。
这种情况可以通过参数控制,恢复优先,或查询优先。 可以配置时间窗口。
而且这种冲突出现的概率也非常的小,除非用户在备库使用repeatable read,同时是非常大的事务。
而通常用户用的都是read committed.
锁复制冲突
在备用服务器上,对正在执行查询的表上,获得了ACCESS EXCLUSIVE的排他锁。因此,必须在备用服务器上回放主服务器上的任何获取ACCESS EXCLUSIVE锁的操作(与ACCESS SHARE冲突),以防止发生表上不兼容的操作。
PostgreSQL在诸如DROP TABLE,TRUNCATE和大多数ALTER TABLE等操作会获取与SELECT相冲突的锁,如果备用服务器在正在查询使用的表上回放这样的锁,就会发生锁冲突。
Access Exclusive locks taken on the primary server, including both explicit LOCK commands and various DDL actions, conflict with table accesses in standby queries.
主库的访问排它锁,与备库对应的锁产生冲突。
例如主库truncate a表, 备库查询a表。
这种情况的冲突面很窄。
Buffer pin复制冲突
One way to reduce the need for VACUUM is to use HOT updates. Then any query on the primary that accesses a page with dead heap-only tuples and can get an exclusive lock on it will prune the HOT chains. PostgreSQL always holds such page locks for a short time, so there is no conflict with processing on the primary. There are other causes for page locks, but this is perhaps the most frequent one.When the standby server should replay such an exclusive page lock and a query is using the page (“has the page pinned” in PostgreSQL jargon), you get a buffer pin replication conflict. Pages can be pinned for a while, for example during a sequential scan of a table on the outer side of a nested loop join.HOT chain pruning can of course also lead to snapshot replication conflicts.
减少对VACUUM需求的一种方法便是使用HOT更新。
然后,如果访问了一个只有死堆的元组的页面,并且可以获得该页面上的排他锁,就会删除HOT链。
然后,任何在主数据库上访问仅包含死堆元组的页面并可以在该页面上获取exclusive锁的查询都将修剪HOT链。PostgreSQL总是在很短的时间内持有这种页面锁,因此与主数据库上的处理没有冲突。页面锁定还有其他原因,但这可能是最常见的原因。
当备用服务器回放此类的排它页面锁并且有查询正在使用该页面时(在PostgreSQL中的术语为“已将页面固定”),则会出现buffer pin复制冲突。页面可以被固定一段时间,例如在嵌套循环连接外侧的表的顺序扫描期间。
当然,HOT链剪枝也可能导致快照复制冲突。
Application of a vacuum cleanup record from WAL conflicts with queries accessing the target page on the standby, whether or not the data to be removed is visible.
同上,但是当query访问的页就是要清理垃圾的页时,也是有冲突的。
罕见的复制冲突
以下类型的冲突很少见,不会困扰您:
死锁复制冲突
A query on the standby blocks while using the shared buffer that is needed to replay WAL from the primary. PostgreSQL will cancel such a query immediately.在备用数据库上的查询使用到了回放WAL所需的共享缓冲区。PostgreSQL将立即取消此类查询。
表空间复制冲突
表空间位于备用服务器上的temp_tablespaces中,并且查询中具有临时文件。当主数据库出现DROP TABLESPACE时,会发生冲突。在这种情况下,PostgreSQL会取消备用数据库上的所有查询。
Dropping a tablespace on the primary conflicts with standby queries using that tablespace for temporary work files.
主库删除表空间,备库使用这个表空间产生临时文件。 例如主库删除TBS,备库的一个大的查询需要写临时文件,并且这个临时文件是写到这个表空间的。这种情况非常少见,也很容易规避,新建一个临时表空间不要删除即可。
数据库复制冲突
如果备用数据库在数据库上具有活动会话,则复制DROP DATABASE会导致冲突。在这种情况下,PostgreSQL会终止备用数据库上的的所有连接。
Dropping a database on the primary conflicts with sessions connected to that database on the standby.
主库删除数据库,备库刚好连在这个数据库上。
这种情况也非常的少见。
3、备用服务器如何解决复制冲突?
PG中的参数max_standby_streaming_delay决定了当wal重放遇到复制冲突时会发生什么(存在类似的参数max_standby_archive_delay对归档恢复执行相同的操作)。
它表示当遇到复制冲突,服务器暂停wal信息回放的最长时间,如果冲突的查询在此时间之后仍然在运行,pg将强行取消它。此时会给出如下信息:
1 2 | ERROR: canceling statement due to conflict with recovery DETAIL: User query might have needed to see row versions that must be removed. |
max_standby_streaming_delay的默认值为30秒,表示如果备用数据库上的查询引起复制冲突,则在取消之前,该查询将获得半分钟的“宽限时间”以完成。这是介于0(极端设置(PostgreSQL立即取消查询,没有回放延迟)和特殊值-1(PostgreSQL从不取消查询,任意长的重放延迟)之间的中间设置。
4、怎么监控流复制冲突?
使用pg_stat_database_conflicts,该视图包含自上次统计重置以来发生的所有复制冲突的详细信息。您必须在备用服务器(而不是主服务器)上查看该视图,因为这是发生复制冲突的地方。
请注意,该视图不会显示所有已发生的复制冲突,而仅显示导致备用数据库上的查询被取消的那些冲突。
5、怎么避免流复制冲突?
禁用双机备份,避免所有冲突
显然,如果备用服务器上没有查询,就不会有复制冲突。因此,如果在备用服务器上设置hot_standby=off,则根本不必担心这个问题。
但是,只有备用服务器专门用于高可用性才可行,如果用于读写分离场景,这样做就背离了初衷了。
避免快照冲突
减少此类冲突的方法是防止主数据库删除可能在备用数据库上仍然可见的死元组。有两个参数对此有帮助:
1)将主服务器上的hot_standby_feedback设置为on。然后,从备用服务器到主服务器的反馈消息将包含备用服务器上最旧的活动事务的快照xmin,主服务器将不会删除该事务仍然可以看到的任何元组。这将消除大多数这些复制冲突,但是在备用数据库上长时间运行的查询可能导致主数据库上的表膨胀,这就是默认情况下未启用该设置的原因。仔细考虑风险。
2)将主数据库上的vacuum_defer_cleanup_age设置为大于0的值。然后,VACUUM不会立即清除无效的元组,除非超过了vacuum_defer_cleanup_age指定事务数量的旧数据。这不如hot_standby_feedback更具有明确性,并且还可能导致表膨胀。
请注意,尽管hot_standby_feedback = on将消除大多数快照复制冲突,但由于备用数据库使用的页面可能包含一些非常旧的元组,因此不一定消除buffer pin冲突。而且,即使打开hot_standby_feedback,我也看到了数据库中的快照冲突,尽管在咨询了之后也不知道为什么还会发生。也许读者可以启发我)
避免锁冲突
避免锁冲突的明显措施是不发出对表使用ACCESS EXCLUSIVE锁的语句。例如:DROP TABLE、TRUNCATE、LOCK、DROP INDEX、DROP TRIGGER、ALTER TABLE。
但是有一种ACCESS排他锁是无法通过这种方式避免的:来自VACUUM截断的锁。当VACUUM完成了对表的处理,并且表末尾的页变为空时,它会尝试在表上获得一个短的ACCESS EXCLUSIVE锁。如果成功,它将截断空页并立即释放锁。虽然这类锁不会中断主节点上的处理,但它们可能导致备用节点上的复制冲突
有两种方法可以避免VACUUM截断:
a.从PostgreSQL v12开始,您可以针对单个表的禁用此功能
ALTER TABLE some_table SET(vacuum_truncate = off);
b.您可以将主数据库上的old_snapshot_threshold设置为-1以外的值。这会禁用VACUUM截断,这是未被记录的副作用。
流复制的用例
高可用
流复制是大多数高可用性解决方案的基础。与管理故障转移的Patroni之类的软件一起,它提供了一个强大的shared-nothing架构来构建容错系统。
显然,具有高可用性的主要目标是使复制延迟尽可能小。这样,备用数据库提升为主的速度会很快,并且在故障转移期间几乎没有数据丢失。在这种情况下,您希望将max_standby_streaming_delay设置的低一点。
请注意,如果备用数据库落后于应用WAL,则在故障转移期间您无需丢失更多数据– WAL信息仍将流式传输到备用数据库并写入到pg_wal目录中。但是会导致备用数据库追赶数据所需的时间更长,因此故障转移的时间会增加。
卸载大查询
对于报表或数据分析的大查询可能使生产系统过载。最好的解决方案是专门为此类查询设计数仓。但通常备用服务器可以充当“穷人的数据仓库”。另一个减轻负载的示例是数据库备份:备份备用服务器不会对主系统造成压力。在这种情况下,主要目标是使查询(或备份)完成而不会中断。
在这种情况下,您希望将max_standby_streaming_delay设置为大于最长查询的执行时长的值,并且如果存在延迟回放WAL的问题也没问题。
水平扩展
您可以使用多台备用服务器,将数据库工作负载分布到多台机器上。在实践中,这种方法的有效性有几个限制
1.所有写入语句都必须转到主服务器,因此只能读可以扩展
2.该应用程序必须能够将查询和数据修改定向到不同的数据库
3.应用程序必须解决以下问题:查询可能无法立即看到数据的修改(同步复制避免了这种情况,但是对写入事务的性能影响是惊人的)
4.您面临的另一个困难是max_standby_streaming_delay没有好的设置:较低的值将使备用数据库上的查询失败,而较高的值将导致备用数据库上的查询看到陈旧的数据。
结论
避免复制冲突的最佳方案是拥有专门的备用服务器:一个用于高可用性,另外一个用于查询或者备份。然后我们就可以轻松配置每个以避免复制冲突。
官网处理查询冲突
http://postgres.cn/docs/13/hot-standby.html#HOT-STANDBY-CONFLICT
主服务器和后备服务器在多方面都松散地连接在一起。主服务器上的动作将在后备服务器上产生效果。结果是在它们之间有潜在的负作用或冲突。最容易理解的冲突是性能:如果在主服务器上发生一次大数据量的载入,那么将在后备服务器上产生一个相似的 WAL 记录流,因而后备服务器查询可能要竞争系统资源(例如 I/O)。
随着热备发生的还可能有其他类型的冲突。对于可能需要被取消的查询和(某些情况中)解决它们的已断开会话来说,这些冲突是硬冲突\。用户可以用几种方式来处理这种冲突。
冲突情况包括:
- 在主服务器上取得了访问排他锁(包括显式
LOCK
命令和多种DDL动作)与后备查询中的表访问冲突。 - 在主服务器上删除一个表空间与使用该表空间存储临时工作文件的后备查询冲突。
- 在主服务器上删除一个数据库与在后备服务器上连接到该数据库的会话冲突。
- 从 WAL 清除记录的应用与快照仍能“看见”任意要被移除的行的后备事务冲突。
- 从 WAL 清除记录的应用与在后备服务器上访问该目标页的查询冲突,不管要被移除的数据是否为可见。
在主服务器上,这些情况仅仅会导致等待;并且用户可以选择取消这些冲突动作中间的一个。但是,在后备服务器上则没有选择:已被 WAL 记录的动作已经在主服务器上发生,那么后备服务器不能在应用它时失败。此外,允许 WAL 应用无限等待是非常不可取的,因为后备服务器的状态将变得逐渐远远落后于主服务器的状态。因此,提供了一种机制来强制性地取消与要被应用的 WAL 记录冲突的后备查询。
该问题情形的一个例子是主服务器上的一位管理员在一个表上运行DROP TABLE
,而该表正在后备服务器上被查询。如果DROP TABLE
被应用在后备服务器上,很明显该后备查询不能继续。如果这种情况在主服务器上发生,DROP TABLE
将等待直到其他查询结束。但是当DROP TABLE
被运行在主服务器上,主服务器没有关于运行在后备服务器上查询的信息,因此它将不会等待任何这样的后备查询。WAL 改变记录在后备查询还在运行时来到后备服务器上,导致一个冲突。后备服务器必须要么延迟 WAL 记录的应用(还有它们之后的任何事情),要么取消冲突查询这样DROP TABLE
可以被应用。
当一个冲突查询很短时,我们通常期望能延迟 WAL 应用一小会儿让它完成;但是在 WAL 应用中的一段长的延迟通常是不受欢迎的。因此取消机制有参数,max_standby_archive_delay和max_standby_streaming_delay,它们定义了在 WAL 应用中的最大允许延迟。当应用任何新收到的 WAL 数据花费了超过相关延迟设置值时,冲突查询将被取消。设立两个参数是为了对从一个归档读取 WAL 数据(即来自一个基础备份的初始恢复或者“追赶”一个已经落后很远的后备服务器)和通过流复制读取 WAL数据的两种情况指定不同的延迟值。
在一台后备服务器上这主要是为了该可用性而存在,最好把延迟参数设置得比较短,这样服务器不会由于后备查询导致的延迟落后主服务器太远。但是,如果该后备服务器是位了执行长时间运行的查询,则一个较高甚至无限的延迟值更好。但是记住一个长时间运行的查询延迟了 WAL 记录的应用,它可能导致后备服务器上的其他会话无法看到主服务器上最近的改变。
一旦max_standby_archive_delay
或max_standby_streaming_delay
指定的延迟被超越,冲突查询将被取消。这通常仅导致一个取消错误,尽管在重放一个DROP DATABASE
的情况下整个冲突会话都将被中断。另外,如果冲突发生在一个被空闲事务持有的锁上,该冲突会话会被中断(这种行为可能在未来被改变)。
被取消的查询可能会立即被重试(当然是在开始一个新的事务后)。因为查询取消依赖于 WAL 记录被重放的本质,如果一个被取消的查询被再次执行,它可能会很好地成功完成。
记住延迟参数是从 WAL 数据被后备服务器收到后流逝的时间。因此,留给后备服务器上任何一个查询的宽限期从不会超过延迟参数,并且如果后备服务器已经由于等待之前的查询完成而落后或者因为过重的更新负载而无法跟上主服务器,宽限期可能会更少。
在后备查询和 WAL 重播之间发生冲突的最常见原因是“过早清除”。正常地,PostgreSQL允许在没有事务需要看到旧行版本时对它们进行清除,这样可以保证根据 MVCC 规则的正确的数据可见性。不过,这个规则只能被应用于执行在主控机上的事务。因此有可能主控机上的清除会移除对一个后备服务器事务还可见的行版本。
有经验的用户应当注意行版本清除和行版本冻结都可能与后备查询冲突。即便在一个没有被更新或被删除行的表上运行一次手工VACUUM FREEZE
也可能导致冲突。
用户应当清楚,主服务器上被正常和重度更新的表将快速地导致后备服务器上长时间运行的查询被取消。在这样的情况下,max_standby_archive_delay
或max_standby_streaming_delay
的有限制设置可以被视作statement_timeout
设置。
如果发现后备查询取消的数量不可接受,还是有补救的可能。第一种选项是设置参数 hot_standby_feedback
,它阻止VACUUM
移除最近死亡的元组并且因此清除冲突不会产生。如果你这样做,你应当 注意这将使主服务器上的死亡元组清除被延迟,这可能会导致不希望发生 的表膨胀。不过,清除的情况不会比在主服务器上直接运行后备查询时更糟, 并且你仍然能够享受将执行分流到后备服务器的好处。如果后备服务器频繁地连接和 断开,你可能想要做些调整来处理无法提供hot_standby_feedback
反馈的时期。例如,考虑增加max_standby_archive_delay
,这样 在断开连接的期间查询就不会快速地被 WAL 归档文件中的冲突取消。你也应该考虑 增加max_standby_streaming_delay
来避免重新连接后新到达的流 WAL 项导致的快速取消。
另一个选项是增加主服务器上的vacuum_defer_cleanup_age,这样死亡行不会像平常那么快地被清理。这将允许在后备服务器上的查询能在被取消前有更多时间执行,并且不需要设置一个很高的max_standby_streaming_delay
。但是,这种方法很难保证任何指定的执行时间窗口,因为vacuum_defer_cleanup_age
是用主服务器上被执行的事务数来衡量的。
查询取消的数量和原因可以使用后备服务器上的pg_stat_database_conflicts
系统视图查看。pg_stat_database
系统视图也包含汇总信息。
参考
https://github.com/digoal/blog/blob/master/201608/20160815_03.md
https://www.cybertec-postgresql.com/en/streaming-replication-conflicts-in-postgresql/