设计可靠的计划外故障转移
Admin 无法判断主库是真的死了,还是只是看不见。Neki 于是去问副本,把它们就地冻结,再提升持有最多 WAL 的那一个。
中文
复制

在为最小化中断而设计计划内切换一文中,我们讨论了 Neki 在所有参与者都健康时如何迁移 Postgres 主库。控制平面 Admin 事先知道主库何时会让位,因此可以检查分片、选定目标,并在造成任何中断之前准备好交接。这就是“计划内切换”中“计划内”的含义。
计划外故障转移一开始就没有这些优势。它发生在故障模式出乎意料(但仍能被妥善处理)的情况下,比如网络链路被切断或服务器硬件故障。
Neki 的 Admin 系统无法判断主库是真的故障了,还是仅仅从 Admin 所在的位置不可达。它没有预先选好提升目标,也无法向主库索取最终的 WAL 位置。与此同时,应用无法可靠地写入。
这个场景下有两个问题要回答。
- 主库是否实际上已不可用?
- 哪个副本可以安全接管?
主库何时算死亡?
一个 Neki 分片由一个 Postgres 主库和多个副本组成。主库接受写入,每个副本持续接收并重放主库的 WAL。主库一旦故障,就需要有一个副本接管。
假设分片的主库是 P,副本是 R1、R2 和 R3。Admin 对 P 做健康检查,结果……没有响应!
你可能会问,Admin 是不是该提升某个副本了。答案是还不行。P 可能已经崩溃,也可能网络被隔离。又或者故障只存在于 Admin 与 P 之间,而应用和副本之间的通信一切正常。
一种常见的做法是让 Admin 对主库重复多次健康检查。如果尝试若干次后主库仍无响应,就可以判定它死亡。重复检查能应对瞬时的丢包,但如果 Admin 处在网络分区的隔离一侧,只要分区还在,这样做也无济于事。
其他高可用系统使用租约。主库上的一个 agent 在分布式存储中持有一个定期续期的 key,key 一旦过期,就认为主库故障。这是成熟的设计。Neki 虽然也有分布式拓扑服务,但并未用租约来判断主库健康状况。
Neki 则去询问副本。
每个副本会汇报它从 pg_stat_wal_receiver 看到的物理复制状态。这样就能区分出几种情况。
- 如果 Admin 无法连接
P,但仍有副本在从它接收流式复制,那么仅凭这些证据不足以触发 故障转移。 - 如果 Admin 无法连接
P,至少有一个可达副本报告复制已中断,且没有任何可达副本给出相反的说法,Neki 就可以判定P已死亡并尝试恢复。 - 如果 Admin 既无法连接
P,也无法连接任何副本,Neki 不会执行 故障转移。它既没有佐证,也看不到可以提升的候选副本。
最后一种情况下,Admin 只能等待。仅凭一次失败的健康检查就提升副本,可能会造出第二个可写的主库。
副本的视角为什么重要
Neki 默认的同步持久化策略给 Postgres 配置如下。
synchronous_standby_names = 'ANY 1 (R1, R2, R3)'
在 synchronous_commit = on 下,P 要等到至少一个符合条件的副本把 WAL 持久化刷盘之后,才会确认提交。每次提交负责确认的副本可能都不一样。R1 可能在 T 确认一次提交,而下一次提交由 R3 在 T+1 确认。
再看触发故障转移的情形,Admin 连不上 P,任何副本也连不上。P 可能已经崩溃,也可能仍在运行,只是从 Admin 节点不可达。无论哪种情况,P 都无法完成同步提交,因为没有副本能 ack。即使 P 还活着,写入实际上也已被阻塞。提升一个副本,就替换掉了这个已无法写入的主库。
当部分副本同样不可达时,它们的视角是未知的,其中一个(比如 R1)可能仍然连着 P。因此我们无法确定 P 没有在接受写入。
两者可能仍在正常协作,只是与 Admin 及其他副本隔离开了。这都在 ANY 1 契约允许的范围之内。缓解办法是让 Neki 只接受来自跨可用区副本的复制确认,降低两者同时被网络隔离的可能性。
只要至少有一个可达副本报告复制中断,且没有副本给出相反的报告,Neki 在这种情况下仍然会执行故障转移。
Neki 也支持异步持久化。在异步模式下,P 不会等待副本确认写入,因此我们无法推断它已停止接受写入。主库不可达、且所有副本都断开连接,这是一个非常强的信号,但并非保证。
检测故障
检测逻辑和速度因故障类型而异。下面看几个常见场景。
假设 Postgres 进程因某种原因崩溃并以错误退出。Neki 通过 PostgresManager 运行 Postgres,这个组件负责启动、停止、配置以及以其他方式控制 Postgres 进程。一个 goroutine 等待 Postgres 进程,当主库 P 上的进程退出时,PostgresManager 立刻就能知道。它向 Admin 发送一个 PROCESS_EXITED 事件,Admin 将 P 标记为不可达,并启动一轮分片分析,与 P 的副本交叉验证。此时检测仍不完整,还要等待副本确认。
另一个常见场景是连接被拒绝。也许是防火墙挡在了 P 和分片其余部分之间,主动拒绝它的流量。这种情况很直接,Admin 的健康检查和副本的复制连接都会立刻报错失败。
丢包更难处理。如果 P 所在的主机宕机,或者网络静默丢弃它的流量,没有人会收到错误。一条连接可以在对端早已不存在之后,看起来仍然处于已建立状态。
而 Postgres 进程冻结也许是最难的一种。服务器的内核在 TCP 层面仍在应答,所以连接看起来健康,尽管 Postgres 并没有做任何工作。
副本如何区分这些情况?它不需要区分。Neki 调整 Postgres 的复制设置,让每一种情况,以及我们未列出的许多组合,都以同样的方式收场,副本报告复制中断。
- 复制连接上较短的 TCP keepalive 设置和
tcp_user_timeout可以捕获丢包。 wal_receiver_timeout可以捕获冻结的进程,这是单靠 TCP 做不到的。如果P在几秒内没有发送任何数据,副本就会关闭连接并报告复制中断。
决定执行故障转移
一旦 Neki 认定 P 已经死亡,这个分片就需要一个新的主库。我们必须挑选一个合适的候选副本,将其提升为主库,同时重新配置其余副本,让它们接受这个新主库。不过这里有两个问题。
P随时可能重新变为“存活”状态。- 在复制中断之前,各副本收到的 WAL 数量可能各不相同。
断开副本
复制已经中断了,为什么还要主动断开副本?因为 P 可能会回来。如果它在 Admin 决定执行故障转移之前回来,那没有危害。副本重新连上,短暂波动之后分片就恢复健康了。问题在于 P 在 Admin 已经决定故障转移之后回来。到那时,Admin 正在让分片收敛到新的主库上,但一条断掉的连接仍然可以重新连上。WAL 位置会重新开始移动,P 又会拥有能够确认其提交的副本。
MySQL 有一条命令可以让管理员停止接收来自主库的变更,而不必完全停止复制,就是 STOP REPLICA IO_THREAD。Postgres 在物理复制上没有对应的命令。Neki 的做法是对每个可达副本的 primary_conninfo 做一处小而可逆的改动,在 P 的主机名前加上 //。副本便再也找不到 P,也就无法重新连接。
计划内切换在交接前冻结副本 WAL 位置,用的正是同一套机制。
断开副本有两个作用。
第一,防止 P 意外恢复健康。如果 P 回来了,已断开的副本不会重新连上它。在 ANY 1 下,P 没有副本确认就无法完成提交,因此它将无法接受任何写入。
第二,冻结副本的 WAL 位置。一旦没有副本能从 P 接收更多 WAL,每个副本报告的位置就是最终值。这些位置是对 WAL 当前视图的一份稳定记录,我们将据此选择新的主库。
从 WAL 位置中选择
在计划内切换中,Admin 会提前选定新的主库,此时旧主库仍然健康。计划外故障转移没有这种余裕,只能从幸存下来的 WAL 里选。
假设各副本停在如下位置。
R1 receive: 120 replay: 120
R2 receive: 125 replay: 122
R3 receive: 123 replay: 123
收到的最高位置是 125,位于 R2。Admin 首先寻找一个已经重放至 125 的可提升副本。这个快照里没有,于是短暂等待后再查一次。如果 R2 把本地存储的 WAL 重放到 125,它就成为候选。
如果等待窗口内没有副本追上,Admin 就选 receive_lsn 最高的可提升副本,并以 replay_lsn 作为下一个判断依据。
Admin 之所以能在各副本之间取最大值,是因为 WAL 是顺序的,收到位置 125 的副本,必然也收到了它之前的 WAL。Neki 不需要知道每一次提交是由 R1、R2 还是 R3 确认的,它只需要一个幸存的候选,其 WAL 覆盖了已确认的历史。
最新的 WAL 也可能落在无法提升的副本上。ANY 1 保证的是每次已确认的提交都到达了某个副本,而不是那个副本仍然存在。如果 P 和那个副本一起故障,这次提交就丢了。要求跨可用区确认能大幅降低这种可能,代价是提交延迟。
提升时会发生什么?
Admin 在新主库上为其他副本创建复制槽,并在 Postgres 中将其提升。随后把它标记为分片的主库,Neki 的 Router 开始把写入发往它。最后,Admin 把其他副本重新指向新主库。
选择最高接收位置在这里还有第二重回报。其他所有副本持有的都是新主库 WAL 的前缀,因此可以直接跟随它进入新的时间线。例外是那些位置领先于候选、却无法被提升的副本,比如正处于维护中。Neki 必须先把它回退或重建,它才能继续跟随。
写入还不会立刻恢复。新主库以 ANY 1 运行,因此它的最初几次提交要等到有副本接入并确认之后才会完成。
P 的情况
Admin 把 P 标记为 TEARDOWN。TEARDOWN 节点不承载流量,不参与复制,Admin 也不再监控它。对分片来说,P 已经不存在了,哪怕它之后又活过来。
计划内切换完成后,Admin 会把 P 变成新主库的副本。计划外故障转移之后,我们不会这么做。P 上可能有新主库没有的写入,我们不想仅仅因为它重新可达就信任它。
这不意味着必须立刻把它处理掉。故障的主库往往是查清问题的最好证据,比如它的日志、最后的 WAL、磁盘的状态。我们可能想留着它做故障复盘。之后才销毁、回收,或者经明确决定,把它作为副本重新加入分片。
小结
计划外故障转移始于不确定,Admin 不知道 P 是已经死了,还是只是脱离了它的视野。Neki 不会仅凭 Admin 的判断行动。它会去询问各副本,只有当它能联系上的副本一致认为 P 已经不在了,才执行故障转移。
接下来,我们对 P 做最坏的假设,它随时可能回来。断开副本可以防止归来的 P 重新拿回这些副本,同时冻结它们的 WAL。为接替者选择最高的 WAL 位置,可以保住可提升副本上每一个已确认的提交,也让其余副本容易跟上。
有些故障超出了自动恢复的范围。Admin 无法提升联系不上的副本,也无法恢复随所有持有它的机器一起消失的 WAL。
Admin 自身也可能出故障,但那是以后文章的话题了。