DeepSeek总结的PostgreSQL 中的二进制只读副本,第三部分

PostgreSQL 中的二进制只读副本,第三部分

第三部分:构建副本,以及故障转移到副本

构建一个副本只需要一条命令。用一个副本替换失败的主节点需要一个流程,而流程正是安装受伤的地方。这一部分按那个比例覆盖两者。

示例使用三台服务器,db1、db2 和 db3,一开始 db1 是主节点。

准备主节点

一个原装的 PostgreSQL 18 服务器几乎准备好为副本供数了。wal_level 已经是 replica,max_wal_senders 和 max_replication_slots 已经是 10,hot_standby 已经是 on。缺少三样东西。

服务器必须在网络上监听。 listen_addresses 默认为 localhost,改变它需要重启。

必须有一个副本可以连接的角色。 它需要 REPLICATION 属性,仅此而已:

sql 复制代码
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '...';

pg_hba.conf 必须接纳它。 复制连接不匹配普通数据库条目。它们匹配数据库列中的关键字 replication:

复制代码
host    replication    replicator    10.0.0.0/24    scram-sha-256

把这个角色的密码当作超级用户的密码对待。该角色不能修改数据,但任何能读取 WAL 流的人都能读取集群中的每一行。

既然你无论如何都要重启主节点,做两个以后难以重访的决定。打开 WAL 归档(archive_mode,以及一个 archive_command 或提供它的备份工具),因为你无论如何都需要归档用于备份,而副本可以如下所述使用它。并检查 pg_rewind 将可用,这要求数据校验和或 wal_log_hints = on。PostgreSQL 18 中由 initdb 创建的集群默认打开校验和。从旧版本通过 pg_upgrade 带过来的集群通常没有,对那些设置 wal_log_hints。为什么你想要 pg_rewind 在故障转移下解释。

你在这里设置的任何东西,在每个副本上完全相同地设置。包括 pg_hba.conf:每个服务器必须接纳来自其他每个服务器的复制连接。它们中任何一个下个月都可能是主节点。

构建一个副本

在新服务器上,安装了相同主版本的 PostgreSQL 和一个空数据目录:

bash 复制代码
pg_basebackup -h db1.example.com -U replicator \
    -D /var/lib/postgresql/18/main \
    -R -C -S db2 -X stream -c fast -P

-D 是拷贝去哪里。-X stream,默认值,打开第二个连接并收集拷贝运行时生成的 WAL,所以结果无需归档帮助就是一致的。-C -S db2 在主节点上创建那个名称的复制槽并使用它,这保证主节点保留副本在拷贝结束到它开始流式传输那一刻之间将需要的每个 WAL 段。-c fast 要求主节点立即检查点而不是等待下一个计划中的。-P 报告进度。

-R 是使拷贝成为副本的东西。它创建 standby.signal 文件,并把 primary_conninfo 和 primary_slot_name 追加到新数据目录中的 postgresql.auto.conf。关于那个连接字符串的两件事。把密码留在副本上的 ~/.pgpass 中;在提示符下输入的密码会以明文写入 primary_conninfo。并给它添加一个 application_name(这里,db2)。那个名字是副本在主节点上的 pg_stat_replication 中出现的方式,以及同步复制引用它的方式。默认是副本的 cluster_name,Debian 的打包在每个服务器上把它设为相同的值。

启动服务器。它的前几行日志中应该按这个顺序有这些:

复制代码
LOG:  entering standby mode
LOG:  consistent recovery state reached at 3/8E000100
LOG:  database system is ready to accept read-only connections
LOG:  started streaming WAL from primary at 3/8F000000 on timeline 1

而在主节点上:

sql 复制代码
SELECT application_name, state, sync_state FROM pg_stat_replication;

应该显示副本的 state 为 streaming。

三个实用说明。基础备份拷贝主节点数据目录中存在的任何配置文件;在 Debian 和 Ubuntu 包上,postgresql.conf 和 pg_hba.conf 住在 /etc/postgresql 中,必须单独放置。拷贝从主节点读取整个集群,对一个大数据库意味着那里数小时的 I/O;--max-rate 节流它,而第二部分描述了从另一个副本取它。而对大型集群,更好的方法通常是恢复最近的备份。例如 pgBackRest 有 restore --type=standby,它产生一个从归档赶上的副本,并给定 primary_conninfo 然后流式传输。那不给主节点负载,并且每次构建副本时测试你的备份。

