暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

备库上的查询为什么会被取消 —HaishanDB Hot Standby冲突解决机制浅析

原创 移动云HaishanDB 2026-07-28
22

如果你用过Haishan DB的流复制,大概率遇到过这个报错:ERROR: canceling statement due to conflict with recovery。明明是只读查询,为什么会被“冲突”取消?本文带你一探究竟。

1 Hot Standby 的“既要又要”

Hot Standby 是备库在接收和回访 WAL 日志的同时,还能对外提供只读查询服务。这听起来很美好:一台机器既做容灾又分担读负载,物尽其用。但这里有个根本性的矛盾:

  • WAL回放(由 Startup 进程负责)在修改数据——创建/删除表、清理死元组、分裂索引页……
  • 只读查询(由普通 backup 进程负责)在读取数据——扫描表、持有 buffer pin、持有快照……

当回放需要做的事情,恰好和查询正在做的事情冲突了,怎么办?这就是 Hot Standby 冲突解决机制要回答的问题。Haishan DB 给出的答案是一个分级等待 + 最终取消的策略:先忍一会儿,实在不行就杀掉查询。

2 为什么不能“等查询自己结束”

你可能会想:让 WAL 回放等一等不就行了,等查询结束再回放。问题在于,主库不会因为你备库上有查询在跑就停下来。主库持续产生 WAL,备库持续接收。如果备库的回放停下来了:

  1. WAL堆积:接收了但不回放,存放日志的目录持续膨胀,可能撑爆磁盘
  2. 复制延迟扩大:备库越来越落后于主库,查询在备库上看到的数据版本越来越旧
  3. failover时恢复时间过长:主库故障时,备库必须把积压的WAL全部回放完才能promotion上线,积压越多切换越慢

所以 Haishan DB 的策略是:可以等,但不能无限等

3 四种冲突类型

深入来看,Hot Stanby 的冲突主要分为四类。每一类都对应一个不同的场景。

3.1 快照冲突(Snapshot Conflict)

最容易遇到的一种。主库上执行了 VACUUM(或 autovauum),清理了一些死元组。对应的 WAL 记录回放到备库时,备库也需要移除这些死元组。

但此时,备库上可能有一个长查询持有快照(Snapshot),这个快照的 xmin 小于被清理元组的 xmax —— 换句话说,这个查询“还需要看到”这些即将被清理的行。如果回放强行清理了,查询就会看到不一致的数据。如果回放等待,查询可能跑很久。

Haishan DB 的做法是:在 max_standby_streaming_delay 时间内等待,超时后向查询发送 PROCSIG_RECOVERY_CONFLICT_SNAPSHOT 信号,查询报错退出。

3.2 锁冲突(Lock Conflict)

DDL操作触发。主库上执行了 ALTER TABLE、DROP TABLE、CREATE INDEX等需要 AccessExclusiveLock 的DDL。WAL回放到备库时,Startup 进程也需要获取同样的 AccessExclusiveLock。问题在于,备库上的只读查询虽然只持有 AccessShareLock(最轻量的表锁),但它和 AccessExclusiveLock 互斥。于是 Startup 进程在等待锁,而查询在悠闲地扫描表,等还是不等?

同样,在上限时间内等待,超时后取消查询。实现上有个有趣的细节:它不仅等,还会在等待超过 deadlock_timeout 后,向持锁 backend 发送 PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK 信号,要求它们检查自己是否陷入了死锁 —— 因为 Startup 等查询释放锁、查询有可能被 Startup 阻塞,形成一个经典的环形等待。

3.3 Buffer Pin 冲突(Buffer Pin Conflict)

最难排查、也最容易造成死锁的一种。Haishan DB 的Buffer Pool中,每个页面在被读取时会被“pin”住(pin count + 1),读完后unpin。WAL回放时有时需要对页面做清理操作(比如 btree 索引页分裂的回放、VM页面的更新),但页面被某个查询pin住了。

回放进程必须等unpin。如果查询的pin是因为正在等另一个锁,而这个锁有需要WAL回放才能释放,这样死锁就形成了。

代码层面对于这种现象的处理方式比较特殊:它不针对某个特定的backend,而是向所有backend广播信号(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN),让每个backend自己检查“是不是我pin住了这个页面”。如果是,主动报错或FATAL退出。死锁检查不做在第一时间,而是等到dead_timeout之后才触发,因为死锁极少发生,而检查成本较高。

3.4 表空间和数据库冲突

