Patroni + etcd + PostgreSQL 高可用架构(四):容量规划与变更管理
一、连接数不是调大的,是“预算出来的”
PostgreSQL 的 max_connections 是后端进程的上限,每个连接对应一个独立的操作系统进程,占用内存、文件描述符和锁表槽位。反复调大 max_connections 并不是容量规划,而是把资源约束从连接层推到了内存和内核层。PostgreSQL 设计实践中,max_connections 的基线通常建议在 100 到 200 之间,更高的客户端并发数应当由连接池来承接,而不是直接映射为数据库后端连接。
这就引出了 PgBouncer 的两个相互独立的容量维度,它们在配置中由不同的参数控制,不能混为一谈:
-
max_client_conn:PgBouncer 接受多少客户端连接,这是“前端排队容量”;
-
default_pool_size,或 pool_size:PgBouncer 为每个数据库与用户组合的池维护多少到 PostgreSQL 的服务端连接,这是“后端并发容量”。
两者独立意味着一个常见的误解可以澄清:调大 max_client_conn 不会增加后端并发。它只是让更多客户端进入 PgBouncer 的等待队列。当 pool_size 已满、sv_idle 为零时,新查询进入先进先出队列,cl_waiting 开始增长;应用侧的查询超时通常短于 PgBouncer 的 query_wait_timeout,默认 120 秒,于是应用先放弃并重试,重试又追加到队列尾部,形成“排队、超时、重试、队列更长”的自我放大循环,直到 max_client_conn 被触及,新连接被直接拒绝。
在 Patroni 集群中,连接预算需要沿着同一条链路做一次全局加总,而不是在每个组件上分别“够用就行”:
应用进程数或应用侧连接池的总上限乘以每个进程的并发连接数,不应超过 max_client_conn;
PgBouncer 各池的 pool_size 之和,加上 Patroni 自身与复制槽占用的保留连接,不应超过 PostgreSQL 的 max_connections。
Pigsty 的默认模板给出了一个可参考的起点:OLTP 与 CRIT 场景在经由 PgBouncer 接入时,PostgreSQL 的 max_connections 默认取 500;如果服务绕过连接池直连数据库,默认值翻倍至 1000。同一个模板中,work_mem 的取值是共享缓冲区除以最大连接数,并限定在 64MB 到 1GB 之间。这意味着调大 max_connections 的代价不只是进程数,还包括每个连接可用的 work_mem 被压缩,复杂排序和哈希连接会因此退化到磁盘。
一个可操作的巡检信号是 SHOW POOLS 输出中的 sv_active、sv_idle 和 cl_waiting。sv_active 等于 pool_size 且 sv_idle 为零、cl_waiting 持续上升,说明后端池已经饱和,此时继续加 max_client_conn 只会让队列更深,正确动作是排查是否有慢查询或 idle-in-transaction 占住了服务端连接,或者重新评估 pool_size 是否与真实事务并发匹配。
二、动态配置:区分“广播即生效”和“需要重启才能落地”
Patroni 的动态配置存储在 DCS 中,通过 patronictl edit-config 或 Patroni REST API 修改后,变更会广播到所有集群节点。这给人一种“所有参数都可以在线安全修改”的印象,但实际存在两类不同性质的变更,混用这两类会制造出“配置已经改了,但行为没有变”的假象。
第一类:Patroni 自身的动态参数,广播后由 Patroni 循环直接消费。 例如:
-
maximum_lag_on_failover:备库参与主库竞选所允许的最大落后字节数;
-
maximum_lag_on_syncnode:同步备库在被健康的异步备库替换之前允许落后的最大字节数;
-
synchronous_mode:启用同步复制模式,可选值为 off、on、quorum。
这些参数的语义是“Patroni 在选主和同步备库管理时的判定阈值”,修改后由 Patroni 在后续循环中读取并应用,通常不需要重启 PostgreSQL 进程。但“不需要重启”不等于“没有即时影响”:将 maximum_lag_on_failover 从较宽松的值收紧,会让一部分原本有资格参与竞选的备库失去资格,故障切换的候选集合立即缩小;将 synchronous_mode 从 off 切到 on,主库会在下一次循环开始要求同步确认,写入延迟的形态随之改变。
第二类:PostgreSQL 自身的参数,写入 Patroni 配置不等于已经生效。 当一个 PostgreSQL 参数被修改后,它需要经过 PostgreSQL 自身的重载或重启才能落地。Patroni 管理 PostgreSQL 进程的生命周期,因此这类变更需要区分 pending_restart 状态:配置已经写入 DCS,但节点尚未以新参数重启。此时 patronictl list 或 REST API 会报告该节点存在待重启的配置变更,而实际的数据库行为仍然是旧参数。
这两类变更的工程含义不同:第一类需要关注判定阈值改变带来的候选集合或同步语义变化,第二类需要安排重启窗口,并且重启本身在 Patroni 中是一次受控的、可能涉及角色切换的操作。一个安全的做法是先用 patronictl edit-config 修改动态参数,再用 patronictl restart 显式安排需要重启的节点,并利用 Patroni 的 restart 节奏控制,避免同时重启多个可能影响法定人数的节点。
synchronous_mode 的切换还有一个容易被低估的中间状态。在 synchronous_mode 为 on 但 synchronous_mode_strict 为 off 时,如果唯一的同步备库崩溃,提交操作会阻塞,直到 Patroni 下一次循环将主库切换为独立模式,最坏写入延迟为 ttl 秒。这意味着从异步切到同步,或者从同步切回异步,都不是瞬时完成的,而是在一个以 ttl 为量级的窗口内存在写入停顿或降级。变更窗口应当避开业务写入高峰,并且在切换后确认同步状态已经稳定在 DCS 中。
作为前置检查,pg_rewind 的可用性也属于“变更之前就应该确认”的项。Patroni 在启用 use_pg_rewind 时会通过 pg_controldata 检查目标是否启用了 wal_log_hints 或数据校验和;如果两者都不满足,rewind 不会生效,旧主回归会退化为一次基础备份重建。这不是一个可以在故障发生后再补的配置,而是在集群初始化或至少在任何可能产生时间线分叉的变更之前就必须确认的。
三、在线扩容:“加入集群”和“可接收流量”之间有一段不可压缩的时间
Patroni 集群增加一个只读副本,表面上只是“启动一个新节点上的 Patroni 服务”。但实际发生的过程是:Patroni 服务启动、在 DCS 中注册节点、触发一次 reinit,删除本地的 PGDATA 目录,从当前领导者传输基础备份;基础备份完成后,才开始流复制并追平 WAL 差距;只有当复制延迟收敛到可接受范围后,这个节点才应该被加入负载均衡的读池。
这段时间的长度由数据量和复制带宽决定。公开的运维实践中,基础备份本身被描述为“以小时计”,而基础备份之后的 WAL 追平还可能额外增加一段时间。这意味着新节点从“进程启动”到“可以安全接收读流量”之间存在一个不可跳过的窗口,在这个窗口内,它既不应该被 HAProxy 的 replica 角色健康检查视为合格,也不应该被任何自动化流程提前纳入轮转。
Patroni 的 replica 角色端点在这里提供了一个精确的控制手段。该端点仅在节点处于 running 状态、角色为 replica 且 noloadbalance 未设置时返回成功;还可以附加 lag 参数,使成功状态进一步要求复制延迟低于指定阈值。lag 的计算使用 DCS 中 cluster.last_leader_operation 记录的领导者 WAL 位置与本地重放位置的差值。因此,负载均衡层不需要自行判断“这个新节点是否追上了”,只需要在健康检查中使用带 lag 阈值的 replica 端点,Patroni 会把“尚未追平”的节点自动排除在成功响应之外。
扩容操作的另一个工程细节是 reinit 的触发方式。节点在 Patroni 启动后自动注册并触发 reinit 是一种常见路径;如果需要显式重建一个已经存在但数据不一致的节点,patronictl reinit 是标准命令。Percona 的文档建议在执行 reinit 之前先运行 patronictl reload,确保运行中的 Patroni 实例已经加载了最新的配置,而不是让 reinit 基于一个尚未落地的旧配置执行。
最后,扩容不是一个“只加节点”的动作,它同时改变了故障切换的候选集合和法定人数的分布。新增副本在同步模式下是否被纳入 synchronous_standby_names、它的 maximum_lag_on_failover 资格、以及它与 etcd 节点的网络位置,都会影响切换时的行为。把扩容当作一次普通的配置变更来评审,而不是当作一次纯基础设施操作,是避免“节点加上了,但切换语义被悄悄改变”的前提。
- 点赞
- 收藏
- 关注作者
评论(0)