保持 WAL 可用

一个落后的副本需要主节点一段时间前生成的 WAL。如果主节点已经回收了它,副本的日志这么说:

复制代码
FATAL:  could not receive data from WAL stream: ERROR:  requested WAL segment 000000010000000300000091 has already been removed

而副本待在原地直到段出现或副本被重建。有三种方式保留 WAL,而它们最好一起用。

wal_keep_size 使主节点保留至少那么多过去的 WAL,无论是否有东西需要它。它简单,而它是一个猜测:太小没有帮助,太大浪费每个服务器的磁盘。

复制槽 使主节点精确保留那个副本尚未收到的。它精确,而没有限制时它危险,如第一部分所述:一个宕机一周的副本在主节点上持有一周的 WAL。设置 max_slot_wal_keep_size,以便一个死副本让你损失副本而不是主节点。

归档持有一切。 给副本一个从它获取的 restore_command,而一个落后到主节点保留之外的副本从归档赶上然后回到流式传输。副本在它运行期间自己在两个来源之间交替。

我的建议是全部按其正确角色:每个副本一个槽,以便普通延迟从不丢失任何东西;max_slot_wal_keep_size,以便槽不能填满主节点的磁盘;以及每个副本上的 restore_command,以便超过那个限制意味着更慢的赶上而不是重建。

其中一部分确实需要手动。一个已过其限制的槽被无效化(pg_replication_slots 中 wal_status 为 lost)且不能再次使用。副本从归档赶上,但它在你在主节点上删除槽并再次创建它之前不能通过那个槽恢复流式传输。

提升和时间线

一次故障转移,机械上,是一台服务器上的一个操作。一个副本被告知停止作为副本:

sql 复制代码
SELECT pg_promote();

或从 shell 的 pg_ctl promote。(旧指南描述的触发文件在 PostgreSQL 16 中被移除。)服务器完成重放它拥有的任何 WAL,离开恢复,开始接受写入。没有 WAL 积压要重放,那花片刻。

提升不能撤销。 从新主节点写其第一条 WAL 记录的那一刻起,它的历史与旧主节点的分道扬镳,而 PostgreSQL 用一条新时间线标记这个分道。每个服务器从时间线 1 开始。一次提升移动到下一个时间线号,写一个小小的记录新时间线从旧时间线何处分叉的时间线历史文件,从那时起把新时间线的号戳进每个 WAL 段的名字。(WAL 文件名的前八个十六进制数字是时间线。)然后两个服务器可以在同一位置产生 WAL 而不会有一个的文件被误认为另一个的。

其他副本可以跟随一次提升。 recovery_target_timeline 为 latest(默认值)的副本,在连接到新主节点时注意到新时间线,获取历史文件并继续,只要它尚未越过时间线分叉的点。

故障转移一切困难的东西都在那一条命令周围:决定做它、确保旧主节点完成、选择副本、移动客户端,以及把碎片重新拼起来。流程因旧主节点是否活着而差异很大。

计划中的切换

当主节点健康而你故意移动角色(为了维护、为了次版本升级、为了测试流程)时,不需要丢失任何东西,也不需要重建任何东西。这里 db1 是主节点而 db2 要替换它。

  1. 使用第二部分的查询确认 db2 已连接、正在流式传输且紧跟在后面。
  2. 停止应用程序的写入,或接受下一步将结束它们。
  3. 干净地关闭 db1:pg_ctl stop -m fast。(先运行 CHECKPOINT,以便关闭自己的检查点没剩多少要做。)干净关闭写一条最终检查点记录,然后等待,最多 wal_sender_timeout,直到每个连接的副本都有全部 WAL,包括它。
  4. 验证 db2 拥有一切。在 db1 上,pg_controldata 应报告 Database cluster state 为 shut down,并报告 Latest checkpoint location。在 db2 上,SELECT pg_last_wal_receive_lsn() 必须越过那个位置。
  5. 提升 db2。
  6. 在 db2 上,为现在将成为其副本的每个服务器创建一个复制槽,包括 db1:SELECT pg_create_physical_replication_slot('db1');。槽不被复制,所以新主节点在你这样做之前没有。
  7. 把客户端和其他副本指向 db2。
  8. 把 db1 作为副本带回来:在其数据目录中创建 standby.signal,设置 primary_conninfo 和 primary_slot_name,并启动它。因为它干净关闭且 db2 收到了它的所有 WAL,两者没有分叉。它跟随新时间线,不需要 rewind 或重建。然后删除 db1 从它作为主节点时仍然持有的槽,否则它们会在它上面无限期保留 WAL。

