Patroni 读取配置文件(包含Zookeeper中)

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:程序将会验证配置文件的有效性。
    • 导入 ConfigConfigParseError,以及 populate_validate_paramsschema
    • 调用 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函数。

  1. 从环境变量中获取name, namespace, scope三个配置的值。

  2. 从环境变量中获取日志相关的配置信息。如levelformatdateformat。注意:因为patroni以前采用了不同的名称,这里会对名称做一定的处理,会使用旧的环境变量配置获取到值,并且如果此时新的环境变量配置不存在环境变量之中,就会设置新的环境变量配置为就环境变量配置的值,这样方便之后进行统一的配置。

  3. 获取section相关的环境变量的值。例如:restapictlpostgresqllograft等的配置。

  4. 动态获取PostgreSQL二进制文件的路径,并且存储在ret[postgresql][bin_name]的字典中。

    ‘pg_ctl’, ‘initdb’, ‘pg_controldata’, ‘pg_basebackup’, ‘postgres’, ‘pg_isready’, ‘pg_rewind’

  5. 将获取某些环境变量的值转换为python中的对象。

    • 转换为bool对象:restapi.allowlist_include_membersctl.insecure。(在构建是会直接删除这个键,然后解析出来值之后再添加这个键。)
    • 转换为int对象:'restapi', ('request_queue_size',)'log', ('max_queue_size', 'file_size', 'file_num', 'mode')
    • 转换为list对象:raft.partner_addrsrestapi.allowlist。如果log.format不包含 % 格式化字符,也转换为列表。
    • 转换为dict对象:'restapi', ('http_extra_headers', 'https_extra_headers')'log', ('static_fields', 'loggers'
  6. 获取ctlrestapi节点的认证信息和获取 replicationsuperuserrewind 用户类型的认证信息。

  7. 遍历环境变量,根据前缀过滤出与 Patroni 相关的环境变量(以PATRONI_开头的),并根据变量后缀的不同类型对值进行适当的转换(如整型、布尔值等),然后存储到 ret 字典中。

  8. 获取 etcdetcd3 节点的认证信息,并更新到 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类初始化是动态配置为空。

  1. 创建动态配置的安全副本config,以免错误修改了动态配置的值。

  2. 遍历本地配置的所有配置项:

    • 移除其中无效的citus配置。
    • 处理postgresql下的子项。
    • 如果存在parameters调用_process_postgresql_parameters来处理并更新config配置。
    • 如果是其他的配置项(除了use_slots)会深拷贝配置值并更新config配置。
  3. 如果配置名不存在config中或者名为watchdog,就将该值复制到config中。

  4. 格式化restapictlauth配置项的为‘username:password’的格式。

  5. 如果配置中有 zookeeper 并且 zookeeper 下有 exhibitor,则将 exhibitor 提升至顶层,并移除 zookeeper。

  6. 如果 postgresql 下没有 authentication,则创建一个 authentication 字段,并从中提取 replication 和 superuser 的配置。(如果superuser没有在提取出的 authentication中,就将 pg_rewind配置项作为superuser)。

  7. 过滤和更新 pg_config['authentication'] 中每个项的子项,只保留在 _AUTH_ALLOWED_PARAMETERS 中列出的参数。

  8. 更新config中没有但是更新后pg_config['authentication']有的配置项。

  9. 如果配置中有 citus,则在 bootstrap.dcs 中设置 synchronous_modequorum

  10. 更新 postgresql 配置,将 updated_fields 中的字段从顶层配置移到 postgresql 中。

从上述可以看出构建有效的配置便是讲本地配置和patroni的动态配置进行合并,将本地配置的值更新到动态配置中,并且动态配置中也会更新authentication配置项的值。

1.2.4 是否要加载缓存?

如果配置了validator则会加载缓存并验证故障转移标签,使用ctlvalidatorNone,因为patronictl需要确保操作的准确性和实时性。如果加载了本地缓存的配置,可能会使用到过时的配置数据,从而影响操作的正确性。缓存文件的位置为pgdata目录下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_versionconfigurationmodify_version 是否相等,相等说明配置未改变。
    • 不相等就是发生了改变,更新_modify_version的值并将configuration替换为configuration.data
  • 比较当前的动态配置_dynamic_configuration和新的配置项configuration是否相同。
    • 不相同时首先调用_validate_and_adjust_timeouts函数验证并调整 loop_waitretry_timeoutttl 的值。
    • 然后调用_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()获取所有模块的名称,然后遍历这些模块,获取这个模块的模块名称,如何zookeeperetcd这样的名称,看是否在config配置中,如果在说明就是需要这个模块下的DCS配置类,然后找到这个模块中是AbstractDCS抽象类子类的类对象,这个类对象就是符合config配置的DCS类对象。

2.2 获取Zookeeper中的配置来创建集群

patroni/__main__.py文件的main函数中,调用了ensure_dcs_access函数来持续尝试从 DCS 中检索集群,并延迟一定时间。在这个函数中调用了dcsget_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 对象,versionmzxid 是该节点的版本信息和事务 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解析:

      • retget 方法的返回值,通常是一个包含两个元素的元组。
      • ret[0]:表示从 DCS 中获取的值,通常是一个字节串(bytes)。
      • ret[1]:表示与该值相关的元数据,通常是一个 Stat 对象,包含版本号、修改时间等信息。
    • 调用 get_status函数获取主节点的日志序列号(LSN)和插槽信息,可能用于数据库恢复或故障转移。

    • 检查路径下的failover节点是否存在,如果存在,加载故障转移信息并转换为 Failover 对象。如果不存在就记录为None。

    • 检查路径下的failsafe节点是否存在,如果存在,尝试解析该节点的 JSON 数据,表示集群的失败保护策略。如果解析失败,则将 failsafe 设为 None。如果不存在就记录为None

    • 返回这些节点信息构建的Cluster类。

3. 主备切换和故障转移读取Zk配置

这个高可用的主备切换和故障转移存在patroni/ha.pypatroni/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_errordata_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 不匹配且未暂停,记录一条致命错误信息并退出程序。
      • 检查集群是否未锁定且未暂停且未调用回调。
        • 检查 PostgreSQL 实例是否正在运行且不是主节点。记录一条错误信息,说明没有初始化键且 PostgreSQL 作为副本运行,中止启动,并退出程序。
        • 如果集群未锁定且未暂停且未调用回调,初始化 DCS。
      • 检查 PostgreSQL 实例是否健康(is_healthy函数)。
        • 如果当前节点处于暂停状态。
          • 将状态设置为 stopped(调用set_state函数)。
          • 如果当前节点是主节点。删除主节点锁(调用_delete_leader函数),并返回一条信息,说明删除锁的原因是因为 PostgreSQL 没有运行。
          • 检查回滚是否失败或未执行,如果满足条件,返回一条信息,说明 PostgreSQL 没有运行。
        • 如果当前状态是 runningstarting,将状态设置为 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 回调。
        • 如果未提升且创建了槽且集群有主节点。
          • 异步复制逻辑槽(调用try_run_async函数)。
          • 如果复制槽成功,返回一条信息,说明复制了逻辑槽。
        • 返回处理结果。
    • 结束try:①,捕获与 DCS 通信时的错误,记录错误信息,并调用 _handle_dcs_error 函数处理错误。
    • 结束try:①,捕获与 PostgreSQL 通信时的错误,返回一条信息,说明将稍后再试。
    • 最终块:(永远执行)
      • 如果 DCS 未失败:
        • 如果当前节点是主节点,设置 failsafe 为活动状态(调用set_is_active函数)。
      • 触碰成员以更新状态。

函数解释:

  • 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 集群初始化后的步骤。它涉及到系统的引导过程、状态设置、节点角色配置、集群同步等多个方面。

3.1 主备切换

3.2 故障转移

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值