Patroni在Zookeeper中保存的信息

Patroni在Zookeeper中保存的信息

1. 保存的信息

在使用 Patroni 配置高可用 PostgreSQL 集群时,etcd 或 Zookeeper(作为分布式协调系统)通常用于存储和管理与集群状态和配置相关的元数据。具体来说,Patroni 在这些系统中保存了以下几类信息:

1.1 集群状态信息

Patroni 需要通过 etcdZookeeper 来协调多个节点之间的状态,确保每个节点对集群状态的感知是同步的。主要包括以下内容:

  • 主节点(Primary)信息:
    • 主节点的身份信息,包括 PostgreSQL 的主节点 当前的主机名、IP 地址和端口等。
    • 记录哪个节点是当前的主节点,方便其它节点在发生故障时进行切换。
    • 记录主节点的 timeline_id(时间轴 ID),用于版本控制,确保从节点(standby)与主节点保持一致。
  • 从节点(Standby)信息:
    • 每个从节点(备份节点)的状态和位置。
    • 包括从节点的复制位置,是否正在同步主节点等信息。
  • 集群故障转移信息:
    • 记录集群是否处于故障转移过程,或者是否正在进行主节点选举。
    • Patroni 会在主节点失败时发起选举,etcdZookeeper 会保存选举的状态,并使得集群中的其它节点能够感知到状态变化。

1.2 PostgreSQL 配置信息

  • PostgreSQL 配置文件(postgresql.conf)的同步:
    • Patroni 通过 etcdZookeeper 可以保存数据库实例的配置文件内容或者配置参数。这样,集群中的所有节点能够确保一致的配置。
  • 自动化配置调整:
    • Patroni 会根据集群的需求,动态更新和调整 PostgreSQL 配置参数。例如,配置 synchronous_standby_names(同步从节点的名称)或者 primary_conninfo(主节点连接信息)等。

1.3 Leader 选举与锁定信息

  • 领导者选举(Leader Election):
    • 在 Patroni 中,etcdZookeeper 用于记录和同步选举信息,确保在任意时刻,只有一个节点能够担任主节点(Primary)。选举信息包括选举的投票者、候选者以及当前的选举状态。
    • 通过存储选举信息,Patroni 确保主节点故障时,其他节点可以通过选举快速选出新的主节点。
    • 在选举过程中,Patroni 会创建一个 leader key,该 key 记录当前的领导者。其它节点可以通过监视该 key 来感知主节点的变化。

1.4 事务日志(WAL)和同步位置

  • WAL(Write-Ahead Log)同步位置
    • Patroni 会记录每个节点的 WAL 同步位置。对于每个从节点,etcdZookeeper 会保存一个条目,记录该节点当前同步的 WAL 日志位置。这个信息帮助 Patroni 确定哪些从节点需要更新或者重新同步。
  • 同步延迟
    • 在某些配置中,Patroni 会在 etcdZookeeper 中记录各节点的复制延迟,以便做出更加精确的故障转移决策。

1.5 故障转移与恢复过程的控制信息

  • 故障转移信息:
    • 如果主节点失效,Patroni 会通过 etcdZookeeper 来协调其它节点进行主节点选举,并确保新的主节点能够正确地接管。
  • 恢复过程中的锁定信息:
    • 在故障转移过程中,Patroni 可能需要使用锁定机制(例如在 etcd 中创建锁定 key),确保在集群内的各个节点之间不会发生竞争条件。

1.6 Patroni 的内部元数据

  • Patroni 状态:
    • Patroni 会在 etcdZookeeper 中保存与自身的状态相关的信息。例如,当前 Patroni 实例是否已初始化,是否正在进行故障转移等。
  • 节点的角色信息:
    • 记录每个节点在集群中的角色(如主节点、从节点或备用节点),以及每个节点当前的状态(如是否同步、是否启动等)。

1.7 集群配置与备份信息

  • 备份信息:
    • 如果集群启用了自动备份,Patroni 可能会将备份状态信息保存在 etcdZookeeper 中,以便备份和恢复操作可以统一管理和协调。