第 4 步是人们跳过的那一步,也是使第 8 步工作的那一步。如果主节点关闭时 db2 没有连接,db1 持有新主节点从未见过的 WAL,你就进入了下一节。

切换也是几乎无停机地做次版本升级的方式:升级副本,切换到一个,升级旧主节点,并(如果你在乎哪个服务器是哪个)切回来。

计划外故障转移

主节点宕机、不可达,或以让你希望它宕机的方式行为。

决定

慢的主节点不是死的主节点,而你无法到达的主节点不一定是你应用程序无法到达的。本不该发生的故障转移比发生得晚的故障转移造成更多损害。在提升任何东西之前,确定什么失败了,以及它是否会在故障转移能被完成和清理之前回来。

隔离

在一个副本被提升之前,旧主节点必须被弄得无法接受写入,也无法自行恢复:断电、从网络移除、其服务禁用。如果它仅仅从你坐的地方不可达,它可能仍在服务客户端。在那种状态下提升一个副本就有两个主节点,各自接受另一个永远不会看到的写入。这是脑裂。PostgreSQL 没有之后合并两个历史的机制,而理清它意味着选择一方的事务丢弃并手工重建另一方的。

选择

提升持有最多 WAL 的副本。问每个候选:

sql 复制代码
SELECT GREATEST(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn());

取最高的。两个位置都需要。提升在完成之前重放副本已收到的一切,所以收到的位置通常是算数的那个。但那个函数只报告自副本上次启动以来通过流式传输到达的东西,而一个被重启或从归档赶上的副本,可能重放得比它报告收到的更远。排除故意落后或故意不同的副本:延迟副本、重放停顿的报告副本。

丢失什么

在异步复制下,旧主节点提交但尚未发送的事务不在新主节点上。它们的客户端被告知它们提交了。在一个健康的系统上这是一个不到一秒的工作量。如果候选在落后,就是那么多落后,这就是为什么故障转移候选的延迟是最重要的告警。

重新组装

提升。在新主节点上为将跟随它的副本创建槽。移动客户端,然后重新指向剩余副本。一个落后于分叉点的副本毫无困难地跟随新主节点。一个从旧主节点收到的比被提升的副本更多的副本,与旧主节点处于同样位置:它持有新时间线不包含的 WAL。

对那些服务器有 pg_rewind。它把服务器的历史与新主节点的比较,找到它们分叉的点,并只替换服务器在那个点之后更改的块(以及任何新文件)。在一个大数据库上那是几分钟,而一个新的基础备份是几小时。

bash 复制代码
pg_rewind --target-pgdata=/var/lib/postgresql/18/main \
    --source-server='host=db2.example.com user=rewind_user dbname=postgres' -P

它需要什么:

  • 自故障转移之前起启用的数据校验和,或 wal_log_hints。
  • 目标服务器关闭。如果它没有干净关闭(被隔离的主节点通常没有),pg_rewind 先以单用户模式启动它来完成崩溃恢复。在把 postgresql.conf 保持在数据目录之外的布局上,那需要 --config-file。
  • 目标自己的 WAL,回到分叉前的最后一个检查点。-c 使用目标的 restore_command 从归档获取缺失的段。
  • 到新主节点的一个普通数据库连接,不是复制连接。它需要自己的 pg_hba.conf 条目,以及一个超级用户或被授予 pg_rewind 文档列出的四个函数的 EXECUTE 的角色。

之后,手工把服务器设置为副本:standby.signal、primary_conninfo、primary_slot_name。pg_rewind 从新主节点的数据目录复制其配置文件,包括 postgresql.auto.conf,所以被 rewind 的服务器继承描述新主节点作为副本时的设置。(有一个 -R 选项为你写 primary_conninfo。它记录给 pg_rewind 的连接,那不是复制角色的,并且它不设置槽。)在启动服务器之前读配置。

