Patroni在Zookeeper中保存的信息
Patroni在Zookeeper中保存的信息
1. 保存的信息
在使用 Patroni 配置高可用 PostgreSQL 集群时,etcd 或 Zookeeper(作为分布式协调系统)通常用于存储和管理与集群状态和配置相关的元数据。具体来说,Patroni 在这些系统中保存了以下几类信息:
1.1 集群状态信息
Patroni
需要通过 etcd
或 Zookeeper
来协调多个节点之间的状态,确保每个节点对集群状态的感知是同步的。主要包括以下内容:
- 主节点(Primary)信息:
- 主节点的身份信息,包括
PostgreSQL
的主节点 当前的主机名、IP 地址和端口等。 - 记录哪个节点是当前的主节点,方便其它节点在发生故障时进行切换。
- 记录主节点的
timeline_id
(时间轴 ID),用于版本控制,确保从节点(standby)与主节点保持一致。
- 主节点的身份信息,包括
- 从节点(Standby)信息:
- 每个从节点(备份节点)的状态和位置。
- 包括从节点的复制位置,是否正在同步主节点等信息。
- 集群故障转移信息:
- 记录集群是否处于故障转移过程,或者是否正在进行主节点选举。
Patroni
会在主节点失败时发起选举,etcd
或Zookeeper
会保存选举的状态,并使得集群中的其它节点能够感知到状态变化。
1.2 PostgreSQL 配置信息
- PostgreSQL 配置文件(
postgresql.conf
)的同步:Patroni
通过etcd
或Zookeeper
可以保存数据库实例的配置文件内容或者配置参数。这样,集群中的所有节点能够确保一致的配置。
- 自动化配置调整:
Patroni
会根据集群的需求,动态更新和调整PostgreSQL
配置参数。例如,配置synchronous_standby_names
(同步从节点的名称)或者primary_conninfo
(主节点连接信息)等。
1.3 Leader 选举与锁定信息
- 领导者选举(Leader Election):
- 在 Patroni 中,
etcd
或Zookeeper
用于记录和同步选举信息,确保在任意时刻,只有一个节点能够担任主节点(Primary)。选举信息包括选举的投票者、候选者以及当前的选举状态。 - 通过存储选举信息,
Patroni
确保主节点故障时,其他节点可以通过选举快速选出新的主节点。 - 在选举过程中,
Patroni
会创建一个leader key
,该 key 记录当前的领导者。其它节点可以通过监视该 key 来感知主节点的变化。
- 在 Patroni 中,
1.4 事务日志(WAL)和同步位置
- WAL(Write-Ahead Log)同步位置:
Patroni
会记录每个节点的 WAL 同步位置。对于每个从节点,etcd
或Zookeeper
会保存一个条目,记录该节点当前同步的 WAL 日志位置。这个信息帮助Patroni
确定哪些从节点需要更新或者重新同步。
- 同步延迟:
- 在某些配置中,
Patroni
会在etcd
或Zookeeper
中记录各节点的复制延迟,以便做出更加精确的故障转移决策。
- 在某些配置中,
1.5 故障转移与恢复过程的控制信息
- 故障转移信息:
- 如果主节点失效,
Patroni
会通过etcd
或Zookeeper
来协调其它节点进行主节点选举,并确保新的主节点能够正确地接管。
- 如果主节点失效,
- 恢复过程中的锁定信息:
- 在故障转移过程中,
Patroni
可能需要使用锁定机制(例如在etcd
中创建锁定key
),确保在集群内的各个节点之间不会发生竞争条件。
- 在故障转移过程中,
1.6 Patroni 的内部元数据
- Patroni 状态:
- Patroni 会在
etcd
或Zookeeper
中保存与自身的状态相关的信息。例如,当前Patroni
实例是否已初始化,是否正在进行故障转移等。
- Patroni 会在
- 节点的角色信息:
- 记录每个节点在集群中的角色(如主节点、从节点或备用节点),以及每个节点当前的状态(如是否同步、是否启动等)。
1.7 集群配置与备份信息
- 备份信息:
- 如果集群启用了自动备份,Patroni 可能会将备份状态信息保存在
etcd
或Zookeeper
中,以便备份和恢复操作可以统一管理和协调。
- 如果集群启用了自动备份,Patroni 可能会将备份状态信息保存在
1.8 总结
etcd
或Zookeeper
在Patroni
集群中主要作为 分布式协调服务,用于保存集群的 状态信息、节点角色信息、选举信息 和 故障转移控制信息,确保集群中的所有节点能够保持一致,支持高可用性和故障恢复机制。
这些信息对于维护一个高可用的 PostgreSQL
集群是至关重要的,因为它们能够帮助 Patroni
在主节点失效时进行快速的故障转移、选举新的主节点,并保证集群的一致性。
2. 保存信息的类型
要查看 Patroni
在 Zookeeper
中保存的信息,通常需要使用 Zookeeper 的客户端工具来访问和查询存储在 Zookeeper 中的节点数据。下面是一些常用的操作步骤和命令来查看 Zookeeper 中的数据。
2.1 查看服务器状态
zkServer.sh status
2.2 启动客户端
zkCli.sh -server 192.168.198.167:2181
基本命令:
帮助 help
查看目录 ls
创建目录 create
获取数据 get
设置数据 set
检查状态 stat
删除路径 delete
2.3 四类节点
2.3.1 无序持久节点(默认的)
create /hanchao_test/defatul_node "persistent,no-sequence"
ls /hanchao_test
2.3.2 有序持久节点
- 有序节点名称=有序节点路基+10位顺序号
-s
:sequence
create -s /hanchao_test/sequence_node "persistent,sequence"
ls /hanchao_test
create -s /hanchao_test/sequence_node "persistent,sequence"
ls /hanchao_test
2.3.3 无序临时节点
- 临时节点在client端口连接之后关闭
-e
: temp
# client1 创建之后断开连接
create -e /hanchao_test/temp_node "no-persistent,no-sequence"
ls /hanchao_test
# client2
# client1断开之前的状态
ls /hanchao_test
# client1断开之后的状态
ls /hanchao_test
2.3.4 有序临时节点
create -s -e /hanchao_test/temp_node_p "no-persistent,order"
ls /hanchao_test
create -s -e /hanchao_test/temp_node_p "no-persistent,order"
ls /hanchao_test
- 同一个目录下,顺序节点的顺序号是共用的。
2.4 监视watch
- 当指定的znode或znode的子数据更改时,监视器会显示通知。
- 你只能在 get 命令中设置watch。
# 在client1中监视
[zk: localhost:2181(CONNECTED) 14] get /hanchao_test/defatul_node 1
persistent,no-sequence
cZxid = 0x4000092f3
ctime = Thu Mar 07 10:50:52 CST 2019
mZxid = 0x4000092f6
mtime = Thu Mar 07 10:54:22 CST 2019
pZxid = 0x4000092f3
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 22
numChildren = 0
# 在client2中set
[zk: localhost:2181(CONNECTED) 0] set /hanchao_test/defatul_node "hhhhh"
cZxid = 0x4000092f3
ctime = Thu Mar 07 10:50:52 CST 2019
mZxid = 0x400009301
mtime = Thu Mar 07 11:10:26 CST 2019
pZxid = 0x4000092f3
cversion = 0
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
# client1监视到数据变化
[zk: localhost:2181(CONNECTED) 15]
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/hanchao_test/defatul_node
3. 查看保存的信息
登录到zookeeper的客户端,查看所有的目录
zkCli.sh -server 192.168.198.167:2181
ls /
WatchedEvent state:SyncConnected type:None path:null
[zk: 192.168.198.167:2181(CONNECTED) 3] ls /
[pgsql, zookeeper]
可以看到两个节点,在pgsql
节点下就是patroni
在zookeeper
中保存的数据。然后再次查看两个节点下的数据,
[zk: 192.168.198.167:2181(CONNECTED) 0] ls /pgsql
[cn, demo, dn1, dn2]
[zk: 192.168.198.167:2181(CONNECTED) 1] ls /zookeeper
[config, quota]
可以发现pgsql
节点下保存的是以scope
区分的节点结构,因此这个节点下就是patroni
在zookeeper
中保存的数据,而zookeeper
节点下的config
就是zookeeper
集群保存的信息。下面来验证猜想。
3.1 查看zookeeper节点
查看zookeeper
节点下的config
和quota
两个节点,发现都没有下级节点了,使用get
命令来获取两个节点的信息。
ls /zookeeper/config
ls /zookeeper/quota
get /zookeeper/config
get /zookeeper/quota
[zk: 192.168.198.167:2181(CONNECTED) 2] ls /zookeeper/config
[]
[zk: 192.168.198.167:2181(CONNECTED) 3] ls /zookeeper/quota
[]
[zk: 192.168.198.167:2181(CONNECTED) 4] get /zookeeper/config
server.1=192.168.198.167:2888:3888:participant
server.2=192.168.198.168:2888:3888:participant
server.3=192.168.198.169:2888:3888:participant
version=0
[zk: 192.168.198.167:2181(CONNECTED) 5] get /zookeeper/quota
发现config
节点保存的就是在zoo.cfg
中配置的服务器信息,因此这个就是zookeeper
集群保存的信息,以便于维护zookeeper
集群,而quota
是个空列表,表示没有数据。
/zookeeper/config
:存储了 Zookeeper 集群的成员配置信息,列出了集群中的所有节点以及它们的角色(participant
)。这些配置对于 Zookeeper 集群的启动和维护非常重要。/zookeeper/quota
:当前为空,表示没有存储配额信息。通常用于限制某些操作的配额,防止某个客户端过度使用 Zookeeper 资源。
3.2 查看pgsql节点
上文已经说pgsql
节点下是按照patroni
的配置文件中的scope
来进行区分节点的。因为之前用过的scope
都在这里面并且与之前配置一致,这里以正在运行的集群demo
为例,来查看patroni
保存的信息。
ls /pgsql/demo
ls /pgsql/demo/0
ls /pgsql/demo/1
ls /pgsql/demo/2
[zk: 192.168.198.167:2181(CONNECTED) 2] ls /pgsql/demo
[0, 1, 2]
[zk: 192.168.198.167:2181(CONNECTED) 4] ls /pgsql/demo/0
[config, failover, history, initialize, leader, members, status]
[zk: 192.168.198.167:2181(CONNECTED) 5] ls /pgsql/demo/1
[config, failover, history, initialize, leader, members, status]
[zk: 192.168.198.167:2181(CONNECTED) 6] ls /pgsql/demo/2
[config, history, initialize, leader, members, status]
可以看见这个demo
节点下保存的是0,1,2这样的数字,对应了在patroni
中配置的citus
中的集群数字,猜想这个节点代表一个group
集群,在这个group
集群中只能有一个主节点。现在以group为0的集群为例,查看patroni在zookeeper中保存的了什么信息。
3.2.1 config节点
config表示配置,观察节点存储的信息,与patroni中的配置有关,应该是存储patroni中的配置项。
[zk: 192.168.198.167:2181(CONNECTED) 38] get /pgsql/demo/0/config
{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":1048576,"master_start_timeout":300,"synchronous_mode":false,"postgresql":{"use_pg_rewind":true,"use_slots":true,"parameters":{"listen_addresses":"0.0.0.0","port":5432,"wal_level":"logical","hot_standby":"on","wal_keep_segments":1000,"max_wal_senders":10,"max_replication_slots":10,"wal_log_hints":"on"}}}
整理配置,得到一个JSON字符串:
{
"ttl": 30,
"loop_wait": 10,
"retry_timeout": 10,
"maximum_lag_on_failover": 1048576,
"master_start_timeout": 300,
"synchronous_mode": false,
"postgresql": {
"use_pg_rewind": true,
"use_slots": true,
"parameters": {
"listen_addresses": "0.0.0.0",
"port": 5432,
"wal_level": "logical",
"hot_standby": "on",
"wal_keep_segments": 1000,
"max_wal_senders": 10,
"max_replication_slots": 10,
"wal_log_hints": "on"
}
}
}
因为是同一个group
中并且只有一个主节点,其他都是从节点,因此只需要保存一份配置即可,并且在进行主备切换和故障转移是可以使用这份配置来修改postgresql
的配置项。这里发现保存的是patroni配置文件中bootstrap部分的配置,bootstrap
部分主要用于初始化和启动一个新的 PostgreSQL 集群。它包含了启动时所需的配置参数,特别是在集群启动初期没有主节点的情况下,如何启动一个新的主节点以及它的基本配置。
3.2.2 failover节点
通过failover这个名字可以猜想这个节点保存的信息与故障转移有关,通常用于存储与集群的故障切换(failover)相关的元数据。具体来说,它保存了有关当前故障切换状态的信息,Patroni 可以使用这些信息来管理主节点的选举、故障恢复以及备节点的角色切换。
[zk: 192.168.198.167:2181(CONNECTED) 39] get /pgsql/demo/0/failover
{}
3.2.3 history节点
这个节点保存了patroni集群在一段时间内的操作日志。
[zk: 192.168.198.167:2181(CONNECTED) 40] get /pgsql/demo/0/history
[[1,29913856,"no recovery target specified"],[2,29914312,"no recovery target specified"],[3,38559736,"no recovery target specified"],[4,38698032,"no recovery target specified","2024-11-06T01:26:49.844460+08:00","pgsql1"],[5,38716272,"no recovery target specified","2024-11-06T19:25:49.695258+08:00","pgsql1"],[6,67128552,"no recovery target specified","2024-11-06T19:42:05.763083+08:00","pgsql1"],[7,67154400,"no recovery target specified","2024-11-06T20:13:04.353534+08:00","pgsql1"],[8,67186152,"no recovery target specified","2024-11-06T22:46:36.683335+08:00","pgsql4"],[9,67212160,"no recovery target specified","2024-11-07T00:28:03.239284+08:00","pgsql1"],[10,67236696,"no recovery target specified","2024-11-06T23:43:35.594507+08:00","pgsql4"],[11,67252976,"no recovery target specified","2024-11-06T23:43:46.479423+08:00","pgsql4"],[12,100678608,"no recovery target specified","2024-11-07T00:55:37.086102+08:00","pgsql1"],[13,100707968,"no recovery target specified","2024-11-07T00:10:50.229976+08:00","pgsql4"],[14,100727448,"no recovery target specified","2024-11-07T01:18:49.931042+08:00","pgsql1"],[15,100891880,"no recovery target specified","2024-11-07T02:56:41.609702+08:00","pgsql1"]]
整理节点信息:
[[1,29913856,"no recovery target specified"],
[2,29914312,"no recovery target specified"],
[3,38559736,"no recovery target specified"],
[4,38698032,"no recovery target specified","2024-11-06T01:26:49.844460+08:00","pgsql1"],
[5,38716272,"no recovery target specified","2024-11-06T19:25:49.695258+08:00","pgsql1"],
[6,67128552,"no recovery target specified","2024-11-06T19:42:05.763083+08:00","pgsql1"],
[7,67154400,"no recovery target specified","2024-11-06T20:13:04.353534+08:00","pgsql1"],
[8,67186152,"no recovery target specified","2024-11-06T22:46:36.683335+08:00","pgsql4"],
[9,67212160,"no recovery target specified","2024-11-07T00:28:03.239284+08:00","pgsql1"],
[10,67236696,"no recovery target specified","2024-11-06T23:43:35.594507+08:00","pgsql4"],
[11,67252976,"no recovery target specified","2024-11-06T23:43:46.479423+08:00","pgsql4"],
[12,100678608,"no recovery target specified","2024-11-07T00:55:37.086102+08:00","pgsql1"],
[13,100707968,"no recovery target specified","2024-11-07T00:10:50.229976+08:00","pgsql4"],
[14,100727448,"no recovery target specified","2024-11-07T01:18:49.931042+08:00","pgsql1"],
[15,100891880,"no recovery target specified","2024-11-07T02:56:41.609702+08:00","pgsql1"]]
3.2.4 initialize节点
在这个节点保存的信息是一个初始化标识符,可以标明集群已经被初始化,可以避免后续的节点重复执行初始化操作。也可以用来表示初始化状态的一个“唯一 ID”。它可能与集群的时间戳或其他状态信息相关,帮助各个节点同步集群状态。故障恢复时,初始化标识符也可能帮助系统确认集群是否已经成功初始化,并确保一致性。
[zk: 192.168.198.167:2181(CONNECTED) 41] get /pgsql/demo/0/initialize
7433284568963638752
3.2.5 leader节点
这个节点存储了当前group
的主节点名称。
[zk: 192.168.198.167:2181(CONNECTED) 42] get /pgsql/demo/0/leader
pgsql1
3.2.6 members节点
这个节点存储了当前group
的所有成员信息。members
节点下保存的是以集群成员名称name
为名的节点,这些成员名称节点内都存储了对应名称的成员的配置文件的具体信息,具体有postgresql
的连接地址conn_url
、restapi
调用地址api_url
,目前启动信息state
,当前成员的角色信息role
,patroni
的版本version
,还会存储一些从节点的特有信息。
[zk: 192.168.198.167:2181(CONNECTED) 43] ls /pgsql/demo/0/members
[pgsql1, pgsql4]
[zk: 192.168.198.167:2181(CONNECTED) 44] get /pgsql/demo/0/members/pgsql1
{"conn_url":"postgres://192.168.198.167:5432/postgres","api_url":"http://192.168.198.167:8008/patroni","state":"start failed","role":"primary","version":"4.0.3"}
[zk: 192.168.198.167:2181(CONNECTED) 45] get /pgsql/demo/0/members/pgsql4
{"conn_url":"postgres://192.168.198.177:5432/postgres","api_url":"http://192.168.198.177:8008/patroni","state":"running","role":"replica","version":"4.0.3","xlog_location":100902528,"replication_state":"streaming","timeline":16}
3.2.7 status节点
这个节点存储了集群中所有成员的一些基本信息,保存的 JSON 数据包含了 Patroni 集群节点的状态信息。
optime
是一个操作时间(Operation Time)的标识符,通常表示数据库(如 PostgreSQL)日志的写入位置,用于标记当前节点的 WAL(Write Ahead Log)位置。slots
存储了每个节点在集群中的复制槽信息。retain_slots
是一个数组,列出了哪些复制槽需要保持在当前状态。
[zk: 192.168.198.167:2181(CONNECTED) 46] get /pgsql/demo/0/status
{"optime":100902528,"slots":{"pgsql4":100902528,"pgsql1":100902528},"retain_slots":["pgsql1","pgsql4"]}
4. 代码解析
上次已经说到了存储了七个节点的信息,现在从代码层面来解析:
4.1 路径构造代码
在patroni/dcs/__init__.py
文件中AbstractDCS
类,里面定义了如何构造保存在dcs中的路径。代码如下:
_INITIALIZE = 'initialize'
_CONFIG = 'config'
_LEADER = 'leader'
_FAILOVER = 'failover'
_HISTORY = 'history'
_MEMBERS = 'members/'
_OPTIME = 'optime'
_STATUS = 'status' # JSON, contains "leader_lsn" and confirmed_flush_lsn of logical "slots" on the leader
_LEADER_OPTIME = _OPTIME + '/' + _LEADER # legacy
_SYNC = 'sync'
_FAILSAFE = 'failsafe'
def __init__(self, config: Dict[str, Any], mpp: 'AbstractMPP') -> None:
"""Prepare DCS paths, MPP object, initial values for state information and processing dependencies.
:param config: :class:`dict`, reference to config section of selected DCS.
i.e.: ``zookeeper`` for zookeeper, ``etcd`` for etcd, etc...
:param mpp: an object implementing :class:`AbstractMPP` interface.
"""
self._mpp = mpp
self._name = config['name']
self._base_path = re.sub('/+', '/', '/'.join(['', config.get('namespace', 'service'), config['scope']]))
self._set_loop_wait(config.get('loop_wait', 10))
self._ctl = bool(config.get('patronictl', False))
self._cluster: Optional[Cluster] = None
self._cluster_valid_till: float = 0
self._cluster_thread_lock = Lock()
self._last_lsn: int = 0
self._last_seen: int = 0
self._last_status: Dict[str, Any] = {'retain_slots': []}
self._last_retain_slots: Dict[str, float] = {}
self._last_failsafe: Optional[Dict[str, str]] = {}
self.event = Event()
def client_path(self, path: str) -> str:
"""Construct the absolute key name from appropriate parts for the DCS type.
:param path: The key name within the current Patroni cluster.
:returns: absolute key name for the current Patroni cluster.
"""
components = [self._base_path]
if self._mpp.is_enabled():
components.append(str(self._mpp.group))
components.append(path.lstrip('/'))
return '/'.join(components)
@property
def initialize_path(self) -> str:
"""Get the client path for ``initialize``."""
return self.client_path(self._INITIALIZE)
@property
def config_path(self) -> str:
"""Get the client path for ``config``."""
return self.client_path(self._CONFIG)
@property
def members_path(self) -> str:
"""Get the client path for ``members``."""
return self.client_path(self._MEMBERS)
@property
def member_path(self) -> str:
"""Get the client path for ``member`` representing this node."""
return self.client_path(self._MEMBERS + self._name)
@property
def leader_path(self) -> str:
"""Get the client path for ``leader``."""
return self.client_path(self._LEADER)
@property
def failover_path(self) -> str:
"""Get the client path for ``failover``."""
return self.client_path(self._FAILOVER)
@property
def history_path(self) -> str:
"""Get the client path for ``history``."""
return self.client_path(self._HISTORY)
@property
def status_path(self) -> str:
"""Get the client path for ``status``."""
return self.client_path(self._STATUS)
@property
def leader_optime_path(self) -> str:
"""Get the client path for ``optime/leader`` (legacy key, superseded by ``status``)."""
return self.client_path(self._LEADER_OPTIME)
@property
def sync_path(self) -> str:
"""Get the client path for ``sync``."""
return self.client_path(self._SYNC)
@property
def failsafe_path(self) -> str:
"""Get the client path for ``failsafe``."""
return self.client_path(self._FAILSAFE)
从这个代码可以知道,首先初始化的时候构建了_base_path
,包含了namespace(service)、scope
,比如构建了/pgsql/demo
,这是基本路径,然后再client_path
函数中和各种路径进行组合,同时注意如果开启了mpp
(这里只有citus
),则会先拼接group
,再拼接下面的路劲,这与zookeeper
中实际存放的节点一致。以config_path
举例,components
列表中数据为[/pgsql/demo, 0, config]
,然后在拼接位/pgsql/demo/0/config
,与实际情况保持一致。除了已经存在zookeeper中的7个节点信息,还有optime
、optime/leader
、sync
、failsafe
这几种信息。
4.2 实现代码
在AbstractDCS
这个抽象类中定义了如何获取存放信息的路径,并且也定义了设置(set)、写入(write)、删除(delete)这些对dcs
中节点和存储的信息的操作。现在我们具体来看patroni/dcs/zookeeper.py
中的ZooKeeper
类,它继承了AbstractDCS
并实现了其中的抽象方法。下面是其构造函数:
class ZooKeeper(AbstractDCS):
def __init__(self, config: Dict[str, Any], mpp: AbstractMPP) -> None:
# 调用父类构造函数
super(ZooKeeper, self).__init__(config, mpp)
# 处理 hosts 配置项
hosts: Union[str, List[str]] = config.get('hosts', [])
if isinstance(hosts, list):
hosts = ','.join(hosts)
# 处理与 SSL 和证书相关的配置项
mapping = {'use_ssl': 'use_ssl', 'verify': 'verify_certs', 'cacert': 'ca',
'cert': 'certfile', 'key': 'keyfile', 'key_password': 'keyfile_password'}
kwargs = {v: config[k] for k, v in mapping.items() if k in config}
# 处理 set_acls 配置项
if 'set_acls' in config:
default_acl: List[ACL] = []
for principal, permissions in config['set_acls'].items():
normalizedPermissions = [p.upper() for p in permissions]
default_acl.append(make_acl(scheme='x509',
credential=principal,
read='READ' in normalizedPermissions,
write='WRITE' in normalizedPermissions,
create='CREATE' in normalizedPermissions,
delete='DELETE' in normalizedPermissions,
admin='ADMIN' in normalizedPermissions,
all='ALL' in normalizedPermissions))
kwargs['default_acl'] = default_acl
# 初始化 ZooKeeper 客户端
self._client = PatroniKazooClient(hosts, handler=PatroniSequentialThreadingHandler(config['retry_timeout']),
timeout=config['ttl'], connection_retry=KazooRetry(max_delay=1, max_tries=-1,
sleep_func=time.sleep), command_retry=KazooRetry(max_delay=1, max_tries=-1,
deadline=config['retry_timeout'], sleep_func=time.sleep),
auth_data=list(config.get('auth_data', {}).items()), **kwargs)
# 保存成员数据
self.__last_member_data: Optional[Dict[str, Any]] = None
# 重写连接方法
self._orig_kazoo_connect = self._client._connection._connect
self._client._connection._connect = self._kazoo_connect
# 启动客户端
self._client.start()
在这个构造函数中调用了AbstractDCS
的构造函数,对一些配置参数进行解析设置,保存上一轮的成员数据,最重要的就是初始化ZooKeeper
客户端。
下面来解析具体对节点信息操作的函数:
def _set_or_create(self, key: str, value: str, version: Optional[int] = None,
retry: bool = False, do_not_create_empty: bool = False) -> Union[int, bool]:
value_bytes = value.encode('utf-8')
try:
# 如果 retry 为 True,则使用 self._client.retry 包装 set 操作,执行时会根据 ZooKeeper 的设置进行重试
if retry:
ret = self._client.retry(self._client.set, key, value_bytes, version=version or -1)
else:
# 如果 retry 为 False,则调用 set_async 异步设置节点的值
ret = self._client.set_async(key, value_bytes, version=version or -1).get(timeout=1)
return ret.version
# 这是一个 ZooKeeper 错误,表示目标节点不存在
except NoNodeError:
if do_not_create_empty and not value_bytes:
return True
elif version is None:
# 创建目标节点
if self._create(key, value_bytes, retry):
return 0
else:
return False
except Exception:
logger.exception('Failed to update %s', key)
return False
这个方法用于设置或创建 ZooKeeper 节点,如果该节点已经存在就更新数据,不存在就创建。
def _create(self, path: str, value: bytes, retry: bool = False, ephemeral: bool = False) -> bool:
try:
if retry:
self._client.retry(self._client.create, path, value, makepath=True, ephemeral=ephemeral)
else:
self._client.create_async(path, value, makepath=True, ephemeral=ephemeral).get(timeout=1)
return True
except Exception:
logger.exception('Failed to create %s', path)
return False
这个方法用于在zookeeper中创建一个新的节点。
4.2.1 config
# 创建或设置值
def set_config_value(self, value: str, version: Optional[int] = None) -> bool:
return self._set_or_create(self.config_path, value, version, retry=True) is not False
跟踪调用这个方法的value
发现,传入这个函数的value
是加载pg/data
目录下一个名为patroni.dynamic.json
的文件的信息。查看这个文件的内容,这个文件在patroni/config.py
中的Config
类的_load_cache
函数中加载,不过加载的时候对json数据做了处理,将其中的空格都去掉了self.dcs.set_config_value(json.dumps(self.patroni.config.dynamic_configuration, separators=(',', ':')))
:
{"ttl": 30, "loop_wait": 10, "retry_timeout": 10, "maximum_lag_on_failover": 1048576, "master_start_timeout": 300, "synchronous_mode": false, "postgresql": {"use_pg_rewind": true, "use_slots": true, "parameters": {"listen_addresses": "0.0.0.0", "port": 5432, "wal_level": "logical", "hot_standby": "on", "wal_keep_segments": 1000, "max_wal_senders": 10, "max_replication_slots": 10, "wal_log_hints": "on"}}}
与实际一致。
4.2.2 failover
# 创建或设置值
def set_failover_value(self, value: str, version: Optional[int] = None) -> bool:
return self._set_or_create(self.failover_path, value, version) is not False
查看这个节点设置的信息:
def manual_failover(self, leader: Optional[str], candidate: Optional[str],
scheduled_at: Optional[datetime.datetime] = None, version: Optional[Any] = None) -> bool:
"""Prepare dictionary with given values and set ``/failover`` key in DCS.
:param leader: value to set for ``leader``.
:param candidate: value to set for ``member``.
:param scheduled_at: value converted to ISO date format for ``scheduled_at``.
:param version: for conditional update of the key/object.
:returns: ``True`` if successfully committed to DCS.
"""
failover_value = {}
# 当前主节点的标识
if leader:
failover_value['leader'] = leader
# 故障转移过程中候选节点的标识
if candidate:
failover_value['member'] = candidate
# 可选参数,表示故障转移的预定时间
if scheduled_at:
failover_value['scheduled_at'] = scheduled_at.isoformat()
# version 一个可选参数,用于确保在当前版本匹配的情况下才进行更新
return self.set_failover_value(json.dumps(failover_value, separators=(',', ':')), version)
这个方法用于准备一个字典存储与故障转移相关的数据,并通过更新分布式配置存储(DCS)来设置 failover
节点,保存这些数据。
这是进行故障转移才使用的节点,与之前的猜测一致。
4.2.3 history
# 创建或设置值
def set_history_value(self, value: str) -> bool:
return self._set_or_create(self.history_path, value) is not False
查看这个节点设置的信息:
def update_cluster_history(self) -> None:
# 获取当前主节点的时间线(primary_timeline)
primary_timeline = self.state_handler.get_primary_timeline()
# 获取集群历史记录
cluster_history = self.cluster.history.lines if self.cluster.history else []
# 当时间线为 1 时,清空历史记录
if primary_timeline == 1:
if cluster_history:
self.dcs.set_history_value('[]')
# 检查历史记录的有效性
elif not cluster_history or cluster_history[-1][0] != primary_timeline - 1 or len(cluster_history[-1]) != 5:
# 构建历史记录字典
cluster_history_dict: Dict[int, List[Any]] = {line[0]: list(line) for line in cluster_history}
# 获取新的历史记录
history: List[List[Any]] = list(map(list, self.state_handler.get_history(primary_timeline)))
# 根据配置限制历史记录的条数
if self.cluster.config:
history = history[-global_config.max_timelines_history:]
# 将历史记录进行丰富
for line in history:
# enrich current history with promotion timestamps stored in DCS
cluster_history_line = cluster_history_dict.get(line[0], [])
if len(line) == 3 and len(cluster_history_line) >= 4 and cluster_history_line[1] == line[1]:
line.append(cluster_history_line[3])
if len(cluster_history_line) == 5:
line.append(cluster_history_line[4])
# 将更新后的历史记录提交到 DCS
if history:
self.dcs.set_history_value(json.dumps(history, separators=(',', ':')))
这个节点保存的信息即为集群的历史记录。
4.2.4 initialize
# 创建或设置值
def initialize(self, create_new: bool = True, sysid: str = "") -> bool:
sysid_bytes = sysid.encode('utf-8')
return self._create(self.initialize_path, sysid_bytes, retry=True) if create_new \
else self._client.retry(self._client.set, self.initialize_path, sysid_bytes)
# 内部删除
def _cancel_initialization(self) -> None:
node = self.get_node(self.initialize_path)
if node:
self._client.delete(self.initialize_path, version=node[1].version)
# 外部调用的删除方法
def cancel_initialization(self) -> bool:
try:
self._client.retry(self._cancel_initialization)
return True
except Exception:
logger.exception("Unable to delete initialize key")
return False
查看这个节点设置的信息,可以看见这个节点保存的信息为sysid
,查找这个sysid
,发现sysid=self.state_handler.sysid)
,所以只要查找到state_handler.sysid
即可。
@property
def sysid(self) -> str:
if not self._sysid and not self.bootstrapping:
data = self.controldata()
self._sysid = data.get('Database system identifier', '')
return self._sysid
作用是获取和缓存数据库系统的标识符(sysid
)。如果系统标识符(_sysid
)还未设置且当前系统没有处于引导过程(bootstrapping
),则通过某种方式获取系统标识符并将其缓存。
4.2.5 leader
def take_leader(self) -> bool:
return self.attempt_to_acquire_leader()
# 创建或设置值
def attempt_to_acquire_leader(self) -> bool:
try:
self._client.retry(self._client.create, self.leader_path, self._name.encode('utf-8'),
makepath=True, ephemeral=True)
return True
except (ConnectionClosedError, RetryFailedError) as e:
raise ZooKeeperError(e)
except Exception as e:
if not isinstance(e, NodeExistsError):
logger.error('Failed to create %s: %r', self.leader_path, e)
logger.info('Could not take out TTL lock')
return False
# 更新leader节点的值
def _update_leader(self, leader: Leader) -> bool:
if self._client.client_id and self._client.client_id[0] != leader.session:
logger.warning('Recreating the leader ZNode due to ownership mismatch')
try:
self._client.retry(self._client.delete, self.leader_path)
except NoNodeError:
pass
except (ConnectionClosedError, RetryFailedError) as e:
raise ZooKeeperError(e)
except Exception as e:
logger.error('Failed to remove %s: %r', self.leader_path, e)
return False
try:
self._client.retry(self._client.create, self.leader_path,
self._name.encode('utf-8'), makepath=True, ephemeral=True)
except (ConnectionClosedError, RetryFailedError) as e:
raise ZooKeeperError(e)
except Exception as e:
logger.error('Failed to create %s: %r', self.leader_path, e)
return False
return True
# 删除leader节点
def _delete_leader(self, leader: Leader) -> bool:
self._client.restart()
return True
查看这个节点设置的信息,发现是设置的这个Zookeeper
类中的属性_name
,这个_name
属性是提取config
这个字典的name
,这个传入的config
不止有主节点的也有从节点的,但是在ha.py
中使用了acquire_lock
函数来进行判断是不是主节点,是主节点才能执行attempt_to_acquire_leader
这个函数。
4.2.6 members
# 创建或设置值
def touch_member(self, data: Dict[str, Any]) -> bool:
# 获取成员信息
cluster = self.cluster
member = cluster and cluster.get_member(self._name, fallback_to_leader=False)
member_data = self.__last_member_data or member and member.data
if member and member_data:
# We want delete the member ZNode if our session doesn't match with session id on our member key
# 检查成员的会话 ID 是否匹配
if self._client.client_id is not None and member.session != self._client.client_id[0]:
logger.warning('Recreating the member ZNode due to ownership mismatch')
# 删除成功,则将 member 设置为 None,重新创建该成员的 ZNode
try:
self._client.delete_async(self.member_path).get(timeout=1)
except NoNodeError:
pass
except Exception:
return False
member = None
# 比较数据是否相同,若相同则无需更新
encoded_data = json.dumps(data, separators=(',', ':')).encode('utf-8')
if member and member_data:
if deep_compare(data, member_data):
return True
# 创建新的 ZNode(如果当前没有该节点)
else:
try:
self._client.create_async(self.member_path, encoded_data, makepath=True, ephemeral=True).get(timeout=1)
self.__last_member_data = data
return True
except Exception as e:
if not isinstance(e, NodeExistsError):
logger.exception('touch_member')
return False
# 更新已有 ZNode(如果成员已经存在)
try:
self._client.set_async(self.member_path, encoded_data).get(timeout=1)
self.__last_member_data = data
return True
except Exception:
logger.exception('touch_member')
return False
通过与分布式系统的节点交互来更新或创建一个“成员”节点(通常是分布式系统中的一个 ZNode)。
在这个里面创建的路径就是/pgsql/demo/0/members
加上name
,设置的值找到发现是传入的data
进行json格式化,原本的字典为:
data: Dict[str, Any] = {
'conn_url': self.state_handler.connection_string,
'api_url': self.patroni.api.connection_string,
'state': self.state_handler.state,
'role': self.state_handler.role,
'version': self.patroni.version
}
与zookeeper现存的信息保持一致。
4.2.7 status
# 创建或设置值
def _write_status(self, value: str) -> bool:
return self._set_or_create(self.status_path, value) is not False
查找设置的值,在patroni/dcs/__init__.py
文件的write_status
函数中。
status: Dict[str, Any] = {self._OPTIME: last_lsn, 'slots': slots or None,
'retain_slots': self._build_retain_slots(cluster, slots)}
和zookeeper中保存的信息一致。
4.2.8 optime/leader
# 创建或设置值
def _write_leader_optime(self, last_lsn: str) -> bool:
return self._set_or_create(self.leader_optime_path, last_lsn) is not False
设置的值为value[self._OPTIME]
,即为status
节点保存的信息中的last_lsn
。
4.2.9 failsafe
# 创建或设置值
def _write_failsafe(self, value: str) -> bool:
return self._set_or_create(self.failsafe_path, value) is not False
status、optime/leader和failsafe这三个节点都和patroni/dcs/__init__.py
中AbstractDCS
类中的update_leader
函数有关。
# 更新集群中的领导者信息、状态信息和故障保护信息。方法接受多个参数,包括有关当前集群状态的信息、日志序列号(LSN)、已确认的槽位数据以及故障保护字典等。
def update_leader(self,
cluster: Cluster,
last_lsn: Optional[int],
slots: Optional[Dict[str, int]] = None,
failsafe: Optional[Dict[str, str]] = None) -> bool:
"""Update ``leader`` key (or session) ttl, ``/status``, and ``/failsafe`` keys.
:param cluster: :class:`Cluster` object with information about the current cluster state.
:param last_lsn: absolute WAL LSN in bytes.
:param slots: dictionary with permanent slots ``confirmed_flush_lsn``.
:param failsafe: if defined dictionary passed to :meth:`~AbstractDCS.write_failsafe`.
:returns: ``True`` if ``leader`` key (or session) has been updated successfully.
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(cluster.leader, Leader)
ret = self._update_leader(cluster.leader)
if ret and last_lsn:
status: Dict[str, Any] = {self._OPTIME: last_lsn, 'slots': slots or None,
'retain_slots': self._build_retain_slots(cluster, slots)}
self.write_status(status)
if ret and failsafe is not None:
self.write_failsafe(failsafe)
return ret
# ha.py文件中生成故障保护的配置
def _failsafe_config(self) -> Optional[Dict[str, str]]:
if self.is_failsafe_mode():
ret = {m.name: m.api_url for m in self.cluster.members if m.api_url}
if self.state_handler.name not in ret:
ret[self.state_handler.name] = self.patroni.api.connection_string
return ret
4.2.10 sync
# 创建或设置值
def set_sync_state_value(self, value: str, version: Optional[int] = None) -> Union[int, bool]:
return self._set_or_create(self.sync_path, value, version, retry=True, do_not_create_empty=True)
# 删除节点
def delete_sync_state(self, version: Optional[int] = None) -> bool:
return self.set_sync_state_value("{}", version) is not False
if not self.cluster.sync.is_empty and self.dcs.delete_sync_state(version=self.cluster.sync.version):
这段代码与设置的sync节点有关,查当前集群是否没有同步复制状态。如果当前的同步状态不为空,意味着当前集群正在启用同步复制,如果同步复制状态存在,调用 self.dcs.delete_sync_state
删除与同步复制相关的状态。version
参数是为了确保删除的是与当前版本一致的同步状态。
def disable_synchronous_replication(self) -> None:
"""Cleans up ``/sync`` key in DCS and updates ``synchronous_standby_names``.
.. note::
We fall back to using the value configured by the user for ``synchronous_standby_names``, if any.
"""
# If synchronous_mode was turned off, we need to update synchronous_standby_names in Postgres
# 检查同步状态
if not self.cluster.sync.is_empty and self.dcs.delete_sync_state(version=self.cluster.sync.version):
# 如果同步复制被成功禁用
logger.info("Disabled synchronous replication")
# 清空同步备用库的名称
self.state_handler.sync_handler.set_synchronous_standby_names(CaseInsensitiveSet())
# As synchronous_mode is off, check if the user configured Postgres synchronous replication instead
# 如果同步复制已禁用,恢复用户配置的同步备用库名称
ssn = self.state_handler.config.synchronous_standby_names
self.state_handler.config.set_synchronous_standby_names(ssn)
4.2.11 其他
# 删除/pgsql/demo/0下所有节点
def delete_cluster(self) -> bool:
try:
return self._client.retry(self._client.delete, self.client_path(''), recursive=True)
except NoNodeError:
return True
这个函数在使用patronictl remove
命令时调用。
5. 问题
5.1 为什么删除leader节点信息的时候需要重启zk客户端?
在使用 Patroni 管理 PostgreSQL 集群时,zk
客户端(即 ZooKeeper 客户端)需要重启的原因与 Patroni 如何监控和管理 PostgreSQL 主节点(Leader 节点)相关。当 Patroni 检测到集群中的主节点(Leader)发生变化时,它会更新相关的信息(如领导者的身份、节点状态等)。为了确保客户端能够正确感知新的主节点并连接到它,通常需要重启 zk
客户端。下面是几个关键的原因:
- Leader 节点的变化通知
Patroni 使用 分布式协调服务(如 ZooKeeper 或 etcd)来管理 PostgreSQL 集群的状态。在集群中,Patroni 会在 ZooKeeper 中维护一个表示主节点(Leader)的状态信息。当主节点发生变化时,Patroni 会在 ZooKeeper 中更新这个信息。
- Leader 变更:当当前的主节点因为故障或人为操作(例如,手动故障转移)变更时,Patroni 会更新 ZooKeeper 中的 leader 信息。这通常意味着新的节点将成为主节点,旧的主节点将成为备节点或不可用状态。
- 通知机制:客户端(如应用程序或其他集群组件)通常通过 ZooKeeper 客户端来监控这个状态变化,以便实时了解哪个节点是新的主节点。然而,ZooKeeper 客户端可能会缓存一些旧的节点信息,导致它在新领导者产生后无法立即识别新的主节点。
- ZooKeeper 客户端的状态更新
ZooKeeper 是一个高效的分布式协调系统,它的客户端通过 watcher 机制来监听数据的变化。当 Leader
节点发生变化时,ZooKeeper 会通知所有监听该节点的客户端。然而,有时客户端需要清理或重新初始化状态,以便确保它能够正确地连接到新的主节点。
- 连接重置:
zk
客户端通常会缓存一些连接信息(如主节点的地址或身份),如果它在内部没有及时更新这些信息,可能会导致连接失败或连接到错误的节点。在某些情况下,重启zk
客户端可以确保它从 ZooKeeper 中获取到最新的节点信息。 - 配置更新:有时,ZooKeeper 客户端在运行时无法自动感知到更新的配置,特别是在较复杂的动态场景中(如 Patroni 中频繁的领导者选举)。重启客户端可以强制它重新加载配置并更新连接信息。
- 缓存和连接池问题
在某些实现中,ZooKeeper 客户端可能会缓存连接池中的信息或维护旧的连接状态。如果主节点发生变化且 zk
客户端未能及时更新这些状态,可能导致以下问题:
- 客户端尝试连接到已失效的旧主节点。
- 客户端与新的主节点之间的通信出现延迟或中断。
通过重启客户端,所有缓存信息都会被清除,客户端可以重新与 ZooKeeper 服务器建立连接并重新获取更新后的领导者信息。
- Patroni 配置与 Zookeeper 客户端的协同工作
Patroni 在领导者节点变化时,通常会通过 ZooKeeper 或其他协调系统向客户端广播更新信息。这种变化不仅仅涉及领导者的节点信息,还可能涉及集群的其他状态变化(例如,节点是否正在进行恢复、是否有新的同步备库等)。为了确保客户端能够感知到这些状态变化,特别是在高度动态的集群环境中,ZooKeeper 客户端有时需要重新启动,以便重新同步集群的当前状态。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统