跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

HA 集群

用 pgcli 管理 Patroni 高可用集群 —— pg ha 命令集、动态配置、REST API、扩展、集群备份与恢复、直连 SQL

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 插件成员)
pg ha create app --member node1 --etcd m1
pg ha create app --member node2 --etcd m1

# 其它主机用同一套密码登记自己的成员
pg ha passwords app --file app-passwd.yml
ssh other-host
pg ha create app --member node3 --advertise-host 10.0.0.12 \
    --etcd-endpoints 10.0.0.9:2379 --passwords-file app-passwd.yml

# 直接跑 SQL —— leader 由 pgcli 从 DCS 解析
pg ha exec app "SELECT version()"
pg ha psql app

# 备份它,并且能回到过去
pg backup setup --s3-endpoint ...
pg ha snapshot create app --type full
pg ha restore app --time "2026-08-26 15:30:00+00"

相关

1 - Patroni 高可用

以 pgcli HA 模式运行 Patroni 版 PostgreSQL 高可用——自动故障切换、switchover,基于 DCS 的集群

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 锁:

  1. 某个 scope 的第一次 pg ha create 完成 bootstrap:Patroni 跑 initdb,抢下 leader,成为 leader。
  2. 之后的每个成员自动从当前 leader pg_basebackup 并开始流复制 —— 没有 flag 区分"添加"还是"加入"。
  3. 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 里的完整前缀是:

/service/<scope>[-<namespace>]/          # 例:/service/app/(无 namespace)
                                         #     /service/app-prod/(namespace: prod)
├── initialize       # bootstrap 标记(只写一次)
├── leader           # 当前主;值 = 成员名
├── members/<member> # 每个成员的注册信息(conn_url、api_url、状态)
├── status           # 集群 LSN / 状态
├── config           # 动态配置(pause 标记也在这里)
├── history          # 配置修订历史
├── failover         # 手动 failover 请求
└── sync             # 同步复制状态

前缀以下的键属于 Patroni 自己的 DCS 布局。想查看,把 pg etcdctl 指向同机 器/同配置里一个运行中的 etcd 成员:

ETCDCTL_ENDPOINTS=http://127.0.0.1:2379 pg etcdctl get /service/ -- --prefix --keys-only

注意:即使建集群时用的是裸名字,这里显示的 scope 也会带着 namespace 后缀。

安装

前置条件:一个 DCS。要么复用本地 etcd addon 成员,要么指向外部 etcd。

# 1. 一个 DCS —— 这里用 etcd addon(多节点配置见 etcd 页面)
pg addon install etcd --name m1

# 2. 第一个成员 bootstrap 集群(成为 leader)
pg ha create app --member node1 --etcd m1

# 3. 后续成员自动以副本身份加入
pg ha create app --member node2 --etcd m1
pg ha create app --member node3 --etcd m1

# 4. 观察
pg ha status app

pg ha status app 渲染 patronictl list —— 恰好一行 Leader,其余是 Replica … streaming:

+ Cluster: app (7683433951661608987) +-----------+----+-------------+-----+------------+-----+
| Member | Host            | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node1  | 127.0.0.1:5432  | Leader  | running   |  1 |             |     |            |     |
| node2  | 127.0.0.1:5433  | Replica | streaming |  1 |   0/3000060 |   0 |  0/3000060 |   0 |
| node3  | 127.0.0.1:5434  | Replica | streaming |  1 |   0/3000060 |   0 |  0/3000060 |   0 |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+

不带参数时,pg ha status 汇总每个集群:容器状态、成员数、每个成员的 pg=/rest= 端口。

DCS 选型

Patroni 只需要一个可达的 etcd 集群,因此有两种布局:

  • 同机部署 —— 把 etcd 成员作为 addon 跑在和 Patroni 成员相同的主机上,用 --etcd m1,m2,m3 指定。最省事:一台机器同时扮演两个角色,不多占主机。适合 起步的 3 节点 HA 集群。

  • 专用 DCS 主机 —— 把 etcd 集群跑在单独的(虚拟)机器上,再用 --etcd-endpoints 让每个 Patroni 成员指向它。下面每一行都在不同主机上执行 (pgcli 是按主机管理的):

    # 主机 E1 (10.0.0.20) —— bootstrap etcd 集群
    pg addon install etcd --name e1 --cluster prod \
        --advertise-host 10.0.0.20 --client-port 2379 --peer-port 2380
    
    # 主机 E2 (10.0.0.21) —— 加入
    pg addon install etcd --name e2 --cluster prod \
        --advertise-host 10.0.0.21 --client-port 2379 --peer-port 2380 \
        --join http://10.0.0.20:2379
    
    # 主机 E3 (10.0.0.22) —— 加入
    pg addon install etcd --name e3 --cluster prod \
        --advertise-host 10.0.0.22 --client-port 2379 --peer-port 2380 \
        --join http://10.0.0.20:2379
    
    # 主机 A / B / C —— 每台一个 Patroni 成员,endpoint 列表完全相同
    pg ha create app --member node1 --advertise-host 10.0.0.11 \
        --etcd-endpoints 10.0.0.20:2379,10.0.0.21:2379,10.0.0.22:2379

    这是可用性更高的拓扑: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 的一部分视图。

# host A (10.0.0.11) —— bootstrap(自带 etcd,或外部 DCS)
pg ha create app --member node1 --advertise-host 10.0.0.11 \
    --etcd-endpoints 10.0.0.9:2379,10.0.0.10:2379

# host A —— 导出第一台生成的密码,供其他主机使用
pg ha passwords app --file app-passwd.yml

# host B (10.0.0.12) —— 加入,共用同一个 DCS 与同一套密码
pg ha create app --member node2 --advertise-host 10.0.0.12 \
    --etcd-endpoints 10.0.0.9:2379,10.0.0.10:2379 \
    --passwords-file app-passwd.yml

跨主机清单(以下每一项都必须在每台主机上对齐):

  1. 端口 —— 自动分配是按主机的,但 connect_address 存在 DCS 里全集群共 享。请在每台主机上把 --host-port / --restapi-port 设成相同值,否则 副本之间连不通。
  2. 密码 —— Patroni 的复制 / rewind / REST-API 鉴权是全集群一致的。用 pg ha passwords app --file app-passwd.yml 导出第一台生成的那套,对每台其 他 pg ha create 传 --passwords-file app-passwd.yml。
  3. --advertise-host —— 跨主机成员必传;它把监听地址翻成 0.0.0.0 并把 可达 IP 写进 connect_address。留空则该成员只走回环。第一个(bootstrap) 成员也不例外:它就是 leader,其 connect_address 会写进 DCS,之后每台主 机的 pg_basebackup 都拨这个地址 —— 用默认值引导的集群永远无法再加跨机成 员。只要以后可能在别的主机加成员,第一次 create 就该传本机 LAN IPv4。
  4. 防火墙 —— 在成员之间两两放行两个端口(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 管。

刷新备份配置即可:

pg backup setup

跨机备份互信现在全自动。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 起,通常首个成员即猜对)。猜错时,手动登记这一个成员覆盖之(不创建任何容器):

pg ha remote app --member node3 --ssh-port 42301   # 写 members.<m>.remote_host
pg ha remote remove app --member node3             # 撤销该登记

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 的格式,供其它主机复用:

pg ha passwords app                          # 把存储的这套以 YAML 打到 stdout
pg ha passwords app --file app-passwd.yml    # 或写入文件(权限 0600)

(不加 --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:

# app-passwd.yml
superuser: <postgres superuser 密码>
replication: <replicator 密码>
rewind: <rewind_user 密码>
restapi_user: postgres          # 可选,默认 postgres
restapi_password: <REST API basic-auth 密码>
pg ha create app --member node1 --etcd m1 --passwords-file app-passwd.yml

pg ha create 会打印每份密码的来源(生成并存盘 vs. 来自文件路径)。渲染出的 patroni.yml 以 0600 权限写入,因为它内嵌了所有这些密码。

开机自启

和其他基础 addon 一样,Patroni 成员的容器可以在主机重启后被拉起 —— 但它只 启动已存在的容器,读取盘上已有的 patroni.yml;绝不重新渲染配置或重建 数据。

pg autostart enable --ha --scope app --name node1

成员一次切一个(--ha --scope <scope> --name <member>)。相对 DCS 的启动顺序 无所谓:Patroni 会重试直到 etcd 应答,然后正常重选。启动服务的机制见 autostart 页面。

日志

pg logs addon patroni --scope app --name node1          # 最近 50 行
pg logs addon patroni --scope app --name node1 -f       # 跟踪

容器名为 pgcli-patroni-<scope>-<member>(设置了 namespace 时带前缀)。

连接

pg ha exec 和 pg ha psql 是零配置路径:pgcli 自己从 DCS 解析出 leader,用存储的超管密码认证,在一次性的临时容器里跑 psql —— 不用手拼 dsn、不用进成员容器,而且远端成员同样可达(就是普通 TCP + scram,所以另一台主机上的副本也能连;用 --member 指定具体节点)。

pg ha exec app "SELECT version()"
pg ha exec app --member node2 "SELECT pg_is_in_recovery()"   # 只读,打在副本上
pg ha psql app                                               # 交互式

pgcli 之外的客户端,连的是 leader 的 PostgreSQL 端口。用 pg ha status app 找 leader (Leader 行的 Host 就是它的 connect_address),再把 pg psql 指过去。 单机默认(仅回环)成员下,就是 127.0.0.1:<host_port>。

pg psql --dsn postgres://postgres@127.0.0.1:<leader_port>/postgres

failover 后 leader 会变,所以固定连接串应当避免 —— 除非前面有连接池或 Patroni REST API 的 leader 重定向兜着。

如果需要稳定的、能扛住故障切换的连接端点 —— 以及可选的读写分离 —— 在集群前面 放一个 HAProxy。

计划内主从切换

failover 由 Patroni 掌握,pg ha 只是包装了 patronictl 里"主动挪 leader"的 几个动词。三个命令都只吃 scope:

pg ha switchover app                     # patronictl 交互式询问候选
pg ha switchover app --candidate node2   # 事先指定
pg ha switchover app --candidate node2 --yes   # 脚本化,跳过所有确认

pg ha failover   app --candidate node2 --yes   # 立即提升,不做握手
pg ha pause      app                          # 关掉自动 failover
pg ha resume     app                          # 恢复
  • 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> 派 生。一条典型记录:

addons:
  patroni:
    app:
      name: app
      etcd_members: [m1]                # 或 etcd_endpoints 指向外部 DCS
      passwords:
        superuser: <superuser-password>
        replication: <replication-password>
        rewind: <rewind-password>
        restapi_user: postgres
        restapi_password: <restapi-password>
      members:
        node1:
          container_name: pgcli-patroni-app-node1
          image_tag: ghcr.io/mars-base/pgcli/pgcli-patroni:18-4.1.5
          host_port: 35590
          restapi_port: 39090
          data_dir: /home/you/.pgcli/addon/patroni/app/node1
          autostart: false

端口来自两个独立池 —— 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: app-default
namespace: /service/
name: node1

etcd3:
    hosts: 10.0.0.11:2379       # 来自 --etcd-endpoints 或本地 etcd 成员
    protocol: http

restapi:
    listen: 0.0.0.0:8009
    connect_address: 10.0.0.11:8009
    authentication:
        username: postgres
        password: <自动生成>

bootstrap:
    dcs:
        ttl: 30
        loop_wait: 10
        retry_timeout: 10
        maximum_lag_on_failover: 1048576
        postgresql:
            use_pg_rewind: true
            use_slots: true
            parameters:
                wal_level: replica
                hot_standby: "on"
    initdb:
        - encoding: UTF8
        - data-checksums
    pg_hba:
        - local all all trust
        - local replication all trust
        - host all all all scram-sha-256
        - host replication all all scram-sha-256

postgresql:
    listen: 0.0.0.0:35532
    connect_address: 10.0.0.11:35532
    data_dir: /var/lib/postgresql/data
    bin_dir: /usr/lib/postgresql/18/bin
    pgpass: /patroni/.pgpass
    use_unix_socket: true
    use_unix_socket_repl: true
    authentication:
        superuser:
            username: postgres
            password: <自动生成>
        replication:
            username: replicator
            password: <自动生成>
        rewind:
            username: rewind_user
            password: <自动生成>
    parameters:
        unix_socket_directories: /var/lib/postgresql

要点:

  • 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 edit-config app --show

这是 pg ha ctl app -- show-config 的便捷别名。输出是存在 /service/<scope>/config 里的完整 JSON。

修改参数

# 设置单个参数
pg ha edit-config app -- -s loop_wait=5

# 设置多个参数
pg ha edit-config app -- -s loop_wait=5 -s retry_timeout=3

# 不确认直接应用(脚本场景)
pg ha edit-config app -- -s loop_wait=5 --force

# 交互式编辑(用 $EDITOR 打开当前配置)
pg ha edit-config app

改动后,所有成员在下一个循环(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 下设置:

pg ha edit-config app -- -s 'postgresql.parameters.max_connections=200'
pg ha edit-config app -- -s 'postgresql.parameters.work_mem=64MB'

这会触发 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 可调参数,包含默认值、约束条件和示例。

扩展安装

pg ha extension install app pg_stat_statements,pg_cron   # 安装
pg ha extension list app                                  # 列出
pg ha extension remove app pg_cron                        # 卸载

完整参考: 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 中的所有可动态配置参数参考

这些参数存储在 DCS(etcd)的 /service/<scope>/config 下,应用于集群的所有成员。使用 pg ha edit-config 修改:

pg ha edit-config app -- -s loop_wait=5
pg ha edit-config app -- -s 'postgresql.parameters.max_connections=200'
pg ha edit-config app --show          # 查看当前配置

参考: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 时的约束:

loop_wait + 2 * 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 参数:

pg ha edit-config app -- -s 'postgresql.parameters.max_connections=200'
pg ha edit-config app -- -s 'postgresql.parameters.work_mem=64MB'
pg ha edit-config app -- -s 'postgresql.parameters.wal_log_hints=on'

备用集群

如果定义了此节,集群将作为备用集群引导,从远端主库流式复制。

参数 说明
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 不应触碰的槽(属性集列表)。任何子集匹配都会导致该槽被忽略

永久槽示例

slots:
  permanent_physical_slot:
    type: physical
  permanent_logical_slot:
    type: logical
    database: my_db
    plugin: pgoutput

ignore_slots:
  - name: externally_managed_slot
    type: physical

节点固定的物理槽

对于固定的集群拓扑,为每个节点定义永久物理槽,防止临时中断期间槽被删除:

slots:
  node1:
    type: physical
  node2:
    type: physical
  node3:
    type: physical

警告: 永久复制槽仅从 primary/standby_leader 同步到副本。应用程序应在 leader 节点上使用它们。在副本上使用永久槽会导致整个集群的 pg_wal 无限增长。例外:与 Patroni 成员名匹配的物理槽(由 Patroni 创建和维护)在所有节点间同步,用于节点间复制。

查看当前配置

pg ha edit-config app --show

或直接从 etcd 读取:

ETCDCTL_ENDPOINTS=http://10.0.0.1:2379 pg etcdctl get /service/app-default/config -- --prefix

3 - Patroni REST API

Patroni REST API 端点参考:健康检查、监控、集群管理

Patroni 在每个成员上暴露一个 HTTP REST API,提供健康检查端点(供负载均衡器和 Kubernetes 探针使用)、监控数据(含 Prometheus 指标)以及集群管理操作。

参考:Patroni 官方文档、 Pigsty REST API。

查找 REST API

每个成员的 REST API 端口由 pgcli 自动分配,存储在 pg.yaml 的 addons.patroni.<scope>.members.<name>.restapi_port 下。端口也写入渲染后的 patroni.yml 的 restapi.connect_address。

# 查找 REST API 端口
pg ha status app          # 显示每个成员的 pg= 和 rest= 端口

认证

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 成员能执行写操作。

所以负载均衡器健康检查不需要认证:

# 无需 -u —— leader 返回 200,replica 返回 503
curl -s http://<host>:<port>/primary -w "%{http_code}"

写操作才需要认证。密码与 patroni.yml 的 restapi.authentication 中使用的 restapi_password 相同。通过导出命令获取:

# 导出密码到文件,然后提取 restapi_password
pg ha passwords app --file app-passwd.yml
grep restapi_password app-passwd.yml
# restapi_password: <密码>

# 示例:带认证的写操作
curl -u postgres:<restapi-密码> -X PATCH http://<host>:<port>/config -d '{"ttl": 60}'

健康检查端点

所有健康检查端点响应 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 的传统别名
# leader 返回 200,replica 返回 503
curl -s -u postgres:<pw> http://<leader-host>:<port>/primary -w "%{http_code}"

这些端点适用于负载均衡器健康检查,将写操作仅路由到当前主库。

仅副本端点(仅 replica 返回 200)

端点 说明
GET /replica 副本健康检查 —— 节点运行中、角色为 replica、且未设置 noloadbalance 标签时返回 200
GET /replica?replication_state=streaming 仅当副本正在流式复制(而非通过归档恢复追赶)时返回 200
GET /replica?lag=<max> 仅当复制延迟低于阈值时返回 200(字节或可读格式:10MB、1GB)
# 仅流式副本
curl -s -u postgres:<pw> "http://<replica-host>:<port>/replica?replication_state=streaming"

# 延迟 < 1 MB 的副本
curl -s -u postgres:<pw> "http://<replica-host>:<port>/replica?lag=1048576"

只读端点(主库和副本均返回 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。
# Kubernetes 探针示例
livenessProbe:
  httpGet:
    scheme: HTTP
    path: /liveness
    port: 8008          # REST API 端口
  initialDelaySeconds: 3
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    scheme: HTTP
    path: /readiness
    port: 8008
  initialDelaySeconds: 3
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

监控端点

GET /patroni

以 JSON 返回详细的节点状态。Patroni 在 leader 竞选期间内部调用,也供监控系统使用:

curl -s -u postgres:<pw> http://<host>:<port>/patroni | jq .

响应字段:

字段 说明
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

返回完整的集群拓扑 —— 所有成员及其角色、状态和复制状态:

curl -s -u postgres:<pw> http://<host>:<port>/cluster | jq .
{
  "members": [
    {
      "name": "node1",
      "role": "leader",
      "state": "running",
      "api_url": "http://10.0.0.11:8008/patroni",
      "host": "10.0.0.11",
      "port": 35532,
      "timeline": 1
    },
    {
      "name": "node2",
      "role": "replica",
      "state": "streaming",
      "host": "10.0.0.11",
      "port": 35533,
      "timeline": 1,
      "receive_lag": 0,
      "receive_lsn": "0/3000168",
      "replay_lag": 0,
      "replay_lsn": "0/3000168"
    }
  ],
  "scope": "app-default"
}

GET /config

返回存储在 DCS 中的当前动态配置:

curl -s -u postgres:<pw> http://<host>:<port>/config | jq .

等同于 pg ha edit-config <scope> --show。

GET /history

返回时间线历史。未发生时间线切换时(新集群)为空数组 []。

GET /metrics

以 Prometheus 格式返回监控数据,供 Prometheus 或兼容系统抓取:

curl -s -u postgres:<pw> http://<host>:<port>/metrics

关键指标:

指标 说明
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 节中定义的自定义 标签过滤:

# 仅带 dc=us-east 标签的副本
curl -u postgres:<pw> "http://<host>:<port>/replica?dc=us-east"

# 仅带 region=primary 标签的 leader
curl -u postgres:<pw> "http://<host>:<port>/leader?region=primary"

实用模式

负载均衡器路由

由于 GET 健康检查无需认证,负载均衡器可以直接轮询 REST API 端点。模式 (源自 Patroni 官方 haproxy.cfg 示例)是:后端 TCP 端口是 PostgreSQL 端口,但健康检查 走 REST API 端口,用 GET /,仅 leader 返回 200。

单一读写入口(所有流量 → 当前 leader):

global
    maxconn 100

defaults
    log global
    mode tcp
    retries 2
    timeout client 30m
    timeout connect 4s
    timeout server 30m
    timeout check 5s

listen stats
    mode http
    bind *:7000
    stats enable
    stats uri /

listen app
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server node1 <ip>:35532 maxconn 100 check port 8008
    server node2 <ip>:35533 maxconn 100 check port 8009
  • 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 承接读流量:

listen app_rw
    bind *:5000
    mode tcp
    option httpchk GET /
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server node1 <ip>:35532 maxconn 100 check port 8008
    server node2 <ip>:35533 maxconn 100 check port 8009

listen app_ro
    bind *:5001
    mode tcp
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2
    server node1 <ip>:35532 maxconn 100 check port 8008
    server node2 <ip>:35533 maxconn 100 check port 8009
  • app_rw(:5000)—— GET / → 仅 leader 为 UP → 写操作落到 leader。
  • app_ro(:5001)—— GET /replica → 仅 replica 为 UP → 读操作分摊到副本。

配合上文副本端点里的 GET /replica?lag=1MB,可把延迟过大的副本踢出读池。

curl 监控脚本

快速健康检查脚本:

#!/bin/bash
# 检查所有成员 —— GET 无需认证
for port in 8008 8009; do
  code=$(curl -s http://10.0.0.11:$port/health -o /dev/null -w "%{http_code}")
  echo "端口 $port: HTTP $code"
done

Prometheus 抓取配置

GET /metrics 无需认证,因此抓取配置不带凭据也能工作:

scrape_configs:
  - job_name: patroni
    static_configs:
      - targets:
        - '10.0.0.11:8008'   # node1
        - '10.0.0.11:8009'   # node2
    metrics_path: /metrics

4 - HA 集群扩展安装

在 Patroni 管理的 HA 集群中安装和管理 PostgreSQL 扩展

在 Patroni 管理的 HA 集群中安装 PostgreSQL 扩展,与单节点实例有本质区别。 本页解释原因,并介绍 pg ha extension 命令子树。

为什么不能用 pg extension install?

单节点流程(pg extension install <instance> <ext>)的工作方式是:

  1. 用 Pigsty 包构建 -ext 派生镜像
  2. 停止并用新镜像重建容器
  3. 编辑 postgresql.conf 设置 shared_preload_libraries
  4. 在容器内执行 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 正确编排了所有这些步骤。

工作原理

安装流程遵循特定顺序,避免上述陷阱:

1. 验证扩展名(IsExtensionKnown)
2. 用 Pigsty 包构建 -ext 镜像
3. patronictl pause <scope> --wait            ← 禁用自动故障切换
4. 逐个重建成员容器(每次一个,等待重新加入)
5. patronictl resume <scope> --wait           ← ⚠ 必须在 edit-config 之前
6. patronictl edit-config(设置 shared_preload_libraries)
   → Patroni 触发滚动重启(replica 先,leader 最后)
7. 等待滚动重启完成
8. 在 leader 上执行 CREATE EXTENSION
9. 将扩展列表保存到 pg.yaml

关键顺序是 resume 必须在 edit-config 之前:暂停状态的 Patroni 集群不会 应用 edit-config 变更。如果在暂停状态下 edit-config,shared_preload_libraries 的更新会被静默丢失。

纯内置扩展快速路径

如果所有请求的扩展都是内置的(contrib 扩展,如 hstore、uuid-ossp,随 PostgreSQL 一起发布),步骤 2-4 会完全跳过 —— 不构建镜像、不重建容器、不 暂停/恢复。流程直接走 edit-config + CREATE EXTENSION。

命令

安装

pg ha extension install <scope> <extension>[,<extension>...] [flags]

构建 -ext 镜像,暂停集群,重建本地成员容器。

  • 单主机集群(所有成员在本地):自动执行 apply(resume + edit-config + CREATE EXTENSION)
  • 跨主机集群:只执行 pause + recreate,集群保持暂停。需要在每台主机上分别运行 install,最后在任意主机上运行 pg ha extension apply 完成安装。

扩展以逗号分隔传入:

# 安装单个扩展
pg ha extension install app pg_stat_statements

# 安装多个扩展到指定数据库
pg ha extension install app pg_cron,pg_stat_statements --database mydb

# 跳过滚动重启确认提示
pg ha extension install app pgvector --auto-restart

参数:

参数 默认值 说明
--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 即可。

卸载

pg ha extension remove <scope> <extension>[,<extension>...] [flags]

在 leader 上执行 DROP EXTENSION,然后通过 patronictl edit-config 更新 shared_preload_libraries(触发滚动重启)。不会重建镜像或重建容器 —— -ext 镜像只增不减;磁盘回收很少见且需手动操作。

扩展以逗号分隔传入:

pg ha extension remove app pg_cron
pg ha extension remove app pg_stat_statements,pg_cron --auto-restart

列表

pg ha extension list <scope>

显示集群扩展的三个视图:

  • Config (pg.yaml): 存储在 pg.yaml 中的 extensions 列表
  • DCS (preload): 来自 patronictl show-config 的 shared_preload_libraries 值
  • Leader (installed): leader 上 pg_extension 中的实际扩展
$ pg ha extension list app
Cluster "app" extensions:

  Config (pg.yaml):  [pg_stat_statements pg_cron]
  DCS (preload):     shared_preload_libraries: pg_stat_statements,pg_cron
  Leader (installed): [pg_cron pg_stat_statements]

应用

pg ha extension apply <scope> [flags]

手动触发安装流程的后半段:恢复集群、执行 patronictl edit-config、在 leader 上执行 CREATE EXTENSION。

用于跨主机集群 —— 在每台主机上运行 install(各自构建镜像并重建本地 成员)后,在任意主机上运行一次 apply 完成 DCS 更新和扩展创建。

pg ha extension apply app
pg ha extension apply app --database mydb --auto-restart

⚠ --database 对所有已安装扩展生效。 apply 会对集群中全部扩展在指定数据库执行 CREATE EXTENSION IF NOT EXISTS,不仅限于本次新增的扩展。

跨主机工作流

在跨主机集群中,每台主机只管理自己的成员。扩展安装流程分为两个阶段, 全局只暂停/恢复一次,避免不必要的 leader 漂移:

第一阶段 —— 每台主机依次运行 install:

每台主机执行:

  • 在本地构建 -ext 镜像(合并 DCS 已有的扩展列表,确保包含所有包)
  • 暂停集群(幂等 —— 第二台主机不会因"已暂停"而报错)
  • 用新镜像重建自己的本地成员(同主机内 replica 先、leader 后)
  • 保存配置 —— 集群保持暂停状态,不 resume

推荐顺序:从 replica 所在主机开始。 如果 leader 所在主机先执行 install, leader 重建会触发故障切换(leader 漂移到其他主机)。从 replica 主机开始, leader 始终不动,直到最后才处理 leader 所在主机,最大限度减少 leader 漂移 带来的数据同步开销。

# 主机 A(只有 replica,先执行)—— 集群变为暂停状态
pg ha extension install app pg_stat_statements,pg_cron

# 主机 B(有 leader,后执行)—— 集群保持暂停
pg ha extension install app pg_stat_statements,pg_cron

第二阶段 —— 一次,在任意主机运行 apply:

  • 恢复集群(幂等 —— tolerate “not paused”)
  • 通过 patronictl edit-config 更新 shared_preload_libraries
  • 等待滚动重启
  • 在 leader 上执行 CREATE EXTENSION
pg ha extension apply app --auto-restart

注意: 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 集群配置中以集群级别跟踪:

addons:
  patroni:
    app:
      name: app
      extensions:
        - pg_stat_statements
        - pg_cron
      passwords:
        superuser: ...
      members:
        node1: { ... }
        node2: { ... }

这个列表是 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

pg ha extension install app pg_stat_statements,pg_cron --database mydb --auto-restart

构建 -ext 镜像,重建所有成员(带 pause/resume),在 DCS 中设置 shared_preload_libraries=pg_stat_statements,pg_cron 和 cron.database_name=mydb,然后在 mydb 中创建两个扩展。

安装 Citus(必须排在 preload 第一位)

pg ha extension install app pg_stat_statements,citus

即使 citus 列在第二位,preload CSV 也会生成为 citus,pg_stat_statements —— 带 PreloadFirst 的扩展始终放在位置 0。

卸载扩展

pg ha extension remove app pg_cron

从 leader 删除 pg_cron,从 shared_preload_libraries 中移除,并清除 cron.database_name。触发滚动重启。

查看已安装的扩展

pg ha extension list app

限制

  • 镜像重建是增量的。 卸载扩展不会重建 -ext 镜像或缩小它。要回收磁盘, 需用 podman image prune 手动清理旧镜像。
  • 没有按成员的扩展列表。 扩展是集群级别的 —— 所有成员共享相同的 -ext 镜像和相同的 shared_preload_libraries。
  • CREATE EXTENSION 只针对一个数据库。 PostgreSQL 扩展是按数据库的。要在 多个数据库中安装,需用 --database 分别指定每个数据库重新运行。
  • 跨主机需要手动协调。 每台主机必须先运行 install,然后才能运行一次 apply。pgcli 不会 SSH 到远程主机。

参考

5 - Patroni 集群备份

Patroni HA 集群的 pgBackRest 备份:backup setup、S3 仓库与 WAL 归档、pg ha snapshot 快照操作,以及副本 LSN 卡住时的 reinit 修复

本页给出 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 一次跑完,把备份基础设施全部拉起并让集群具备归档能力:

# 最简(备份数据落在默认 base-dir 下)
pg backup setup

# S3 对象仓库(Patroni 集群归档到 MinIO / AWS S3 的前提)。
# 存储主机在别处、手头还没有 ca.crt 时,先用一次 TLS 握手把它拉下来——无需 scp:
pg backup fetch-ca 10.0.0.9:9000          # 打印 SHA-256 指纹供核对
pg backup setup --s3-endpoint 10.0.0.9:9000 --s3-bucket pgbackrest \
    --s3-access-key admin --s3-ca-file ~/.pgcli/backup/repo-ca/ca-10.0.0.9-9000.crt

setup 做的事(配了 S3 仓库时,动手前先做一次 endpoint 预检:普通 TCP 拨号,打错主机名或存储没起会立刻失败退出,而不是拖到镜像拉取 / 容器启动之后才 把真正原因埋进噪声里):

  1. 构建/拉取 pgbackrest 镜像、建网络与目录、生成 backup 容器视图的 pgbackrest.conf 与成员本地归档视图的 pgbackrest-archive.conf。
  2. 拉起共享 backup 容器,随后验证仓库连通性:跑一次 pgbackrest repo-ls, 验证整条链路(TLS、凭证、bucket),并把失败映射成可操作的提示——证书错误指向 pg backup fetch-ca、access-denied 指向凭证、拒绝/超时指向 endpoint。
  3. 打通跨主机备份互信:各主机 pg ha create 时把本机备份公钥发布进集群的 etcd 注册表,setup 把全集群成员公钥合并成一份每集群一份的 authorized_keys bind-mount 进成员容器——sshd 每次登录重读该文件,所以后加入的主机无需重启就被 信任。S3 仓库 CA 走同一条路:配了 ca_file 的主机把证书发布进注册表,留空的 加入者自动拉取。(详见 HA → 备份跨主机 leader。)
  4. 开启 WAL 归档:检测到成员归档配置过期时,pause 集群、按"副本先、leader 后" 重建过期成员容器(recreate 同时就是 archive_mode 所需的 postmaster 重启)、 resume,并把同一份 archive_command 渲染进每个成员的 patroni.yml。
  5. 对每个集群 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)。

配好后确认:

pg backup status

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 集群侧的对应物。

# 全量备份(默认 --type full),--tail-logs 流式打印 pgBackRest 日志
pg ha snapshot create app --tail-logs

# 增量备份(自上次备份以来的变化)
pg ha snapshot create app --type incr

# 差异备份(自上次全量以来的变化)
pg ha snapshot create app --type diff

# 列出该集群的全部快照(表格:Start / Stop / Name / Type)
pg ha snapshot list app
pg ha snapshot list app --limit 5

# 删除指定快照(label 从 list 拿)——不带 --force 有交互确认
pg ha snapshot delete app 20260916-140131F_20260917-015154I
pg ha snapshot delete app <label> --force

说明:

  • 每次命令前,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 回放断点、复制流实际已停:

# 1) leader 侧无复制客户端、槽 inactive
pg exec --dsn "postgres://<user>@<leader_host>:<port>/postgres" \
  "SELECT client_addr, state FROM pg_stat_replication"
pg exec --dsn "postgres://<user>@<leader_host>:<port>/postgres" \
  "SELECT slot_name, active, confirmed_flush_lsn FROM pg_replication_slots"

# 2) 卡住的副本上没有 walreceiver
pg exec --dsn "postgres://<user>@<replica_host>:<port>/postgres" \
  "SELECT status FROM pg_stat_wal_receiver"

# 3) 副本容器日志在反复刷同一条(pg logs ha 按 scope+成员名取,
#    -f 持续跟随,-n 取更多行):
pg logs ha <scope> -m <member> -n 50 | grep -iE "prev-link|waiting for WAL"
#   LOG: record with incorrect prev-link ... at 0/20000060
#   LOG: waiting for WAL to become available at 0/20000078

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:

# CLUSTER_NAME 是带 namespace 的 scope(pg ha status 里 Cluster: 那行的名字)
pg ha ctl <scope> -- reinit <scope>-<ns-suffix> <member> --force
# 例(namespace=default → 后缀 -default):
pg ha ctl app -- reinit app-default node1 --force

注意 -f 无效:patronictl reinit 只认全写 --force(不像 switchover/failover 的 --yes)。Success: reinitialize for member node1 即已下发。

验证恢复。 reinit 会清空并重建副本数据目录,随后 basebackup + 回放。轮询确认回到 recv == replay 且 status=streaming:

pg exec --dsn "postgres://<user>@<replica_host>:<port>/postgres" \
  "SELECT pg_last_wal_receive_lsn() recv, pg_last_wal_replay_lsn() replay,
          (SELECT status FROM pg_stat_wal_receiver) wr"
#   期望:recv == replay,wr = streaming

pg ha status app    # 该副本 Replay LSN 追平、Lag 收敛

reinit 完成后,恢复 pg ha snapshot create app --type full 一次全量,把快照基线对齐到 健康的拓扑。

6 - Patroni 集群恢复

用 pg ha restore 为 Patroni HA 集群做时间点恢复(PITR):自定义 bootstrap 机制、leader 本机性预检、端到端操作流程,以及恢复之后的重新基线步骤

本页是 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 配方:

  1. 先 pause 集群(patronictl pause)。在任何东西被停止之前先冻结 failover:集群存活时停掉本机 leader,会让 Patroni 把一台本机停不掉的远端 副本 promote 成新 leader,那个远端 leader 随后会抢着在旧时间线上重新夺回 DCS——从破坏路径内部瓦解了 leader 本机性预检。pause 正是为了堵住这一点。 若上一次尝试已经把集群拆掉(DCS 已清、成员已停),重跑的 restore 没有可 pause 的活集群;那本就是我们要的"已冻结"状态,因此空 DCS 下的 pause 失败被容忍, 流程继续。

  2. 清除集群的 DCS 身份(patronictl remove)。Patroni 的 bootstrap.dcs 只在 DCS 没有 config key 时才执行,所以移除它正是重新武装 bootstrap 路径的动作。

  3. 选一个本机成员,改写它的 patroni.yml,把默认的 initdb bootstrap 替换成一条指向 目标时间点的 pgbackrest restore method:

    bootstrap:
      method: pgbackrest
      pgbackrest:
        command: 'pgbackrest --stanza=pgcli_<scope> --type=time --target="<time>" --target-action=promote --delta restore'
        no_params: true
        keep_existing_recovery_conf: true

    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 上作为运行时参数生效。

  4. 清空该成员的 PGDATA 并重建其容器。Patroni 面对空数据目录 + 无 DCS 配置,跑该 method,在新时间线上恢复到目标点,并借 --target-action=promote 把自己 promote 成可写 leader。

  5. 其余成员重新加入新 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 在本机(从而可被停止)就消除了这个竞态。

你会看到什么。

# dry-run:一条醒目告警,不拒绝——你仍可检视计划
pg ha restore app --time "2026-08-26 15:30:00+00" --dry-run
#   [!!] current leader "node3" is NOT a local member — the real run REFUSES until
#        leadership is on this host...

# 在 leader 为远端的主机上真实执行:硬报错,什么都不碰
pg ha restore app --time "2026-08-26 15:30:00+00"
#   current leader "node3" is not a local member of scope "app" ...
#   Move leadership here first (pg ha switchover app ...) or run on the leader's host

怎么办。 把 leadership 挪到你正站着的这台主机的某个成员上,再重试:

pg ha switchover app    # 或: pg ha failover app ...
pg ha status app        # 确认 leader 现在是本机成员

或者改到 leader 自己所在的主机上跑 pg ha restore。leader 为空 / 读不到时(例如最早 的 bootstrap、还没有任何成员 publish 自己)不阻断执行。

与单实例 pg restore 的差异

  • 总是 promote。 没有"只读挂起、检查、再换个时间重试"的两步——bootstrap 恢复以一个 新时间线上的可写 leader 收场。执行前先用 --dry-run 确认目标时间。(机制第 1 步的 patronictl pause 与此无关:它是在恢复期间冻结 failover,不是只读检查窗口;而且 DCS 被清除后由新 bootstrap 重新发布配置,其中没有 pause 键,集群自动处于未暂停状态。)
  • 没有 --promote flag——promote 是自动的(它烧进了 bootstrap 命令里)。
  • 必须在拥有成员的主机上运行,且 leader 必须是本机的(见上文预检)。

操作流程:把集群恢复到某个时间点

完整 flag 集:--time(必填,格式见恢复)、 --member、--dry-run、--tail-logs、--force。

1. 用 dry-run 确认目标安全。 什么都不碰;你能看到 stanza、选中的 bootstrap 成员、确切的 pgbackrest 命令,以及(若相关)leader 本机性告警。

pg ha restore app --time "2026-08-26 15:30:00+00" --dry-run

2. 确保 leader 在本机(仅当 dry-run 告警时)。switchover,然后复查状态。

pg ha switchover app
pg ha status app

3. 恢复。 默认会弹确认提示,列出 scope、stanza、目标时间、bootstrap 成员,以及 “PERMANENTLY LOST” 警告。用 --tail-logs 流式输出恢复日志;用 --force 跳过提示以便 自动化。

pg ha restore app --time "2026-08-26 15:30:00+00" --tail-logs
# 或者显式指定承载 bootstrap 的成员:
pg ha restore app --time "2026-08-26 15:30:00+00" --member node1 --force

4. 确认新集群起来。 waitForLeader 轮询 DCS 直到出现 leader(上限 15 分钟),随后 重新加入的成员作为副本启动。

pg ha status app                     # 新 leader 在已切换的时间线上,副本在 streaming
pg exec --dsn "postgres://<user>@<leader_host>:<port>/postgres" \
  "SELECT ... FROM your_table"       # 数据停在目标时间;之后的提交都没了

恢复之后:给新时间线重新基线

恢复不是最后一步。数据目录被重建过,所以在集群重新具备备份能力之前,有两件收尾必须做:

  1. 建一个新的全量快照,让后续 PITR 在新时间线上有基点:

    pg ha snapshot create app --type full

    这步通常直接成功:同一 stanza 的 pgBackRest 恢复在时间线切换时保持 PostgreSQL 的 system-id 不变(实测验证),所以恢复后的首个快照不会撞上 [051] system-id ... do not match stanza。集群恢复之后不需要 pg backup stanza-upgrade。(真正会改 system-id 的是往同一个 stanza 里重新 initdb——比如全新集群复用了旧 stanza 名——那才是 [051] 和 stanza-upgrade 的适用场景。)

  2. 拉回任何没有自动重新加入的成员——通常是丢了旧 leader 的跨主机成员。从新 leader 重新初始化它(只破坏该副本的数据目录):

    pg ha ctl app -- reinit app-<ns> <member> --force

    (完整细节见 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 的 bootstrap pgbackrest ... restore 命令上加 --recovery-option=recovery_target_timeline=<旧TL>。 目标时间点不跨越已 promote 分支的首次 PITR 不会遇到这个。

7 - HA Exec / psql

用 pg ha exec 和 pg ha psql 对 Patroni HA 集群执行 SQL:从 DCS 零配置解析 leader、免手拼 dsn、免进成员容器,并可用 –member 指定只读副本或跨主机节点

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)接受任意来源,所以远端 成员就像复制流量本来就能跨主机到达一样,可达。

用法

# 对 leader 跑一次性 SQL
pg ha exec app "SELECT version()"
pg ha exec app "SELECT count(*) FROM pg_stat_activity"

# 对 leader 上的另一个库
pg ha exec app --database mydb "SELECT * FROM t LIMIT 5"

# 只读:打副本而不是 leader
pg ha exec app --member node2 "SELECT pg_is_in_recovery()"

# 对 leader 开交互式 psql
pg ha psql app

# 对指定成员(哪怕跨主机)开交互式 psql
pg ha psql app --member node3

# -- 之后的参数透传给 psql
pg ha psql app -- -c "SELECT 1"
pg ha psql app --database mydb -- -x

pg ha exec 把 SQL 作为尾随参数接收(所以通常整体引成一个字符串);pg ha psql 开一个 交互式 shell,把 -- 之后的内容原样转给 psql。--database 在两条命令上都选择目标库 (默认 postgres)。没有 --user——超管是 pgcli 唯一持有凭证的角色,所以连上去的就是它。

交互式与脚本化

pg ha psql 在你的 stdin 是 tty 时分配一个 TTY;不是 tty 时(管道喂脚本、在 CI 里跑)它会 关掉分页器,让会话跑完就退出而不是卡在 less 里。所以下面两种用法都符合预期:

pg ha psql app < migrations.sql      # 管道喂文件,无分页器,跑完即退
printf '\conninfo\n' | pg ha psql app   # 脚本化单行,输出流回终端

关于输出

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

完整且实测通过的端到端流程:单 member 的 Patroni 集群,备份到一台对外提供自带(自签)域名证书的 MinIO 插件——全程运行在一个完全隔离的 pgcli 环境中

这是一页实操示例,每一步都真实跑通并验证过:创建 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 开发/测试绰绰有余:

pg addon install etcd --name m1
# pgcli-etcd-default-m1,client 127.0.0.1:2379,peer 2380,cluster "pgcli-etcd"

(如果要把这个 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):

mkdir -p ~/.pgcli-app1/certs
pg cert --host "minio1.test,127.0.0.1,<主机IP>" --valid-duration 8760h \
    --cert-file ~/.pgcli-app1/certs/store1.crt --key-file ~/.pgcli-app1/certs/store1.key
chmod 600 ~/.pgcli-app1/certs/store1.key

openssl x509 -in ~/.pgcli-app1/certs/store1.crt -noout -subject -issuer -ext subjectAltName -dates
# subject=...
# issuer=...                         <- 自签:叶证书与根证书就是同一张
# DNS:minio1.test, IP Address:127.0.0.1, IP Address:<主机IP>

pg cert 写出的是单张自签叶证书(CA:FALSE、服务端认证用途),它自己就是自 己的信任锚——形态与公共 CA/企业 CA 签发的证书一致,只是没花钱去买。若是公 共 CA(或企业 CA)证书,这一步就只是"文件你本来就有"——把 flag 指向你的 fullchain.pem(先叶、后中间证书)和私钥即可,客户端侧反而更简单:锚定在受信 CA 的链根本不需要 --s3-ca-file。

第 1 步 —— 隔离环境

mkdir -p ~/.pgcli-app1
pg config init -o ~/.pgcli-app1/pg.yaml \
    --base-dir /home/fish/bucket/pgcli-data-app1 \
    --namespace app1 \
    --pg-start-port 36500 --pg-ssh-port 43500

--namespace 让这份配置创建的所有容器名都与众不同,于是同一台 podman 上可以 并排跑两套环境。后续每条命令都要带 -c——本页此后的每个 pg 都指 pg -c ~/.pgcli-app1/pg.yaml。

端口段。 默认端口池(etcd 2379、MinIO 9000、Patroni 35532……)与生产配 置的起始值相同,而 host 网络容器的回环端口会被 podman 发布出来——所以处处 显式给端口、两套环境互不重叠,别让它们自动分配到同一批数字。

第 2 步 —— 服务自带证书的 MinIO

pg addon install minio --name store1 \
    --api-port 9010 --console-port 9011 --listen 0.0.0.0 \
    --tls-cert ~/.pgcli-app1/certs/store1.crt \
    --tls-key  ~/.pgcli-app1/certs/store1.key
-> MinIO BYO cert: CN="" issuer="" valid 2026-09-18 → 2027-09-18
  [OK] TLS certs (BYO: /home/…/.pgcli-app1/certs/store1.crt, valid 2026-09-18 → 2027-09-18)
         self-signed: clients pin the issuing CA (pg backup setup --s3-ca-file <ca.pem>)
  [OK] MinIO container started

安装期校验会把证书与密钥配对(不匹配、已过期、CA 当叶用、仅限非服务端用途的 证书都在此处直接失败),打印有效期;又因为是自签证书,提示客户端将把这张证书 钉为信任锚。这里用 --listen 0.0.0.0 而非回环:备份容器经 host 网络用主机 IP 访问存储,而该 IP 就在 SAN 里。快速证明它服务的是你的证书:

curl -s --cacert ~/.pgcli-app1/certs/store1.crt \
     https://127.0.0.1:9010/minio/health/live -o /dev/null -w "%{http_code}\n"
# 200 —— 且 curl 校验的链正是我们给的那张证书

建仓库桶(pgBackRest 不会代建):

pg mc alias set store1 https://127.0.0.1:9010 admin '<root密码>' -- --insecure
pg mc mb store1/pgbackrest

第 3 步 —— Patroni 集群

pg ha create app1 --member nodea \
    --etcd-endpoints 127.0.0.1:2379 \
    --host-port 35632 --restapi-port 8028 \
    --advertise-host <主机IP>
!  No S3 backup repo configured on this host (backup.repo.s3): the member will
   be created WITHOUT WAL archiving ... Configure a repo and run `pg backup
   setup` to wire archiving in (it recreates members as needed).
  [OK] Patroni member nodea started
✓ Patroni member "nodea" installed in scope "app1"
  scope (DCS):  app1-app1        # 命名空间限定:"app1-" + scope "app1"
  pg:           <主机IP>:35632

那条警告是设计好的操作顺序,不是问题:创建时尚无仓库,成员先不开归档上线; 下一步的 backup setup 配好仓库后会重建成员并带上归档。注意 DCS 里的 scope 是 app1-app1(命名空间前缀 + scope)——给 pg ha 命令传的都是裸 scope(app1),pgcli 自己换算限定形式;限定名也让 stanza 与同一 bucket 上 别的命名空间可能跑的 app1 互不冲突。

塞点值得备份的数据(顺带验证客户端 scram 认证链路):

pg ha exec app1 "CREATE TABLE IF NOT EXISTS byo_probe(id serial primary key,
    note text, ts timestamptz default now());
    INSERT INTO byo_probe(note) SELECT 'row-'||g FROM generate_series(1,500) g;"
pg ha exec app1 "SELECT count(*) FROM byo_probe"   # 500

第 4 步 —— 把备份栈指向自带证书的 store

pg backup setup \
    --s3-endpoint <主机IP>:9010 --s3-bucket pgbackrest \
    --s3-access-key admin --s3-secret-key '<root密码>' \
    --s3-ca-file ~/.pgcli-app1/certs/store1.crt

--s3-ca-file 直接把我们自己的证书当信任锚——自签证书的叶里就含其根, 所以传给 --tls-cert 的那个文件就是 CA 文件。pgBackRest 把 PEM bundle 原 样交给校验器、不检查内容,因此公共 CA 证书可以完全不传这个 flag,私有 CA 把 链传在这里即可。随后这次运行会验穿整条链路并重接成员:

  [OK] <主机IP>:9010 reachable
  [OK] repo CA published to 1 cluster registry/registries (from …/store1.crt)
  [OK] repository reachable                      # 一次经 TLS 的 repo-ls:凭据 + CA + 桶全通
  [stale] app1/nodea: mounts the backup-side pgbackrest.conf (pg1-host breaks archive-push)
-> Pausing cluster app1 (no auto-failover during recreate)
  -> Recreating member nodea...                  # 此次带上了 archive-push
  [OK] app1: archive-ready after recreating 1 member(s)
  [OK] stanza pgcli_app1-app1 created
  [OK] check pgcli_app1-app1

“repo CA published to the cluster registry” 这行是 etcd 分发通道:本 scope 的 其他每个成员(或未来新加的,比如第二台主机)都从 DCS 自动取到 CA,无需手工 scp、无需额外 flag。

第 5 步 —— 备份,并证明数据真的到了

pg ha snapshot create app1 --type full
#   Name:    20260918-091519F
#   Type:    full
pg ha snapshot list app1                          # pgBackRest 在仓库里能看到这份备份

独立证据是对象确实躺在桶里——而写入它们的是由我们的证书终结的 TLS 会话:

pg mc ls store1/pgbackrest -- --recursive
# …/backup/pgcli_app1-app1/20260918-091519F/backup.manifest
# …/backup/pgcli_app1-app1/20260918-091519F/pg_data/base/…  (.zst 文件)
# …/archive/pgcli_app1-app1/18-1/0000000200000000/000000020000000000000005-….zst
# …/archive/pgcli_app1-app1/18-1/0000000200000000/000000020000000000000005.00000028.backup
# …/archive/pgcli_app1-app1/archive.info

archive/ 是持续的 WAL 归档(重建后的成员跑的 archive-push),backup/ 是全量快照。确认归档器健康:

pg ha exec app1 "SELECT archived_count, failed_count, last_archived_wal
                 FROM pg_stat_archiver"

一个来自实跑的细枝末节,知道可免追鬼: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 配置名下,拆除也按该配置逆序进行(数据目录显式删 除):

pg -c ~/.pgcli-app1/pg.yaml ha remove app1 --scope-all --clean-data
pg -c ~/.pgcli-app1/pg.yaml backup remove --clean-data
pg -c ~/.pgcli-app1/pg.yaml addon remove minio --name store1 --clean-data
rm -rf ~/.pgcli-app1 /home/fish/bucket/pgcli-data-app1

要用 --scope-all(而不是 --member):它会 patronictl remove 整个 scope 的 DCS 键并清空 pgcli 成员注册表;逐个 member 删除会把这些残留留在 共享 etcd 里。

etcd m1 属于生产环境——上面所有操作都碰不到它,这正是隔离的用意。

相关

9 - 生成证书 —— pg cert

pg cert 签发带域名与 IP SAN 的自签证书,服务于任何不想走 CA 又需要 TLS 的场合——开发测试服务器、内部端点、对外提供自带 TLS 证书的 MinIO。flag 一览,写出的内容(单张自签叶证书,不是一条链),以及它如何充当自己的信任锚

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:

pg cert --host "minio.test,127.0.0.1,10.0.0.9" \
  --cert-file minio.crt --key-file minio.key

它不往 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 使用的独立程序:

make gencert          # -> bin/gencert
bin/gencert -host "minio.test,10.0.0.9" -cert-file minio.crt -key-file minio.key

flag 完全相同,只是单横线形式(-host、-cert-file……)。两者背后是同一个 internal/certgen 包,产出的证书类型一致——pg cert 只是让你留在 CLI 里的那 条路径。

示例:一台主机上的完整自带证书链路

# 1. 造一张 MinIO 将要对外提供的证书(SAN 覆盖客户端拨号用的名字与 IP)
mkdir -p ~/.pgcli/certs
pg cert --host "minio.test,127.0.0.1,<主机IP>" --valid-duration 8760h \
  --cert-file ~/.pgcli/certs/store.crt --key-file ~/.pgcli/certs/store.key
chmod 600 ~/.pgcli/certs/store.key

# 2. MinIO 以 HTTPS 提供它
pg addon install minio --name store --listen 0.0.0.0 \
  --tls-cert ~/.pgcli/certs/store.crt --tls-key ~/.pgcli/certs/store.key

# 3. 集群的备份指向它;被提供的那张叶证书本身就是 CA 文件
pg backup setup --s3-endpoint <主机IP>:9000 --s3-bucket pgbackrest \
  --s3-access-key admin --s3-ca-file ~/.pgcli/certs/store.crt

第 3 步直接引用你刚造好的 .crt,因为本机已经持有它。从没见过这个文件的 远端主机则用一次握手把它取回来,无需 scp——fetch-ca 能认出 MinIO 提供的 自签叶证书并把它作为锚保存:

# 在另一台主机上,替代把 ~/.pgcli/certs/store.crt 拷过去:
pg backup fetch-ca <主机IP>:9000
#   [OK] CA fetched from <主机IP>:9000
#        saved:    <base-dir>/backup/repo-ca/ca-<主机IP>-9000.crt
#        SHA-256:  d4df…81e2   (与存储主机的 sha256sum 对拍)
pg backup setup --s3-ca-file <base-dir>/backup/repo-ca/ca-<主机IP>-9000.crt

相关

10 - S3 存储高可用方案

让 pgBackRest 背后的 MinIO/silo 对象存储也高可用:pgcli 暴露的四种部署形态(SNSD/SNMD/MNSD/MNMD)、原生多盘与 ZFS 磁盘冗余层的取舍,以及「每台主机多块盘的分布式集群」的两条路

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-pool)
sudo zpool create minio-pool raidz1 /dev/sdb /dev/sdc /dev/sdd

# 为 store 开一个 dataset:recordsize 1M 适配对象大文件,关掉 atime 写放大
sudo zfs create -o recordsize=1M -o atime=off minio-pool/store

pg addon install minio --name store --data-dir /minio-pool/store   # 或 silo

池必须落在与根文件系统不同的设备上——这恰好也是 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 说明)。