理解一次 rewind 丢弃什么。旧主节点提交且从未发送的事务不存在于别处,而 pg_rewind 移除它们。如果它们重要,在 rewind 之前把旧数据目录拷贝到一边。而如果 pg_rewind 中途失败,目标不可用。取一个新的基础备份。

同步复制

如果丢失那些最后事务不可接受,主节点可以被弄得等待。用

sql 复制代码
synchronous_standby_names = 'ANY 1 (db1, db2, db3)'

提交直到至少一个命名副本确认提交的 WAL 已刷到其磁盘才返回给客户端。(FIRST 1 (...) 等待列出的第一个连接的副本,并把其余的当作备用。)名字是副本的 application_name 值。客户端看到成功的提交然后在两台服务器上。

列出每个服务器,包括主节点,并在所有服务器上放同一行。服务器从不匹配自己的条目,所以额外的名字无害,而设置然后无论哪个服务器是主节点都正确。一个用空 synchronous_standby_names 提升的副本是一个异步运行的主节点,而没有什么会告诉你。

成本和警告:

  • 每个提交等待一次网络往返和另一台机器上的一次磁盘刷。synchronous_commit 可以按事务设置,所以可以承受丢失的写入(SET LOCAL synchronous_commit = local)不必付。
  • 如果没有列出的副本可用,提交无限期等待。有一个同步副本的同步复制比完全没有复制可用性更低。至少有两个。
  • 保证只在你提升一个曾是同步的副本时成立。用两个中的 ANY 1,它们中有一个拥有每个已确认的提交,而你通过如上比较它们的位置找出是哪个。
  • 提交在等待开始之前本地写入。如果客户端在等待期间放弃并取消,服务器警告事务"has already committed locally, but might not have been replicated to the standby"。对主节点崩溃并重启时正在等待的提交也是如此。客户端没有看到成功,但事务在那里。

多少服务器

两台服务器,一个主节点和一个副本,是最常被称为高可用的安排,而它正是那个不是的。故障转移后它是一个没有副本的单个服务器,而在重建所需的时间里一直如此。用同步复制它更糟,因为刚刚给出的原因:一个同步副本使每个提交依赖两台机器。

三台是实际最小值:一个主节点和两个副本,在三个不会一起失败的地方。一次失败留下一个主节点和一个副本。同步复制可以等待两个中的 ANY 1。而三台机器还可以承载自动故障转移所需的共识存储的三个成员,这是下一节的主题。

有其他工作的副本(第二部分)被加到这个数字上,而不是计入其中。一个重放停顿在六小时查询后面的报告副本不是故障转移候选,而它不应该成为你相信自己有一个的理由。

自动化故障转移

计划外流程中的一切都可以由软件完成,而在凌晨三点,很容易希望它是。软件必须做对的是同样的列表:正确决定主节点没了、保证它保持没了、选择最好的副本,并重新配置其他一切。前两个是分布式系统问题,而内部写的脚本每次都以同样的方式弄错它们,通过在一个旧主节点仍在网络分区另一侧活着时提升一个副本。

Patroni 是我会用的工具。一个代理在每个 PostgreSQL 服务器旁边运行,而代理通过一个共识存储(etcd、Consul、ZooKeeper 或 Kubernetes API)协调。主节点在那个存储中持有一个 leader 锁,必须不断续期;锁的生命周期默认 30 秒。无法续期锁的主节点自我降级。当锁过期时,副本争夺它,而一个落后太多的(maximum_lag_on_failover)不站。Patroni 创建并维护复制槽,可以用 pg_rewind 回绕一个回来的旧主节点(use_pg_rewind,默认关闭),并用 patronictl switchover 做计划中的切换。它的 REST 端点,在第二部分描述,告诉负载均衡器哪个服务器是哪个。对代理本身挂起的情况,它支持一个重置机器的 watchdog 设备。(最常遇到的替代品是 repmgr,它可以用一个见证服务器打破平局,以及 pg_auto_failover,它依赖一个单独的监控节点。)

那个设计产生两个后果。

共识存储需要多数才能运作,这意味着三个成员,在三个不会一起失败的地方。两个数据中心不够:持有少数的那一侧无法区分死掉的同伴和断掉的链路,必须假设是链路。两个站点之间的自动故障转移需要第三个位置,哪怕只是为存储的一个打破平局的成员。

