每个DBA都不愿面对的噩梦
一天,我做了一个噩梦,那是每个数据库管理员都害怕的那种。我的经理打电话给我说:“嘿……聪明先生。我们团队不再需要你了。”我惊慌失措。“我做错了什么?!”他回答说:“你来告诉我。想想你昨天的‘好’工作吧。”我试图为自己辩护,自豪地说:“先生,有一个节点宕机了。PostgreSQL服务处于非活动状态,所以我启动了它……然后节点就恢复了!很聪明,对吧?”他叹了口气,停顿了一下,以制造戏剧效果,然后说:“祝你早日康复。”接着他挂了电话。
我立刻惊醒了,心跳加速。不是因为老板,而是因为真正的恐惧击中了我:我们正在运行一个Patroni集群……而我却在梦中手动启动了PostgreSQL。
那不是“聪明的工作”。那是数据库级别的犯罪,会招致诸如“为什么我的集群现在坏了?”、“为什么我的副本在另一个时间线上?”以及可能“为什么ETCD那样看着我?”这样的惩罚。
幸运的是,这只是一个噩梦。这篇博客旨在帮助防止任何人经历那种噩梦。它清晰地解释了发生了什么,为什么这很危险,以及如何避免它。
理解Patroni在集群中的作用
Patroni负责管理:
- PostgreSQL的启动和关闭
- 复制配置
- 领导者选举和故障转移/隔离决策
- WAL(预写日志)时间线协调
- 恢复决策和集群一致性
- 与DCS(例如ETCD)协调集群状态
- 防止分歧和损坏
在Patroni外部启动PostgreSQL时实际会发生什么
在基于Patroni的高可用性(HA)集群中,不建议在运行的Patroni集群中使用 postgres 服务或 pg_ctl 手动启动PostgreSQL。如果在Patroni关闭时手动启动PostgreSQL:
当Patroni关闭时手动启动PostgreSQL,该节点就不再受Patroni控制,并开始作为独立实例运行。然而,这不会立即创建冲突的时间线。
只有在该节点独立运行时发生了故障转移或切换,才会出现冲突的(分歧的)时间线。如果手动启动的节点以前是副本,并且没有发生故障转移,它可能仍会继续从主节点复制,但对Patroni是不可见的,并且无法参与协调的HA。如果手动启动的节点是前领导者,并且Patroni在其他地方选出了新的领导者,这时才会发生真正的时间线分歧。
- 独立的副本 + 无故障转移 → 复制可能继续,没有新的时间线
- 独立的主节点 + 发生故障转移 → 时间线分歧,需要
pg_rewind 或完全重新初始化
要点:手动启动的主节点是最危险的情况;如果选举出新领导者,它会立即导致时间线分歧。手动启动的副本仍然危险,但只有在它独立运行时发生了领导者变更才会出现分歧。
在深入探讨之前,我想与你分享一个测试,以便更好地理解潜在的结果。
详细测试案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
操作系统:
Red Hat Enterprise Linux release 9.6 (Plow)
集群拓扑:
节点 IP地址 角色(初始)
node1 192.168.183.131 Leader
node2 192.168.183.129 Replica
node3 192.168.183.130 Replica
PostgreSQL版本:17.7
Patroni:所有节点上均处于活动状态
DCS:etcd 3节点集群
注意:验证ETCD主版本兼容性。较旧的Linux发行版可能仍包含与现代Patroni版本不兼容的旧版ETCD。
复制:流复制
pg_rewind:已启用
|
捕获基线状态
1
2
3
4
5
6
7
8
9
|
[root@node1 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: postgres (7575009857136431951) -------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node1 | 192.168.183.131 | Leader | running | 4 | | | | |
| node2 | 192.168.183.129 | Replica | streaming | 4 | 0/5055240 | 0 | 0/5055240 | 0 |
| node3 | 192.168.183.130 | Replica | streaming | 4 | 0/5055240 | 0 | 0/5055240 | 0 |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
|
这确认了集群健康:副本已接收并重放WAL至主节点的当前LSN,所有节点都在相同的时间线上。
崩溃测试与故障模拟:
现在,模拟在node1上的崩溃:
- 在node1(当前领导者)上终止Patroni。
- 手动启动PostgreSQL:
1
|
[root@node1 ~]# systemctl start postgresql-17.service
|
- Node1现在成为时间线4上的独立主节点。
- 与此同时,Patroni将node3提升为领导者 → 时间线5。
Patroni现在显示变更后的集群状态:
1
2
3
4
5
6
7
|
[root@node1 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: postgres (7575009857136431951) -------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node2 | 192.168.183.129 | Replica | streaming | 5 | 0/5055380 | 0 | 0/5055380 | 0 |
| node3 | 192.168.183.130 | Leader | running | 5 | | | | |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
|
从上面显示的输出中,我们可以看到发生了时间线分歧,并且node1缺失了,因为它的服务已关闭。我没有启动Patroni服务,而是直接启动了PostgreSQL服务。输出还表明node3现在是领导者,node2作为副本运行,两者都在相同的时间线上操作。此外,很明显node1不再是Patroni集群的一部分。
分歧演示
在独立的node1上写入:
1
2
3
4
5
6
7
8
9
10
11
|
postgres=# SELECT timeline_id FROM pg_control_checkpoint();
timeline_id
-------------
4
(1 row)
postgres=# SELECT pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
0/50552F0
(1 row)
|
现在在独立的node1上执行事务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
postgres=# create table crash_patroni (id int);
CREATE TABLE
postgres=# SELECT pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
0/506EDF8
(1 row)
postgres=# insert into crash_patroni values(1), (2), (3);
INSERT 0 3
postgres=# SELECT pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
0/5070E68
(1 row)
|
与此同时,在新的领导者(node3)上:
1
2
3
4
5
6
7
8
9
|
postgres=# create table crash_new_leader (id int);
CREATE TABLE
postgres=#
postgres=# SELECT pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
0/5070B80
(1 row)
|
Node2正常复制Node3的更改。
1
2
3
4
5
6
7
|
postgres=# select * from crash_new_leader;
id
----
1
2
3
(3 rows)
|
在node3上列出Patroni:
1
2
3
4
5
6
7
8
|
[root@node3 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: postgres (7575009857136431951) -------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node2 | 192.168.183.129 | Replica | streaming | 5 | 0/5070B80 | 0 | 0/5070B80 | 0 |
| node3 | 192.168.183.130 | Leader | running | 5 | | | | |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
|
重新加入node1(启动Patroni)
当在node1上重新启动Patroni时,它会检测到分歧并运行 pg_rewind 以将数据目录与新领导者同步:
1
2
|
[root@node1 ~]# patronictl list
node1 | Replica | TL 5
|
Node1被重绕并作为副本加入。
1
2
3
4
5
6
7
8
9
|
[root@node1 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: postgres (7575009857136431951) -------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node1 | 192.168.183.131 | Replica | streaming | 5 | 0/5070CC0 | 0 | 0/5070CC0 | 0 |
| node2 | 192.168.183.129 | Replica | streaming | 5 | 0/5070CC0 | 0 | 0/5070CC0 | 0 |
| node3 | 192.168.183.130 | Leader | running | 5 | | | | |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
|
现在检查之前在node1上创建的表:
1
2
|
postgres=# select * from crash_patroni;
ERROR: relation "crash_patroni" does not exist
|
在node3上创建的表(crash_new_leader)现在存在于node1上:
1
2
3
4
5
6
7
|
postgres=# select * from crash_new_leader;
id
----
1
2
3
(3 rows)
|
发生了什么:让我解释一下。Patroni检测到:
- Node1在时间线4上。
- 集群已推进到时间线5(通过提升node3)。
- 发生了分歧。
Patroni调用了 pg_rewind:
1
2
3
|
pg_rewind: servers diverged at WAL location...
pg_rewind: rewinding...
pg_rewind: Done!
|
结果:
- Node1的数据目录被重写以匹配新领导者(node3)。
- 在node1独立运行时创建的表
crash_patroni 丢失了(数据丢失)。
- Node1成功重新加入为时间线5上的副本。
- 在独立模式下执行的所有写入都被擦除了。
Pg_rewind:先决条件和失败模式
pg_rewind 是修复分歧节点的最安全的恢复方法,因为它:
- 重置分歧的时间线。
- 重建数据目录以匹配新领导者。
- 重新加入节点,无需完整的基准备份。
重要的先决条件:
wal_log_hints = on 或者必须启用数据校验和。Patroni通常会为你确保 wal_log_hints = on,但请始终验证该设置,尤其是在手动构建的集群或升级的节点上。
如果 pg_rewind :
- 被禁用。
- 配置错误。
- 因缺少WAL段或目录不匹配而失败。
- 意外崩溃。
那么重新初始化(完整的基准备份 + 恢复)是唯一的恢复选项。
关键风险和注意事项
- Patroni必须是唯一的控制器:手动启动PostgreSQL = 流氓实例。
- 独立写入的数据将会丢失:在独立模式下执行的每一个操作都会消失。
- 类似脑裂的症状:即使不是真正的脑裂,症状包括:
- 分歧的时间线。
- 孤立的表。
- 不一致的LSN。
- 令人困惑的恢复决策。
- 操作混乱:不同的节点可能对表是否存在存在分歧。
预防措施与实践步骤
- 屏蔽操作系统服务:通过屏蔽单元来防止意外的
systemctl start postgresql-XX.service:
1
|
sudo systemctl mask postgresql-XX.service
|
- 同时监控Patroni和PostgreSQL服务:为以下情况创建告警:
- 当
patroni.service 关闭时,意外的 postgresql-XX.service = active。
- 缺少Patroni心跳。
- 时间线分歧检测。
- 副本未接收到WAL。
- 意外的PID或进程名称不匹配。
- Patroni日志中的
pg_rewind 事件。
- 验证
pg_rewind 先决条件:在生产集群节点之前,确认 wal_log_hints = on 或数据校验和已启用。
- 检查ETCD兼容性:确保你的ETCD主版本与你的Patroni版本兼容;较旧的发行版可能包含旧版ETCD包。
- 使用基于端口的健康检查:使用对端口5432的netstat检查,而不是跨异构操作系统家族进行进程名称匹配。
- 有疑问时进行备份:如果你怀疑在重新启动Patroni之前发生了独立写入,如果可能,请对数据目录进行文件系统级备份 —— 这有助于取证调查。
早期检测对于防止数据丢失至关重要。
总结
在Patroni管理的高可用集群中手动启动PostgreSQL不是一个无害的错误 —— 它是一个数据一致性风险。它可能导致:
- 时间线分歧。
- 类似脑裂的行为。
- 丢失的事务和DDL。
- 集群混乱和管理开销。
- 强制性的
pg_rewind 或完全重新初始化。
遵循上述预防措施,确保 pg_rewind 先决条件,监控Patroni和PostgreSQL,并始终通过Patroni启动PostgreSQL。这样做,你将大大降低醒来面对DBA噩梦的风险。
如果你正在使用Patroni集群,请始终通过Patroni服务启动PostgreSQL。你的数据库会感谢你的。让我们保持一切平稳运行!