1.8 总结

  • etcdZookeeperPatroni 集群中主要作为 分布式协调服务,用于保存集群的 状态信息、节点角色信息、选举信息 和 故障转移控制信息,确保集群中的所有节点能够保持一致,支持高可用性和故障恢复机制。

这些信息对于维护一个高可用的 PostgreSQL 集群是至关重要的,因为它们能够帮助 Patroni 在主节点失效时进行快速的故障转移、选举新的主节点,并保证集群的一致性。

2. 保存信息的类型

要查看 PatroniZookeeper 中保存的信息,通常需要使用 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节点下就是patronizookeeper中保存的数据。然后再次查看两个节点下的数据,

[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区分的节点结构,因此这个节点下就是patronizookeeper中保存的数据,而zookeeper节点下的config就是zookeeper集群保存的信息。下面来验证猜想。

3.1 查看zookeeper节点

查看zookeeper节点下的configquota两个节点,发现都没有下级节点了,使用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_urlrestapi调用地址api_url,目前启动信息state,当前成员的角色信息rolepatroni的版本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个节点信息,还有optimeoptime/leadersyncfailsafe这几种信息。

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__.pyAbstractDCS类中的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 客户端。下面是几个关键的原因:

  1. Leader 节点的变化通知

Patroni 使用 分布式协调服务(如 ZooKeeper 或 etcd)来管理 PostgreSQL 集群的状态。在集群中,Patroni 会在 ZooKeeper 中维护一个表示主节点(Leader)的状态信息。当主节点发生变化时,Patroni 会在 ZooKeeper 中更新这个信息。

  • Leader 变更:当当前的主节点因为故障或人为操作(例如,手动故障转移)变更时,Patroni 会更新 ZooKeeper 中的 leader 信息。这通常意味着新的节点将成为主节点,旧的主节点将成为备节点或不可用状态。
  • 通知机制:客户端(如应用程序或其他集群组件)通常通过 ZooKeeper 客户端来监控这个状态变化,以便实时了解哪个节点是新的主节点。然而,ZooKeeper 客户端可能会缓存一些旧的节点信息,导致它在新领导者产生后无法立即识别新的主节点。
  1. ZooKeeper 客户端的状态更新

ZooKeeper 是一个高效的分布式协调系统,它的客户端通过 watcher 机制来监听数据的变化。当 Leader 节点发生变化时,ZooKeeper 会通知所有监听该节点的客户端。然而,有时客户端需要清理或重新初始化状态,以便确保它能够正确地连接到新的主节点。

  • 连接重置:zk 客户端通常会缓存一些连接信息(如主节点的地址或身份),如果它在内部没有及时更新这些信息,可能会导致连接失败或连接到错误的节点。在某些情况下,重启 zk 客户端可以确保它从 ZooKeeper 中获取到最新的节点信息。
  • 配置更新:有时,ZooKeeper 客户端在运行时无法自动感知到更新的配置,特别是在较复杂的动态场景中(如 Patroni 中频繁的领导者选举)。重启客户端可以强制它重新加载配置并更新连接信息。
  1. 缓存和连接池问题

在某些实现中,ZooKeeper 客户端可能会缓存连接池中的信息或维护旧的连接状态。如果主节点发生变化且 zk 客户端未能及时更新这些状态,可能导致以下问题:

  • 客户端尝试连接到已失效的旧主节点。
  • 客户端与新的主节点之间的通信出现延迟或中断。

通过重启客户端,所有缓存信息都会被清除,客户端可以重新与 ZooKeeper 服务器建立连接并重新获取更新后的领导者信息。

  1. Patroni 配置与 Zookeeper 客户端的协同工作

Patroni 在领导者节点变化时,通常会通过 ZooKeeper 或其他协调系统向客户端广播更新信息。这种变化不仅仅涉及领导者的节点信息,还可能涉及集群的其他状态变化(例如,节点是否正在进行恢复、是否有新的同步备库等)。为了确保客户端能够感知到这些状态变化,特别是在高度动态的集群环境中,ZooKeeper 客户端有时需要重新启动,以便重新同步集群的当前状态。

posted @   零の守墓人  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
点击右上角即可分享
微信分享提示