这两类比较直观:

  • 表空间冲突:主库DROP TABLESPACE,备库上有查询在该表空间里有临时文件。回放时取消所有活跃查询,因为DROP TABLESPACE是非事务性的,不能等。
  • 数据库冲突:主库DROP DATABASE,备库上所有连接挂在那个数据库上。不等待直接强制断开所有连接。

4 优雅的等待机制:退避与超时

上面反复提到了“在时限内等待”,具体是怎么等的?其中的实现逻辑就是:等待时间从1毫秒起步,每次翻倍,最大到1秒;每次醒来检查:当前时间是否超过了截止时间(截止时间 = 最后一次收到WAL的时间 + max_standby_streaming_delay)。

这是一个指数退避(exponential backoff)策略,好处是:

  • 冲突通常很快就解决了(查询结束、unpin、释放锁),此时只需等1ms,几乎无感
  • 如果冲突持续,休眠时间逐渐加长,避免CPU空转
  • 一旦到了截止时间,不在犹豫,立即出手取消

相关GUC参数有两个:

  • max_standby_streaming_delay(默认30s):通过流复制接收WAL时的等待上限
  • max_standby_archive_delay(默认30s):通过归档文件恢复WAL时的等待上限

设置为-1表示“永远等下去”—— 这在某些场景下是合理的(宁可备库延迟也绝不杀查询),但风险是备库可能无限落后。

5 一个巧妙的预防机制:hot_standby_feedback

等冲突发生了再去解决,终究是被动的。Haishan DB还提供了一个主动预防的机制:hot_standby_feedback。

工作原理:备库定期把当前所有查询中最老的xmin(即最老的活跃事务ID)告知主库。主库的VACUUM在清理死元组时,会跳过那些仍然被备库xmin需要的行。这样,当WAL回放到备库时,就不会出现“需要清理但查询还需要看”的快照冲突了。

代价:如果备库上有一个“长查询”(比如跑8个小时的报表),hot_standby_feedback会让主库VACUUM在8小时内都不敢清理那些死元组,导致主库表膨胀。这是一个典型的空间换时间的权衡。

最佳实践:

  • 开启hot_standby_feedback,但设置合理的statement_timeout和idle_in_transaction_session_timeout,避免出现超级长查询
  • 如果备库只用于只读分析、对表膨胀敏感,可以不开启,转而调大max_standby_streaming_delay

6 实战建议

遇到“canceling statement due to conflict with recovery”怎么办?

第一步:判断是哪种冲突

从数据库日志(需要开启log_recovery_conflict_waits = on)中可以看到:

recovery still waiting after 30005.123 ms: recovery conflict on buffer pin

recovery still waiting after 30012.456 ms:recovery conflict on snapshot

不同冲突类型对应不同的解决思路。

第二步:分类施策

冲突类型

常见原因

解决方案

快照冲突

备库长查询 + 主库频繁VACUUM

开启hot_standby_feedback;杀掉长查询

锁冲突

主库DDL操作

在业务低峰期做DDL;调大max_standby_streaming_delay

Buffer pin冲突

备库某些查询pin住页面太久

检查是否有慢查询;设置statement_timout

表空间/数据库冲突

主库删了表空间或数据库

操作前确认备库没有连接在使用

第三步:合理配置参数

# 推荐的一组设置

max_standby_streaming_delay = 60s # 给备库查询60秒的“逃生窗口”

hot_standby_feedback= on # 主动预防快照冲突

log_recovery_conflict_waits = on # 记录冲突日志,方便排查

statement_timeout = 5min # 防止超级长查询

第四步:监控指标

关注这几个值来判断备库的健康状况:

  • pg_stat_replication.replay_lag:备库回放延迟
  • pg_stat_database_conflicts视图:按冲突类型统计冲突次数
  • 备库日志中recovery still waiting的出现频率

7 总结

Hot Standby的冲突解决机制,本质上是在回答一个分布式系统中常见的问题:当多个操作需要互斥访问共享资源时,谁让步?

Haishan DB的设计哲学是务实且克制的:

  • 不是“完美解决”冲突,而是“有节制地等待 + 有尊严地失败”
  • 给DBA充分的控制权(多个GUC参数可调)
  • 在设计和实践中不断迭代

理解这套机制,不仅能帮你排查“canceling statement due to conflict with recovery”这个经典报错,更能让你在设计高可用架构时,对备库的行为有清晰的预期。毕竟,知道系统“什么时候会杀掉你的查询”以及“为什么”,是每个后端工程师的安全感来源。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论