# 任意一台上执行一次——一张 SAN 覆盖所有节点地址的自签叶子:
pg cert --host 10.0.0.11,10.0.0.12,10.0.0.20,10.0.0.21 \
        --cert-file grid.crt --key-file grid.key
# 把 grid.crt + grid.key 复制到每个节点——各处都是逐字节相同的文件

# 节点 1(10.0.0.11),四块数据盘已挂载;节点 2-4:同样的命令、同一份
# grid.crt/grid.key、各自的 --drive 路径、完全相同的 16 条端点矩阵、完全相同
# 的 --root-password:
pg addon install minio --name store \
  --tls-cert grid.crt --tls-key grid.key \
  --listen 0.0.0.0 \
  --drive /mnt/minio/disk1 --drive /mnt/minio/disk2 \
  --drive /mnt/minio/disk3 --drive /mnt/minio/disk4 \
  --root-password '<共享密码>' \
  --endpoint https://10.0.0.11:9000/data1 --endpoint https://10.0.0.11:9000/data2 \
  --endpoint https://10.0.0.11:9000/data3 --endpoint https://10.0.0.11:9000/data4 \
  --endpoint https://10.0.0.12:9000/data1 --endpoint https://10.0.0.12:9000/data2 \
  --endpoint https://10.0.0.12:9000/data3 --endpoint https://10.0.0.12:9000/data4 \
  --endpoint https://10.0.0.20:9000/data1 --endpoint https://10.0.0.20:9000/data2 \
  --endpoint https://10.0.0.20:9000/data3 --endpoint https://10.0.0.20:9000/data4 \
  --endpoint https://10.0.0.21:9000/data1 --endpoint https://10.0.0.21:9000/data2 \
  --endpoint https://10.0.0.21:9000/data3 --endpoint https://10.0.0.21:9000/data4

在活体的 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 个节点上各自执行:建本地池(布局可按节点不同——见下)
sudo zpool create minio-pool raidz1 /dev/sdb /dev/sdc /dev/sdd
sudo zfs create -o recordsize=1M -o atime=off minio-pool/store

# 节点 1(10.0.0.11):
pg addon install minio --name store \
  --listen 10.0.0.11 \
  --data-dir /minio-pool/store \
  --root-password '<共享密码>' --tls \
  --endpoint http://10.0.0.11:9000/data \
  --endpoint http://10.0.0.12:9000/data \
  --endpoint http://10.0.0.20:9000/data \
  --endpoint http://10.0.0.21:9000/data

# 节点 2-4:同样的命令、各自的 --listen、完全一致的 endpoint 列表与密码

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)。盘确实不 一致时,精简置备(稀疏 zvol dataset,或直接 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 签发的自 带证书)行为一致——见生成证书。

相关