手动启动Patroni集群中的PostgreSQL服务:一个数据库管理员的噩梦与启示

本文深入探讨了在Patroni管理的PostgreSQL高可用集群中手动启动PostgreSQL服务的灾难性后果。通过详尽的测试场景,阐述了该操作如何导致时间线分歧、数据丢失,并详细说明了pg_rewind的恢复机制及关键的预防措施。

每个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上的崩溃:

  1. 在node1(当前领导者)上终止Patroni。
  2. 手动启动PostgreSQL:
1
[root@node1 ~]# systemctl start postgresql-17.service
  1. Node1现在成为时间线4上的独立主节点。
  2. 与此同时,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段或目录不匹配而失败。
  • 意外崩溃。

那么重新初始化(完整的基准备份 + 恢复)是唯一的恢复选项。

关键风险和注意事项

  1. Patroni必须是唯一的控制器:手动启动PostgreSQL = 流氓实例。
  2. 独立写入的数据将会丢失:在独立模式下执行的每一个操作都会消失。
  3. 类似脑裂的症状:即使不是真正的脑裂,症状包括:
    • 分歧的时间线。
    • 孤立的表。
    • 不一致的LSN。
    • 令人困惑的恢复决策。
  4. 操作混乱:不同的节点可能对表是否存在存在分歧。

预防措施与实践步骤

  • 屏蔽操作系统服务:通过屏蔽单元来防止意外的 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。你的数据库会感谢你的。让我们保持一切平稳运行!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计