而一旦 Patroni 管理一个集群,它拥有这个集群。它写 primary_conninfo,它启动和停止 PostgreSQL,而在其同步模式下它设置 synchronous_standby_names。更改通过 Patroni 的配置进行。在它背后做的更改被还原,或更糟,不被还原。

手动故障转移仍然是一个体面的选择。如果总有人随叫随到、流程是写下来的,而业务可以容忍一个人醒来并运行它所需的十五分钟,一个排练过的手动故障转移会比一个不被理解的自动故障转移服务得更好。

提升之后

新主节点起来了并接受写入。事件没有结束。相当多东西仍然指向旧安排。

客户端。 写者必须找到新主节点。选项是第二部分角色反转的那些:一个带 target_session_attrs=read-write 的 libpq 主机列表、一个其健康检查向 Patroni 询问 /primary 的负载均衡器,或一个故障转移流程移动的虚拟 IP 或 DNS 名。无论是什么,它必须是流程的一部分,而且它必须已被测试。

复制槽。 物理槽不被复制,这就是为什么上面流程手工创建它们。逻辑槽也不被复制,除非它们用 failover 选项创建且副本被配置为同步它们(sync_replication_slots,PostgreSQL 17 及以后)。没有那个,每个逻辑复制订阅者和变更数据捕获消费者都必须重新设置。

统计信息。 规划器的统计信息是数据的一部分,完好地过来了。驱动 autovacuum 的活动计数器没有。重放不维护它们,所以新主节点的死行计数和自上次 analyze 以来更改的行计数不反映提升之前发生的任何事情。除了回绕保护,autovacuum 不会对一张表采取行动,直到足够的新活动累积。对已经到期的表,故障转移后手动 VACUUM (ANALYZE) 是谨慎的。

无日志表。 它们在新主节点上是空的。如果应用程序在其中保留任何它期望再次找到的东西,它不会找到。

备份和归档。 新主节点开始归档它自己的 WAL,在新时间线上,如果它被配置为这样做。它不归档它作为副本时收到的,所以死主节点生成但尚未归档的 WAL 从未到达归档,而跨一次计划外故障转移的时间点恢复可能有一个洞。取一个新的基础备份。按名字瞄准旧主节点的备份作业、监控和计划维护现在瞄准什么都没有,或瞄准一个副本。

容量。 读池少一个副本,而集群距离没有副本可提升只有一次失败。重建或回绕旧主节点是故障转移的一部分。在关闭事件之前恢复冗余。

然后安排下一个。一个从未运行过的故障转移流程是一个假设。按日历的计划中切换是测试它的方式,而它们是找出健康检查、DNS 记录或那个有硬编码主机名的应用程序没有做每个人假设的事的唯一方式。

(我通过 PGX Inc. 提供 PostgreSQL 咨询服务。)

相关

  • PostgreSQL 中的二进制只读副本,第一部分
  • PostgreSQL 中的二进制只读副本,第二部分
  • 你的所有 GUC 排成一行:max_slot_wal_keep_size
相关推荐
从零数据分析1 小时前
零基础学SQL 23:和上月比总对不上?LAG/LEAD 一步取出上一次(附 4 个模板)
java·数据库·sql·mysql·oracle·sqlite·database
码士集团小青1 小时前
Redis 缓存击穿(失效)、缓存穿透、缓存雪崩怎么解决?
数据库·redis·缓存
太理摆烂哥2 小时前
MySQL的查询
数据库·mysql
码士集团小青2 小时前
硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战
数据库·redis·缓存
钱栈up2 小时前
配料秤料位查出来是负数——IoTDB存的是周期增量,我却在拿首尾相减
前端·数据库
人丰2 小时前
一次生产卡顿复盘:BI 数据抽取引发 PostgreSQL 长事务事故
数据库·c#
程序员塔塔喵2 小时前
【MySQL篇|第四篇】事务隔离级别
数据库·mysql·事务隔离级别
oradh2 小时前
Oracle 外部表(External Tables)使用总结
数据库·oracle
长安米粒贵2 小时前
事务隔离级别选对了,为什么还是会出现丢失更新?聊聊 5 个并发误区
数据库