Patroni 读取配置文件(包含Zookeeper中)
1. patroni读取配置文件
1.1 什么时候创建配置类
patroni读取配置文件则是对Config
类进行初始化的过程,在patroni/__main__.py
文件的main
函数中,调用了process_arguments
函数首先进行了配置文件的解析。
# main函数调用
args = process_arguments()
在process_arguments
函数会对进行一些参数处理(比如处理 ignore_listen_port
),然后加载配置文件并进行验证。如果验证通过,程序会退出。如果验证失败,则抛出异常并退出(进行一些参数处理需要配置相关的配置项)。
if args.generate_sample_config:
generate_config(args.configfile, True, None)
sys.exit(0)
elif args.generate_config:
generate_config(args.configfile, False, args.dsn)
sys.exit(0)
elif args.validate_config:
from patroni.config import Config, ConfigParseError
from patroni.validator import populate_validate_params, schema
populate_validate_params(ignore_listen_port=args.ignore_listen_port)
try:
Config(args.configfile, validator=schema)
sys.exit()
except ConfigParseError as e:
sys.exit(e.value)
结果1:配置了相关配置
- –generate-sample-config:调用
generate_config
来生成一个来生成一个示例配置文件。 - –generate-config:调用
generate_config
来根据指定的 PostgreSQL 实例(通过--dsn
参数传递)生成配置文件,并退出程序。 - –validate-config:程序将会验证配置文件的有效性。
- 导入
Config
和ConfigParseError
,以及populate_validate_params
和schema
。 - 调用
populate_validate_params
函数,传递ignore_listen_port
参数来决定是否忽略已占用的端口。 - 尝试加载并验证配置文件。如果验证成功,程序退出。如果验证失败,捕获异常并显示错误信息后退出。
- 导入
结果2:没有配置相关配置
直接返回args
。然后再patroni/daemon.py
文件的abstract_main
函数中解析配置文件,创建配置类和运行主程序。
1.2 Config类怎么根据配置文件创建的?
上面所示,都是创建Config
的类,传入的参数就是配置文件的位置,因此首先调用的则是Config
的构造函数__init__
,
def __init__(self, configfile: str,
validator: Optional[Callable[[Dict[str, Any]], List[str]]] = default_validator) -> None:
"""创建一个新的 Config 类实例,并使用 validator 验证加载的配置。
.. 注意:
Patroni 将按照以下顺序读取配置:
如果存在并且可以解析通过命令行参数传递的文件或目录路径(configfile),否则
如果通过环境变量传递的 YAML 文件存在并且可以解析(参见 :attr:PATRONI_CONFIG_VARIABLE),否则
从作为环境变量定义的配置值中读取(参见 :meth:~Config._build_environment_configuration)。
:configfile:Patroni 配置文件的路径。
:validator:用于验证 Patroni 配置的函数。它应该接收一个字典,代表 Patroni 的配置,并根据验证返回一个零个或多个错误消息的列表
抛出异常:
ConfigParseError:如果 validator 报告任何问题
"""
# 初始化版本修改标志为 -1,动态配置为空字典
self._modify_version = -1
self._dynamic_configuration = {}
# 从环境变量构建配置信息
self.__environment_configuration = self._build_environment_configuration()
# 如果 configfile 存在并且是一个有效的路径,则将其赋值给 _config_file
self._config_file = configfile if configfile and os.path.exists(configfile) else None
if self._config_file:
# 从文件加载配置信息
self._local_configuration = self._load_config_file()
else:
# 从环境变量加载配置信息
config_env = os.environ.pop(self.PATRONI_CONFIG_VARIABLE, None)
self._local_configuration = config_env and yaml.safe_load(config_env) or self.__environment_configuration
# 如果提供了 validator 函数
if validator:
# 使用该函数验证 _local_configuration
errors = validator(self._local_configuration)
if errors:
raise ConfigParseError("\n".join(errors))
# 构建有效的配置,并从中提取 PostgreSQL 数据目录的路径
self.__effective_configuration = self._build_effective_configuration({}, self._local_configuration)
self._data_dir = self.__effective_configuration.get(`postgresql`, {}).get(`data_dir`, "")
# 计算缓存文件的路径
self._cache_file = os.path.join(self._data_dir, self.__CACHE_FILENAME)
if validator: # patronictl使用validator=None
# 加载缓存并验证故障转移标签
self._load_cache() # 我们不想为ctl从本地缓存加载任何东西
self._validate_failover_tags() # ctl
# 始化缓存是否需要保存的状态为 False
self._cache_needs_saving = False
可以发现这个函数中首先会从环境变量构建配置信息,然后对 configfile
进行判断,这里的configfile
是一个字符串,因此要判断其路径是否存在并且为一个有效的路径(会把配置文件的地址复制给Config
的_config_file
属性)。然后对_config_file
属性进行判断,为None就从环境变量中加载配置信息,不为None就从配置文件中加载信息(加载的配置信息都会赋值给_local_configuration
属性)。
加载完配置之后,会对_local_configuration
属性进行validator
验证(配置了才会验证,这个校验配置文件的对象和函数定义在patroni/validator.py
文件中),然后再次根据_local_configuration
属性来构建一个有效的配置赋值给定义有效配置的__effective_configuration
属性,并从这个属性中提取出postgresql数据库的数据目录的路径,然后根据数据库目录的路径构造缓存文件的路径,然后再次进行validator
验证(如果配置,会加载缓存并验证故障转移标签),然后初始化缓存保存状态标志_cache_needs_saving
属性。
综上所述,可以观测到重点是四个地方:
- 如何从环境构建配置信息?
- 如何从从配置文件或者环境变量中加载配置信息?
- 怎么构建有效的配置以便于提取pg的数据目录?
- 是否要加载缓存?
下面对此四个问题进行意义解析。
1.2.1 怎么从环境构建配置信息?
代码详情请见_build_environment_configuration
函数。
-
从环境变量中获取
name
,namespace
,scope
三个配置的值。 -
从环境变量中获取日志相关的配置信息。如
level
、format
、dateformat
。注意:因为patroni以前采用了不同的名称,这里会对名称做一定的处理,会使用旧的环境变量配置获取到值,并且如果此时新的环境变量配置不存在环境变量之中,就会设置新的环境变量配置为就环境变量配置的值,这样方便之后进行统一的配置。 -
获取section相关的环境变量的值。例如:
restapi
、ctl
、postgresql
、log
、raft
等的配置。 -
动态获取PostgreSQL二进制文件的路径,并且存储在
ret[postgresql][bin_name]
的字典中。‘pg_ctl’, ‘initdb’, ‘pg_controldata’, ‘pg_basebackup’, ‘postgres’, ‘pg_isready’, ‘pg_rewind’
-
将获取某些环境变量的值转换为python中的对象。
- 转换为bool对象:
restapi.allowlist_include_members
和ctl.insecure
。(在构建是会直接删除这个键,然后解析出来值之后再添加这个键。) - 转换为int对象:
'restapi', ('request_queue_size',)
和'log', ('max_queue_size', 'file_size', 'file_num', 'mode')
。 - 转换为list对象:
raft.partner_addrs
和restapi.allowlist
。如果log.format
不包含 % 格式化字符,也转换为列表。 - 转换为dict对象:
'restapi', ('http_extra_headers', 'https_extra_headers')
和'log', ('static_fields', 'loggers'
。
- 转换为bool对象:
-
获取
ctl
和restapi
节点的认证信息和获取replication
、superuser
和rewind
用户类型的认证信息。 -
遍历环境变量,根据前缀过滤出与 Patroni 相关的环境变量(以
PATRONI_
开头的),并根据变量后缀的不同类型对值进行适当的转换(如整型、布尔值等),然后存储到 ret 字典中。 -
获取
etcd
和etcd3
节点的认证信息,并更新到 ret 字典中。
1.2.2 如何从从配置文件或者环境变量中加载配置信息?
1.2.2.1 从配置文件加载
下面是加载配置文件的代码,从这里可以知道从配置文件加载的配置信息会和环境中加载的配置信息合并,并且以环境中加载的配置信息优先。
def _load_config_file(self) -> Dict[str, Any]:
"""加载来自文件系统的配置文件,并应用那些通过环境变量设置的值。
:returns: 在合并配置文件和环境变量之后的最终配置。
"""
# 检查是否正在进行类型检查
if TYPE_CHECKING: # pragma: no cover
assert self.config_file is not None
# 加载指定的配置文件
config = self._load_config_path(self.config_file)
# 更新 config
patch_config(config, self.__environment_configuration)
return config
下面是加载知道你个的配置文件的代码:
def _load_config_path(self, path: str) -> Dict[str, Any]:
"""从 *path* 加载 Patroni 配置文件。
如果 *path* 是一个文件,加载由 *path* 指定的 YAML 文件。
如果 *path* 是一个目录,按字母顺序加载该目录中的所有 YAML 文件。
:param path: 路径指向一个 YAML 配置文件,或者包含 YAML 配置文件的文件夹。
:returns: 从 *path* 读取配置文件后的配置。
:raises:
:class:`ConfigParseError`: 如果 *path* 无效。
"""
# 如果 path 指向的是一个文件,那么将其存储在一个列表 files 中
if os.path.isfile(path):
files = [path]
# 如果 path 是一个目录,那么列出该目录下所有的 YAML 文件,并按字母顺序排序,然后将这些文件路径存储在 files 列表中
elif os.path.isdir(path):
files = [os.path.join(path, f) for f in sorted(os.listdir(path))
if (f.endswith(`.yml`) or f.endswith(`.yaml`)) and os.path.isfile(os.path.join(path, f))]
else:
# 既不是一个文件也不是一个目录
logger.error(`config path %s is neither directory nor file`, path)
raise ConfigParseError(`invalid config path`)
# 用于存储合并后的配置信息
overall_config: Dict[str, Any] = {}
for fname in files:
with open(fname) as f:
# 打开文件并使用 yaml.safe_load 解析其内容
config = yaml.safe_load(f)
# 将解析后的配置信息合并到 overall_config 中
patch_config(overall_config, config)
return overall_config
从这段代码可以看出,如果是一个文件则解析这个文件返回,如果是一个目录就会把这个目录下的所有yaml
文件解析,并且因为排序为按字母排序,合并是是后面的配置信息覆盖前面的配置信息。
1.2.2.2 从环境变量中加载
config_env = os.environ.pop(self.PATRONI_CONFIG_VARIABLE, None)
self._local_configuration = config_env and yaml.safe_load(config_env) or self.__environment_configuration
这段代码就是从环境变量中加载,这里直接找名称为PATRONI_CONFIGURATION
的环境变量而不是进行一次环境变量的值的解析,如果存在就使用yaml解析加载为一个配置并且因为这个配置也是从环境变量中加载的也是最新的变量,因此不需要再和环境变量中加载的配置进行合并,如果不存在则直接使用之前从环境构建的配置信息即可。
1.2.3 怎么构建有效的配置以便于提取pg的数据目录?
根据_local_configuration
属性构建__effective_configuration
的代码如下所示:
def _build_effective_configuration(self, dynamic_configuration: Dict[str, Any],
local_configuration: Dict[str, Union[Dict[str, Any], Any]]) -> Dict[str, Any]:
"""通过合并 *dynamic_configuration* 和 *local_configuration* 构建有效的配置。
.. note::
如果在两者中都定义了相同的设置,则 *local_configuration* 的优先级高于 *dynamic_configuration*。
:param dynamic_configuration: Patroni 动态配置。
:param local_configuration: Patroni 本地配置。
:returns: 合并后的有效配置。
"""
# 创建安全副本
config = self._safe_copy_dynamic_configuration(dynamic_configuration)
# 遍历 local_configuration 中的所有项
for name, value in local_configuration.items():
if name == `citus`: # 移除无效的 citus 配置
if isinstance(value, dict) and isinstance(value.get(`group`), int)\
and isinstance(value.get(`database`), str):
config[name] = value
# 进一步处理 postgresql 下的子项
elif name == `postgresql`:
for name, value in (value or {}).items():
if name == `parameters`:
config[`postgresql`][name].update(self._process_postgresql_parameters(value, True))
elif name != `use_slots`: # replication slots 必须全局启用或禁用
config[`postgresql`][name] = deepcopy(value)
# 如果项名不在 config 中,或者项名为 watchdog,则将该项的值复制到 config 中
elif name not in config or name in [`watchdog`]:
config[name] = deepcopy(value) if value else {}
# restapi 服务器期望接收如下的格式 restapi.auth = 'username:password' 同样适用于 `ctl`
for section in (`ctl`, `restapi`):
if section in config and `authentication` in config[section]:
config[section][`auth`] = `{username}:{password}`.format(**config[section][`authentication`])
# 对旧版配置的特殊处理
# `exhibitor` inside `zookeeper`:
# 如果配置中有 zookeeper 并且 zookeeper 下有 exhibitor,则将 exhibitor 提升至顶层,并移除 zookeeper
if `zookeeper` in config and `exhibitor` in config[`zookeeper`]:
config[`exhibitor`] = config[`zookeeper`].pop(`exhibitor`)
config.pop(`zookeeper`)
# 如果 postgresql 下没有 authentication,则创建一个 authentication 字段,并从中提取 replication 和 superuser 的配置
pg_config = config[`postgresql`]
# postgresql 中没有 'authentication',但是有 'replication' 和 'superuser'
if `authentication` not in pg_config:
pg_config[`use_pg_rewind`] = `pg_rewind` in pg_config
pg_config[`authentication`] = {u: pg_config[u] for u in (`replication`, `superuser`) if u in pg_config}
# postgresql.authentication 中没有 'superuser',但是有 'pg_rewind'
if `superuser` not in pg_config[`authentication`] and `pg_rewind` in pg_config:
pg_config[`authentication`][`superuser`] = pg_config[`pg_rewind`]
# handle设置可能可用的附加连接参数
# 在配置文件中,如SSL连接参数
for name, value in pg_config[`authentication`].items():
pg_config[`authentication`][name] = {n: v for n, v in value.items() if n in _AUTH_ALLOWED_PARAMETERS}
# no `name` in config
if `name` not in config and `name` in pg_config:
config[`name`] = pg_config[`name`]
# 当引导新的 Citus 集群(协调者/工作者)时,在全局配置中启用同步复制
# 如果配置中有 citus,则在 bootstrap.dcs 中设置 synchronous_mode 为 quorum
if `citus` in config:
bootstrap = config.setdefault(`bootstrap`, {})
dcs = bootstrap.setdefault(`dcs`, {})
dcs.setdefault(`synchronous_mode`, `quorum`)
updated_fields = (
`name`,
`scope`,
`retry_timeout`,
`citus`
)
# 更新 postgresql 配置,将 updated_fields 中的字段从顶层配置移到 postgresql 中
pg_config.update({p: config[p] for p in updated_fields if p in config})
return config
通过合并dynamic_configuration
(动态配置)和local_configuration
(本地配置),Config
类初始化是动态配置为空。
-
创建动态配置的安全副本
config
,以免错误修改了动态配置的值。 -
遍历本地配置的所有配置项:
- 移除其中无效的
citus
配置。 - 处理
postgresql
下的子项。 - 如果存在
parameters
调用_process_postgresql_parameters
来处理并更新config
配置。 - 如果是其他的配置项(除了
use_slots
)会深拷贝配置值并更新config
配置。
- 移除其中无效的
-
如果配置名不存在
config
中或者名为watchdog
,就将该值复制到config
中。 -
格式化
restapi
和ctl
的auth
配置项的为‘username:password’
的格式。 -
如果配置中有
zookeeper
并且zookeeper
下有exhibitor
,则将exhibitor
提升至顶层,并移除 zookeeper。 -
如果
postgresql
下没有authentication
,则创建一个authentication
字段,并从中提取 replication 和 superuser 的配置。(如果superuser
没有在提取出的authentication
中,就将pg_rewind
配置项作为superuser
)。 -
过滤和更新
pg_config['authentication']
中每个项的子项,只保留在_AUTH_ALLOWED_PARAMETERS
中列出的参数。 -
更新
config
中没有但是更新后pg_config['authentication']
有的配置项。 -
如果配置中有
citus
,则在bootstrap.dcs
中设置synchronous_mode
为quorum
。 -
更新 postgresql 配置,将
updated_fields
中的字段从顶层配置移到postgresql
中。
从上述可以看出构建有效的配置便是讲本地配置和patroni的动态配置进行合并,将本地配置的值更新到动态配置中,并且动态配置中也会更新authentication
配置项的值。
1.2.4 是否要加载缓存?
如果配置了validator
则会加载缓存并验证故障转移标签,使用ctl
是validator
为None
,因为patronictl
需要确保操作的准确性和实时性。如果加载了本地缓存的配置,可能会使用到过时的配置数据,从而影响操作的正确性。缓存文件的位置为pg
的data
目录下patroni.dynamic.json
这个文件。
下面是加载缓存配置的_load_cache
函数:
def _load_cache(self) -> None:
"""从 ``patroni.dynamic.json`` 文件加载动态配置。"""
# 检查 self._cache_file 是否是一个存在的文件
if os.path.isfile(self._cache_file):
try:
# 如果文件存在,尝试打开文件,并使用 json.load() 方法将文件内容解析为 JSON 对象,然后调用 self.set_dynamic_configuration 方法设置动态配置
with open(self._cache_file) as f:
self.set_dynamic_configuration(json.load(f))
except Exception:
logger.exception(`Exception when loading file: %s`, self._cache_file)
判断设置的_cache_file
属性(存放缓存文件的位置)是否是一个文件,如果是尝试用解析为JSON
对象,然后调用set_dynamic_configuration
函数设置为动态配置。
# 配置可以是ClusterConfig或dict
def set_dynamic_configuration(self, configuration: Union[ClusterConfig, Dict[str, Any]]) -> bool:
"""使用给定的 *configuration* 设置动态配置值。
:param configuration: 新的动态配置值。支持 :class:`dict` 以兼容旧版本。
:returns: 如果检测到当前动态配置和新的动态 *configuration* 之间存在变化,则返回 ``True``,否则返回 ``False``。
"""
# 检查传入的 configuration 是否是 ClusterConfig 类型
if isinstance(configuration, ClusterConfig):
# 如果当前的 _modify_version 与 configuration 的 modify_version 相等,则说明配置未更改
if self._modify_version == configuration.modify_version:
return False # 如果版本未改变,则无需做任何事情
# 如果版本号发生了变化,则更新 _modify_version,并将 configuration 替换为其数据部分 configuration.data
self._modify_version = configuration.modify_version
configuration = configuration.data
# 使用 deep_compare 函数比较当前的 _dynamic_configuration 和新的 configuration 是否相同
if not deep_compare(self._dynamic_configuration, configuration):
try:
self._validate_and_adjust_timeouts(configuration)
self.__effective_configuration = self._build_effective_configuration(configuration,
self._local_configuration)
self._dynamic_configuration = configuration
self._cache_needs_saving = True
return True
except Exception:
logger.exception(`Exception when setting dynamic_configuration`)
return False
这个函数是将传入的配置字典或者ClusterConfig
类设置为动态配置。
- 如果传入的是ClusterConfig:
- 比较当前的
_modify_version
和configuration
的modify_version
是否相等,相等说明配置未改变。 - 不相等就是发生了改变,更新
_modify_version
的值并将configuration
替换为configuration.data
。
- 比较当前的
- 比较当前的动态配置
_dynamic_configuration
和新的配置项configuration
是否相同。- 不相同时首先调用
_validate_and_adjust_timeouts
函数验证并调整loop_wait
、retry_timeout
和ttl
的值。 - 然后调用
_build_effective_configuration
构建有效的配置项(传入的参数为configuration
配置和本地配置_local_configuration
并且以本地配置优先)。 - 然后将动态配置的值修改为
configuration
配置并设置保存标识。
- 不相同时首先调用
2. patroni读取Zookeeper中的配置
2.1 获取Zookeeper的DCS配置类
patroni获取DCS中的信息,需要先获取当前使用的DCS的类型。在patroni/__main__.py
文件的main
函数中,调用了get_dcs
函数获取到DCS的类型。
def get_dcs(config: Union[`Config`, Dict[str, Any]]) -> `AbstractDCS`:
"""尝试从已知可用的实现中加载一个分布式配置存储(Distributed Configuration Store,简称 DCS)
注意事项:使用由 iter_dcs_classes 函数返回的可用 DCS 类列表,动态实例化实现了 DCS 的类,这些类继承自抽象类 AbstractDCS。
从 config 中检索的基本顶级配置参数会在传递给模块的 DCS 类之前传播到特定于 DCS 的配置部分。
如果没有找到满足配置的模块,则报告并记录一个错误。这将导致 Patroni 退出。
异常说明:如果尝试加载所有可用的 DCS 模块均未成功,则引发 PatroniFatalException 异常。
参数说明:config 是一个包含 Patroni 配置的对象或字典。这通常表示 Patroni 的主要配置
返回值说明:返回第一个成功加载的实现了 AbstractDCS 的 DCS 模块。
"""
# 遍历 iter_dcs_classes 函数返回的 DCS 类列表。
for name, dcs_class in iter_dcs_classes(config):
# 将一些参数从定义的配置顶层传播到特定于DCS的配置部分。
config[name].update({
p: config[p] for p in (`namespace`, `name`, `scope`, `loop_wait`,
`patronictl`, `ttl`, `retry_timeout`)
if p in config})
from patroni.postgresql.mpp import get_mpp
# 返回 dcs_class 的实例,传入更新后的配置和通过 get_mpp 函数获取的 MPP(大规模并行处理)配置。
return dcs_class(config[name], get_mpp(config))
# 如果没有找到合适的 DCS 实现,则构造一个包含所有可用 DCS 实现名称的字符串。
available_implementations = `, `.join(sorted([n for n, _ in iter_dcs_classes()]))
# 抛出 PatroniFatalException 异常,指明无法找到合适的 DCS 配置,并列出所有可用的实现
raise PatroniFatalException("Can not find suitable configuration of distributed configuration store\n"
f"Available implementations: {available_implementations}")
这个get_dcs
函数中调用了iter_dcs_classes
函数来根据配置自动获取可用的DCS模块,并且每个成功导入的模块都会返回其名称和对应的类对象。以便后面进行使用。
def iter_dcs_classes(
config: Optional[Union[`Config`, Dict[str, Any]]] = None
) -> Iterator[Tuple[str, Type[`AbstractDCS`]]]:
"""尝试导入存在于给定配置中的 DCS 模块。
注意事项:如果一个模块成功导入,我们可以假定其所有的依赖都已经安装。
参数说明:config 是一个配置信息,其中可能包含 DCS 名称作为键。如果提供了 config,则仅尝试导入配置中定义的 DCS 模块。否则,如果为 None,则尝试导入任何支持的 DCS 模块。
返回值说明:返回一个迭代器,每个迭代项是一个包含模块名称和导入的 DCS 类对象的元组。
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(__package__, str)
return iter_classes(__package__, AbstractDCS, config)
iter_dcs_classes
函数中则是调用patroni/dynamic_loader.py
文件的iter_classes
函数,dynamic_loader.py
这个模块就是负责导入需要的模块和对应的类对象。
iter_classes()
函数:根据提供的包名和类类型,尝试导入并返回所有实现了指定类类型的模块。iter_modules()
函数:根据提供的包名,动态地获取该包下所有模块的名称。find_class_in_module()
函数:在给定的模块中查找一个特定类型的类,并且该类的名称与模块的名称相匹配。
下面是执行初始化DCS的函数流程图:
可以总结出get_dcs
函数的流程:
get_dcs
函数首先根据传入的config
配置,然后调用iter_dcs_classes
函数去获取配置中对应的DCS配置类,如config
中保存的是zookeeper
的配置,那么传入的配置中包含了zookeeper
这个名称,然后在iter_dcs_classes
函数调用iter_classes()
函数是传入AbstractDCS
抽象类(所有DCS配置类的父类)、config
配置和DCS配置的包名(因为对应的DCS配置类都在这个包中),然后iter_classes()
函数先调用iter_modules()
获取所有模块的名称,然后遍历这些模块,获取这个模块的模块名称,如何zookeeper
、etcd
这样的名称,看是否在config
配置中,如果在说明就是需要这个模块下的DCS配置类,然后找到这个模块中是AbstractDCS
抽象类子类的类对象,这个类对象就是符合config
配置的DCS类对象。
2.2 获取Zookeeper中的配置来创建集群
在patroni/__main__.py
文件的main
函数中,调用了ensure_dcs_access
函数来持续尝试从 DCS 中检索集群,并延迟一定时间。在这个函数中调用了dcs
的get_cluster
函数(这里的dcs
根据上文可以知道,已经是zookeeper
这个dcs
配置类),在这个函数中根据是否是mpp
的主节点来选择mpp
加载pg
集群(_get_mpp_cluster
函数)还是的dcs
加载pg
集群(__get_postgresql_cluster
函数)。
-
mpp
加载pg
集群(_get_mpp_cluster
函数):在这个函数中调用
zookeeper
配置类的_load_cluster
函数,传入客户端的路径客户端的路径(namespace/scope/)和
zookeeper配置类的
_mpp_cluster_loader`加载器。def _mpp_cluster_loader(self, path: str) -> Dict[int, Cluster]: """从单个 MPP 集群中加载并构建所有 PostgreSQL 集群。 :param path: 在 DCS 中加载 Cluster(s) 的路径。 :returns: 所有 MPP 组作为 :class:`dict`,以组 ID 为键,:class:`Cluster` 对象为值。 """ # 初始化字典 ret: Dict[int, Cluster] = {} # 遍历子节点 for node in self.get_children(path): # 正则匹配 if self._mpp.group_re.match(node): # 加载 PostgreSQL 集群并添加到字典 ret[int(node)] = self._postgresql_cluster_loader(path + node + '/') return ret
- 初始化了一个空字典
ret
,其类型注解为Dict[int, Cluster]
。这个字典将用于存储最终的 MPP 集群数据,其中键是集群 ID(整数),值是Cluster
对象。 - 调用
get_children
函数来获取客户端路径namespace/scope/
下的所有节点,遍历这些子节点。 - 对每个子节点
node
,它会检查node
是否符合某个正则表达式模式。 - 如果能通过正则匹配,则调用
_postgresql_cluster_loader
加载集群,但是传入的路径为namespace/scope/group/
了,这样就做到了分group
来加载集群,因为集群中只能出现一个主节点。
- 初始化了一个空字典
-
dcs
加载pg
集群(__get_postgresql_cluster
函数):在这个函数中首先拿到客户端的路径(
namespace/scope[/group]
,因为如果开启了mpp
的话会走mpp
加载集群,因此这个里面也是没有[/group]
的,因此路径也是namespace/scope/
),然后调用zookeeper
配置类的_load_cluster
函数,传入客户端路径和从zookeeper
配置类中获取的pg
集群加载器来加载pg
集群。这个_load_cluster
函数就是让zookeeper
中_postgresql_cluster_loader
这个集群加载器来重试加载pg
集群。_postgresql_cluster_loader
函数如下所示:def _postgresql_cluster_loader(self, path: str) -> Cluster: """从 DCS 加载并构建表示单个 PostgreSQL 集群的 :class:`Cluster` 对象。 :param path: 在 DCS 中加载 :class:`Cluster` 的路径。 :returns: :class:`Cluster` 实例。 """ # 获取子节点 nodes = set(self.get_children(path)) # 获取初始化标志 initialize = (self.get_node(path + self._INITIALIZE) or [None])[0] if self._INITIALIZE in nodes else None # 获取全局配置 config = self.get_node(path + self._CONFIG, watch=self._watcher) if self._CONFIG in nodes else None config = config and ClusterConfig.from_node(config[1].version, config[0], config[1].mzxid) # 获取时间线历史 history = self.get_node(path + self._HISTORY) if self._HISTORY in nodes else None history = history and TimelineHistory.from_node(history[1].mzxid, history[0]) # 获取同步状态 sync = self.get_node(path + self._SYNC) if self._SYNC in nodes else None sync = SyncState.from_node(sync and sync[1].version, sync and sync[0]) # 加载成员列表 members = self.load_members(path) if self._MEMBERS[:-1] in nodes else [] # 获取主节点 leader = self.get_node(path + self._LEADER, watch=self._watcher) if self._LEADER in nodes else None if leader: member = Member(-1, leader[0], None, {}) member = ([m for m in members if m.name == leader[0]] or [member])[0] leader = Leader(leader[1].version, leader[1].ephemeralOwner, member) # 获取主节点状态 status = self.get_status(path, leader) # 获取故障转移信息 failover = self.get_node(path + self._FAILOVER) if self._FAILOVER in nodes else None failover = failover and Failover.from_node(failover[1].version, failover[0]) # 获取失败保护拓扑 failsafe = self.get_node(path + self._FAILSAFE) if self._FAILSAFE in nodes else None try: failsafe = json.loads(failsafe[0]) if failsafe else None except Exception: failsafe = None # 返回 Cluster 对象 return Cluster(initialize, config, leader, status, members, failover, sync, history, failsafe)
-
在这个函数中首先调用
get_children
函数来获取zk中保存的客户端路径namespace/scope[/group]
下的所有子节点,保存的信息详情请参考241108-Patroni在Zookeeper中保存的信息.md
。 -
检查路径下的
initialize
节点是否存在,如果存在就调用get_node
获取该值,如果不存在就记录为None。 -
检查路径下的
config
节点是否存在,如果存在就调用get_node
获取该值,并传入watch
参数以便观察配置的变化。然后使用ClusterConfig.from_node
方法将节点值转换为ClusterConfig
对象,version
和mzxid
是该节点的版本信息和事务 ID(用于保证数据一致性)。如果不存在就记录为None。 -
检查路径下的
history
节点是否存在,如果存在就调用get_node
获取该值,并使用TimelineHistory.from_node
方法将该节点转换为TimelineHistory
对象。这里mzxid
是事务 ID,可能用于同步历史状态。如果不存在就记录为None。 -
检查路径下的
sync
节点是否存在,如果存在就调用get_node
获取该值,并从该节点加载同步状态,转换为SyncState
对象。如果不存在就记录为None。 -
检查路径下的
sync
节点是否存在,如果存在调用load_members
来加载所有的集群成员,在这个函数中拿到members
节点下的所有子节点,然后遍历这些子节点,获取每个子节点的信息,返回这个子节点信息的集合。如果不存在就记录为空列表。 -
检查路径下的
leader
节点是否存在,如果存在就调用get_node
获取该值,然后,通过检查成员列表members
,找到对应的成员(主节点成员)(如果找不到,就用默认的member
)。最后,使用Leader
类构建主节点对象。如果不存在就记录为None。对于
get_node
返回的结果ret
解析:ret
是get
方法的返回值,通常是一个包含两个元素的元组。ret[0]
:表示从 DCS 中获取的值,通常是一个字节串(bytes
)。ret[1]
:表示与该值相关的元数据,通常是一个Stat
对象,包含版本号、修改时间等信息。
-
调用
get_status
函数获取主节点的日志序列号(LSN)和插槽信息,可能用于数据库恢复或故障转移。 -
检查路径下的
failover
节点是否存在,如果存在,加载故障转移信息并转换为Failover
对象。如果不存在就记录为None。 -
检查路径下的
failsafe
节点是否存在,如果存在,尝试解析该节点的 JSON 数据,表示集群的失败保护策略。如果解析失败,则将failsafe
设为None
。如果不存在就记录为None
。 -
返回这些节点信息构建的
Cluster
类。
-
3. 主备切换和故障转移读取Zk配置
这个高可用的主备切换和故障转移存在patroni/ha.py
和patroni/ctl.py
中,ha
中的则是patroni
自行的监控和开启循环实现的,ctl
中则是通过patronictl
命令来触发的。这里先介绍ha
中如何进行的。外部通过Ha
类中的run_cycle
函数来调用循环,Ha
类内部则通过_run_cycle
内部方法来具体实现。这个代码过长,因此在这里只做一些功能的解析。
- 初始化一个布尔变量
dcs_failed
,用于标记 DCS(分布式协调服务)是否失败。 - 开始一个
try:①
块:- 开始一个
try:②
块:- 调用
load_cluster_from_dcs
函数从 DCS 中加载集群信息。 - 将加载的集群信息更新到全局配置中
- 调用
reset_cluster_info_state
函数重置集群的状态信息。
- 调用
- 结束
try:②
:如果加载集群信息失败,调用reset_cluster_info_state
函数重置状态信息为None
。 - 检查是否处于故障安全模式(
is_failsafe_mode
函数)。- 如果 DCS 不可访问且处于故障安全模式,尝试获取最新的 WAL LSN(日志序列号)。
- 检查是否处于暂停状态(
is_paused
函数)。- 如果处于暂停状态,禁用看门狗并且标记已暂停状态。
- 如果未处于暂停状态,检查是否之前已暂停,如果之前已暂停,安排恢复后的健康检查(调用
schedule_sanity_checks_after_pause
函数),在暂停期间,用户可能手动操作 PostgreSQL,因此需要重新检查回滚条件,并可能在主节点上运行 CHECKPOINT(调用reset_state
函数)。清除已暂停标记。
- 检查当前节点是否在集群成员中(
has_member
函数),如果不在集群成员中,调用touch_member
函数触碰成员以加入集群(touch_member
函数)。 - 检查当前节点是否有锁且集群的初始化信息无效。如果初始化信息无效,设置新的配置值(
set_config_value
函数),并从dcs
中获取最新的集群信息(get_cluster
函数)。 - 检查异步执行器是否正在处理任务。如果异步执行器忙碌,处理长时间操作并返回(
handle_long_action_in_progress
函数)。 - 处理实例启动(获取
handle_starting_instance
函数),并将结果存储在msg
中。 - 检查
msg
是否不为空,如果不为空就返回消息msg
。 - 检查是否在引导过程中。如果是引导过程,处理引导后的操作并返回(调用
post_bootstrap
函数)。 - 检查是否在恢复过程中。
- 清除恢复标记。
- 检查是否需要回滚。如果不需要回滚,检查是否从 PostgreSQL 崩溃中恢复失败(调用
post_recover
函数),并将结果存储在msg
中。 - 检查
msg
是否不为空。如果不为空就返回消息msg
。 - 重置崩溃恢复状态。
- 检查回滚是否成功执行且未失败,如果是就重置回滚状态(调用
reset_state
函数)。 - 检查是否为 Raft 集群且未锁定,如果是 Raft 集群且未锁定,返回作为次级启动的信息。
- 初始化
data_directory_error
和data_directory_is_empty
变量。 - 开始一个
try:③
块:- 检查数据目录是否为空且可访问。
- 结束
try:③
块,如果发生异常,标记数据目录不可访问。 - 检查数据目录是否不可访问或为空。
- 如果数据目录不可访问或为空,将当前节点的角色设置为
uninitialized
。 - 立即停止 PostgreSQL 实例,并设置超时时间为
retry_timeout
。 - 如果数据目录消失且当前节点是主节点,禁用看门狗。
- 检查当前节点是否持有锁(即是否为主节点)。如果当前节点是主节点,自愿释放锁(调用
release_leader_key_voluntarily
函数)。返回一条信息,说明释放锁的原因是因为数据目录为空或不可访问。 - 如果数据目录不可访问,返回一条包含错误信息的消息。
- 如果当前节点处于暂停状态,返回一条信息,说明正在使用空的数据目录运行。
- 如果数据目录为空且未暂停,引导新节点(调用
bootstrap
函数)。
- 如果数据目录不可访问或为空,将当前节点的角色设置为
- 如果数据目录可访问且不为空。
- 获取数据目录的系统 ID。
- 检查数据目录的系统 ID 是否有效(
sysid_valid
函数)。如果系统 ID 无效,返回一条信息,建议重新初始化。 - 检查集群的初始化系统 ID 是否有效。
- 检查集群的初始化系统 ID 是否与数据目录的系统 ID 匹配。
- 如果当前节点处于暂停状态。记录一条警告信息,说明系统 ID 在暂停模式下发生了变化。
- 如果当前节点是主节点。自愿释放锁,并返回一条信息,说明释放锁的原因是因为系统 ID 不匹配。
- 如果系统 ID 不匹配且未暂停,记录一条致命错误信息并退出程序。
- 如果当前节点处于暂停状态。记录一条警告信息,说明系统 ID 在暂停模式下发生了变化。
- 检查集群的初始化系统 ID 是否与数据目录的系统 ID 匹配。
- 检查集群是否未锁定且未暂停且未调用回调。
- 检查 PostgreSQL 实例是否正在运行且不是主节点。记录一条错误信息,说明没有初始化键且 PostgreSQL 作为副本运行,中止启动,并退出程序。
- 如果集群未锁定且未暂停且未调用回调,初始化 DCS。
- 检查 PostgreSQL 实例是否健康(
is_healthy
函数)。- 如果当前节点处于暂停状态。
- 将状态设置为
stopped
(调用set_state
函数)。 - 如果当前节点是主节点。删除主节点锁(调用
_delete_leader
函数),并返回一条信息,说明删除锁的原因是因为 PostgreSQL 没有运行。 - 检查回滚是否失败或未执行,如果满足条件,返回一条信息,说明 PostgreSQL 没有运行。
- 将状态设置为
- 如果当前状态是
running
或starting
,将状态设置为crashed
。 - 尝试启动已死的 PostgreSQL 实例(调用
recover
函数),并返回恢复结果。
- 如果当前节点处于暂停状态。
- 如果集群未锁定,处理不健康的集群(调用
process_unhealthy_cluster
函数),并将结果存储在ret
中。 - 如果集群健康,处理健康的集群(调用
process_healthy_cluster
函数),并评估计划的重启(调用evaluate_scheduled_restart
函数),将结果存储在ret
中。 - 检查是否正在执行提升操作。
- 检查异步执行器是否忙碌或是否正在提升,并且当前状态不是
starting
。- 同步复制槽(调用
_sync_replication_slots
函数)。 - 检查是否调用了回调。
- 如果未提升且不是主节点,触发检查分歧 LSN(调用
trigger_check_diverged_lsn
函数)。 - 调用
ON_START
回调。
- 如果未提升且不是主节点,触发检查分歧 LSN(调用
- 如果未提升且创建了槽且集群有主节点。
- 异步复制逻辑槽(调用
try_run_async
函数)。 - 如果复制槽成功,返回一条信息,说明复制了逻辑槽。
- 异步复制逻辑槽(调用
- 返回处理结果。
- 同步复制槽(调用
- 结束
try:①
,捕获与 DCS 通信时的错误,记录错误信息,并调用_handle_dcs_error
函数处理错误。 - 结束
try:①
,捕获与 PostgreSQL 通信时的错误,返回一条信息,说明将稍后再试。 - 最终块:(永远执行)
- 如果 DCS 未失败:
- 如果当前节点是主节点,设置 failsafe 为活动状态(调用
set_is_active
函数)。
- 如果当前节点是主节点,设置 failsafe 为活动状态(调用
- 触碰成员以更新状态。
- 如果 DCS 未失败:
- 开始一个
函数解释:
-
load_cluster_from_dcs
:根据dcs配置来加载集群并更新当前集群信息,如果集群未锁定且故障安全模式已启用,则注入实际的领导者信息到集群状态中。如果当前节点没有主节点锁,设置当前节点的领导状态为False,如果当前集群有领导者,保存领导者的信息到节点。 -
global_config.update:在故障安全模式(failsafe mode)下被调用,返回和更新集群信息。
-
reset_cluster_info_state:重置监控查询缓存。这个方法在心跳循环的开始或
synchronous_standby_names
发生变化时被调用。 -
touch_member:更新当前节点的状态信息,并将这些信息传递给分布式协调系统(DCS),以便集群中的其他节点能及时获知当前节点的状态。
-
handle_starting_instance:处理的 PostgreSQL 实例的启动过程,尤其是在高可用性架构下,如何管理主节点的启动。
-
post_bootstrap:处理 PostgreSQL 集群初始化后的步骤。它涉及到系统的引导过程、状态设置、节点角色配置、集群同步等多个方面。