在 PostgreSQL 集群中,“脑裂”(Split-Brain)是一种极其危险的故障场景,指集群中的两个或多个节点因通信中断,都误认为自己是唯一的主库,并同时接受写入操作,导致数据分叉和不一致。
一旦发生脑裂,原主库的数据状态会与新主库产生分歧,形成一条独立的时间线。这使得原主库无法简单地作为备库重新加入集群,因为它的 WAL 日志序列与新主库不再连续,直接加入会引发复制冲突。
脑裂发生的主要原因
脑裂的根本原因在于缺乏一个可靠的、基于多数派共识(Quorum)的仲裁机制。当网络分区发生时,集群的不同部分无法就“谁是唯一的主库”达成一致。常见场景是网络闪断或手动停主库的过程中,剩下的2个从库做了选主。
网络分区(Network Partition)这是最常见的原因。主库与备库之间的网络连接中断,但双方实例本身仍在运行。备库因收不到主库的心跳,误判主库已宕机,并自行提升为新主库。而原主库因网络隔离,并未感知到故障,继续提供服务,从而形成“双主”。
脑裂发生后,原主库无法直接作为备库加入新集群,主要面临两大障碍:
- 数据状态分歧(WAL 分叉)
在脑裂期间,新旧两个主库各自独立地处理写入请求,产生了不同的 WAL 日志。这意味着它们的历史轨迹(即时间线)已经分叉。新主库的时间线 ID 会递增,而原主库仍停留在旧的时间线上。PostgreSQL 的流复制要求备库的 WAL 位置必须是主库历史的一个连续子集,因此,原主库的数据目录与新主库不再兼容。 - 数据不一致风险
如果强行将原主库接入,可能会导致严重的数据问题。
如何将原主库重新加回集群?
要将发生脑裂的原主库重新纳入集群,必须将其数据状态“回滚”并“对齐”到新主库。PostgreSQL 提供了 pg_rewind 工具来高效地完成这项工作,而无需从头重建备库。或者手动重建新备库。
在使用 pg_rewind 之前,必须确保集群中的所有节点都启用了以下参数之一:
wal_log_hints = ondata_checksums = on
pg_rewind 的工作原理是:
- 扫描新旧两个数据目录。
- 找到它们最后共同的检查点(Common Checkpoint)。
- 将原主库中从共同检查点之后的所有数据文件变更“回滚”掉。
- 从新主库拉取必要的 WAL 日志,确保原主库的数据状态与新主库在某个点完全一致。
最近遇到的极端情况
1,场景一
- 网络分区(导火索):新主库发生网络闪断,导致它与集群中的其他节点(包括原来的主库,此时已降级为备库)失去联系。
- 误判与提升(核心动作):原来的备库因为收不到新主库的心跳,误以为主库已经宕机。在没有可靠仲裁机制的情况下,它触发了故障转移(Failover),将自己提升为新的主库。
- VIP 抢占(扩大影响):新晋主库接管了虚拟IP(VIP),并通过 ARP 广播告知网络中的其他设备“VIP 现在在我这里”。这使得新的客户端连接被导向了它。
- 旧主“复活”(形成双主):原主库网络恢复。但由于故障转移流程不完善,它可能没有感知到自己已被“废黜”,或者其上的 VIP 资源未能被强制释放。于是,它继续以主库身份运行,并且可能也持有 VIP。
- 双主并存(最终恶果):此时,网络中出现了两个主库。旧的客户端连接可能还连在原主库上,新的连接则被导向新主库。两边同时接受写入,数据开始出现不可逆的分歧。
要杜绝此类问题,关键在于引入一个可靠的“裁判”来做出唯一的、权威的决策。不能仅靠节点间的简单心跳来判断,因为网络分区时,心跳是不可靠的。方案引入引入分布式共识系统,如 Patroni + etcd/ZooKeeper/Consul 这样的高可用架构。
2,场景二
对于长时间未重启的数据库,在停原主库的过程中,如应用未停止,可能出现主库长时间无法关闭的现象,此时避免不了要kill 主库进程,结果可能会导致主库crash, 而剩下的从库做了failover,自动选主,导致原主库无法启动加入到新集群。
3, 场景三
postgresql的checkpoint和时间线在wal和控制文件中,但是控制文件更新为异步,如果主库重启,备库已failover,激活为新主库,但并未来的及更新控制文件,导致原主库启动后,在验证了对方的控制文件发现时间线与自己一致直接加入的现象,结果后期新主库更新时间线,导致原主库无法同步。
什么时PostgreSQL时间线
在 PostgreSQL 中,时间线(Timeline) 是一个与数据库备份和恢复(特别是时间点恢复 PITR)紧密相关的核心概念。
简单来说,时间线就像是一个数据库历史的分支标识符。每当数据库从一个备份中恢复后继续运行,就会开启一条新的时间线,以此来区分不同历史分支所产生的数据变更日志。 如果在oracle 数据库中做resetlogs 后,变更的 database incarnation.(RMAN> list incarnation;)
想象一个场景:你在周三不小心删除了一个重要的表,但直到周五才发现。于是,你使用备份将数据库恢复到周二晚的状态。恢复完成后,数据库会继续运行并产生新的日志。
如果没有时间线机制,这些新生成的日志文件可能会与原始历史中的日志文件同名,从而覆盖掉旧的日志。这会导致你再也无法恢复到周三或周四的任何一个时间点,因为关键的日志文件已经丢失了。
时间线的引入正是为了解决这个问题。它确保了恢复后产生的新日志与旧日志能够被清晰地区分开,互不干扰,让你可以灵活地恢复到任何一个历史分支上的任意时间点。
PostgreSQL 通过以下两个关键机制来实现时间线:
- WAL 文件名包含时间线ID
PostgreSQL 的预写日志(WAL)文件名中包含了时间线ID。一个典型的 WAL 文件名格式为:时间线ID + 日志ID + 段ID。- 例如:
0000000100000000000000C4 - 前8位
00000001就代表时间线ID为1。
当数据库从备份恢复并开启一条新时间线(例如ID为2)后,所有新生成的 WAL 文件都会以00000002开头,从而确保不会覆盖时间线1的日志文件。
- 例如:
- 时间线历史文件(.history)
每次创建一个新的时间线时,PostgreSQL 都会生成一个文本格式的.history文件(例如00000002.history)。这个文件记录了新时间线是从哪个父时间线、在哪个日志位置(LSN)分支出来的,以及分支的原因。
当你需要进行恢复时,PostgreSQL 会读取这些历史文件,自动理清所有时间线之间的分支关系,从而准确地找到并应用正确的 WAL 日志文件来完成恢复。
何时会产生新的时间线?
通常在以下两种情况下,PostgreSQL 会创建一个新的时间线:
- 执行时间点恢复(PITR)后:当你将数据库恢复到某个特定的过去时间点并重新打开数据库时,一个新的时间线就会被创建。
- 备库提升(Standby Promote)后:在主备架构中,当备库被提升为新的主库时,也会产生一条新的时间线。
你可以将时间线想象成一个“平行宇宙”。每次恢复都像是从历史的某个节点创造了一个新的平行世界,你可以在这个新世界里继续操作,而不会影响其他“宇宙”的历史记录。
— over —