备库上的查询为什么会被取消: Hot Standby冲突解决机制浅析— HaishanDB团队技术调研

备库上的查询为什么会被取消: Hot Standby冲突解决机制浅析— HaishanDB团队技术调研 如果你用过HaishanDB的流复制大概率遇到过这个报错ERROR: canceling statement due to conflict with recovery。明明是只读查询为什么会被冲突取消本文带你一探究竟。1. Hot Standby 的既要又要Hot Standby 是备库在接收和回放 WAL 日志的同时还能对外提供只读查询服务。这听起来很美好一台机器既做容灾又分担读负载物尽其用。但这里有个根本性的矛盾WAL 回放由 Startup 进程负责在修改数据——创建/删除表、清理死元组、分裂索引页……只读查询由普通 backend 进程负责在读取数据——扫描表、持有 buffer pin、持有快照……当回放需要做的事情恰好和查询正在做的事情冲突了怎么办这就是 Hot Standby 冲突解决机制要回答的问题。HaishanDB 给出的答案是一个分级等待 最终取消的策略先忍一会儿实在不行就杀掉查询。2. 为什么不能等查询自己结束你可能会想让 WAL 回放等一等不就行了等查询结束再回放。问题在于主库不会因为你备库上有查询在跑就停下来。主库持续产生 WAL备库持续接收。如果备库的回放停下来了WAL 堆积接收了但不回放存放日志的目录持续膨胀可能撑爆磁盘复制延迟扩大备库越来越落后于主库查询在备库上看到的数据版本越来越旧Failover 恢复时间过长主库故障时备库必须把积压的 WAL 全部回放完才能 promotion 上线积压越多切换越慢所以 HaishanDB 的策略是可以等但不能无限等。3. 四种冲突类型深入来看Hot Standby 的冲突主要分为四类。每一类都对应一个不同的场景。3.1 快照冲突Snapshot Conflict最常见主库上执行了VACUUM或 autovacuum清理了一些死元组。对应的 WAL 记录回放到备库时备库也需要移除这些死元组。但此时备库上可能有一个长查询持有快照Snapshot这个快照的 xmin 小于被清理元组的 xmax —— 换句话说这个查询还需要看到这些即将被清理的行。如果回放强行清理了查询就会看到不一致的数据。如果回放等待查询可能跑很久。HaishanDB 的做法在 max_standby_streaming_delay 时间内等待超时后向查询发送 PROCSIG_RECOVERY_CONFLICT_SNAPSHOT 信号查询报错退出。3.2 锁冲突Lock ConflictDDL 触发主库上执行了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最难排查HaishanDB 的 Buffer Pool 中每个页面在被读取时会被pin住pin count 1读完后 unpin。WAL 回放时有时需要对页面做清理操作比如 btree 索引页分裂的回放、VM 页面的更新但页面被某个查询 pin 住了。回放进程必须等 unpin。如果查询的 pin 是因为正在等另一个锁而这个锁又需要 WAL 回放才能释放——死锁就形成了。代码层面的处理方式比较特殊它不针对某个特定的 backend而是向所有 backend 广播信号PROCSIG_RECOVERY_CONFLICT_BUFFERPIN让每个 backend 自己检查是不是我 pin 住了这个页面。如果是主动报错或 FATAL 退出。死锁检查不做在第一时间而是等到 deadlock_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等冲突发生了再去解决终究是被动的。HaishanDB 还提供了一个主动预防的机制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_delay6. 实战建议遇到 canceling statement due to conflict with recovery 怎么办第一步判断是哪种冲突从数据库日志需要开启 log_recovery_conflict_waits on中可以看到不同冲突类型对应不同的解决思路。第二步分类施策第三步合理配置参数第四步监控指标关注这几个值来判断备库的健康状况pg_stat_replication.replay_lag 备库回放延迟pg_stat_database_conflicts 视图按冲突类型统计冲突次数备库日志中 recovery still waiting 的出现频率7. 总结Hot Standby 的冲突解决机制本质上是在回答一个分布式系统中常见的问题当多个操作需要互斥访问共享资源时谁让步HaishanDB 的设计哲学是务实且克制的不是完美解决冲突而是有节制地等待 有尊严地失败给 DBA 充分的控制权多个 GUC 参数可调在设计和实践中不断迭代理解这套机制不仅能帮你排查 canceling statement due to conflict with recovery 这个经典报错更能让你在设计高可用架构时对备库的行为有清晰的预期。知道系统什么时候会杀掉你的查询以及为什么是每个后端工程师的安全感来源。本文作者宋梁浩#HaishanDB #海山数据库 #中国移动HaishanDB #中国移动海山数据库中国移动大云海山数据库He3DB于‌2026 年 6 月‌正式焕新升级启用全新品牌标识——‌HaishanDB‌。此次更名不仅是品牌语言的统一更标志着该自研数据库在技术、产品与生态全面就绪后正式迈入‌体系化、规模化、AI 原生化‌的全新阶段 。‌‌