这是本节的多页打印视图。 .
HA 集群
- 1: Patroni 高可用
- 2: Patroni 动态配置
- 3: Patroni REST API
- 4: HA 集群扩展安装
- 5: Patroni 集群备份
- 6: Patroni 集群恢复
- 7: HA Exec / psql
- 8: 示例:HA 集群 + 自签 CA 的 MinIO
- 9: 生成证书 —— pg cert
- 10: S3 存储高可用方案
Patroni 是 PostgreSQL 高可用的事实标准:它掌管每个
postmaster 的生命周期、在成员间流式复制、并在 leader 失联时执行自动 failover。pgcli
把 Patroni 作为独立的顶级命令 pg ha 暴露出来——它是一种独立的模式,既不是普通的 pg
实例,也不是用 pg addon install 安装的插件。
关于归属。 Patroni 相关页面原先挂在 插件 Addons 下。现在它们独立成 HA 集群这一节,因为
pg ha并不通过插件系统安装——插件索引里提到它只是为了方便 检索。etcd DCS 与 HAProxy 负载均衡仍是插件页面(etcd、 HAProxy);本节会在相关处链回去。
子页面
| 页面 | 内容 |
|---|---|
| Patroni HA | pg ha 命令集:创建、status、switchover/failover、pause、跨主机成员、密码、namespace 与 DCS 布局 |
| 动态配置 | pg ha edit-config / pg ha ctl —— 存在 DCS 里的运行时配置,以及为什么它绝不重建容器 |
| REST API | 每个成员的 Patroni REST API:健康检查、leader 重定向、HAProxy 探测什么 |
| HA 集群扩展 | 在集群范围内安装/卸载 PostgreSQL 扩展(滚动修改 shared_preload_libraries) |
| 集群备份 | pg backup setup + pg ha snapshot —— stanza、WAL 归档到 S3、跨主机的备份 SSH 通道 |
| 集群恢复 | pg ha restore —— 走自定义 bootstrap 机制的集群 PITR、leader 本机性预检、恢复后的重新基线 |
| Exec / psql | pg ha exec / pg ha psql —— 直连 leader 或任意成员跑 SQL,免 dsn、免进容器 |
| 示例:HA 集群 + 自签 CA 的 MinIO | 实测通过的端到端流程:集群备份到一台服务自带证书的 MinIO,全程在完全隔离的第二套环境里 |
| 生成证书 | pg cert —— 签发带域名与 IP SAN 的自签证书,服务于任何不想走 CA 又需要 TLS 的场合(开发测试服务器、内部端点,或服务自带证书的 MinIO):flag 一览,以及单张自签叶证书如何充当自己的信任锚 |
| S3 存储高可用 | 让仓库本身(MinIO/silo)也容错:pgcli 暴露的 SNSD 与 MNSD 两种形态及为何只有这两种,加上在数据目录之下用 ZFS —— 单机 raidz、分布式集群每节点各自建池、异构节点 |
典型路径
相关
- 插件:etcd(DCS)、HAProxy (可扛故障切换的稳定客户端端点,可选读写分离)、MinIO(备份所在的 S3 仓库)
- 恢复 → Patroni 集群:与单实例恢复共通的 PITR 基础
- Failover:副本提升:非 Patroni 的单副本提升路径(
pg replica) - Namespace 隔离 —— scope、stanza、容器名都带 namespace 前缀
1 - Patroni 高可用
Patroni 是 PostgreSQL 高可用的事实标准:它
管理每个 postmaster 的生命周期、在成员间流式复制,并在 leader 失联时执行
自动故障切换。pgcli 将 Patroni 作为独立的模式暴露 —— pg ha —— 而不是
把它揉进普通的 pg 实例管理路径。
这是独立模式,不是 addon 子命令。 拥有 PostgreSQL 进程的是 Patroni (不是 pgcli)。
pg ha成员不会出现在cfg.Instances里,不复用实例 生命周期代码,也不是pg create创建的。它唯一借鉴 addon 体系的地方,是 用 etcd 做 DCS。
仅支持 Linux,与 etcd addon 一样:Patroni 成员依赖 podman 的 host 网络。root 和 rootless podman 均可使用。macOS 上这些命令会快速失败并给出 清晰提示。
所有权边界
最需要先理解的一件事,是谁拥有什么:
| 职责 | 归属 |
|---|---|
Patroni 容器(run/start/stop/rm)、镜像、patroni.yml、端口分配、密码、DCS 接线 |
pgcli |
postmaster 生命周期、initdb、PostgreSQL 配置渲染、复制槽、故障切换 |
Patroni |
| switchover / failover / pause / edit-config | pgcli 在临时容器里包装 patronictl |
pgcli 从不直接编辑运行中 PostgreSQL 的配置,也从不跑 initdb —— 这两件事都
是 Patroni 做的。这也是为什么普通 PG 镜像的 docker-entrypoint-initdb.d
约定(admin 角色 / 默认库)在这里不适用:Patroni 自己 bootstrap 集群,
角色体系是 postgres(superuser)、replicator、rewind_user。
容器生命周期 ≠ 安全的 PG 重启
因为 Patroni 是容器里的 PID 1:
pg ha create是重装语义(stop + 重建容器)。重建 leader 会让该节点 完全离线并触发故障切换。改成员配置靠重跑create是安全的;动态参数 请优先用pg ha edit-config,它绝不碰容器。pg ha start/pg ha stop是裸的容器 start/stop。停掉 leader 容器和 该节点宕机一样具有破坏性 —— Patroni 会切到副本。计划内运维请先pg ha pause(关掉自动 failover),再停容器。- paused 的集群在 resume 之前没有自动 failover。
工作原理
pg ha 每个成员跑一个 Patroni 容器。同一 scope 的所有成员指向同一个
DCS(一个 etcd 集群),DCS 保存集群的动态配置和 leader 锁:
- 某个 scope 的第一次
pg ha create完成 bootstrap:Patroni 跑initdb,抢下 leader,成为 leader。 - 之后的每个成员自动从当前 leader
pg_basebackup并开始流复制 —— 没有 flag 区分"添加"还是"加入"。 pg ha的控制命令(switchover、pause等)在临时容器里对 DCS 跑patronictl,所以即使本机没有成员容器在跑也能工作。
DCS 是唯一真相来源:Patroni 每个周期都从 DCS 重新渲染每个成员的
postgresql.conf/pg_hba.conf,手工改盘上文件会丢 —— 请用
pg ha edit-config。
Scope 与 DCS 存储路径
pg ha create <scope> 里的 <scope> 就是 Patroni 集群名 —— 一个 HA 集群
的身份标识。所有接收 scope 的命令(status、switchover、failover、
pause、ctl 等)指的都是同一个集群;某 scope 的第一次 create 完成
bootstrap,之后同 scope 的 create 是加成员。(--member 是集群内部逐主
机的节点名 —— 每台机器一个成员。)
pg.yaml 中集群以该 scope 为 key 存放在 addons.patroni.<scope> 下。
etcd 里实际存了什么
Patroni 把一切存在固定的 etcd namespace + scope 之下:
- namespace:
/service/—— Patroni 的顶层键,pgcli 不改动。 - scope: pgcli 写入
patroni.yml的是PatroniScope(scope),即你起的名字 加上 pgcli 的 namespace 后缀。Patroni 自己没有 namespace 概念(其 etcd 前缀就是裸 scope),pgcli 把后缀烘进 scope,防止两个 pgcli namespace 共用一 套 etcd 时串群。
因此一个集群在 etcd 里的完整前缀是:
前缀以下的键属于 Patroni 自己的 DCS 布局。想查看,把 pg etcdctl 指向同机
器/同配置里一个运行中的 etcd 成员:
注意:即使建集群时用的是裸名字,这里显示的 scope 也会带着 namespace 后缀。
安装
前置条件:一个 DCS。要么复用本地 etcd addon 成员,要么指向外部 etcd。
pg ha status app 渲染 patronictl list —— 恰好一行 Leader,其余是
Replica … streaming:
不带参数时,pg ha status 汇总每个集群:容器状态、成员数、每个成员的
pg=/rest= 端口。
DCS 选型
Patroni 只需要一个可达的 etcd 集群,因此有两种布局:
-
同机部署 —— 把 etcd 成员作为 addon 跑在和 Patroni 成员相同的主机上,用
--etcd m1,m2,m3指定。最省事:一台机器同时扮演两个角色,不多占主机。适合 起步的 3 节点 HA 集群。 -
专用 DCS 主机 —— 把 etcd 集群跑在单独的(虚拟)机器上,再用
--etcd-endpoints让每个 Patroni 成员指向它。下面每一行都在不同主机上执行 (pgcli 是按主机管理的):这是可用性更高的拓扑:etcd 的 quorum 不会随某台 PG 主机一起消失,重装数据库 机器也不会顺带带走 DCS。PG 主机只是访问 DCS;
--etcd-endpoints列出全部 成员的 client URL,因此某一台 etcd 主机宕机时 Patroni 会自动顺延到下一个 endpoint(写入仍然需要 etcd 自身的 quorum —— 3 台中要活 2 台)。每台 PG 主机 上的 endpoint 列表要保持一致。只写一个 endpoint(
--etcd-endpoints 10.0.0.20:2379)也能用 —— 那一台 etcd 成员会服务整个集群,另外两台被它挡在后面。但这又把单点引回来了:E1 一 宕,Patroni 就够不到 DCS,哪怕 etcd 的 quorum 是健康的;leader 续不上锁 就会自我降级。你实际有几个成员,就把它们全部列出来。
生产环境推荐独立的奇数(3 或 5)成员 etcd 集群;同机部署用于开发和小型部署即 可。
命令
| 命令 | 作用 |
|---|---|
pg ha create <scope> --member <m> … |
登记 + (重)装一个成员 —— 重建 = 节点离线 |
pg ha list |
所有 HA 集群的紧凑表格(scope、成员数、DCS、状态) |
pg ha status [scope] |
所有集群,或单个集群的 patronictl list |
pg ha switchover <scope> |
计划内切换 leader(patronictl 会二次确认;--yes 可脚本化) |
pg ha failover <scope> |
立即提升一个副本 |
pg ha pause / resume <scope> |
关闭 / 重新开启自动 failover |
pg ha edit-config <scope> -- … |
查看或修改 DCS 里的动态配置(绝不重建容器) |
pg ha start / stop <scope> --member m | --all |
裸容器 start/stop(见生命周期警告) |
pg ha remove <scope> --member m | --scope-all [--clean-data] [--force] |
移除成员;--scope-all 同时清 DCS |
pg ha passwords <scope> [--file F] |
导出存储的密码集(--passwords-file 的格式,供其它主机使用) |
pg ha remote <scope> [--member m] [--ssh-port P] |
登记另一台主机上的成员(典型为跨主机 leader),供备份 SSH 使用 |
pg ha remote remove <scope> --member m |
撤销上述登记(不触碰远端容器) |
pg ha extension install/remove/list/apply |
安装、卸载或列出 PostgreSQL 扩展(见 HA 集群扩展) |
pg ha exec <scope> "<sql>" [--member m] [--database db] |
对 leader(或指定成员)跑一次性 SQL —— 免 dsn、免进容器 |
pg ha psql <scope> [--member m] [--database db] [-- <psql 参数>…] |
交互式 psql,目标解析方式同上 |
pg ha ctl <scope> -- <patronictl 参数…> |
透传任意 patronictl 命令 |
-- 之后的 flag 原样到达 patronictl(cobra 会剥掉 --),所以
pg ha ctl app -- show-config、pg ha ctl app -- topology、
pg ha edit-config app -- -s synchronous_mode=true --force 都能用。
edit-config --show 是 ctl … -- show-config 的便捷别名。
跨主机成员
每台主机各自跑自己的 pgcli,只管本机的成员;集群通过共享 DCS 汇合,所以
两台主机的 pg.yaml 各自持有同一个 scope 的一部分视图。
跨主机清单(以下每一项都必须在每台主机上对齐):
- 端口 —— 自动分配是按主机的,但
connect_address存在 DCS 里全集群共 享。请在每台主机上把--host-port/--restapi-port设成相同值,否则 副本之间连不通。 - 密码 —— Patroni 的复制 / rewind / REST-API 鉴权是全集群一致的。用
pg ha passwords app --file app-passwd.yml导出第一台生成的那套,对每台其 他pg ha create传--passwords-file app-passwd.yml。 --advertise-host—— 跨主机成员必传;它把监听地址翻成0.0.0.0并把 可达 IP 写进connect_address。留空则该成员只走回环。第一个(bootstrap) 成员也不例外:它就是 leader,其connect_address会写进 DCS,之后每台主 机的pg_basebackup都拨这个地址 —— 用默认值引导的集群永远无法再加跨机成 员。只要以后可能在别的主机加成员,第一次create就该传本机 LAN IPv4。- 防火墙 —— 在成员之间两两放行两个端口(PG + REST API)。
pgcli 不校验对端配置;pg ha status 会显示每个成员的 connect_address,便于
自查可达性。
备份跨主机 leader
每台主机的 pg.yaml 只记录本机成员,所以本机默认看不到另一台主机上的 leader
—— 而 pgBackRest 的 stanza-create / 全量备份恰恰要求连到 leader。
这一步现在自动完成:pg ha create 成功装好一个成员后,会把它 pgcli 私有的
端口(SSH / REST API)写进 etcd 的 /pgcli/ha/<scope>/<member> 注册表(拓扑本身
—— 成员名 + host:pgport —— Patroni 早已存在 DCS 里)。备份配置生成时用
patronictl list 拿全集群拓扑、再查注册表补齐每个远端成员的 SSH 端口,于是
pgbackrest.conf / ssh_config 自动包含所有主机的成员,pg1-host 与 SSH
HostName 直接指向远端 IP(本机成员仍走 127.0.0.1)。pg ha start/stop/remove、
autostart 与 pg ha extension 会跳过非本机成员 —— 它们归各自主机的 pgcli 管。
刷新备份配置即可:
跨机备份互信现在全自动。cluster-wide stanza 把每个成员列成一个 pg*-host,
所以从任意主机发起全量备份 / check,pgBackRest 会 SSH 探测所有主机的成员来
找 primary——这些成员的 sshd 都必须接受发起方主机的备份公钥,且其
--advertise-host 已把监听翻成 0.0.0.0(见上文),跨机 SSH 才可达。
pg backup setup 负责打通:各主机 create 时把本机备份公钥发布进同一个
/pgcli/ha/<scope>/<member> 注册表;setup 把全集群成员的公钥合并成一个每集群
一份的 authorized_keys 文件,成员容器把它作为额外的 AuthorizedKeysFile
bind-mount 进去。sshd 每次登录都重新读该文件,合并又是原地覆写,所以后来加入的
主机无需重启就被运行中的成员信任(该挂载只在首次出现时随归档配置的 pause 窗口
recreate 一次性补上)。pg ha remove 掉成员会同时删其注册表 key,下次合并就不再
信任它。
S3 仓库 CA 也走同一条路:配了 ca_file 的主机把证书发布到
/pgcli/ha/<scope>/.repo/ca,ca_file 留空的加入者自动拉取、并把本地 ca_file
指到拉下来的副本。若存储主机又是另一台机器,首台主机也能免拷文件拿到 CA——
pg backup fetch-ca <存储主机>:<端口> 直接从端点的 TLS 链里取回(见 MinIO 文档)。
注册表里只允许公开材料——SSH 公钥与自签 CA 证书;私钥、
密码、S3 secret_key 绝不写入(etcd 这条链路无认证)。
兜底:若某远端成员是那台主机升级 pgcli 之前创建的、尚未注册端口,生成器会把
它的 SSH 端口回退到本 scope 的基础端口(每台主机都从 patroni_ssh_start_port
起,通常首个成员即猜对)。猜错时,手动登记这一个成员覆盖之(不创建任何容器):
S3 备份与 WAL 归档
Patroni 集群默认不开归档——只有全量备份永远做不到时间点恢复。开启 S3 仓库
(见备份文档)即随之打开。每个集群是一个
stanza——pgcli_<scope>,与带 namespace 的 DCS scope 一致,两个 pgcli
namespace 共用一个 S3 桶也撞不上——其备份侧配置把每个成员列为一个
pg*-host,pgBackRest 自己找到 primary,failover 后也无需改配置。
pg backup setup 会把同一份 archive_command 渲染进每个成员本地的
patroni.yml(pgcli 绝不把归档 GUC 写进 DCS——那是 edit-config 的地盘;
而只要 DCS 不管理某个 key,Patroni 就会采用本地值),在 pause 窗口内按"副本
先、leader 后"重建过期的成员容器(recreate 同时就是 archive_mode 需要的
postmaster 重启),并为集群执行 stanza-create + check。此后 WAL 由持有
leader 锁的成员持续推送到 S3。
每个成员会有短暂离线、最后有一次计划内的 leader 降级——与 pg ha create 的
recreate 语义相同。
密码
某个 scope 的第一次 pg ha create 生成四份凭据 —— superuser、
replication、rewind,以及 restapi basic-auth 对(restapi_user /
restapi_password)—— 存在 pg.yaml 的对应集群下。命令行上永远不用传密码:
第一个成员自动生成,pg ha passwords 把存储的这套导出成 --passwords-file
的格式,供其它主机复用:
(不加 --file 则输出到 stdout —— 建议用 --file,免得密码落进 shell 历史和
滚屏。)
四个角色及其默认用户名(由 pgcli 固定 —— 你只设置密码,密码默认按每个角色
生成随机 16 位字符串)。长度可用 --password-length 覆盖(pg ha create /
pg create,8–64);它只对"生成"这条路径生效,对 --passwords-file 或已存储的
密码集无效:
| pg.yaml 键 | 用途 | 默认用户名 | 默认密码 |
|---|---|---|---|
superuser |
PostgreSQL 超级用户 | postgres |
随机 16 位,自动生成 |
replication |
复制 / 流复制 | replicator |
随机 16 位,自动生成 |
rewind |
pg_rewind 角色 |
rewind_user |
随机 16 位,自动生成 |
restapi_user / restapi_password |
Patroni REST API basic-auth | postgres |
随机 16 位,自动生成 |
postgres / replicator / rewind_user 这些用户名被写进渲染出的 patroni.yml
(postgresql.authentication);可生成 / 用 --passwords-file 提供的只是密码部分。
restapi_user 是唯一可通过密码文件覆盖的用户名。
需要完全自己掌控(或想手写这份文件)时,用同样格式配 --passwords-file:
pg ha create 会打印每份密码的来源(生成并存盘 vs. 来自文件路径)。渲染出的
patroni.yml 以 0600 权限写入,因为它内嵌了所有这些密码。
开机自启
和其他基础 addon 一样,Patroni 成员的容器可以在主机重启后被拉起 —— 但它只
启动已存在的容器,读取盘上已有的 patroni.yml;绝不重新渲染配置或重建
数据。
成员一次切一个(--ha --scope <scope> --name <member>)。相对 DCS 的启动顺序
无所谓:Patroni 会重试直到 etcd 应答,然后正常重选。启动服务的机制见
autostart 页面。
日志
容器名为 pgcli-patroni-<scope>-<member>(设置了 namespace 时带前缀)。
连接
pg ha exec 和 pg ha psql 是零配置路径:pgcli 自己从 DCS 解析出 leader,用存储的超管密码认证,在一次性的临时容器里跑 psql —— 不用手拼 dsn、不用进成员容器,而且远端成员同样可达(就是普通 TCP + scram,所以另一台主机上的副本也能连;用 --member 指定具体节点)。
pgcli 之外的客户端,连的是 leader 的 PostgreSQL 端口。用 pg ha status app 找 leader
(Leader 行的 Host 就是它的 connect_address),再把 pg psql 指过去。
单机默认(仅回环)成员下,就是 127.0.0.1:<host_port>。
failover 后 leader 会变,所以固定连接串应当避免 —— 除非前面有连接池或 Patroni REST API 的 leader 重定向兜着。
如果需要稳定的、能扛住故障切换的连接端点 —— 以及可选的读写分离 —— 在集群前面 放一个 HAProxy。
计划内主从切换
failover 由 Patroni 掌握,pg ha 只是包装了 patronictl 里"主动挪 leader"的
几个动词。三个命令都只吃 scope:
switchover是计划内的、优雅的那一种:当前 leader 先降级,候选再被提升, 切换瞬间没有在途事务。前提是双方健康且已追平。旧 leader 会在几秒后自动 以streaming副本身份回归(Patroni 重启它的 postmaster 时,短暂看到它stopped是正常的)。切换会开一条新的 timeline —— 这是正常的,不是脑裂。failover不做握手,直接把副本立即提升。只在 leader 已经挂了、或你有意 丢弃它时使用;对健康集群跑一次只是多一次抖动。凡是计划内的操作,都用switchover。pause在整个集群范围内关掉自动 failover。任何有计划的容器操作之前 都应该先 pause —— 例如pg ha stop --all、pg ha create重建、pg ha extension—— 否则 Patroni 会在你脚下把一个副本提升走。pg ha resume再打开。(paused 的集群没有自动 failover,直到 resume 为止。)
这些都是纯 DCS 操作 —— 在一次性容器里跑 patronictl,不碰任何成员的容器或
数据,成员分散在不同主机也照样能切。和所有 pg ha 控制命令一样,scope 会被
解析成带 namespace 后缀的 DCS 键(app → app-default),本机 leader 和
跨主机 leader 的切法完全一样。
由于客户端连的是 leader 的端口,switchover 之后连接目标会跟着变 —— 参见 连接;想要一个稳定的端点,就在前面放 HAProxy。
pg ha vs. pg replica —— 怎么选
pgcli 有两种拿到副本的方式:
pg replica + pg failover |
pg ha(Patroni) |
|
|---|---|---|
| 模型 | 手动:pg replica 建副本,pg failover 按需提升 |
自动:Patroni 保持 N 个成员同步并自愈 |
| Failover | 人工跑 pg failover;提升前主库一直宕着 |
Patroni 检测到失联,约 30s 内提升副本 |
| 所有权 | pgcli 驱动 postmaster(和普通实例一样) | Patroni 驱动 postmaster;pgcli 只拥有容器 |
| 适合 | 简单读扩展、单次计划内提升、沿用 pg 实例工具链 |
零 RTO 可用性要求、无人值守 failover |
需要靠手工控制的副本,用 pg replica。需要集群在节点宕机时无人介入也能活,
用 pg ha。
配置
每个成员渲染的 patroni.yml 完全由 pg.yaml 里 addons.patroni.<scope> 派
生。一条典型记录:
端口来自两个独立池 —— patroni_start_port(默认 35532)给 PostgreSQL,
patroni_restapi_start_port(默认 39060)给 REST API —— 所以 Patroni 成员永远
不会和普通实例或 addon 端口相撞。
DCS 的 scope 键 = scope 加命名空间后缀(Patroni 自己没有 namespace 概念,所以 把前缀烙进 scope,好让两个 pgcli namespace 共用一个 etcd 时不串台)。
默认 patroni.yml
下面是 pgcli 渲染并以只读方式挂载到每个成员容器里的配置文件。密码在 bootstrap
时自动生成;可用 --passwords-file 提供自己的密码集。
要点:
scope= Patroni 集群名。与namespace组合后形成 etcd 键前缀。etcd3.hosts= 只写host:port(不带http://前缀)。多端点逗号分隔。bootstrap.dcs里的值仅为默认值 —— 只在首次 bootstrap 时生效;之后用pg ha edit-config修改。postgresql.listen: 0.0.0.0在传了--advertise-host(跨主机)时才会设置;否则监听127.0.0.1。- 密码 在首次 bootstrap 时自动生成并存入
pg.yaml;可通过pg ha passwords导出。
动态配置
Patroni 的动态配置存在 DCS(etcd)的 /service/<scope>/config 下。每个成员
在每个循环(每 loop_wait 秒)都读取并应用这些配置 —— 所以 pg ha edit-config
是调优运行时参数的正确方式,无需重启容器。
bootstrap.dcs是一次性的。patroni.yml里的bootstrap.dcs块只在某 scope 的第一个成员执行pg ha create(即集群 bootstrap)时生效。一旦 Patroni 把配置写入 DCS,之后对 YAML 文件中bootstrap.dcs的任何修改都会被 完全忽略 —— 即使重新执行pg ha create(重装)也一样。bootstrap 之后要 改动态配置,请用pg ha edit-config。常见的误区:重新跑
pg ha create不会重新读取 YAML 里的bootstrap.dcs—— Patroni 看到 DCS 里已有config键,就直接使用它。
方式 说明 pg ha edit-config app -- -s key=value推荐,pgcli 的标准方式 pg ha ctl app -- edit-config透传到 patronictl,效果一样 Patroni REST API( PATCH /config)需要能访问到某个成员的 REST API 端口
持久化: 改动直接写入 etcd,不是容器里的文件。容器重启、pg ha start/stop,
甚至 pg ha create(重装)都不会丢失这些设置 —— 新成员会自动从 DCS 拿到最新配置。
查看当前配置
这是 pg ha ctl app -- show-config 的便捷别名。输出是存在 /service/<scope>/config
里的完整 JSON。
修改参数
改动后,所有成员在下一个循环(loop_wait 秒内)应用。无需重启。
常用可调参数
| 参数 | 默认值 | 说明 |
|---|---|---|
loop_wait |
10 |
leader 循环间隔(秒),用于锁续租、DCS 更新 |
ttl |
30 |
leader 锁的 TTL。leader 在此窗口内未续租,副本触发 failover |
retry_timeout |
10 |
DCS/PostgreSQL 操作超时。必须 < ttl - loop_wait,给 leader 至少一次重试机会 |
maximum_lag_on_failover |
1048576 |
副本可被提升的最大复制延迟(字节),默认 1 MB |
synchronous_mode |
false |
启用同步复制(零数据丢失,更高延迟) |
synchronous_node_count |
1 |
同步 standby 节点数(synchronous_mode=true 时) |
use_pg_rewind |
true |
用 pg_rewind 让失败的 leader 重新加入(比全量 pg_basebackup 快) |
use_slots |
true |
启用复制槽(副本断开时防止 WAL 丢失) |
failover_timeout |
0 |
failover 前等待时间(0 = leader 丢失时立即切换) |
PostgreSQL 运行时参数也可以在 postgresql.parameters 下设置:
这会触发 PostgreSQL reload(或重启,取决于参数的 context)。查阅 pg_hba.conf
和 postgresql.conf 参数文档,了解哪些设置需要重启。
不要直接编辑的内容
- 不要直接编辑磁盘上的
patroni.yml—— 每次pg ha create都会从pg.yaml重新生成,且 Patroni 的动态配置从 DCS 读取。 - 不要直接编辑
postgresql.conf—— Patroni 每个循环都会从 DCS 配置覆盖它。 - 不要改
scope或namespace—— 这些在 bootstrap 时烙进 DCS 键,无法修改, 只能重建集群。
完整参数参考: Patroni 动态配置 涵盖所有 DCS 可调参数,包含默认值、约束条件和示例。
扩展安装
完整参考: HA 集群扩展安装 涵盖编排顺序、跨主机工作流、
shared_preload_libraries排序规则以及纯内置扩展快速路径。
注意
- 仅 Linux(root 或 rootless)。 rootless 成员通过
--userns=keep-id以宿主用户身份运行;root 成员会将配置和数据目录 chown 给 postgres (uid 999),这样容器才能读0600的配置、写自己的数据目录。 pg_hba.conf有意保持宽松(host all all all scram-sha-256+replication一行,另加两行local ... trust)。rootless podman 的 pasta 会改写回环源地址, 而 Patroni 在自定义 bootstrap 期间生成的pg_hba只放行它自己解析出的回环 TCP 地址——两者相加会让 Patroni 连不上自己的实例、卡死pg ha restore。所以postgresql.use_unix_socket/use_unix_socket_repl被设为true,让 Patroni 改用 unix socket 连自己的 postmaster,绕过改写。收紧成固定白名单是后续待做的 改进 —— 现阶段别把这些端口暴露给不受信网络。- DCS(etcd)本身也有安全注意事项 —— 见 etcd 页面:pgcli 管理 的 etcd 不启用 TLS 或鉴权。
- 没有
init.sh/docker-entrypoint-initdb.d。 Patroni 自己 bootstrap 集群,所以普通实例的admin/默认库约定在这里不存在;连postgres(superuser)来建角色。 use_slots/use_pg_rewind已启用:Patroni 接管复制槽,并能用pg_rewind让宕过的 leader 重新加入,而不必全量 rebase。
2 - Patroni 动态配置
这些参数存储在 DCS(etcd)的 /service/<scope>/config 下,应用于集群的所有成员。使用 pg ha edit-config 修改:
参考:Patroni 官方文档、 Pigsty 动态配置。
bootstrap.dcs是一次性的。patroni.yml里的bootstrap.dcs块只在某 scope 的第一个成员 bootstrap 集群时生效。一旦 Patroni 把配置写入 DCS,之后 对 YAML 文件中bootstrap.dcs的任何修改都会被完全忽略 —— 即使重新执行pg ha create(重装)也一样。重新跑pg ha create不会重新读取bootstrap.dcs—— Patroni 看到 DCS 里已有config键,就直接使用它。
| 方式 | 说明 |
|---|---|
pg ha edit-config app -- -s key=value |
推荐,pgcli 的标准方式 |
pg ha ctl app -- edit-config |
透传到 patronictl,效果一样 |
Patroni REST API(PATCH /config) |
需要能访问到某个成员的 REST API 端口 |
核心时序参数
| 参数 | 默认值 | 最小值 | 说明 |
|---|---|---|---|
loop_wait |
10 |
1 | 主循环每轮休眠的秒数(锁续租、DCS 更新、状态刷新) |
ttl |
30 |
20 | leader 锁的 TTL(秒)。实际上是自动故障切换触发前的等待时间 |
retry_timeout |
10 |
3 | DCS 和 PostgreSQL 操作的重试超时。如果 DCS 或网络中断时间短于此值,Patroni 不会对主库执行降级 |
修改
loop_wait、retry_timeout或ttl时的约束:
故障切换参数
| 参数 | 默认值 | 说明 |
|---|---|---|
maximum_lag_on_failover |
1048576 |
副本有资格参与 leader 竞选的最大复制延迟(字节) |
maximum_lag_on_syncnode |
-1 |
同步 standby 被健康的异步副本替换前允许的最大延迟(字节)。≤ 0 时 Patroni 不主动替换不健康的同步 standby。设得足够高以避免高事务量期间频繁替换 |
max_timelines_history |
0 |
DCS 中保留的时间线历史条目最大数量。0 = 保留全部 |
primary_start_timeout |
300 |
leader 从故障恢复的秒数,超时则触发故障切换。0 = 检测到崩溃立即切换(异步复制下可能丢失事务)。最大切换时间 = loop_wait + primary_start_timeout + loop_wait;设为 0 时仅为 loop_wait |
primary_stop_timeout |
— | 停止 PostgreSQL 时的等待秒数(仅在 synchronous_mode 启用时生效)。如果停止操作超过此超时,Patroni 向 postmaster 发送 SIGKILL。≤ 0 或未设置 = 无效果 |
failover_timeout |
0 |
leader 丢失后触发故障切换前的等待秒数。0 = 立即 |
复制模式
| 参数 | 默认值 | 说明 |
|---|---|---|
synchronous_mode |
false |
启用同步复制(off / on / quorum)。leader 管理 synchronous_standby_names;只有最后已知的 leader 或同步 standby 可以竞选 leader。保证零数据丢失,代价是无法保证持久性时写入不可用 |
synchronous_mode_strict |
false |
没有可用的同步 standby 时,拒绝禁用同步复制——阻塞客户端对 leader 的所有写入 |
synchronous_node_count |
1 |
同步 standby 节点数。成员加入/离开时动态调整。自动限制为符合条件的节点数 |
failsafe_mode |
false |
启用 DCS 故障安全模式:DCS 不可达时,leader 保持运行而不自我降级 |
PostgreSQL 设置
| 参数 | 默认值 | 说明 |
|---|---|---|
postgresql.use_pg_rewind |
false |
使用 pg_rewind 让失败的 leader 重新加入(比全量 pg_basebackup 快)。需要数据页校验和(initdb 时 --data-checksums)或 wal_log_hints=on |
postgresql.use_slots |
true |
使用复制槽(副本断开时防止 WAL 丢失)。PostgreSQL 9.4+ 默认启用 |
postgresql.pg_hba |
— | 生成 pg_hba.conf 的规则。如果 PostgreSQL 的 hba_file 参数设为非默认值则忽略 |
postgresql.pg_ident |
— | 生成 pg_ident.conf 的规则。如果 ident_file 为非默认值则忽略 |
postgresql.parameters |
— | PostgreSQL GUC 的键值对,例如 {max_connections: 100, wal_level: "replica", wal_log_hints: "on"}。许多是复制正常工作所必需的 |
postgresql.recovery_conf |
— | standby 配置的附加 recovery.conf 条目(PG12+ 透明处理) |
通过 postgresql.parameters 键设置 PostgreSQL 参数:
备用集群
如果定义了此节,集群将作为备用集群引导,从远端主库流式复制。
| 参数 | 说明 |
|---|---|
standby_cluster.host |
远端主库地址 |
standby_cluster.port |
远端主库端口 |
standby_cluster.primary_slot_name |
复制用的槽名(可选,默认为成员名) |
standby_cluster.create_replica_methods |
从远端主库引导备用 leader 的方法有序列表 |
standby_cluster.restore_command |
WAL 恢复命令 |
standby_cluster.archive_cleanup_command |
备用 leader 的归档清理命令 |
standby_cluster.recovery_min_apply_delay |
应用 WAL 记录前的延迟 |
复制槽
| 参数 | 默认值 | 说明 |
|---|---|---|
member_slots_ttl |
30min |
副本关闭后其物理复制槽的保留时间。0 = 成员键从 DCS 过期后立即删除。仅在 PostgreSQL 11+ 生效 |
slots |
— | 永久复制槽(哈希映射)。在 switchover/failover 期间保留。PG11+ 的物理槽在所有节点上创建,每 loop_wait 秒推进一次。逻辑槽通过重启从主库复制到副本,然后每 loop_wait 秒推进一次。需要 use_slots: true |
ignore_slots |
— | 由外部管理、Patroni 不应触碰的槽(属性集列表)。任何子集匹配都会导致该槽被忽略 |
永久槽示例
节点固定的物理槽
对于固定的集群拓扑,为每个节点定义永久物理槽,防止临时中断期间槽被删除:
警告: 永久复制槽仅从 primary/standby_leader 同步到副本。应用程序应在 leader 节点上使用它们。在副本上使用永久槽会导致整个集群的
pg_wal无限增长。例外:与 Patroni 成员名匹配的物理槽(由 Patroni 创建和维护)在所有节点间同步,用于节点间复制。
查看当前配置
或直接从 etcd 读取:
3 - Patroni REST API
Patroni 在每个成员上暴露一个 HTTP REST API,提供健康检查端点(供负载均衡器和 Kubernetes 探针使用)、监控数据(含 Prometheus 指标)以及集群管理操作。
查找 REST API
每个成员的 REST API 端口由 pgcli 自动分配,存储在 pg.yaml 的
addons.patroni.<scope>.members.<name>.restapi_port 下。端口也写入渲染后的
patroni.yml 的 restapi.connect_address。
认证
pgcli 在 REST API 上启用了 basic-auth(用户名 postgres,自动生成密码)。
但认证是按方法区分的,而非一律要求:
| 方法 | 是否需要认证 | 端点 |
|---|---|---|
GET / HEAD / OPTIONS |
不需要 | 所有只读端点 —— /、/primary、/replica、/health、/cluster、/config、/metrics、/patroni 等 |
POST / PATCH / PUT / DELETE |
需要 | 写操作端点 —— /failover、/switchover、/config(修改)、/reload、/restart 等 |
这是有意设计:只读健康检查保持开放,让负载均衡器或 Prometheus 无需凭据即可
轮询;而改变集群状态的操作(POST /failover、PATCH /config)需要认证。凭据
的存在是为了让 patronictl 和其他 Patroni 成员能执行写操作。
所以负载均衡器健康检查不需要认证:
写操作才需要认证。密码与 patroni.yml 的 restapi.authentication 中使用的
restapi_password 相同。通过导出命令获取:
健康检查端点
所有健康检查端点响应 GET 请求。Patroni 返回描述节点状态的 JSON 文档,附带
HTTP 状态码。仅需状态码时可用 HEAD 或 OPTIONS 代替 GET(无响应体)。
仅主库端点(仅 leader 返回 200)
以下端点仅在节点为当前持有 leader 锁的 leader 时返回 HTTP 200:
| 端点 | 说明 |
|---|---|
GET / |
根路径 —— 主库健康检查 |
GET /primary |
/ 的别名 |
GET /read-write |
/ 的别名 |
GET /leader |
类似 /,但不区分 primary 和 standby_leader |
GET /master |
/leader 的传统别名 |
这些端点适用于负载均衡器健康检查,将写操作仅路由到当前主库。
仅副本端点(仅 replica 返回 200)
| 端点 | 说明 |
|---|---|
GET /replica |
副本健康检查 —— 节点运行中、角色为 replica、且未设置 noloadbalance 标签时返回 200 |
GET /replica?replication_state=streaming |
仅当副本正在流式复制(而非通过归档恢复追赶)时返回 200 |
GET /replica?lag=<max> |
仅当复制延迟低于阈值时返回 200(字节或可读格式:10MB、1GB) |
只读端点(主库和副本均返回 200)
| 端点 | 说明 |
|---|---|
GET /read-only |
任何运行中的节点(主库或副本) |
GET /synchronous / GET /sync |
仅同步 standby |
GET /asynchronous / GET /async |
仅异步 standby |
GET /read-only-sync |
主库 + 同步 standby |
GET /read-only-quorum |
主库 + quorum standby |
GET /quorum |
仅 quorum standby |
PostgreSQL 健康
| 端点 | 说明 |
|---|---|
GET /health |
PostgreSQL 运行中时返回 200(不论角色) |
Kubernetes 探针
| 端点 | 说明 |
|---|---|
GET /liveness |
Patroni 心跳循环正常运行时返回 200。主库上次心跳超过 ttl 秒、或副本超过 2*ttl 秒时返回 503。轻量级——不执行 SQL。适用于 livenessProbe。 |
GET /readiness |
节点为 leader 时返回 200;或 PostgreSQL 运行中、正在复制、且延迟在允许范围内时返回 200。接受 ?lag=<max>(默认 maximum_lag_on_failover)和 ?mode=apply|write(默认 apply)。适用于 readinessProbe。 |
监控端点
GET /patroni
以 JSON 返回详细的节点状态。Patroni 在 leader 竞选期间内部调用,也供监控系统使用:
响应字段:
| 字段 | 说明 |
|---|---|
state |
节点状态:running、stopped、starting 等 |
role |
primary 或 replica |
server_version |
PostgreSQL 版本(整数) |
xlog.location |
当前 WAL 位置(仅主库) |
xlog.received_location |
从主库接收的 WAL(仅副本) |
xlog.replayed_location |
已回放的 WAL(仅副本) |
timeline |
当前时间线编号 |
replication |
已连接的副本数组(仅主库) |
cluster_unlocked |
未持有 leader 锁时为 true |
pause |
自动故障切换暂停时为 true |
dcs_last_seen |
上次成功联系 DCS 的 epoch 时间戳 |
patroni.version |
Patroni 版本 |
patroni.scope |
集群 scope 名 |
patroni.name |
成员名 |
GET /cluster
返回完整的集群拓扑 —— 所有成员及其角色、状态和复制状态:
GET /config
返回存储在 DCS 中的当前动态配置:
等同于 pg ha edit-config <scope> --show。
GET /history
返回时间线历史。未发生时间线切换时(新集群)为空数组 []。
GET /metrics
以 Prometheus 格式返回监控数据,供 Prometheus 或兼容系统抓取:
关键指标:
| 指标 | 说明 |
|---|---|
patroni_primary |
本节点为 leader 时为 1 |
patroni_replica |
本节点为副本时为 1 |
patroni_postgres_running |
PostgreSQL 运行中时为 1 |
patroni_postgres_streaming |
PostgreSQL 正在流式复制时为 1(副本) |
patroni_xlog_location |
当前 WAL 位置(仅主库) |
patroni_xlog_received_location |
接收的 WAL(仅副本) |
patroni_xlog_replayed_location |
回放的 WAL(仅副本) |
patroni_cluster_unlocked |
未持有 leader 锁时为 1 |
patroni_is_paused |
自动故障切换禁用时为 1 |
patroni_pending_restart |
节点需要重启时为 1 |
patroni_postgres_timeline |
当前时间线 |
patroni_dcs_last_seen |
上次 DCS 联系的 epoch |
patroni_server_version |
PostgreSQL 版本 |
基于标签的过滤
健康检查端点接受查询参数,按成员 patroni.yml 的 tags 节中定义的自定义
标签过滤:
实用模式
负载均衡器路由
由于 GET 健康检查无需认证,负载均衡器可以直接轮询 REST API 端点。模式
(源自 Patroni 官方 haproxy.cfg 示例)是:后端 TCP 端口是 PostgreSQL 端口,但健康检查
走 REST API 端口,用 GET /,仅 leader 返回 200。
单一读写入口(所有流量 → 当前 leader):
check port 8008/8009—— HTTP 健康检查走 REST API 端口,而非 PG 端口。GET /仅在 leader 返回 200 → HAProxy 只把 leader 标记为UP。on-marked-down shutdown-sessions—— 故障切换时,旧 leader 的连接被切断,客户端重连到新 leader。fall 3/rise 2配合inter 3s—— 约 9 秒标记下线,约 6 秒标记上线。
读写分离 —— 再加一个用 GET /replica(仅 replica 返回 200)的 listener 承接读流量:
app_rw(:5000)——GET /→ 仅 leader 为UP→ 写操作落到 leader。app_ro(:5001)——GET /replica→ 仅 replica 为UP→ 读操作分摊到副本。
配合上文副本端点里的 GET /replica?lag=1MB,可把延迟过大的副本踢出读池。
curl 监控脚本
快速健康检查脚本:
Prometheus 抓取配置
GET /metrics 无需认证,因此抓取配置不带凭据也能工作:
4 - HA 集群扩展安装
在 Patroni 管理的 HA 集群中安装 PostgreSQL 扩展,与单节点实例有本质区别。
本页解释原因,并介绍 pg ha extension 命令子树。
为什么不能用 pg extension install?
单节点流程(pg extension install <instance> <ext>)的工作方式是:
- 用 Pigsty 包构建
-ext派生镜像 - 停止并用新镜像重建容器
- 编辑
postgresql.conf设置shared_preload_libraries - 在容器内执行
CREATE EXTENSION
在 Patroni 集群中,步骤 2-4 全部失效:
- Patroni 是 PID 1。 重建容器 = 该节点离线。如果是 leader,Patroni 会触发 故障切换 —— 集群在你安装到一半时重新洗牌。
- Patroni 每个循环都从 DCS 重新生成
postgresql.conf。 任何对文件的直接 编辑都会在几秒内被覆盖。shared_preload_libraries必须通过patronictl edit-config(写入 DCS)来设置。 CREATE EXTENSION必须在 leader 上运行,而 leader 可能在远程主机 —— 不在任何本地容器内。
pg ha extension 正确编排了所有这些步骤。
工作原理
安装流程遵循特定顺序,避免上述陷阱:
关键顺序是 resume 必须在 edit-config 之前:暂停状态的 Patroni 集群不会
应用 edit-config 变更。如果在暂停状态下 edit-config,shared_preload_libraries
的更新会被静默丢失。
纯内置扩展快速路径
如果所有请求的扩展都是内置的(contrib 扩展,如 hstore、uuid-ossp,随
PostgreSQL 一起发布),步骤 2-4 会完全跳过 —— 不构建镜像、不重建容器、不
暂停/恢复。流程直接走 edit-config + CREATE EXTENSION。
命令
安装
构建 -ext 镜像,暂停集群,重建本地成员容器。
- 单主机集群(所有成员在本地):自动执行 apply(resume + edit-config + CREATE EXTENSION)
- 跨主机集群:只执行 pause + recreate,集群保持暂停。需要在每台主机上分别运行
install,最后在任意主机上运行
pg ha extension apply完成安装。
扩展以逗号分隔传入:
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--database |
postgres |
CREATE EXTENSION 的目标数据库 |
--auto-restart |
false |
跳过 edit-config 触发的滚动重启确认 |
⚠
--database影响全部扩展。 指定--database后,apply阶段会对所有已安装扩展(不仅是本次新增的)在目标数据库执行CREATE EXTENSION IF NOT EXISTS。例如,集群已安装[pg_cron, pg_stat_statements, pgvector],执行pg ha extension install app hstore --database mydb后,这四个扩展都会在mydb中创建。扩展的.so文件已在镜像中,不会重复安装,只是 SQL 对象(函数、类型等)在目标库中注册。如果只想在特定数据库中启用已有扩展,无需重新 install,直接用 psql 连接对应库执行
CREATE EXTENSION IF NOT EXISTS即可。
卸载
在 leader 上执行 DROP EXTENSION,然后通过 patronictl edit-config 更新
shared_preload_libraries(触发滚动重启)。不会重建镜像或重建容器 ——
-ext 镜像只增不减;磁盘回收很少见且需手动操作。
扩展以逗号分隔传入:
列表
显示集群扩展的三个视图:
- Config (pg.yaml): 存储在
pg.yaml中的extensions列表 - DCS (preload): 来自
patronictl show-config的shared_preload_libraries值 - Leader (installed): leader 上
pg_extension中的实际扩展
应用
手动触发安装流程的后半段:恢复集群、执行 patronictl edit-config、在
leader 上执行 CREATE EXTENSION。
用于跨主机集群 —— 在每台主机上运行 install(各自构建镜像并重建本地
成员)后,在任意主机上运行一次 apply 完成 DCS 更新和扩展创建。
⚠
--database对所有已安装扩展生效。apply会对集群中全部扩展在指定数据库执行CREATE EXTENSION IF NOT EXISTS,不仅限于本次新增的扩展。
跨主机工作流
在跨主机集群中,每台主机只管理自己的成员。扩展安装流程分为两个阶段, 全局只暂停/恢复一次,避免不必要的 leader 漂移:
第一阶段 —— 每台主机依次运行 install:
每台主机执行:
- 在本地构建
-ext镜像(合并 DCS 已有的扩展列表,确保包含所有包) - 暂停集群(幂等 —— 第二台主机不会因"已暂停"而报错)
- 用新镜像重建自己的本地成员(同主机内 replica 先、leader 后)
- 保存配置 —— 集群保持暂停状态,不 resume
推荐顺序:从 replica 所在主机开始。 如果 leader 所在主机先执行 install, leader 重建会触发故障切换(leader 漂移到其他主机)。从 replica 主机开始, leader 始终不动,直到最后才处理 leader 所在主机,最大限度减少 leader 漂移 带来的数据同步开销。
第二阶段 —— 一次,在任意主机运行 apply:
- 恢复集群(幂等 —— tolerate “not paused”)
- 通过
patronictl edit-config更新shared_preload_libraries - 等待滚动重启
- 在 leader 上执行
CREATE EXTENSION
注意:
install后集群处于暂停状态。如果忘记执行apply,集群会一直暂停 (自动故障切换被禁用)。此时运行pg ha extension apply或手动patronictl resume <scope>即可恢复。
对于单主机集群(所有成员都在本地),install 自动执行 apply 步骤 ——
不需要单独的命令。
关于 leader 漂移: 目前无法完全避免 leader 漂移。当 leader 所在主机的 容器被重建时,Patroni 进程停止,DCS 中的 leader key 在 TTL 过期后失效, 即使集群已暂停(pause),其他 replica 仍会检测到 key 失效并触发新一轮选举。
replicas-first重建顺序能确保 leader 所在主机是最后一个执行 install 的, 但无法阻止 leader 漂移本身。这是 Patroni + 容器重建的固有限制。
extensions 配置字段
扩展在 pg.yaml 的 Patroni 集群配置中以集群级别跟踪:
这个列表是 pg ha extension list 报告的内容,也是 apply 安装的依据。
install 和 remove 都会自动更新它。
shared_preload_libraries 排序
某些扩展必须出现在 shared_preload_libraries 的第 0 位(如果不在第一位,
PostgreSQL 会 FATAL)。pgcli 在扩展目录中以 PreloadFirst 属性跟踪此约束 ——
当前设置此标记的扩展:
citus—— 分布式 PostgreSQL,必须最先加载timescaledb—— 时序引擎,同样的约束
部分扩展还需要额外的 DCS 参数:
pg_cron需要cron.database_name
pg ha extension 自动处理所有情况:
- 带
PreloadFirst的扩展始终放在 CSV 开头,不管输入顺序如何 - 当存在
pg_cron时,cron.database_name设为--database的值 (默认postgres) - 当
pg_cron被移除时,cron.database_name被清除
示例
安装 pg_stat_statements 和 pg_cron
构建 -ext 镜像,重建所有成员(带 pause/resume),在 DCS 中设置
shared_preload_libraries=pg_stat_statements,pg_cron 和
cron.database_name=mydb,然后在 mydb 中创建两个扩展。
安装 Citus(必须排在 preload 第一位)
即使 citus 列在第二位,preload CSV 也会生成为
citus,pg_stat_statements —— 带 PreloadFirst 的扩展始终放在位置 0。
卸载扩展
从 leader 删除 pg_cron,从 shared_preload_libraries 中移除,并清除
cron.database_name。触发滚动重启。
查看已安装的扩展
限制
- 镜像重建是增量的。 卸载扩展不会重建
-ext镜像或缩小它。要回收磁盘, 需用podman image prune手动清理旧镜像。 - 没有按成员的扩展列表。 扩展是集群级别的 —— 所有成员共享相同的
-ext镜像和相同的shared_preload_libraries。 CREATE EXTENSION只针对一个数据库。 PostgreSQL 扩展是按数据库的。要在 多个数据库中安装,需用--database分别指定每个数据库重新运行。- 跨主机需要手动协调。 每台主机必须先运行
install,然后才能运行一次apply。pgcli 不会 SSH 到远程主机。
参考
- Patroni HA —— 集群搭建、命令和架构
- 扩展管理 —— 单节点扩展管理和 Pigsty 目录
- Patroni 动态配置 —— DCS 参数参考
5 - Patroni 集群备份
本页给出 Patroni HA 集群备份的完整操作流程:从 pg backup setup 打通备份
基础设施,到 pg ha snapshot 做全量/增量/差异备份,再到一个运行态坑——副本
Replay LSN 卡住导致 pg ha status 显示成员 LSN 不一致时,如何用
patronictl reinit 拉回。命令与前置机制的参考细节见
Patroni HA 与
备份;本页把它们串成一条能照着跑完的路径。
模型:集群一个 stanza,备份容器 SSH 找 primary
一个 Patroni 集群(scope)对应一个 pgBackRest stanza:pgcli_<scope>
(与带 namespace 的 DCS scope 一致,两个 pgcli namespace 共用一个 S3 桶也撞不上)。
这个 stanza 是 cluster-wide 的——备份侧配置把每个成员列成一个 pg*-host,
pgBackRest 自己 SSH 探测、定位当前的 primary,并在 failover 后自动跟随,无需改
任何配置。因此:
- 不存在"per-member 备份"。
full/incr/diff都是对整个集群这一个 stanza 操作,pgBackRest 从当时的 leader 打快照。 - 备份从共享的 backup 容器里发起,通过 SSH 连到 primary。跨主机成员也照样可达
(信任在
setup时自动打通,见下)。 pg ha snapshot命令不需要你指定 leader——传对 scope 即可,primary 由 pgBackRest 定位。
前置:pg backup setup
pg backup setup 一次跑完,把备份基础设施全部拉起并让集群具备归档能力:
setup 做的事(配了 S3 仓库时,动手前先做一次 endpoint 预检:普通 TCP 拨号,打错主机名或存储没起会立刻失败退出,而不是拖到镜像拉取 / 容器启动之后才 把真正原因埋进噪声里):
- 构建/拉取 pgbackrest 镜像、建网络与目录、生成 backup 容器视图的
pgbackrest.conf与成员本地归档视图的pgbackrest-archive.conf。 - 拉起共享 backup 容器,随后验证仓库连通性:跑一次
pgbackrest repo-ls, 验证整条链路(TLS、凭证、bucket),并把失败映射成可操作的提示——证书错误指向pg backup fetch-ca、access-denied 指向凭证、拒绝/超时指向 endpoint。 - 打通跨主机备份互信:各主机
pg ha create时把本机备份公钥发布进集群的 etcd 注册表,setup把全集群成员公钥合并成一份每集群一份的authorized_keysbind-mount 进成员容器——sshd 每次登录重读该文件,所以后加入的主机无需重启就被 信任。S3 仓库 CA 走同一条路:配了ca_file的主机把证书发布进注册表,留空的 加入者自动拉取。(详见 HA → 备份跨主机 leader。) - 开启 WAL 归档:检测到成员归档配置过期时,pause 集群、按"副本先、leader 后"
重建过期成员容器(recreate 同时就是
archive_mode所需的 postmaster 重启)、 resume,并把同一份archive_command渲染进每个成员的patroni.yml。 - 对每个集群 stanza 执行
stanza-create+check。
HTTPS 是硬性要求。 pgBackRest 拒绝明文 S3。要接收集群归档的 MinIO 必须以 TLS 提供服务——用
pg addon install minio --tls(pgcli 自签 CA,把ca.crt填进--s3-ca-file)。MinIO 在另一台机器时,用pg backup fetch-ca <存储主机>:<端口>一次 TLS 握手取回 CA,免 scp(见 MinIO → 远端主机取 CA)。
配好后确认:
backup 容器 Up 且 stanza 就绪,即可开始备份。
每个主机都要先 setup,再 create 成员。 成员的归档能力在创建时就被冻结: 容器只有在
pgbackrest-archive.conf已存在时才挂载它,渲染出的patroni.yml也只有在配置了 S3 仓库时才带归档 GUC。所以在某主机跑pg backup setup之前pg ha create出来的成员,天生不带 WAL 归档——pg ha snapshot/pg ha restore覆盖不到它,要等之后的某次setup把它标记为过期、在 pause 窗口内 重建才能补上。pg ha create会提前就此发出告警;每台主机先跑pg backup setup,创建出来即是可备份的。
快照操作:pg ha snapshot
作用在某个集群(scope)上,通过共享 backup 容器对当前 primary 执行 pgBackRest。
这是 pg snapshot(单实例)在 Patroni 集群侧的对应物。
说明:
- 每次命令前,
pg ha snapshot会自动重渲染 backup 容器视图的pgbackrest.conf(从当前拓扑),使成员增删/端口变化即时反映到 stanza——bind-mount 生效,不重建容器。 - 只删得掉非唯一的 full:pgBackRest 至少保留一个 full,
delete命中唯一 full 会 被拒(先建一个新 full 再删旧的)。 - create 期间 pgBackRest 连到当时的 leader;failover 后换 leader 再跑也成功——这正是 cluster-wide stanza 的价值。
运行态坑:副本 LSN 卡住 → patronictl reinit
症状。 pg ha status app 里某个副本的 Replay LSN 长期落后于自己的
Receive LSN(甚至与另一副本明显不一致),且不随时间收敛。注意 Patroni 的
Lag 列是相对 leader 的差值,leader LSN 一动全体数字就动;判断真卡住要看副本
自身的 recv vs replay,或隔一段时间看 Replay LSN 是否纹丝不动。
确诊。 三条证据指向同一结论——该副本的 WAL 回放断点、复制流实际已停:
record with incorrect prev-link + waiting for WAL to become available 反复出现,
就是 WAL 流在段边界出现空洞/乱序、startup 进程卡在边界等下一段。此时 Patroni 往往
仍每周期报"no action. I am … a secondary, and following a leader"——它认为在跟随、
实际 postmaster 的 walreceiver 已停摆,不会自愈。这与备份无关,是复制运行态问题。
修复:reinit 该副本。 让 Patroni 从 leader 重新 pg_basebackup 一份、重开复制。
这是对该副本数据目录的破坏性重建(不影响 leader 与其他副本),所以 Patroni 要求
--force:
注意 -f 无效:patronictl reinit 只认全写 --force(不像 switchover/failover
的 --yes)。Success: reinitialize for member node1 即已下发。
验证恢复。 reinit 会清空并重建副本数据目录,随后 basebackup + 回放。轮询确认回到
recv == replay 且 status=streaming:
reinit 完成后,恢复 pg ha snapshot create app --type full 一次全量,把快照基线对齐到
健康的拓扑。
6 - Patroni 集群恢复
本页是 Patroni 集群备份 的恢复侧姊妹页。集群一旦有了带 WAL
归档的 stanza(pg backup setup + pg ha snapshot),pg ha restore 就能把整个集群
回到某个时间点——它是单实例 pg restore 的 Patroni 集群对应物。本页
讲清机制(为什么集群 PITR 不是简单跑一次 pgbackrest restore)、关于 leader 所在主机
的安全性预检、端到端操作流程,以及恢复之后必须做的重新基线步骤。命令 / flag 细节同时见
恢复 → Patroni 集群;本页把它们串成一条可照着跑的流程。
前提:本机要有带 WAL 归档的 stanza
PITR 只能重放到已经归档的部分。恢复前,集群必须已经具备:
- 针对 S3 仓库预置好的集群 stanza——
pg backup setup --s3-endpoint ...(见 备份 与 HA 备份); - 至少一个全量快照,加上覆盖目标时间点的 WAL 段
(用
pg ha snapshot create <scope> --type full产生)。
目标时间点被这段历史框住:早于最新全量备份的 stop 时间、或晚于最后归档的 WAL,恢复 都落不到那里。
pg backup setup 必须在本机跑过,且 backup 容器要在运行。 对跨主机集群而言,
这条比听起来更严格:
- 承载 bootstrap 的成员从 S3 恢复,靠的是挂进它容器里的仓库配置——
pgbackrest-archive.conf挂到/etc/pgbackrest.conf,外加 S3 CA 挂到/etc/pgbackrest/ca.crt。两者都由pg backup setup生成,且容器只在文件存在时才挂载。 从没跑过 setup 的主机没有仓库配置可挂,pgbackrest restore的 bootstrap 就连不上 S3、 起不来。 - leader 本机性与 stop 时间的预检,都是在 backup 容器内跑
pgbackrest ... info。pg ha restore只会 best-effort 刷新共享的pgbackrest.conf,不会生成成员的 archive 配置,也不会启动 backup 容器——所以要先把容器拉起来(pg backup setup, 或用pg backup status确认它是Up)。backup 容器没起时,预检会被跳过,真正的失败 要拖到 bootstrap 阶段才暴露;而恢复之后的新快照本来就依赖它在运行。
一句话:在已用 pg backup setup 预置好 S3 仓库、且 backup 容器为 Up 的主机上
运行 pg ha restore。
机制:自定义 bootstrap PITR,而非裸跑 pgbackrest restore
Patroni 集群不能在存活的 postmaster 下直接 pgbackrest restore 来做 PITR——数据目录、
时间线、DCS 身份都归 Patroni 管。pg ha restore 走的是 Patroni 的自定义 bootstrap
配方:
-
先 pause 集群(
patronictl pause)。在任何东西被停止之前先冻结 failover:集群存活时停掉本机 leader,会让 Patroni 把一台本机停不掉的远端 副本 promote 成新 leader,那个远端 leader 随后会抢着在旧时间线上重新夺回 DCS——从破坏路径内部瓦解了 leader 本机性预检。pause 正是为了堵住这一点。 若上一次尝试已经把集群拆掉(DCS 已清、成员已停),重跑的 restore 没有可 pause 的活集群;那本就是我们要的"已冻结"状态,因此空 DCS 下的 pause 失败被容忍, 流程继续。 -
清除集群的 DCS 身份(
patronictl remove)。Patroni 的bootstrap.dcs只在 DCS 没有 config key 时才执行,所以移除它正是重新武装 bootstrap 路径的动作。 -
选一个本机成员,改写它的
patroni.yml,把默认的 initdb bootstrap 替换成一条指向 目标时间点的pgbackrest restoremethod:no_params: true阻止 Patroni 追加--scope/--datadir(pgbackrest会以[031] invalid option拒绝它们);keep_existing_recovery_conf: true保留pgbackrest restore自己写下的recovery.signal+restore_command+recovery_target_*。method/pgbackrest这两个键是bootstrap.dcs的兄弟,绝不嵌进它里面——恢复相关的 GUC 若泄漏进 DCS 配置,会在 promote 后的 leader 上作为运行时参数生效。 -
清空该成员的 PGDATA 并重建其容器。Patroni 面对空数据目录 + 无 DCS 配置,跑该 method,在新时间线上恢复到目标点,并借
--target-action=promote把自己 promote 成可写 leader。 -
其余成员重新加入新 leader:本机其它成员被清空并作为标准副本重启(对新 leader 做
pg_basebackup);跨主机成员经 DCS 重新加入(或被 reinit)。
leader 本机性预检
除非当前 leader 是本机的一个成员,否则 pg ha restore 拒绝执行。 动手之前它先从 DCS
读 leader,判断它是不是本机可控成员之一。
原因。 第 1 步清掉 DCS、第 3 步让一个本机成员在新时间线上重新 bootstrap。活在另一台 主机上的 leader 其 Patroni daemon 一直在跑——本机停不掉它——所以 DCS key 一旦被移除,那个 远端 leader 会抢着在旧时间线上重新夺回 leadership,与本机 bootstrap 相撞。要求 leadership 在本机(从而可被停止)就消除了这个竞态。
你会看到什么。
怎么办。 把 leadership 挪到你正站着的这台主机的某个成员上,再重试:
或者改到 leader 自己所在的主机上跑 pg ha restore。leader 为空 / 读不到时(例如最早
的 bootstrap、还没有任何成员 publish 自己)不阻断执行。
与单实例 pg restore 的差异
- 总是 promote。 没有"只读挂起、检查、再换个时间重试"的两步——bootstrap 恢复以一个
新时间线上的可写 leader 收场。执行前先用
--dry-run确认目标时间。(机制第 1 步的patronictl pause与此无关:它是在恢复期间冻结 failover,不是只读检查窗口;而且 DCS 被清除后由新 bootstrap 重新发布配置,其中没有 pause 键,集群自动处于未暂停状态。) - 没有
--promoteflag——promote 是自动的(它烧进了 bootstrap 命令里)。 - 必须在拥有成员的主机上运行,且 leader 必须是本机的(见上文预检)。
操作流程:把集群恢复到某个时间点
完整 flag 集:--time(必填,格式见恢复)、
--member、--dry-run、--tail-logs、--force。
1. 用 dry-run 确认目标安全。 什么都不碰;你能看到 stanza、选中的 bootstrap 成员、确切的
pgbackrest 命令,以及(若相关)leader 本机性告警。
2. 确保 leader 在本机(仅当 dry-run 告警时)。switchover,然后复查状态。
3. 恢复。 默认会弹确认提示,列出 scope、stanza、目标时间、bootstrap 成员,以及
“PERMANENTLY LOST” 警告。用 --tail-logs 流式输出恢复日志;用 --force 跳过提示以便
自动化。
4. 确认新集群起来。 waitForLeader 轮询 DCS 直到出现 leader(上限 15 分钟),随后
重新加入的成员作为副本启动。
恢复之后:给新时间线重新基线
恢复不是最后一步。数据目录被重建过,所以在集群重新具备备份能力之前,有两件收尾必须做:
-
建一个新的全量快照,让后续 PITR 在新时间线上有基点:
这步通常直接成功:同一 stanza 的 pgBackRest 恢复在时间线切换时保持 PostgreSQL 的 system-id 不变(实测验证),所以恢复后的首个快照不会撞上
[051] system-id ... do not match stanza。集群恢复之后不需要pg backup stanza-upgrade。(真正会改 system-id 的是往同一个 stanza 里重新 initdb——比如全新集群复用了旧 stanza 名——那才是[051]和 stanza-upgrade 的适用场景。) -
拉回任何没有自动重新加入的成员——通常是丢了旧 leader 的跨主机成员。从新 leader 重新初始化它(只破坏该副本的数据目录):
(完整细节见 HA 备份 的"卡住的副本 → reinit"一节。)
故障排查
- “no local member on this host”——你在一台不拥有该 scope 任何成员的主机上运行。
pg ha restore只能重建本机的数据目录 + 容器。到拥有成员的主机、或 leader 所在主机上运行。 - “target time is before the latest backup stop time”——最早可用点是最新全量备份的 stop
时间;报错会打印它并给出建议的
--time。若需要更晚的基点,先做一个更新的全量快照。 - 恢复超出最后归档的 WAL——不能恢复到没归档过的时间点。调早目标时间,或先确保
archive_command在正常投递(pg backup status)再重试。 - 恢复后首个快照报
[051]——按新结论不该出现:同 stanza 恢复会保持 system-id 不变(见"重新基线"一节)。万一真撞上,pg backup stanza-upgrade能非破坏性地修好。 - 对同一仓库重复做 PITR 时报
FATAL: recovery ended before configured recovery target was reached——stanza 里已经住着一条此前 promote 出来的时间线,默认的recovery_target_timeline=latest会跳到那条分支上,而它在目标时间点之前没有提交。 恢复时显式钉住较旧的时间线,即在成员patroni.yml的 bootstrappgbackrest ... restore命令上加--recovery-option=recovery_target_timeline=<旧TL>。 目标时间点不跨越已 promote 分支的首次 PITR 不会遇到这个。
7 - HA Exec / psql
pg ha exec 和 pg ha psql 是对 Patroni 集群直接跑 SQL 的方式。它们是单实例
pg exec / pg psql 的集群对应物:你只给一个
scope,工具负责解析目标并连上去。不用手拼 --dsn、不用 podman exec 钻进成员容器,
而且因为走的是普通 TCP,另一台主机上的 leader 或副本和本机成员一样可达。
为什么不用 pg exec --dsn 或 podman exec
在这两条命令之前,想在集群上临时跑 SQL 只有两条别扭的路:
pg exec --dsn postgres://postgres:<pass>@<leader>:<port>/db—— 结果是对的,但 dsn 要人肉拼:从pg ha status读 leader 的connect_address、从配置里抄超管密码、 再填端口。而且写死的 dsn 在 leader 一切换就失效了。podman exec -it <member> psql ...—— 只能打到本机成员,别的主机上的 leader 根本 够不着;而且你是钻进了一个本该被工具屏蔽掉的容器。
pg ha exec/psql 把这两点都抹平了:pgcli 从集群自身状态解析 leader,用存储的密码认证,
在一个一次性容器里跑 psql。你全程看不到它。
目标是怎么解析的
DCS roster(patronictl list -f json)是唯一能看到整个集群全貌的视图。每台主机的
pg.yaml 只登记自己的成员,所以本机配置连一个跑在别处、名叫什么的节点都看不到——但 DCS
知道每个成员以及它对外宣告的 connect_address。两条命令都走它:
- 默认(不带
--member) → 当前 leader。即使 leadership 自你上次查看后已经迁移, 你依然打在对的节点上——不存在过期的 dsn。 --member <名字>→ 那个指定成员,无论它在哪。指向副本可拿到只读视图 (在 standby 上SELECT,pg_is_in_recovery()返回t),或指向某个具体的远端节点。
认证用集群超管走 scram;pg_hba(host all all all scram-sha-256)接受任意来源,所以远端
成员就像复制流量本来就能跨主机到达一样,可达。
用法
pg ha exec 把 SQL 作为尾随参数接收(所以通常整体引成一个字符串);pg ha psql 开一个
交互式 shell,把 -- 之后的内容原样转给 psql。--database 在两条命令上都选择目标库
(默认 postgres)。没有 --user——超管是 pgcli 唯一持有凭证的角色,所以连上去的就是它。
交互式与脚本化
pg ha psql 在你的 stdin 是 tty 时分配一个 TTY;不是 tty 时(管道喂脚本、在 CI 里跑)它会
关掉分页器,让会话跑完就退出而不是卡在 less 里。所以下面两种用法都符合预期:
关于输出
pg ha exec 流式输出 psql 自己的格式——列头、对齐、行数、错误都直接打到终端,跟 psql -c
打印的一模一样。它不是机器可解析的转储;如果需要那种,自己接管道工具,或用
pg ha psql app -- -c "..." 加 psql 的 --csv 之类 flag。
定位
如果你要一个独立于 pgcli、能扛故障切换的稳定客户端端点,在集群前面放
HAProxy,通过它连。pg ha exec/psql 是给运维和脚本直接驱动集群用的,
不是应用前面连接池的替代品。
pg ha status / pg ha ctl 仍是查看集群状态、以及触达未被包装的 patronictl 命令的方式;
这两条命令专门负责跑 SQL。
8 - 示例:HA 集群 + 自签 CA 的 MinIO
这是一页实操示例,每一步都真实跑通并验证过:创建 Patroni HA 集群,架一台 对外服务自带域名证书的 MinIO 插件(这里用我们自己造的自签 CA——与公共 CA、企业 CA 证书同一种形态,只是没去买),并把集群的 pgBackRest 备份与 WAL 归档指向它。下面每条命令都是实跑过的命令;只有主机名、端口、密码和证书材料 做了泛化处理。
有两个观念让这页值得细读而非略读:
- 存储端自带 TLS 证书。 MinIO 插件不必只服务 pgcli 生成的自签证书对——
--tls-cert/--tls-key可以让它服务你已有的证书。信任签发 CA 的客户端 无需任何额外材料;私有(这里是自签)CA 则把该 CA 经--s3-ca-file交给备 份栈——与用生成证书时完全一样。见 MinIO → 使用自带证书。 - 用第二个配置文件做隔离。
backup.repo.s3是一份 pgcli 环境里全局唯 一的仓库,被所有 stanza 共享。把它改指向一个新 store,会连带把该主机上所 有既有集群的备份目的地一起悄悄改掉。在跑着生产环境的机器上试新东西的正确 姿势是独立配置文件——自己的base_dir、namespace和端口段。本示例就 是这么做的。
我们搭建的环境
| 部件 | 取值 | 说明 |
|---|---|---|
| 配置 | ~/.pgcli-app1/pg.yaml |
与生产的 ~/.pgcli/pg.yaml 分开 |
| 数据根目录 | /home/fish/bucket/pgcli-data-app1 |
完全不碰生产数据目录 |
| 命名空间 | app1 |
给所有容器名加前缀——pgcli-minio-app1-store1、pgcli-patroni-app1-app1-nodea、pgcli-backup-app1 |
| DCS | 复用生产环境运行中的 etcd m1(127.0.0.1:2379) |
早于本示例就在生产配置里创建——见前置;此处以外部端点复用,集群成员按 scope 归组,新 scope 天然隔离 |
| MinIO | store1,端口 9010/9011,监听 0.0.0.0 |
自带自签证书,SAN 为 minio1.test,127.0.0.1,<主机IP> |
| Patroni | scope app1、成员 nodea、PG <主机IP>:35632 |
单成员,即 Leader |
| 备份容器 | pgcli-backup-app1 |
与生产的 pgcli-backup-default 并存 |
前置 —— etcd DCS 是怎么来的
示例复用了这台主机上早已为生产集群服务的那套 etcd。若是从零开始,第一块要 装的也是这个插件,一次一个成员——单成员就是一个健康的单节点 etcd 集群,跑 HA 开发/测试绰绰有余:
(如果要把这个 DCS 交给其他主机的成员加入,安装时就给它一个可达地址——
--advertise-host <主机IP>——让它的 peer/client URL 播报该 IP 而非回环;同
法 --name m2/--name m3 就能扩成真正的 3 节点组。本示例都不需要:集群与
它同机,拨的是 127.0.0.1:2379。)
第 0 步 —— 一张域名证书
任何 PEM 叶证书 + 密钥都行。演示用 pgcli 自带的 pg cert 造了一张自签的
(--valid-duration 控制有效期,SAN 覆盖客户端将要拨号的名字与 IP):
pg cert 写出的是单张自签叶证书(CA:FALSE、服务端认证用途),它自己就是自
己的信任锚——形态与公共 CA/企业 CA 签发的证书一致,只是没花钱去买。若是公
共 CA(或企业 CA)证书,这一步就只是"文件你本来就有"——把 flag 指向你的
fullchain.pem(先叶、后中间证书)和私钥即可,客户端侧反而更简单:锚定在受信
CA 的链根本不需要 --s3-ca-file。
第 1 步 —— 隔离环境
--namespace 让这份配置创建的所有容器名都与众不同,于是同一台 podman 上可以
并排跑两套环境。后续每条命令都要带 -c——本页此后的每个 pg 都指
pg -c ~/.pgcli-app1/pg.yaml。
端口段。 默认端口池(etcd 2379、MinIO 9000、Patroni 35532……)与生产配 置的起始值相同,而 host 网络容器的回环端口会被 podman 发布出来——所以处处 显式给端口、两套环境互不重叠,别让它们自动分配到同一批数字。
第 2 步 —— 服务自带证书的 MinIO
安装期校验会把证书与密钥配对(不匹配、已过期、CA 当叶用、仅限非服务端用途的
证书都在此处直接失败),打印有效期;又因为是自签证书,提示客户端将把这张证书
钉为信任锚。这里用 --listen 0.0.0.0 而非回环:备份容器经 host 网络用主机 IP
访问存储,而该 IP 就在 SAN 里。快速证明它服务的是你的证书:
建仓库桶(pgBackRest 不会代建):
第 3 步 —— Patroni 集群
那条警告是设计好的操作顺序,不是问题:创建时尚无仓库,成员先不开归档上线;
下一步的 backup setup 配好仓库后会重建成员并带上归档。注意 DCS 里的
scope 是 app1-app1(命名空间前缀 + scope)——给 pg ha 命令传的都是裸
scope(app1),pgcli 自己换算限定形式;限定名也让 stanza 与同一 bucket 上
别的命名空间可能跑的 app1 互不冲突。
塞点值得备份的数据(顺带验证客户端 scram 认证链路):
第 4 步 —— 把备份栈指向自带证书的 store
--s3-ca-file 直接把我们自己的证书当信任锚——自签证书的叶里就含其根,
所以传给 --tls-cert 的那个文件就是 CA 文件。pgBackRest 把 PEM bundle 原
样交给校验器、不检查内容,因此公共 CA 证书可以完全不传这个 flag,私有 CA 把
链传在这里即可。随后这次运行会验穿整条链路并重接成员:
“repo CA published to the cluster registry” 这行是 etcd 分发通道:本 scope 的 其他每个成员(或未来新加的,比如第二台主机)都从 DCS 自动取到 CA,无需手工 scp、无需额外 flag。
第 5 步 —— 备份,并证明数据真的到了
独立证据是对象确实躺在桶里——而写入它们的是由我们的证书终结的 TLS 会话:
archive/ 是持续的 WAL 归档(重建后的成员跑的 archive-push),backup/
是全量快照。确认归档器健康:
一个来自实跑的细枝末节,知道可免追鬼:failed_count 是 1,失败对象为
00000002.history——时间线历史文件,一秒后归档成功。它撞上了
backup setup 暂停并重建成员的那一瞬窗口。setup/重建期间出现一个孤立的近期
失败会自愈;同一个 WAL 名字让计数持续增长才是真正的仓库故障。
这个示例证明了什么
- 服务自带证书的 MinIO 插件可以作为 pgBackRest 仓库端到端工作:
stanza-create、
check、全量备份与持续 WAL 归档,全部走以所给证书校验的 TLS。 - 自签不是次等路径:同一个
--s3-ca-file槽位装得下它(叶即根)、公共 CA 的 链、内部 CA 的 bundle——传输与信任管道完全相同。 backup.repo.s3每份配置文件只有一个全局仓库。 在生产环境旁边做实验, 不要去改指它——init 第二份配置(pg config init --namespace … --base-dir …),把整个实验关在里面。命名空间限定的容器名、DCS scope 与 stanza 让两套 环境彼此不可见。
拆除
示例创建的一切都在 app1 配置名下,拆除也按该配置逆序进行(数据目录显式删 除):
要用 --scope-all(而不是 --member):它会 patronictl remove 整个
scope 的 DCS 键并清空 pgcli 成员注册表;逐个 member 删除会把这些残留留在
共享 etcd 里。
etcd m1 属于生产环境——上面所有操作都碰不到它,这正是隔离的用意。
相关
- Patroni 集群备份 —— 备份机制的深入讲解
- MinIO —— 插件本体及其 自带证书一节
- 命名空间隔离 ——
--namespace如何限定名称 - Patroni HA ——
pg ha命令集
9 - 生成证书 —— pg cert
pg cert 签发一张自签证书——SubjectAltNames 覆盖你要求的任意域名 / IP
组合——适用于任何不想走 CA 又需要 TLS 的场合:开发或测试服务器、内部端点、
客户端归你可控的服务。它写出的就是一张普通的 TLS 服务端叶证书,哪里都能用。
本文档树里演练的用例是 pgBackRest 的 S3 路径:Patroni 集群的备份要推给一个
S3 存储(通常是 MinIO 或 silo),而它必须对外提供 TLS——pgBackRest 明确
拒绝明文 S3——pgcli 的 MinIO/silo 插件都支持自带证书(pg addon install minio --tls-cert ... --tls-key ...)。想快速拿到一张证书来走通这条路,最省事的办法就是
pg cert:
它不往 pgcli 里注册任何东西——不碰 pg.yaml、不起容器——只是把两个文件写到
你指定的路径并打印 SAN。服务侧见
MinIO → 使用自带证书,端到端跑法见
示例:HA 集群 + 自签 CA 的 MinIO。
产出什么
单张自签叶证书,不是一条链:
--cert-file里的 PEM 恰好只有一块CERTIFICATE——这张证书自我签名;CA:FALSE、扩展密钥用途serverAuth——一张规规矩矩的 TLS 服务端叶证书;- SAN 自动分流:
--host里任何能解析成 IP 的条目落进 IP SAN,其余进 DNS SAN,所以"minio.test,10.0.0.9"不需要特殊语法(*.wild.test会作为 通配符域名条目)。
因为是自签,这张叶证书就是它自己的信任锚——把同一张 .crt 交给任何允许
指定 CA 文件/证书串的 TLS 客户端即可(curl --cacert、浏览器导入、应用的
SSL_CERT_FILE……)。具体到 pgBackRest 的 S3 路径,就是把 backup.repo.s3.ca_file
/ pg backup setup --s3-ca-file minio.crt 指向它。从没见过这个文件的远端主机
也不必 scp:pg backup fetch-ca <endpoint> 能识别 TLS 握手里的自签叶证书,并
直接把它作为锚保存下来。这不是 pgBackRest 勉强容忍的旁门做法:OpenSSL 把交给
它信任库的任何证书都当作锚点,并不要求 CA:TRUE,而 pgBackRest 的 S3 路径
(curl 走 OpenSSL)用的正是这一套机制——直接实测过:
openssl verify -CAfile minio.crt minio.crt 返回 OK。
换成真正的公共 CA / 私有 CA 证书,这套自锚机制就都不需要了:锚定在受信
CA 的链根本不需要 ca_file,私有 CA 的签发证书你本来就拿在手里。
flag 一览
| flag | 默认值 | 含义 |
|---|---|---|
--host |
127.0.0.1 |
逗号分隔的域名和/或 IP,编码进 SAN(可重复传)。条目自动识别是域名还是 IP;*.wild.test 会作为通配符域名条目。 |
--cert-file |
cert.pem |
写出证书 PEM 的路径。 |
--key-file |
key.pem |
写出私钥 PEM(PKCS8,权限 0600)的路径。 |
--valid-duration |
825 天(19800h) |
证书有效期,例如 --valid-duration 8760h 是一年。 |
--ecdsa |
P-256 |
曲线:P-224/P-256/P-384/P-521。置为空字符串则不生成 ECDSA 密钥(改配 --rsa)。 |
--rsa |
(关) | RSA 密钥位数(如 2048、4096);只在明确需要 RSA 而非默认 ECDSA 密钥时才设。 |
--ca |
false |
让这张证书自成一个 CA(CA:TRUE、keyCertSign)而非服务端叶——见下节。 |
两种密钥类型都不选(--rsa 0 --ecdsa "")是错误:必须有且只有一种产出密钥。
--ca 模式不是给 --tls-cert 用的
--ca 让证书成为自己的 Certificate Authority——用于你想要一把私有根再去签
别的证书。它不是拿来喂 MinIO --tls-cert 的东西:ValidateBYOCert 会检
查文件里的第一块证书并拒绝 CA 证书——CA 的私钥是签名密钥,不是服务端密钥。
默认(不带 --ca)产出的才是你要的服务端叶证书。
独立二进制:gencert
同一套签发逻辑也编成了一个可脱离 pg 使用的独立程序:
flag 完全相同,只是单横线形式(-host、-cert-file……)。两者背后是同一个
internal/certgen 包,产出的证书类型一致——pg cert 只是让你留在 CLI 里的那
条路径。
示例:一台主机上的完整自带证书链路
第 3 步直接引用你刚造好的 .crt,因为本机已经持有它。从没见过这个文件的
远端主机则用一次握手把它取回来,无需 scp——fetch-ca 能认出 MinIO 提供的
自签叶证书并把它作为锚保存:
相关
- Silo —— Pigsty 的 MinIO 分支,自带证书形态相同
- MinIO —— 插件本体,及其 使用自带证书 与生成测试证书两节
- 示例:HA 集群 + 自签 CA 的 MinIO —— 整条链路在一个 完全隔离的环境里端到端跑通
- Patroni 集群备份 —— 消费这张证书的 S3 仓库与 WAL 归档
- Patroni HA ——
pg ha命令集
10 - S3 存储高可用方案
Patroni 集群能扛住一个节点失联;它的 WAL 与备份流向的那个 S3 仓库却必须同 样扛得住,否则"高可用"在备份这条路上悄无声息地就断了。store 是一个 MinIO 或 silo 插件(两者可互换——silo 是 Pigsty 的 MinIO 分支,功能面完全一致)。这里真正要紧的是两个互相正交的 故障域:
| 故障 | 由谁吸收 |
|---|---|
| 一个节点内的一块磁盘坏了 | SNMD/MNMD 下的盘级 EC,或 MinIO 数据目录底下的存储层(ZFS) |
| 一整个节点下线 | MinIO 自己的跨主机纠删码(EC) |
本页记录 pgcli 支持如何把两者组合起来,以及如何在其中取舍。
部署形态
MinIO 按节点数与每节点盘数给自己的布局分类 (SNSD / SNMD / MNSD / MNMD)。pgcli 四种全部暴 露:
| 形态 | 结构 | 适用场景 |
|---|---|---|
| SNSD(单机单盘) | 单节点、单个数据目录——默认 | 开发、测试、演示——以及配上下一节的 ZFS 后,任何需要磁盘冗余的单机部署 |
| SNMD(单机多盘) | 单节点、多块盘——每个 --drive 传一块盘 |
单机、N ≥ 4 块数据盘、要扛住坏盘又不想额外搭文件系统层 |
| MNSD(多机单盘) | ≥ 4 节点、每节点一个数据目录 | 紧凑的高可用部署 |
| MNMD(多机多盘) | ≥ 2 节点、每节点多块盘——--drive 加上完整 --endpoint 矩阵 |
不搭文件系统层就要同时扛住坏盘与坏节点 |
SNMD 原生支持:pg addon install minio --drive /mnt/disk1 --drive ...
(每盘一个 flag)会拉起一个单进程 MinIO,在盘之间做纠删码。它能换来什么(在
活的 4 盘集上实测):默认 2 片校验,可容忍 2 块盘故障;坏 1 块盘时读写都照常,
坏 2 块盘时读仍成功、写被拒(quorum 边界);可用容量约为原始总量的一半;回来
的盘由 MinIO 自己 heal。同样的 --drive 在 silo 上也可用。
MNMD 同样是原生支持:保留 --drive 传本节点的盘,再用 --endpoint 补上整
个集群的 host×drive 端点矩阵——每台、每盘各一条 URL,各自指名该节点的
/data1../dataN 槽。在活体的 4 节点 × 4 盘集上实测(minio 与 silo 皆然):
16 盘在线报单个 stripe 大小 16 的纠删码集合、EC:4;损失一整个节点
(12/16)读写照常;再损失第二个节点(8/16)写被拒、读也失败,节点重启后自
愈回 16/16。完整步骤见
插件 → MinIO → 多机多盘(MNMD)。
SNMD 与 ZFS:扛住坏盘的两条路
两者都保护单主机数据抵御坏盘,区别在冗余放在哪一层、各自代价是什么。
原生 SNMD/MNMD(--drive) |
--data-dir 底下的 ZFS 池 |
|
|---|---|---|
| quorum 单位 | 盘——MinIO 把盘计作故障成员 | 对 MinIO 不可见——一块大盘,池吸收坏盘 |
| 布局日后可否改 | install 时定死(盘集合就是 EC 集合) | 随时可换——换盘、扩容、raidz1→raidz2,--data-dir 始终不变 |
| 4 盘可用容量 | 约一半(2 数据 + 2 校验) | 看 vdev:raidz2 = 2×盘,raidz1 = 3×盘(更省) |
| 重建 | MinIO heal 回来的盘 | ZFS 本地 resilver |
| 服务哪些形态 | SNMD(单机)与 MNMD(跨主机) | SNSD 与 MNSD(同一配方垫在每个节点下) |
| 异构节点 | 每个节点必须贡献相同盘数 | 2 盘节点与 6 盘节点看起来一样(各一个 endpoint) |
坦白的取舍是:原生多盘更简单——一条命令、不用置备文件系统、零 ZFS 配置就
拿到磁盘冗余。ZFS 更灵活——底层布局随时可换、同盘下省出更多可用容量
(raidz1 4 盘留 3 份,而 SNMD 的 EC:2 只留 2 份),而且它能容纳彼此并不一致的
节点。于是:
- 单机、要磁盘冗余又不想碰 ZFS → SNMD
- 单机、要更多可用容量 / 日后还想换布局 → SNSD + ZFS
- 分布式集群、盘和节点都要扛 → 两条路现在都是一等的:原生 MNMD,或 MNSD + 每节点 ZFS——见下文"4 主机、每主机多块盘的高可用"
单机上两条路都不算错——SNMD 那 50% 容量是不用管理池子的代价,ZFS 的灵活是置 备池子的代价。下文继续讲 ZFS 这条路,以及多机场景下的 MNMD 替代方案。
ZFS:更灵活的磁盘层
两种形态(SNSD 与 MNSD)用的是同一套配方:用主机上的数据盘建一个 zpool,为
store 开一个 dataset,把 --data-dir 指到它的挂载点。MinIO 只看到"一块又大
有可靠的盘",永远不知道底下有几块物理盘。
池必须落在与根文件系统不同的设备上——这恰好也是
MinIO 的盘检查与 pgcli install 时那条提示
性警告要的东西(drive is part of root drive, will not be used)。建在自己
盘上的 ZFS 挂载点天然满足。
按盘数选布局
| 数据盘数 | SNSD(ZFS 是唯一防线) | MNSD 节点(上层 EC 兜住节点损失) |
|---|---|---|
| 1 | 无冗余——dev/test 可以,坏一块盘就等于重建 store | 每节点一块大盘就是原生 MNSD 形态,无需 ZFS |
| 2 | mirror |
mirror |
| 3 | raidz1(可用 2D) |
raidz1 |
| 4 | 机械盘用 raidz2;SSD 重建快、容量金贵时用 raidz1;写密集用 2 × mirror |
raidz1——一格校验就足以把坏盘挡在 MinIO 视野外 |
| 5–8 | raidz2(可用 (N−2)D) |
raidz1,大盘为 raidz2 |
| > 8 | 拆成两个较小的 raidz2/raidz1 vdev 条带组池——跨 12+ 块盘的 resilver 暴露窗口太长 |
同理:单个 vdev 控制在 ~8 块盘以内 |
两列的差别只在一层推理:SNSD 下 ZFS 既要扛住坏盘、又要扛住重建期间再出的 事,校验深度直接买安全;MNSD 下 ZFS 只需要阻止"坏盘升级成丢节点",一格校验 加本地快速 resilver 就是它的全部职责——再往上,宁可用更多节点换更深的本地 RAID。
4 盘主机(SNSD)——单机最常见的问题——三种形状细看:
| 布局 | 可用容量 | 可容忍 | 特点 |
|---|---|---|---|
raidz2(类 RAID6) |
2 × 盘 | 2 块盘 | 机械盘的稳妥默认:大盘 resilver 很长,raidz1 的单校验撑不住重建期间再丢一块 |
raidz1(类 RAID5) |
3 × 盘 | 1 块盘 | SSD / 小盘的容量优先之选:重建以分钟计,而非小时 |
2 × mirror(条带化) |
2 × 盘 | 每镜像 1 块盘(分属不同镜像则可容忍 2 块) | 小写性能最好——宽条带 raidz 恰恰是这方面最差的形状 |
SNSD 下 ZFS 是 store 唯一的防线,所以默认往保守了选:除非盘足够快、重建
以分钟计、且那格容量真值那点风险——否则用 raidz2。
MinIO 那句"EC 模式底下不要做 RAID"的建议,针对的是 RAID 与跨主机 EC 双重 冗余的浪费。在 SNSD 下根本没有 EC——ZFS 是数据唯一的保护——那条建议并 不适用;照字面执行(“单盘、不上 ZFS”)在 4 盘主机上意味着一块盘报废就丢掉 整个 store。
与 PostgreSQL 同机时: ZFS 缓存很激进(ARC 默认最多吃掉一半内存)。数据 库同机时请设上限——例如
/etc/modprobe.d/zfs.conf里options zfs:zfs_arc_max=8589934592——免得两个负载抢内存。
4 主机、每主机多块盘的高可用
正是上表指向的那个混合形态,而且有两条一等的路:原生 MNMD——MinIO 在每 台节点的每块盘之间做纠删码;或MNSD + 每个节点数据目录底下的 ZFS—— MinIO 每节点只见一块盘,坏盘由本地池吸收。两条路的失败方式不同,所以这是个 真实的取舍。
原生 MNMD
一条矩阵、没有文件系统层:每个节点用 --drive 传自己的盘,每个节点携带完全
相同的 host×drive 端点列表。TLS 方面,签一张共享叶子、每个节点装同一对文件
(见下文实测之后的 TLS 说明)。
在活体的 4 节点 × 4 盘集上实测(minio 与 silo 结果一致):16 盘集合报单个 stripe 大小 16 的纠删码集、EC:4——16 块盘里坏 4 块仍然健康。
| 事件 | 在线 | 效果(实测) |
|---|---|---|
| 坏 1–4 块盘 | ≥ 12/16 | 读写照常;回来的盘由 MinIO heal |
| 1 个节点下线(它的 4 块盘) | 12/16 | 读和写照常——64 MiB 往返逐字节一致 |
| 2 个节点下线 | 8/16 | 写被拒(Resource requested is unwritable)、读也失败 |
| 节点重启 | 16/16 | 自愈 |
可用容量随校验比例而定:16 盘 EC:4 保留原始字节的 12/16。代价是:布局在
install 时定死(矩阵就是 EC 集合)、每个节点必须贡献相同盘数,而 pgcli 查不
出另一台节点上写歪的矩阵——保持 N 份 pg.yaml 一致是运维的职责。跨 grid 的
TLS,服务一份共享证书:用 pg cert --host <所有节点地址,逗号分隔> 签一
张自签叶子,再用 --tls-cert/--tls-key 装到每个节点(同一对文件各处逐字节
相同,端点矩阵全部 https://)。grid 随即成环,没有任何逐节点 CA 需要对齐
——在活体的 4 节点集上验证过:环以 Network: 4/4 OK 起来、CAs/ 目录为空,
因为那张共享叶子自身就是信任锚,而每个节点本来就持有它。退路才是生成模式:
若让每个节点各跑裸 --tls,每台主机会签出自己的私有 CA,第一次跨节点握手
死在 x509: certificate signed by unknown authority,直到你手工把某一份共享
CA 种进每个节点的证书目录——而共享 pg cert 这条路把这个活儿整个消掉了。
MNSD + 每节点 ZFS
主机之间走 MNSD,每个节点的目录底下各自 ZFS。 每个节点只暴露一个
endpoint(它 ZFS 支撑的 /data):磁盘故障由本地池自愈,根本不上报到
MinIO;节点故障由 EC quorum 吸收。
4 节点集群上,两层各自能容忍什么:
| 事件 | 由谁处理 | 后果 |
|---|---|---|
| 某节点 raidz 池里坏 1 块盘 | ZFS(resilver) | MinIO 无感知,不涉及 quorum |
| 1 个节点下线 | EC(写需要 ⌈4/2⌉+1 = 4 台里的 3 台在线) | 读写照常 |
| 2 个节点下线 | 只剩 EC 读(4 台里的 ⌈4/2⌉ = 2 台) | 可读、拒写,直到有节点回归 |
| 一块盘与它所在的节点一起故障 | EC(3/4) | 依然安全——节点回归后存活池继续自愈 |
让这套方案真正灵活的,是两条性质:
- endpoint 数 = 节点数,而不是盘数。 2 盘节点与 6 盘节点在 MinIO 眼里毫
无区别。4 台主机完全可以跑不同布局——这里
raidz1×4 盘、那里 2 盘mirror、另一台raidz2×6 盘——异构不损失任何东西。 - 池容量应大致对齐。 EC 把整个集群的可用容量定在最弱成员的空闲空间上
(3 × 12T 池 + 1 × 4T 池 → 你拿到的是按 4T 条纹化,而不是 40T)。盘确实不
一致时,精简置备(稀疏
zvoldataset,或直接zfs set refquota)对这层错 配对 MinIO 保密——用 quota 把大池压到小池的尺寸,不多占一分重建的力气。
为什么不用条带化把每节点的池拿回容量。 MinIO"EC 底下别做 RAID"的建议讲
的是容量,而且这笔账是真的:4 节点 MNSD 的 EC 已经把原始总容量砍掉约一半,
所以每节点用无冗余的纯条带池(所有盘 stripe、零本地冗余)确实比 raidz1
多榨出约三分之一。但 EC 是按节点数故障成员,而条带池会把"坏 1 块盘"放大
成"整台节点 offline"——一次普通的坏盘,就烧掉了 EC 为"丢一整机"预留的预算
格,逼出跨网络重建整池,且在重建完成前把余量清零。那点容量不是白捡的,是你
悄悄把磁盘容错降级成了节点容错换来的。每节点 raidz1 正是两层不再互相掏空间
的临界点:池吸收坏盘、对 MinIO 不可见,EC 的预算留给真正的节点故障。(如果
“榨干一切"的答案确实合适,那个形态是每节点一块大盘的纯 MNSD、根本不上
ZFS——而不是一个把同样单点风险藏低一层的条带池。)
两条路怎么选
| 原生 MNMD | MNSD + 每节点 ZFS | |
|---|---|---|
| 置备 | 每节点一条矩阵命令,不用置备文件系统 | 每节点先 zpool + dataset,再每节点一个 endpoint |
| EC 数什么 | 盘——丢一台节点只是 16 个成员里少了 4 个 | 节点——坏盘根本到不了 MinIO 面前 |
| 实测故障余量 | 4 节点 × 4 盘集:到 12/16 盘(一整个节点)都正常,8/16 失效 | 4 节点:读到 2/4,写到 3/4 台节点 |
| 可用容量 | 该集合上是原始的 12/16(EC:4)——校验片数是 MinIO 为这个 stripe 选的 | 本地 raidz1 每池留 3/4,再经跨节点 EC 减半 |
| 日后可否改布局 | install 时定死——矩阵就是 EC 集合 | 可改——换盘、vdev、布局都不碰 MinIO |
| 异构节点 | 不可能:每个节点必须贡献相同盘数 | 天然支持——每节点只是一个 endpoint,本地布局随意 |
| TLS | 一张共享证书服务整个 grid:pg cert 签一张覆盖所有节点地址的叶子,--tls-cert/--tls-key 各处一致地装上(没有 CA 要对齐) |
同理——节点之间仍是 TLS grid,同一套共享叶子配法照用 |
节点彼此一致、盘的规划已经定死、只靠 MinIO 就要同时拿到盘与节点的冗余、底下 什么都不想置备——选 MNMD。各节点的盘数/盘大小不一、布局日后可能变、或者习 惯按"整台节点"而不是"逐块盘"来推理故障——选 MNSD + ZFS。
怎么选
| 场景 | 形态 |
|---|---|
| 笔记本 / 演示 / CI | SNSD,默认数据目录——无需决策 |
| 一台主机、N ≥ 4 块数据盘,备份要扛得住坏盘 | SNMD(--drive × N)——零文件系统配置;或 SNSD + ZFS(机械盘 raidz2、快 SSD raidz1)换更多可用容量与可改布局 |
| 几台主机、每台一块数据盘,要扛得住丢主机 | 纯 MNSD |
| 彼此一致的主机、每台多块数据盘,盘和主机都要扛得住 | MNMD(--drive + 完整 --endpoint 矩阵)——无文件系统层;或 MNSD + 每节点 ZFS,见上文"两条路怎么选” |
| 各主机盘数/盘大小不一致 | MNSD + 每节点 ZFS(各节点布局可不同;容量对齐)——MNMD 要求各节点盘数相同 |
store 的 TLS 故事与拓扑无关:无论选哪种形态,--tls(或 pg cert 签发的自
带证书)行为一致——见生成证书。
相关
- 插件 → MinIO —— 安装、
--tls、分布式模式的约束(本页 赖以展开的硬性规则),以及 MNMD 完整流程 - 插件 → Silo —— 可互换的分支;形态完全相同
- 集群备份 —— 仓库如何接进 Patroni(
pg backup setup) - 示例:HA 集群 + 自签 CA 的 MinIO —— 单机形态的端到 端实测流程