跳转到主要内容

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

返回本页常规视图.

插件 (Addons)

pgcli 插件管理指南

pgcli 支持通过插件系统扩展 PostgreSQL 功能。插件是独立的容器,为 PostgreSQL 实例提供额外能力,无需修改数据库本身。

支持的插件

目前支持以下插件:

插件 说明
pgbouncer 连接池管理器,提供事务级连接池化
etcd 分布式键值存储——独立运行、可组集群,用于 HA / DCS
pgdog Postgres 代理——连接池化、负载均衡与分片
postgrest 将 PostgreSQL schema 暴露为 REST API——单容器无状态,本地或远程(任意 PG 端点)
haproxy Patroni 集群前的 TCP 负载均衡——读写一体或读写分离(仅 Linux)
minio S3 兼容对象存储,含 Web 控制台——单机或跨主机分布式纠删码集群(Linux 与 macOS)
silo S3 兼容对象存储(Pigsty 的 MinIO 分支)——功能面与 minio 插件一致,共用同一端口池;自带 mcli 客户端(Linux 与 macOS)
rustfs S3 兼容对象存储(Rust 重新实现)——SNSD/SNMD/MNMD 纠删码布局,固定的容器 uid 由 pgcli 定制镜像在内部消化,共用同一端口池(仅 Linux)

每个插件都有独立页面,包含命令、参数与故障排除说明。

Patroni 高可用(pg ha)不在此列——它不通过 pg addon 安装,而是独立的顶层命令, 文档也已从本节拆出,见 HA 集群。本节的 etcd 与 HAProxy 仍是可被 pg ha 集群使用的插件。

工作原理

插件作为独立容器运行,通过 pg.yaml 管理:

  1. pg addon install 生成配置并启动插件容器
  2. 配置与数据存放在 <base-dir>/addon/<addon-name>/
  3. 插件容器通过主机网络通信
  4. 配置更新时容器自动重启

命名空间隔离: 插件遵循配置的 namespace 设置 —— 容器名包含命名空间前缀, 因此不同配置文件可以管理互不冲突的独立插件。

常用命令

pg addon install <addon> [flags]   # 安装 / 重新配置(幂等)
pg addon list                       # 查看所有已安装插件及状态
pg addon remove <addon> [flags]     # 移除插件、容器与数据

各插件的具体参数与示例见其独立页面: Pgbouncer · etcd · PgDog · PostgREST · HAProxy · MinIO · Silo · rustfs。Patroni 相关页面见 HA 集群。

1 - PgBouncer

通过 PgBouncer 插件为 PostgreSQL 实例提供连接池

PgBouncer 是轻量级的 PostgreSQL 连接池。作为 pgcli 插件,它以容器形式运行在 一个或多个 PostgreSQL 实例前面,在大量短生命周期客户端会话之间复用服务端连接 —— 默认使用事务级池化。

PgBouncer 支持两种部署模式:

  • 本地模式: pg addon install pgbouncer -i <instance> —— 存储在 instances.<name>.addons
  • 远程模式: pg addon install pgbouncer --dsn <dsn> --pg-name <name> —— 存储在顶层 addons.pgbouncer

平台支持

PgBouncer 支持 Linux(host 网络,含跨主机连接池)与 macOS 单主机 dev/test(容器加入 pgcli-net bridge 网络并发布端口,与 podman machine 下的 PG 实例同一套机制)。macOS 上:

  • 本地模式开箱即用:PgBouncer 自动按容器名访问受管实例,无需配置地址。
  • 远程模式: --dsn 里的主机必须是从 Mac 可达的地址 —— 别填 127.0.0.1,那是 Mac 本机,而不是 podman machine 虚拟机。
  • 客户端连接仍然指向 127.0.0.1:<port>;gvproxy 会把发布端口转发到 Mac 的 回环,与 PG 实例完全一致。

工作原理

  1. pg addon install pgbouncer 生成配置文件并启动容器
  2. 配置文件存储在 <base-dir>/addon/pgbouncer/<instance>/
  3. 容器通过主机网络(Linux)或 pgcli-net bridge 按容器名(macOS)访问 PostgreSQL
  4. 配置文件更新时自动重启容器应用变更

命名空间隔离: PgBouncer 遵循配置的 namespace 设置。容器名和认证用户包含 命名空间前缀(例如 pgb_<namespace>_<instance>),因此使用不同命名空间的不同配置 文件可以为同一个 PostgreSQL 实例创建独立的连接池,而不会相互冲突。

# 使用命名空间 "prod" 的配置
pg -c prod-pg.yaml addon install pgbouncer -i mypg
# → 容器: pgcli-pgbouncer-prod-mypg, 认证用户: pgb_prod_mypg

# 使用命名空间 "staging" 的配置
pg -c staging-pg.yaml addon install pgbouncer -i mypg
# → 容器: pgcli-pgbouncer-staging-mypg, 认证用户: pgb_staging_mypg

命令

安装

# 本地模式:为托管实例安装
pg addon install pgbouncer -i mypg

# 远程模式:为远程 PG 实例安装
pg addon install pgbouncer \
  --dsn "postgres://admin:pass@10.0.0.20:35432/mypg_db" \
  --pg-name my-remote-pool

# 指定连接池参数
pg addon install pgbouncer -i mypg \
  --max-client-conn 200 \
  --default-pool-size 30 \
  --min-pool-size 5 \
  --reserve-pool-size 10 \
  --max-db-connections 50 \
  --query-timeout 60 \
  --admin-users admin \
  --log-connections 1

参数说明:

参数 说明 默认值
--dsn PG 实例连接字符串(远程模式) —
--pg-name 远程 PgBouncer 标识名(与 –dsn 一起使用) —
--max-client-conn 最大客户端连接数 100
--default-pool-size 默认连接池大小 20
--min-pool-size 最小连接池大小(预热) 0
--reserve-pool-size 预留连接池大小(突发) 0
--max-db-connections 每数据库最大连接数 50
--max-user-connections 每用户最大连接数 0(无限制)
--server-idle-timeout 空闲服务端连接超时(秒) 600
--server-lifetime 服务端连接最大生命周期(秒) 3600
--server-connect-timeout 连接 PostgreSQL 超时(秒) 15
--query-timeout 查询超时(秒) 0(无限制)
--query-wait-timeout 等待连接超时(秒) 120
--idle-transaction-timeout 空闲事务超时(秒) 0
--transaction-timeout 事务超时(秒) 0
--admin-users 管理员用户列表 (空)
--stats-users 只读统计用户列表 (空)
--log-connections 记录连接日志 0
--log-disconnections 记录断开日志 0

列出

pg addon list

PgBouncer 出现在 Local add-ons / Remote add-ons 段:

Local add-ons:
  pgbouncer (instance: mypg)
    Status:    running
    Host:      127.0.0.1:56432
    Port:      56432
    Pool mode: transaction
    Container: pgcli-pgbouncer-default-mypg

Remote add-ons:
  pgbouncer (pg-name: my-remote-pool)
    Status:    running
    Host:      10.0.0.20:56433
    Port:      56433
    Pool mode: transaction
    Container: pgcli-pgbouncer-default-my-remote-pool

启动与停止

宿主机重启或手动停掉后,无需重跑 install 即可拉起连接池(配置与 auth 用户不动):

# 本地
pg addon start pgbouncer -i mypg
pg addon stop pgbouncer -i mypg

# 远端
pg addon start pgbouncer --pg-name my-remote-pool
pg addon stop pgbouncer --pg-name my-remote-pool

start 幂等 —— 已在运行则不动。stop 保留容器与配置;要彻底清理请用 pg addon remove。

卸载

# 卸载本地 PgBouncer
pg addon remove pgbouncer -i mypg

# 卸载远程 PgBouncer
pg addon remove pgbouncer --pg-name my-remote-pool

工作流:

  1. 停止并删除插件容器
  2. 删除 <base-dir>/addon/pgbouncer/<instance>/ 目录及配置文件
  3. 从 pg.yaml 中移除插件配置

配置文件

本地模式(存储在 instances.<name>.addons 下):

instances:
  mypg:
    addons:
      pgbouncer:
        enabled: true
        max_client_conn: 200
        default_pool_size: 30
        min_pool_size: 5
        reserve_pool_size: 10
        max_db_connections: 50
        query_timeout: 60
        admin_users: admin
        log_connections: 1

远程模式(存储在顶层 addons 下):

addons:
  pgbouncer:
    my-remote-pool:
      container_name: pgcli-pgbouncer-default-my-remote-pool
      host_port: 56433
      pool_mode: transaction
      dsn: "postgres://admin:pass@10.0.0.20:35432/mypg_db"
      max_client_conn: 200
      default_pool_size: 30

生成的配置文件按实例存储在 <base-dir>/addon/pgbouncer/:

<base-dir>/addon/pgbouncer/
├── mypg/
│   ├── pgbouncer.ini    # PgBouncer 主配置
│   └── userlist.txt     # 认证用户凭据(自动重新生成)
└── my-remote-pool/
    ├── pgbouncer.ini
    └── userlist.txt

认证方式

PgBouncer 使用 auth_query 方式进行动态密码查询:

  1. 为每个连接池在 PostgreSQL 上创建独立的认证用户,命名为 pgb_<namespace>_<instance>(例如 pgb_default_mypg、pgb_test-ns_my-remote)
  2. 安装共享的 SECURITY DEFINER 函数 pgbouncer_lookup() 用于查询 pg_authid
  3. 当客户端连接时,PgBouncer 使用自己的认证用户执行 auth_query,获取真实用户的密码哈希
  4. 密码缓存在 PgBouncer 内存中,后续连接直接使用

userlist.txt 仅包含连接池的认证用户(明文密码)。其他用户通过 auth_query 动态认证——无需密码同步。

每个连接池都有独立的 PG 认证用户,因此多个连接池(本地或跨主机)指向同一 PG 实例时不会相互冲突。

修改 PostgreSQL 用户密码后,重新运行 pg addon install pgbouncer 重置认证缓存,或连接管理控制台执行 RECONNECT。

连接方式

客户端通过插件端口连接:

# 直接连接 PostgreSQL
pg exec -i mypg "SELECT version()"

# 通过 PgBouncer 连接
pg exec --dsn "postgres://user:pass@127.0.0.1:56432/mypg_db" "SELECT version()"

端口分配: PgBouncer 默认使用端口 56432。如果端口被占用,pgcli 会自动分配下一个可用端口。查看当前端口:pg addon list。

使用场景

高并发场景

pg addon install pgbouncer -i mypg \
  --max-client-conn 1000 \
  --default-pool-size 50 \
  --reserve-pool-size 20 \
  --max-db-connections 100

短连接应用

pg addon install pgbouncer -i mypg \
  --pool-mode transaction \
  --server-idle-timeout 60 \
  --server-lifetime 600

长连接应用

pg addon install pgbouncer -i mypg \
  --pool-mode session \
  --server-lifetime 86400

只读副本

pg addon install pgbouncer -i mypg-replica \
  --pool-mode transaction \
  --max-db-connections 30 \
  --query-timeout 30

监控

PgBouncer 提供管理控制台用于监控连接池和运行状态。

连接管理控制台

使用管理员用户连接到 pgbouncer 虚拟数据库:

pg exec --dsn "postgres://<管理员用户>:<密码>@127.0.0.1:<pgbouncer端口>/pgbouncer" "SHOW pools"

示例:

pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW pools"

注意: 只有在 admin_users 中列出的用户才能访问管理控制台。

常用 SHOW 命令

命令 说明
SHOW pools 连接池状态(活跃/等待的客户端和服务端连接数)
SHOW clients 所有当前客户端连接详情
SHOW servers 所有当前服务端(PostgreSQL)连接详情
SHOW databases 已配置的数据库及其连接参数
SHOW stats 流量统计(事务数、查询数、收发字节数)
SHOW config 所有运行时的配置参数
SHOW sockets 底层 TCP 套接字信息
SHOW active_sockets 活跃的 TCP 套接字
SHOW mem 内存使用统计
SHOW lists 各类对象数量汇总

示例

# 检查连接池状态
pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW pools"

# 查看活跃的客户端连接
pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW clients"

# 查看 PostgreSQL 后端连接
pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW servers"

# 查看当前配置
pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW config"

# 查看流量统计
pg exec --dsn "postgres://admin:secret@127.0.0.1:56432/pgbouncer" "SHOW stats"

其他管理命令

命令 说明
RELOAD 重新加载配置文件
PAUSE 暂停连接池(等待事务完成)
RESUME 恢复连接池
RECONNECT 强制重新连接所有服务端连接
SHUTDOWN 关闭 PgBouncer

故障排除

连接池满

ERROR: no more connections allowed

原因:达到 max_client_conn 限制。

# 增加最大连接数
pg addon install pgbouncer -i mypg --max-client-conn 500

# 或减少连接池大小
pg addon install pgbouncer -i mypg --default-pool-size 10

用户认证失败

FATAL: password authentication failed

原因:认证缓存包含过期的密码哈希。

# 重新运行 install 重置认证缓存
pg addon install pgbouncer -i mypg

容器无法启动

# 查看容器日志
podman logs pgcli-pgbouncer-default-mypg

# 检查配置文件
cat <base-dir>/addon/pgbouncer/mypg/pgbouncer.ini

常见原因:配置文件语法错误、用户列表格式不正确、端口被占用。

查询超时

ERROR: query timeout

原因:查询执行时间超过 query_timeout。

# 增加查询超时或禁用
pg addon install pgbouncer -i mypg --query-timeout 300
# 或
pg addon install pgbouncer -i mypg --query-timeout 0

注意事项

  • 插件端口: PgBouncer 默认使用 56432 端口,确保防火墙规则允许访问
  • 用户密码: 修改 PostgreSQL 用户密码后,需重新运行 pg addon install 重置认证缓存
  • 配置文件: 手动编辑配置文件后,重启容器应用变更:
    podman restart pgcli-pgbouncer-default-mypg
  • 事务模式: transaction 模式不支持会话级功能(如临时表),需使用 session 模式

2 - Etcd

以 pgcli addon 形式运行独立的 etcd 集群,用于 HA / DCS 场景

etcd 是一个分布式键值存储。pgcli 可以将一个或多个 etcd 成员作为独立的顶层 addon 运行 —— 它是共享基础设施,而非绑定某个实例的 sidecar。常见用途是为 PostgreSQL 高可用栈充当 DCS(Distributed Concurrent Store),或作为通用的配置 / 锁服务。

安全提示: pgcli 管理的 etcd 成员目前不提供 TLS/CA 证书,也没有 认证 / RBAC —— 任何能连到 client 端口的客户端都拥有完整读写权限。部署时请依靠 网络隔离(默认仅绑定回环;广播端口务必放在受信网络内)。CA/TLS 与认证/RBAC 支持在后续版本中规划开发。

成员通过一次次 pg addon install etcd 管理:第一个成员引导(bootstrap)集群, 后续成员动态加入同名的集群 —— 可以在同一台机器,也可以跨机器。

工作原理

  • 共享基础设施: etcd 存放在 pg.yaml 顶层的 addons.etcd map 中,以成员名 为 key,不隶属于任何单个实例。
  • host 网络 + 可配置监听: 每个成员以 --network host 运行。默认情况下 client/peer URL 绑定到 127.0.0.1 —— etcd 本身不带认证,单机集群仅回环暴露是 预期的安全姿态。跨机集群中每个成员通过 --advertise-host 广播一个对端可达的 地址,同时仍然监听回环,本机 etcdctl 与远端 peer 都能访问。
  • 动态成员管理: 第一个成员以 --initial-cluster-state new 启动;每个后续成员 先对一个运行中的对等成员执行 etcdctl member add 注册,再以 --initial-cluster-state existing 启动。pgcli 自动完成这些步骤 —— 同机走成员 容器,跨机走临时 etcdctl 容器。
  • 集群身份: --cluster 值相同的成员加入同一个 etcd 集群(对应 etcd 的 --initial-cluster-token)。默认值为 pgcli-etcd。
  • 内置调优: 每个成员启动时都带周期性压缩(--auto-compaction-mode periodic、 --auto-compaction-retention 24h)以及 8 GiB 后端配额 (--quota-backend-bytes 8589934592)—— 对于小型 HA 元数据存储是合理的默认值。

部署形态

pgcli 支持两种集群形态,使用的安装命令完全相同 —— 区别只在于成员是否共用一台 主机。

单机 —— 所有成员在同一台机器(测试 / 开发)

开发机或 CI 的经典布局:每个成员用不同的自动分配端口监听回环。无需 --advertise-host(默认 127.0.0.1),无需改防火墙,主机之外什么都访问不到。

pg addon install etcd --name m1              # 127.0.0.1:2379/2380
pg addon install etcd --name m2              # 127.0.0.1:2381/2382
pg addon install etcd --name m3              # 127.0.0.1:2383/2384

三个成员同属 pgcli-etcd 集群;m1 引导,m2/m3 动态加入。这能体验真正的 3 节点 raft 语义,但没有任何主机隔离 —— 整集群随一台机器一起消失,正因如此它只是 测试形态。

跨主机 —— 每台机器一个成员(生产 HA)

生产形态:把成员分散到不同机器(最好 3 或 5 个奇数个 —— 见拓扑与 Quorum)。 每个成员广播自己主机的 LAN 地址;第一个成员 bootstrap 时必须带 --advertise-host,远端 peer 才拨得通。端口可以在各主机上重复,因为每台只绑 自己的网卡。

# 主机 A (10.0.0.1) — 引导
pg addon install etcd --name m1 --cluster prod \
  --advertise-host 10.0.0.1 --client-port 2379 --peer-port 2380

# 主机 B (10.0.0.2) — 加入
pg addon install etcd --name m2 --cluster prod \
  --advertise-host 10.0.0.2 --client-port 2379 --peer-port 2380 \
  --join http://10.0.0.1:2379

# 主机 C (10.0.0.3) — 加入
pg addon install etcd --name m3 --cluster prod \
  --advertise-host 10.0.0.3 --client-port 2379 --peer-port 2380 \
  --join http://10.0.0.1:2379

要求:每个成员的 --advertise-host 互相可达(同一 LAN),每台主机的防火墙为 所有成员的 client 与 peer 端口向其它成员放行(peer 是双向通信 —— 例如 4 成员 默认端口集群放行 2379-2386/tcp),且所有成员使用相同的 --cluster token。任意 一台机器宕机后其余主机照常工作;集群只需要保住机器的多数派。

单机 跨主机
适用 开发、CI、功能测试 生产 HA
--advertise-host 省略(回环默认) 必填,且每个成员都要
--join 不用 除第一个外每个成员都要
端口 同主机内每个成员互不相同 可重复,各主机绑各自网卡
防火墙 无需(仅回环) 每个成员的 client+peer 端口,双向放行
能否扛单机故障 否 能(quorum 成立时)

命令

创建第一个成员

pg addon install etcd --name m1
-> Bootstrapping etcd cluster...
  [OK] etcd container started

✓ etcd installed: "m1"
  Container:    pgcli-etcd-m1
  Cluster:      pgcli-etcd
  Data dir:     ~/.pgcli/addon/etcd/m1/data
  Client port:  2379
  Peer port:    2380
  Advertise:    127.0.0.1

  Client URL: http://127.0.0.1:2379
  Connect (etcdctl): ETCDCTL_ENDPOINTS=http://127.0.0.1:2379

client 端口从 2379 起分配,peer 端口取下一个空闲端口(2380),由 etcd_start_port 自动分配。

往同一集群添加成员

在 m1 运行时,安装相同 --cluster 的其它成员即加入该集群:

pg addon install etcd --name m2
pg addon install etcd --name m3

pgcli 会找到一个运行中的对等成员作为协调者,通过 etcdctl member add 注册 m2/m3,再以 --initial-cluster-state existing 启动它们。端口继续无冲突地自动 分配(m2 → 2381/2382,m3 → 2383/2384)。刚扩容的集群在 leader 重选期间会短暂 失去 quorum;pgcli 会自动重试成员操作,无需在安装之间手动等待。

使用不同的 --cluster 可让成员归属另一个独立的 etcd 集群。

跨机器添加成员

不同主机上的成员也能组成同一个集群。每个跨机成员都必须广播一个对端可达的 地址,用 --advertise-host 指定。在已有成员的主机上,bootstrap 时传入本机 LAN 地址:

# 主机 A (10.0.0.1)
pg addon install etcd --name m1 --cluster prod \
  --advertise-host 10.0.0.1 --client-port 2379 --peer-port 2380

然后在新主机上,把 --join 指向任一现有成员的 client endpoint。pgcli 通过该 endpoint 完成注册(借助临时 etcdctl 容器 —— 本机无需已有成员容器或镜像,镜像会 按需拉取),取响应中的权威 ETCD_INITIAL_CLUSTER,再启动成员:

# 主机 B (10.0.0.2)
pg addon install etcd --name m2 --cluster prod \
  --advertise-host 10.0.0.2 --client-port 2379 --peer-port 2380 \
  --join http://10.0.0.1:2379
-> Registering member with the cluster at http://10.0.0.1:2379...
-> Starting etcd container (joining cluster)...
  [OK] etcd container started

跨机集群注意事项:

  • 每个成员都需要可达的 --advertise-host,第一个也不例外。它广播的 peer URL 会进入集群成员列表,用默认 127.0.0.1 启动的首个成员永远无法被其它主机 加入。计划跨机组网时,bootstrap 时就要传 LAN 地址。
  • --join 必须搭配 --cluster,且取值要与远端集群的 token 一致 —— 成员注册 由远端集群校验,而非本地配置。
  • --join 必须搭配 --advertise-host;否则其它成员会注册一个拨不通的 peer URL。
  • 端口可以跨主机重复(两台机器都用 2379/2380),因为各主机只绑定自己的网卡。
  • 成员间 client/peer 端口必须互通 —— 为每个成员的端口、双向放行防火墙。peer 之间是双向互连(都拨对方的 peer 端口),client(以及 --join/pg etcdctl)需要 能访问 client 端口。自动分配的端口每个成员不同,从安装输出或 pg addon list 里 读取实际端口(或用 --client-port/--peer-port 固定),按范围放行 —— 例如 4 成员默认端口集群放行 2379-2386/tcp。漏配防火墙正是成员长时间 etcdserver: no leader、或 --join 超时的常见原因。
  • 对已注册的名字重复执行相同的 --join 安装会直接报错;先用 pg etcdctl member remove <hex-id> 从集群注销。

用 pg etcdctl 查看

pg etcdctl 从短生命周期容器里运行 etcd 官方客户端 —— 无需钻进 成员容器(本机一个成员都没有时也能用,镜像按需拉取)。目标端点取自 ETCDCTL_ENDPOINTS,未设置时回退到配置中的第一个成员:

export ETCDCTL_ENDPOINTS=http://10.0.0.2:2379
pg etcdctl member list
pg etcdctl endpoint health
pg etcdctl endpoint status -- -w table

pg 自身参数解析器不接受的 etcdctl 参数(-w table、--hex 等)放在 -- 之后。

查看

pg addon list

etcd 成员出现在 Infra add-ons (etcd) 段:

Infra add-ons (etcd):
  etcd (name: m1)
    Status:      running
    Cluster:     pgcli-etcd
    Client URL:  http://127.0.0.1:2379
    Client port: 2379
    Peer port:   2380
    Image:       quay.io/coreos/etcd:v3.5.30
    Container:   pgcli-etcd-m1

启动与停止

宿主机重启或手动停掉后,无需重跑 install 即可把成员拉回来 —— 不会重新执行 member add,复用已有配置与磁盘数据:

pg addon start etcd --name m1
pg addon stop etcd --name m1

start 可重复执行:已在运行的成员是 no-op;容器被删掉的成员会以 --initial-cluster-state existing 从其数据重建。跨机集群启动某成员前,请确认 其对端可达(quorum 成立)。

移除成员

pg addon remove etcd --name m2

移除时会先从集群中注销该成员(先解析其十六进制 member ID —— etcd v3.5 的 member remove 接受 ID 而非名字),再删除容器和该成员的数据目录。只要 quorum 仍成立,其余成员保持健康。

跨主机成员需要单独注销。 上面这一步的自动注销,只在本地 pg.yaml 里存在 同一集群的另一个运行中成员时才会生效 —— pgcli 看不到住在其他主机上的成员。因 此对于跨主机集群,在运行该成员的那台主机上执行移除,只会删掉容器和数据,集 群的成员列表里仍会留下它(一个"残留"成员)—— 存活的成员会一直尝试和已经删 除的这台主机建立 peer 连接。

正确的做法是分两步:

# 1. 在运行该成员的主机上 —— 停止并清理本地状态
pg addon remove etcd --name m4

# 2. 从任意一台仍存活的成员所在主机,把它从集群里注销
export ETCDCTL_ENDPOINTS=http://10.0.0.1:2379   # 指向一个存活成员的 client URL
pg etcdctl member list                            # 找到 m4 的十六进制 ID
pg etcdctl member remove <hex-id>                 # 如 5c7048c8f7521ec7

pg etcdctl 需要一个可达的 peer 来通信 —— 把 ETCDCTL_ENDPOINTS 指向该集群 中仍在运行的成员(在仍有本机运行成员的主机上执行时会自动回退到它;否则就 显式设置),再按 ID 移除(见用 pg etcdctl 查看)。 事后用 pg etcdctl member list 复核:被移除的名字应该已经不在了。

参数

参数 说明 默认值
--name 成员名,同时作为 pg.yaml 里的配置 key etcd
--cluster 集群身份(--initial-cluster-token),取值相同即同集群 pgcli-etcd
--client-port client 主机端口(0 = 从 etcd_start_port 自动分配) auto
--peer-port peer 主机端口(0 = 自动分配,取下一个空闲端口) auto
--image etcd 镜像 tag quay.io/coreos/etcd:v3.5.30
--data-dir 数据目录根——绝对路径,或相对 base_dir;每个成员用 <root>/<name>/data <base_dir>/addon/etcd
--advertise-host 本成员 peer/client URL 中广播的主机(留空 = 127.0.0.1 单机;跨机填 LAN IP 或 FQDN) 127.0.0.1
--join 要跨机加入的现有成员 client endpoint,如 http://10.0.0.1:2379(隐含 --initial-cluster-state existing;需搭配 --advertise-host 与 --cluster) —

容器名遵循命名空间约定:pgcli-etcd-<namespace>-<name>(未设 namespace 时省略)。

数据目录布局

数据始终按 <root>/<name>/data 布局,因此同机多成员绝不共用目录:

<base_dir>/addon/etcd/
├── m1/data/     # 成员 m1
├── m2/data/     # 成员 m2
└── m3/data/     # 成员 m3

--data-dir 指定根目录。相对值按配置的 base_dir 解析;解析后的根会被持久化, 保证配置文件可移植:

# base_dir: /data/pgcli  →  根为 /data/pgcli/etcd,成员 d1 → /data/pgcli/etcd/d1/data
pg addon install etcd --name d1 --data-dir ./etcd

# 绝对路径根
pg addon install etcd --name d2 --data-dir /mnt/ssd/etcd

pg addon remove 只删除该成员的 <name>/data 目录,保留共享根。

连接

用 pg etcdctl —— 它在短生命周期容器里运行 etcd 的 v3 客户端,指向某个 成员的 client URL。目标端点取自 ETCDCTL_ENDPOINTS,未设置时回退到配置中的 第一个成员。etcdctl 的原生参数放在 -- 之后:

pg etcdctl member list
pg etcdctl endpoint status -- -w table
pg etcdctl endpoint health
pg etcdctl put foo bar
pg etcdctl get foo -- --hex

pg etcdctl 始终使用 v3 API(ETCDCTL_API=3,etcdctl 的默认值);不支持 v2。

拓扑与 Quorum

etcd 需要成员多数派(quorum)才能接受写入:

成员数 Quorum 可容忍故障数
1 1 0
3 2 1
5 3 2
  • 使用奇数个成员 —— 生产 HA 推荐 3 或 5。
  • 移除成员可能让集群跌破 quorum(例如 3 台只剩 1 台存活),此时无法提交写入。请始终保持多数在线。

配置

安装后,pg.yaml 在顶层 addons.etcd 下记录每个成员:

addons:
  etcd:
    m1:
      container_name: pgcli-etcd-m1
      name: m1
      cluster_name: pgcli-etcd
      image_tag: quay.io/coreos/etcd:v3.5.30
      data_dir: /home/user/.pgcli/addon/etcd
      client_port: 2379
      peer_port: 2380
      autostart: true
    m2:
      container_name: pgcli-etcd-m2
      name: m2
      cluster_name: pgcli-etcd
      client_port: 2381
      peer_port: 2382

端口起始值可通过顶层 etcd_start_port 配置(默认 2379)。

开机自启

容器还带有 --restart unless-stopped 策略,由每容器的 conmon 监控进程执行 (无需 podman 守护进程)——它能应对进程崩溃,但不覆盖主机重启。要让成员 在重启后自动拉起,按成员启用 autostart:

pg autostart enable --etcd --name m1
pg autostart enable --etcd --name m2
pg autostart enable --etcd --name m3

这会把成员的 autostart: true 写入 pg.yaml,并安装/刷新开机服务(见 开机自启)。开机时只做启动:拉起成员已存在的容器, 绝不重新执行 member add——已初始化的成员从磁盘加载集群状态即可重新加入。 跨主机集群时,在各自主机上为该主机的成员执行命令。pg autostart status 可查看所有成员的状态。

说明

  • 单成员 vs 集群: 仅 --name m1 得到单成员集群;用相同 --cluster 再装更多 成员即可扩容。
  • 生产可用: 每个成员默认已带周期性压缩与 8 GiB 后端配额等调优参数(见上文 「工作原理」),可直接用于生产环境的 DCS 场景。
  • 重复安装是幂等的: 对运行中的成员再次执行 pg addon install etcd --name <m> 会以更新后的参数重建其容器;已在集群中注册的成员不会被重复 add。

3 - PgDog

将 PgDog(Postgres 代理:连接池、负载均衡、分片)作为 pgcli 插件运行

PgDog 是一个用 Rust 编写的高性能 Postgres 代理(PgCat 的继任者)。它提供连接池化、跨副本的读写负载均衡,以及水平分片,位于一个或多 个 PostgreSQL 后端之前。pgcli 可将 PgDog 作为独立的顶层插件运行:它是共享 基础设施,而非依附某个实例的 sidecar。

安全提示: PgDog 使用 users.toml 中的明文密码对客户端做认证。 pgcli 以 0600 权限写出该文件并只读挂载进容器,但密码仍以明文存在于磁盘和 pg.yaml 中。请将监听端口保持在回环地址(默认)或可信网络内,并把配置文件 当作机密对待。

PgDog 由两个 TOML 文件配置——pgdog.toml(代理本身、其后端、以及分片规则) 和 users.toml(客户端凭据)。pgcli 完全根据安装参数生成这两个文件:没有 需要手工编辑的配置文件,因此 pg addon install pgdog 就是唯一事实来源,且具 有幂等性——重新执行会根据所给参数重新渲染文件。

平台支持

PgDog 支持 Linux(host 网络)与 macOS 单主机 dev/test:macOS 下容器加 入 PG 实例在 podman machine 里使用的同一张 pgcli-net bridge 网络,并发布客户 端与 openmetrics 两个端口。macOS 上:

  • 容器内的监听地址会被自动放宽为 0.0.0.0,否则发布端口转发不到只绑回环的进 程;对外连接的地址仍然是 127.0.0.1:<port>。
  • 后端必须是从 bridge 可达的地址。 PgDog 不会自动解析 --backend 里的名字, 所以 127.0.0.1 指向的是 Mac 本机,而不是 podman machine 虚拟机。请给每个 --backend 填目标实例的容器名(用 pg status -i <instance> 查看 Container: 一行,例如 app=pgcli-pg-mypg:5432:...),或 Mac 能路由到的地 址 —— 对受管实例绝对不要填 127.0.0.1。

工作原理

  • 共享基础设施: PgDog 存放在 pg.yaml 顶层的 addons.pgdog map 中,按 代理名索引——不隶属于任何单个实例。
  • 主机网络(Linux)/ bridge 网络(macOS): 代理监听其客户端端口(从 pgdog_start_port 自动分配,默认 7432),并在下一个空闲端口提供 Prometheus 风格的 openmetrics 端口。Linux 上以 --network host 运行;macOS 上加入 pgcli-net 并发布两个端口。
  • 生成的配置: pgdog.toml + users.toml 写在 <base-dir>/addon/pgdog/<name>/ 下,并绑定挂载进容器的 /pgdog。
  • 锁定镜像: 默认 ghcr.io/pgdogdev/pgdog:v0.1.57,可用 --image 覆盖。

安装

最小的单后端代理(仅演示连接池化,见下方说明):

pg addon install pgdog \
  --backend "app=127.0.0.1:35432:default_db" \
  --user "appuser:secret:app" \
  --sharded-table "app:users:id:bigint"

加 --sharded-table 是为了让这份示例成为完整、自洽的配置(与下方"分片与副 本"示例对齐)。注意:单个 --backend 无从分片——不管是否声明,所有数据最 终都会落到那唯一后端,所以这份最小示例只体现连接池化。真正分片需要至少两个不 同 shard 编号的 --backend(见分片与副本)。

  • --backend NAME=HOST:PORT:DBNAME[:SHARD[:ROLE]] —— 一个 [[databases]] 条目。NAME 是客户端连接时使用的逻辑数据库名;DBNAME 是后端上真实的数据 库名。SHARD 默认为 0;ROLE 默认为 PgDog 默认值(primary,读路由可设 replica)。可重复。
  • --user NAME:PASSWORD[:DBNAME] —— 一个 [[users]] 条目。DBNAME 是该用户 可访问的逻辑数据库(某个 --backend 的名字),缺省时取第一个后端的名字。可 重复。
  • 至少需要一个 --backend 和一个 --user。

其他参数:

参数 含义 默认值
--name 代理 / 插件键名 pgdog
--port 客户端主机端口(openmetrics 取下一个空闲端口) 自动分配
--host 监听地址 127.0.0.1
--pool-mode transaction 或 session transaction
--workers 工作线程数 2
--default-pool-size 每个 user/db 对的服务器连接数 10
--image PgDog 镜像 tag ghcr.io/pgdogdev/pgdog:v0.1.57

分片与副本

对每个分片 / 角色重复 --backend,并用 --sharded-table DBNAME:TABLE:COLUMN:DATA_TYPE 声明分片键:

pg addon install pgdog \
  --backend "app=10.0.0.1:5432:shard0:0" \
  --backend "app=10.0.0.2:5432:shard1:1" \
  --backend "app=10.0.0.1:5433:shard0:0:replica" \
  --backend "app=10.0.0.2:5433:shard1:1:replica" \
  --user "appuser:secret:app" \
  --sharded-table "app:users:id:bigint"

这会把两个分片(各含一主一副本)置于逻辑数据库 app 之后,并按 id 列路由 users 表。

准备后端

PgDog 路由到的数据库、表与角色必须已存在于后端——安装代理并不会创建它们。对于 本地 pgcli 实例,用 pg exec 创建每个分片库、其分片表,以及登录角色。以单机 default 实例为例(分片即同一端口上的不同数据库),下面的示例先准备后端(第 1–4 步)、安装代理(第 5 步),再写入数据以体现路由效果(第 6–7 步):

# 1. 创建分片数据库
pg exec -i default -- psql -U admin -d default_db -c "CREATE DATABASE shard0;"
pg exec -i default -- psql -U admin -d default_db -c "CREATE DATABASE shard1;"

# 2. 在每个分片中创建相同结构的分片表
for db in shard0 shard1; do
  pg exec -i default -- psql -U admin -d "$db" \
    -c "CREATE TABLE users (id bigint PRIMARY KEY, name text, email text);"
done

# 3. 创建 PgDog 连后端所用的登录角色——名字必须与某个 --user 相同,
#    密码必须与该用户在 users.toml 中的条目一致
pg exec -i default -- psql -U admin -d default_db \
  -c "CREATE ROLE appuser LOGIN PASSWORD 'secret';"

# 4. 为该角色授予每个分片中分片表的访问权限
for db in shard0 shard1; do
  pg exec -i default -- psql -U admin -d "$db" \
    -c "GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO appuser;"
done

# 5. 在两个分片之上安装代理(参见上文「分片与副本」)
pg addon install pgdog \
  --backend "app=127.0.0.1:35432:shard0:0" \
  --backend "app=127.0.0.1:35432:shard1:1" \
  --user "appuser:secret:app" \
  --sharded-table "app:users:id:bigint"

# 6. 用 pgcli(pg exec --dsn)通过代理写入数据,一条语句只写一行,
#    PgDog 才能按 id 路由(多行 INSERT 会被整体广播到每个分片)
for id in 1 2 3 4 5 6; do
  pg exec --dsn "postgres://appuser:secret@127.0.0.1:7432/app" \
    "INSERT INTO users (id, name, email) VALUES ($id, 'n$id', 'e$id@x');"
done

# 7. 直接查询每个分片,确认数据落在 PgDog 路由到的位置
#    pg psql -- -d <db> 针对单个数据库
pg psql -i default -- -d shard0 -tAc "SELECT id, name FROM users ORDER BY id;"
pg psql -i default -- -d shard1 -tAc "SELECT id, name FROM users ORDER BY id;"

pg exec -i default -- psql -d <db> 在指定实例的容器内针对某个数据库执行命令。 上面的 id bigint 列与 --sharded-table "app:users:id:bigint" 的声明一致,因 此 PgDog 能按 id 路由 users 的行。第 6、7 步让这层路由可见:两个分片的查询 返回互不重叠的 id 子集(具体划分取决于 PgDog 的哈希),两者合起来正好覆盖所 有写入行——没有一行会同时落到两个分片。远程后端则直接对各主机执行等价 SQL。

后端角色不可省略。 默认情况下 PgDog 用客户端的 --user 名字及其 users.toml 密码去连后端。所以每个后端都必须存在名为 appuser(等)的登录角 色,否则连接会失败并报 password for user "..." is wrong, or the database does not exist——即使在回环 trust 配置下密码被忽略,角色仍必须存在。若走密码认证 (非回环 scram-sha-256 的默认行为),users.toml 里的密码必须与角色一致。

解耦客户端与后端用户。 PgDog 本身支持用不同于客户端的用户去连后端:一个 [[users]] 条目可带 server_user / server_password,一个 [[databases]] 条目可带 user / password(后者优先级更高)。但 pgcli 的安装参数目前不会 生成这些字段,因此经 pg addon install pgdog 安装时,后端角色名始终等于 --user 的名字。若你需要一个共享的后端角色(例如统一的 postgres 超级用户, 或与所有客户端用户名都不同的名字),只能手工编辑生成的 users.toml/pgdog.toml 并重启容器——但请注意:重新执行 install 会按参数重新 生成这两个文件,覆盖掉你的手工改动。

连接

安装摘要会打印接入端点。客户端通过代理的客户端端口连接,使用逻辑数据库名和某个 --user 凭据——在 pgcli 里,就是一个指向代理的 --dsn:

# 通过代理进入交互式 psql
pg psql --dsn "postgres://appuser:secret@127.0.0.1:7432/app"

# 一次性执行 SQL
pg exec --dsn "postgres://appuser:secret@127.0.0.1:7432/app" "SELECT version();"

Prometheus 指标在 openmetrics 端口提供:

curl -s http://127.0.0.1:7433/metrics

向分片写入

写入如何落到各分片,取决于语句的形式。以下是在 PgDog v0.1.57 上的实测。

单行 INSERT —— 按分片键路由。 一条语句只带一个 VALUES 元组时, PgDog 对 id 做哈希,把该行送到唯一一个分片:

pg exec --dsn "postgres://appuser:secret@127.0.0.1:7432/app" \
  "INSERT INTO users (id, name, email) VALUES (4, 'n4', 'e4@x');"

以六条单行语句分别插入 id 1–6 时,两分片各得一部分(一次实测:shard0 收到 1,2,shard1 收到 3,4,5,6 —— 具体划分取决于 PgDog 的哈希,不应当作可依赖 的结果)。没有任何一行同时落入两个分片。

多行 INSERT —— 广播而非拆分。 一条语句带多个 VALUES 元组时,会被原 样送到每一个分片,于是每个分片都拿到全部行:

pg exec --dsn "postgres://appuser:secret@127.0.0.1:7432/app" \
  "INSERT INTO users (id, name, email) VALUES (1,'a','a@x'),(2,'b','b@x'),(3,'c','c@x'),(4,'d','d@x');"

这条语句报告 INSERT 0 8 —— 每个分片各 4 行。分片内 PRIMARY KEY 仍然 有效(重复 id 分别落在不同分片,永不冲突),且没有任何错误或告警:每个分 片静默地多出一份完整副本。随后经代理读取就会看到每个 id 都成对重复出现。

正确的批量导入方式: 一条语句一行、逐行经代理插入(各行单独路由);或者 绕开代理,直接对各分片做导入(例如 pg exec -i <inst> -- psql -d <shard> 跑 COPY)。

配置

安装后,pg.yaml 在顶层 addons.pgdog 下记录每个代理:

addons:
  pgdog:
    pgdog:
      container_name: pgcli-pgdog-pgdog
      name: pgdog
      image_tag: ghcr.io/pgdogdev/pgdog:v0.1.57
      host: 127.0.0.1
      host_port: 7432
      openmetrics_port: 7433
      pooler_mode: transaction
      workers: 2
      default_pool_size: 10
      backends:
        - name: app
          host: 127.0.0.1
          port: 35432
          database_name: default_db
          shard: 0
      users:
        - name: appuser
          password: secret
          database: app
      autostart: false

端口基址可通过顶层 pgdog_start_port(默认 7432)配置;openmetrics 端口总是紧接 其上方分配。

查看列表

pg addon list

每个 PgDog 代理都会出现在 Infra add-ons (pgdog) 小节下,附带运行时状态、 端口、后端 / 用户数量、镜像和容器名:

Infra add-ons (pgdog):
  pgdog (name: pgdog)
    Status:      running
    Listen:      127.0.0.1:7432
    Client port: 7432
    Metrics:     http://127.0.0.1:7433/metrics
    Pool mode:   transaction
    Backends:    2
    Users:       1
    Image:       ghcr.io/pgdogdev/pgdog:v0.1.57
    Container:   pgcli-pgdog-pgdog

Backends / Users 取自 pgdog.toml / users.toml 中的条目数;Status 反映容器的实时状态(已停止的代理显示为 stopped)。

启动与停止

宿主机重启或手动停掉后,无需重跑 install 即可拉起代理(配置与 users 不动):

pg addon start pgdog            # 默认名 "pgdog"
pg addon start pgdog --name proxy
pg addon stop pgdog --name proxy

start 幂等 —— 已在运行则不动;容器被删掉的代理会从其配置目录重建。

开机自启

容器带有 --restart unless-stopped 策略(应对崩溃,而非重启)。要在主机重启后 拉起代理:

pg autostart enable --pgdog --name pgdog

这会设置 autostart: true 并安装 / 刷新开机服务(见 开机自启)。开机时是仅启动语义:它启动已存在的容器、读 取磁盘上已有的 pgdog.toml/users.toml,不会重新生成配置或认证。请先安装代理。 pg autostart status 会列出每个目标的状态。

日志

pg logs addon pgdog --name pgdog       # 最后 50 行
pg logs addon pgdog --name pgdog -f    # 持续跟踪

PgDog 以结构化 INFO 行记录连接池事件(新建服务器连接、认证、客户端连接 / 断 开)。若某个连接池不断重试并报 password for user "..." is wrong, or the database does not exist,说明上面「准备后端」的第 3 或第 4 步被跳过——后端没有 该登录角色,或其密码与 users.toml 不一致。

注意事项

  • 重装幂等: 重新执行 pg addon install pgdog --name <p> 会根据所给参数重新 渲染配置并重建容器。后端 / 用户 / 分片列表被本命令的参数完全替换——省略某 个 --backend 是移除它,而非保留。
  • 明文凭据: 密码存放在 users.toml 和 pg.yaml 中。建议为代理用户单独使 用一个低权限角色,并限制对客户端端口的网络访问。
  • 写入形式差异: 单行 INSERT 按分片键路由;多行 INSERT VALUES 会被广 播到每个分片。详见向分片写入。
  • 非实例级 sidecar: 与 PgBouncer 不同,PgDog 通过 --backend 里的地址指向 后端——它不必指向某个 pgcli 管理的实例,一个代理可横跨多个数据库 / 分片。

4 - PostgREST

通过 PostgREST 插件将 PostgreSQL schema 暴露为 REST API

PostgREST 是一个单进程、无状态的 Web 服务器,把一个 PostgreSQL schema 直接变成 RESTful API。作为 pgcli 插件,它是贴在任意 PG 端点前面的一个容器——没有数据目录、 没有渲染出的配置文件,全部通过 PGRST_* 环境变量配置。

PostgREST 是和 PgBouncer、PgDog 同类的代理型 插件:自身不保存状态。两种部署模式与 PgBouncer 对称:

  • 本地模式: pg addon install postgrest -i <instance>——作为 sidecar 存在 instances.<name>.addons.postgrest 下,DSN 由该实例自动拼出。
  • 远程模式: pg addon install postgrest --dsn <dsn> --pg-name <name>——存在 顶层 addons.postgrest 下。

两种模式里的 --dsn 都原样透传进容器的 PGRST_DB_URI,因此可以指向任意 PG 端点:一个直连的托管实例、一个 PgBouncer 连接池,或一个 Patroni 集群前面的 HAProxy 监听口。

平台支持

PostgREST 在 Linux(host 网络,--network host)和 macOS(容器加入 pgcli-net bridge 并发布端口,与 podman machine 下的 PG 实例同形)均可用。

  • macOS 上本地模式开箱可用——容器经 bridge 访问托管实例。
  • macOS 上远程模式:--dsn 必须是从 Mac 本身可达的地址,而不是 podman machine VM 的地址。指向一个 Mac 能解析的真实主机 IP(或指向 VM 上服务的 host.containers.internal)。
  • 客户端始终访问 127.0.0.1:<port>(或 --listen);gvproxy 把发布端口转发到 Mac 的 loopback。

工作原理

  1. pg addon install postgrest 拉取镜像(若缺)并启动一个完全由 PGRST_* env 装配的容器。
  2. 容器经 host 网络(Linux)或 pgcli-net bridge(macOS)访问后端 PG。
  3. PostgREST 启动时内省暴露的 schema,对外提供 REST 服务,并在 pgrst 通道上 LISTEN 等待 schema 缓存重载信号。

PostgREST 在主机上不留任何数据——pg addon remove postgrest 只停并删容器。

容器名: pgcli-postgrest<ns>-<name>(本地模式用实例名作为 <name>;远程模式 用 --pg-name)。namespace 隔离生效:两个不同 namespace 的配置可以各跑各的 API, 互不冲突。

命令

安装

# 本地模式:暴露一个托管实例的 schema
pg addon install postgrest -i mypg --schema api --anon-role web_anon

# 远程模式:任意 PG 端点(直连实例、pgbouncer、或 haproxy LB)
pg addon install postgrest \
  --dsn "postgres://api:pass@127.0.0.1:5000/appdb" \
  --pg-name app-api --schema api --anon-role web_anon

# 调整 PostgREST 面向后端的连接池大小
pg addon install postgrest -i mypg --db-pool 20

# 启用 JWT 认证(未认证请求仍回落到 --anon-role)
pg addon install postgrest -i mypg --schema api --anon-role web_anon --jwt-secret "$(openssl rand -hex 32)"

重复安装是幂等的:已存在的容器会被复用(停着的会被启动)。改过端口、监听地址、 DSN、db-pool、schema、anon-role 或 jwt-secret 后,加 --force 重建容器以生效。

参数:

参数 说明 默认
-i, --instance 托管实例(本地模式) default
--dsn 后端 PG URI(远程模式);原样进 PGRST_DB_URI —
--pg-name 标识一个远程 PostgREST 的名字(配 --dsn 必填) —
--schema 暴露的 schema(可逗号分隔多个);→ PGRST_DB_SCHEMAS PostgREST 默认(public)
--db-pool PostgREST 面向后端的连接池大小;→ PGRST_DB_POOL PostgREST 默认(10)
--anon-role 未认证请求所切换到的角色;→ PGRST_DB_ANON_ROLE —(匿名访问关闭)
--jwt-secret 验证 Authorization: Bearer JWT 所用的密钥;→ PGRST_JWT_SECRET —(JWT 认证关闭)
--port HTTP 主机端口 自动,取自 postgrest_start_port(基 3500)
--listen 绑定地址 127.0.0.1
--image 容器镜像 docker.io/postgrest/postgrest:v16.3
--force 重建已存在的容器以应用改过的参数 关

--db-pool 与后端的 max_connections。 一套 PostgREST 打开的 PG 连接总数 约为 PostgREST 实例数 × --db-pool。若在同一端点后跑多个副本,按此预算 max_connections。

列表

pg addon list

PostgREST 出现在本地插件/远程插件段下,含 Status、REST URL、Backend、 Schema、DB pool、Anon role、JWT auth、Container。

启动 / 停止 / 移除 / 日志

pg addon start postgrest -i mypg
pg addon stop postgrest --pg-name app-api
pg addon remove postgrest -i mypg          # 无状态:只删容器
pg addon remove postgrest --pg-name app-api
pg logs addon postgrest -i mypg            # 本地容器日志
pg logs addon postgrest --pg-name app-api  # 远程容器日志

开机自启

pg autostart enable --postgrest -i mypg
pg autostart enable --postgrest --pg-name app-api
pg autostart status

自启是「只启动」语义:开机时按配置里已有的 PGRST_* env 拉起容器(若容器被删则 从配置重建)。请先安装插件。

配置

PostgREST 的配置在 addons.postgrest(远程)或 instances.<name>.addons.postgrest sidecar(本地)下:

postgrest_start_port: 3500      # HTTP 端口池基址

addons:
  postgrest:
    app-api:
      container_name: pgcli-postgrest-default-app-api
      name: app-api
      image_tag: docker.io/postgrest/postgrest:v16.3
      host_port: 3501
      listen: 127.0.0.1
      dsn: postgres://api:pass@10.0.0.20:35432/appdb
      backend_host: 10.0.0.20:35432
      db_pool: 4
      schemas: api
      anon_role: web_anon
      jwt_secret: <HS256共享密钥>   # 仅在给了 --jwt-secret 时才会出现
      autostart: false

省略 --port 时端口从 postgrest_start_port 端口池(基 3500)自动分配;该池独立 于 PgBouncer / etcd / pgdog / minio 的池。

推荐:用 HAProxy 接 Patroni 集群

对 Patroni 集群,推荐形态是 PostgREST → HAProxy 读写口 → 集群 leader, 而不是让 PostgREST 直连某个成员。把监听口作为 --dsn 传入(远程模式):

# 推荐:写请求始终经 LB 的读写口跟随 leader
pg addon install postgrest --dsn "postgres://api:pass@<lb-host>:5000/appdb" \
  --pg-name app-api --schema api --anon-role web_anon

为什么用监听口、而不是成员的直连 PG 端口:成员直连口在 failover 后会失去写入 (旧 leader 不再接受写,但 DSN 仍指着它)。pgcli 在安装时会检测并给出警告, 建议改指 HAProxy 监听口。

  • failover 自愈,无需重启。 leader 变更后,PostgREST 只需经 LB 重连、 自动选中新 leader——不用重启容器、也不用重跑 install。实测:一次 pg ha switchover 后,正好打在被关闭旧连接上的那一个在途写请求返回 503(SQLSTATE 57P01,连接被终止),紧随其后的重试就在 新 leader 上成功返回 201。把 leader 切换期间那一次瞬时的 503/57P01 当作预期 行为并重试即可。
  • HAProxy 按健康检查路由,与"谁是 leader"解耦。 读写口只放行通过 /primary 检查的成员,只读口只放行通过 /replica 检查的副本——所以 failover 不需要重新配置 haproxy,流量自己会漂移。
  • 横向扩容。 在一个负载均衡器后面跑多个 PostgREST 副本;每个副本各自贡献 --db-pool 份后端连接。

库侧准备(pgcli 不代管)

pgcli 只负责安装并运行 PostgREST 容器,不碰你的数据库。API 要能读到东西, 数据库侧需要先备好未认证请求所 SET ROLE 到的角色,以及对暴露 schema 的授权 ——通常归你的 migration 管,不归 pgcli。以下这段可重复执行:

-- 暴露的 schema(即 --schema 的值;若用 public 可省略,它本就存在):
CREATE SCHEMA IF NOT EXISTS api;

-- 未认证请求所切换到的 NOINHERIT 角色(即 --anon-role 的值)。
DO $$ BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'web_anon') THEN
    CREATE ROLE web_anon NOINHERIT NOLOGIN;
  END IF;
END $$;
GRANT USAGE ON SCHEMA api TO web_anon;
GRANT SELECT ON ALL TABLES IN SCHEMA api TO web_anon;
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT ON TABLES TO web_anon;

-- SET ROLE 要求登录用户是目标角色的成员(DSN 用户是 superuser 时可省略):
GRANT web_anon TO <dsn-user>;

角色视图的列名是 rolname,不是 rolename——写错会让整批语句回滚。 ALTER DEFAULT PRIVILEGES 只对其之后新建的表生效;已存在的表由上面那条 GRANT ... ON ALL TABLES 覆盖。

PostgREST 以 DSN 用户连接、每个请求再 SET ROLE 到 web_anon,因此 API 携带的 就是这个角色的权限:web_anon 只有 SELECT 就只能读,想开写权限就对该表 GRANT INSERT/UPDATE/DELETE。连接用的登录角色本身只需是请求所扮演角色的成员, 不需要别的权限。然后用 --anon-role web_anon 安装。认证请求的两种方式——匿名 (--anon-role)与 JWT(--jwt-secret)——见下文;两者都不设时,每个请求都会被 拒绝(401)。

PostgREST 会缓存它内省到的 schema。schema 变更后需要重载缓存:

NOTIFY pgrst, 'reload schema';

LISTEN 与事务池化。 PostgREST 依赖一条持久的 LISTEN pgrst 会话接收重载 通知。若后端跑在事务池化模式(PgBouncer 默认)下,这条会话级 LISTEN 会被打断 ——通知送不到,经池化连接发出的 NOTIFY pgrst 同样送不到。实测行为:经 事务池化的 PgBouncer 发出的 reload 并不能刷新 PostgREST 的缓存(新表仍 404), 而且即便把 NOTIFY 直发到后端也会丢,因为 PostgREST 自己的 LISTEN 会话没有稳定 连接。要么让 PostgREST 连到会话池化或直连端点,要么重启 PostgREST 容器 (启动时会重新内省)以拾取 schema 变更。

验证 API

安装输出会打印 REST URL(http://127.0.0.1:<port>)。先在暴露的 schema 里建一张 表,再访问 API 确认服务已就绪并能返回它的行:

CREATE TABLE api.widgets (id integer PRIMARY KEY, name text);
INSERT INTO api.widgets VALUES (1, 'bolt'), (2, 'nut');
GRANT SELECT ON api.widgets TO web_anon;
NOTIFY pgrst, 'reload schema';   -- 否则新表仍会 404
# OpenAPI 根——PostgREST 连上后端后即应答。
curl -s http://127.0.0.1:3500/ | head -c 120

# 暴露了哪些表/关系(取自 OpenAPI paths):
curl -s http://127.0.0.1:3500/ | grep -o '"/[a-z_]*"'

# 读取暴露 schema 下某张表的行。路径里 schema 是隐含的——是 /widgets,
# 不是 /api.widgets:
curl -s "http://127.0.0.1:3500/widgets"
curl -s "http://127.0.0.1:3500/widgets?id=eq.1"   # 带过滤条件

全新安装时几点预期:

  • 根路径可能需要一点时间才应答——schema 内省在启动后才跑,最初的请求可能返回 503,直到 PostgREST 连上后端。
  • 刚建的表返回 404、未认证请求返回 401,说明 schema 缓存过期或未设 --anon-role——修法见下方排障表。

JWT 认证

--anon-role 是最简单的模型:每个未认证请求都固定以某个角色运行。 --jwt-secret 则打开按请求划分的身份。安装时传入它(HS256 共享密钥,或用于 RS256 的 JSON Web Key);pgcli 会作为 PGRST_JWT_SECRET 传进容器。

pg addon install postgrest -i mypg --schema api --anon-role web_anon \
  --jwt-secret "$(openssl rand -hex 32)"

若像上面那样内联命令替换,密钥不会进你的 shell 历史。但它会落到 pg.yaml 里(pgcli 要靠它重建容器)——请把这个文件当作含密文件对待,改过 密钥后要加 --force 重新安装。

长度: HS256 的密钥至少 32 个字符(256-bit 密钥材料;HS384 需 48、 HS512 需 64)。更短的密钥会让 PostgREST 拒绝启动——openssl rand -hex 32 (64 字符)是稳妥的默认值。JWK(用于 RS256/ECDSA)没有这个下限,强度由 JWK 自身决定。

设了密钥后,任何带 Authorization: Bearer <token> 的请求,PostgREST 都会先验签, 再以 token 里 role claim 所指名的角色运行:

  • token 必须用同一密钥签名;被篡改的一律 401。
  • role claim 必须指到一个在暴露 schema 上有授权的角色(像上面的 web_anon 那样建,NOINHERIT)。
  • DSN 里的登录角色必须是该角色的成员,因为服务请求的本质就是 SET ROLE 到它。所以对 web_user 这类 JWT 角色,还需要 GRANT web_user TO <dsn-user>; ——与上面匿名角色的要求相同,DSN 用户是 superuser 时同样可省略。缺这个成员 关系,请求会失败并报 permission denied to set role。
  • 若你的签发方用别的字段名,可用 PGRST_JWT_ROLE_CLAIM_KEY 改 claim 键——pgcli 没有暴露该 flag,需要时直接在容器上设置。

--anon-role 与 --jwt-secret 可并存:带合法 JWT 的请求按其 role claim 运行,不带的请求回落到 --anon-role。两者都不设时,每个请求都被拒绝(401)。 常见组合是:公开读用匿名,认证写用 JWT 角色。

生产环境由你应用的认证服务签发 token。手搓一个用同一 HS256 密钥的测试 token:

b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
SECRET='<--jwt-secret 的值>'
HEADER=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url)
PAYLOAD=$(printf '{"role":"web_anon","exp":%d}' $(( $(date +%s) + 3600 )) | b64url)
SIG=$(printf '%s.%s' "$HEADER" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
curl -s -H "Authorization: Bearer $HEADER.$PAYLOAD.$SIG" http://127.0.0.1:3500/widgets

排障

现象 可能原因 / 处理
安装报 cannot connect to source database 容器访问不到 DSN 的 host:port(macOS 上远程 127.0.0.1 指向 Mac 而非 VM)。用 pg exec --dsn <dsn> "SELECT 1" 验证。
所有请求返回 HTTP 401 Anonymous access is disabled 未给 --anon-role 且请求没带合法 JWT——补 --anon-role,或用 --jwt-secret 安装并发一个签名 token。若已设 --jwt-secret 仍 401,说明签名或 role claim 不对。
写请求返回 HTTP 401,但 JSON 响应体里是 42501 / permission denied for table 请求所扮演角色(匿名角色或 JWT 的 role claim)缺该权限——PostgREST 在 HTTP 层把权限不足报成 401,真正的 SQLSTATE 只在响应体里。给请求实际使用的角色补授权。
新建的表/关系在 NOTIFY pgrst 后仍 404 / PGRST205 reload 没送达 PostgREST(见上文事务池化)。重启容器,或改用会话/直连。
Patroni failover 后写请求立刻失败 DSN 指到了成员直连口而非 HAProxy 读写口——安装时已警告。把 DSN 改指 LB。
--db-pool 改动没生效 已存在的容器会被复用;加 --force 重装以重建。

相关

  • PgBouncer——连接池(把 PostgREST 的后端放它前面时,注意上面的事务池化坑)。
  • PgDog——池化 / 分片代理,同样是双模形态。
  • HAProxy——Patroni 集群下 PostgREST 的 DSN 应指向的监听口。
  • HA 集群——--dsn 所指向的 Patroni 拓扑。

5 - RustFS

以 pgcli 插件方式运行 rustfs(Rust 实现的 S3 兼容对象存储)——单机或纠删码多盘/多节点,固定的容器 uid 由定制镜像在内部消化

rustfs 是 Rust 重新实现的 S3 兼容对象存 储:同样的 S3 API、Web 控制台、纠删码多盘/多节点布局——但运行时模型与 MinIO/silo 完全不同。pgcli 将其作为独立的顶层插件运行,CLI 表面与 minio、silo 一致(install、TLS、BYO 证书、 drives、logs、autostart),共用同一个端口池;但有三点决定了本页的写法:

  1. 固定的容器用户。 上游 rustfs 在镜像里写死 User=rustfs(uid/gid 10001)。pgcli 把这件事完全放在自家镜像内部处理——见下节 权限与属主——所以 rustfs 存储不需要任何主机侧属主处理。
  2. 只有三种拓扑,没有多节点单盘。 rustfs 只支持 SNSD / SNMD / MNMD—— 见部署模式。
  3. 自己的 TLS 文件名。 rustfs 从 RUSTFS_TLS_PATH 读取 rustfs_cert.pem / rustfs_key.pem,而不是 MinIO 的 public.crt / private.key。

想要一个精简、Rust 原生的 S3 端点就选 rustfs;它可与 minio、silo 在同一台 主机并存(三者共用一个端口池)。

平台支持: rustfs 插件实际上是仅 Linux。其运行时模型(固定容器 uid、独立块设备上的盘、host 网络)是 Linux 容器路径,也只在 Linux 上做过验 证;macOS 的 bridge 路径代码完整、wrapper 镜像也是双架构,但尚未在真机上跑 过。

工作原理

一个实例一个容器。数据存放在 bind 挂载的主机目录里(默认 <base-dir>/addon/rustfs/<name>/data,或 --drive 的每个盘各自一个目录), 生命周期长于容器。与 minio/silo 不同,pgcli 不直接驱动 rustfs 二进制 ——它运行镜像自带的 /entrypoint.sh(经下面说的 wrapper 转发),因为正是这 个 entrypoint 会展开 RUSTFS_VOLUMES 里的多盘花括号范围、创建各盘目录、并 组装服务端 argv。

pgcli 拉取的镜像不是纯上游镜像——是 ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0,一个建立在 docker.io/rustfs/rustfs:1.0.0(首个 GA 版本)之上的精简 pgcli wrapper。 pg addon install rustfs 只负责 pull;pgcli 从不在运行时构建它。tag 跟随被 pin 的上游版本。

凭据处理方式与 minio/silo 相同:

  • root_user 默认 admin(rustfs 对 access key 无最小长度要求,任意非空值 都可以;admin 还顺带避开了对 rustfsadmin 的告警);
  • root_password 首次 install 时生成(或用 --root-password 显式提 供),存于 pg.yaml(addons.rustfs.<name>.root_password),并在 install 摘要里只打印一次。

它们是 rustfs 的 root access key / secret key,通过 RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY 环境变量传入——即每个 S3 客户端(含 pg mc alias set) 口中的 access key 与 secret key。

权限与属主

这是 rustfs 与 minio/silo 真正不同的地方,而 pgcli 已把它收进镜像、在实际 使用中变得不可见。

上游 rustfs 以一个**固定的、不可配置的 uid/gid(10001)**运行。在 rootless podman 下,主机用户对这个 uid 没有任何权限主张,所以让一个 bind 挂载的数据 目录对该进程可写的「朴素做法」是在主机侧做 chown 腾挪 (podman unshare chown 10001:10001 …)——脆弱,而且和操作者是否为 root 与 其说相关。pgcli 完全绕开了这一切:

  • wrapper 镜像的 entrypoint 以容器内 root 启动,把它自己的 bind 挂载数据 目录 chown 成 10001,然后 su 降到 rustfs 用户,再 exec 未做任何 改动的上游 /entrypoint.sh。pgcli 不传 --user flag,也完全不做主机 侧的属主操作。
  • 两种 daemon 模式下的效果一致;只有主机可见的数字 uid 不同,因为那是 podman 用户命名空间的属性,与 pgcli 无关:
    • rootful podman → 目录落在真实的主机 uid 10001;
    • rootless podman → 落在附属(subordinate)主机 uid (subuid_start + 10001,例如 110001),10001 只在容器内可见。

两种情形都不需要操作者以 root 运行、也不需要手工 chown。唯一还值得知道的 约定是:当你自己挂载盘(--drive)时,挂载点应归运行 pg 的那个用户所有 ——rootless podman 下 root 拥有的挂载点落在命名空间 uid 范围之外,容器内的 chown 无法认领它,跟任何 bind mount 一样。

TLS 是拷贝,不重新属主。 rustfs 必须以 10001 身份读它的 key+cert,但 pgcli 生成的证书目录(0700、pgcli 拥有)必须继续由 pgcli 拥有,pg cert、 CA 刷新、pg backup fetch-ca 都要在它上面工作。所以 wrapper 不 chown 那个目录:它以只读方式挂载该目录,然后以容器内 root 身份把那两个必需文件 拷贝到一个新建的、由 10001 拥有的容器本地目录——RUSTFS_TLS_PATH 指向 那里。任何主机文件都不会被改动;BYO 路径走同一套拷贝机制,BYO 的 key/cert 同样只读挂载、按所需文件名被拷贝、也从不被 chown。

安装

# 默认实例名 "rustfs",端口取自共享池,loopback 绑定
pg addon install rustfs

# 指定名字 + 显式数据目录
pg addon install rustfs --name store --data-dir /srv/rustfs

# 固定端口、换 root 用户
pg addon install rustfs --name store --api-port 9000 --console-port 9001 --root-user admin

# 让存储暴露在网络而不是仅本机
pg addon install rustfs --name store --listen 0.0.0.0

# 走 pgcli 自签 CA 的 HTTPS(作为 pgBackRest S3 仓库的前提)
pg addon install rustfs --name store --tls

# 用你已经有的证书(公网 CA 或私有 CA)
pg addon install rustfs --name store --tls-cert /etc/ssl/rustfs.test.crt --tls-key /etc/ssl/rustfs.test.key

输出会报告端点和 root 凭据:

✓ rustfs installed: "store"
  Container:    pgcli-rustfs-default-store
  Image:        ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0
  Data:         ~/pg/addon/rustfs/store/data
  S3 API:       http://127.0.0.1:9000
  Console:      http://127.0.0.1:9001

  Root user:     admin
  Root password: <generated>

在 Console: URL 用打印出的 root 用户/密码登录控制台。把 S3 客户端(含 pgBackRest)指向 S3 API: URL,或用 pg mc 直 接在终端操作。

对一个在跑的实例重跑 install 是 no-op:容器不会被重建(停着的会被启动并 有提示),flag 会合并进已存配置,已有的 root 密码保留。传 --force 才重建 容器,从而让改动的端口、监听地址、端点列表或凭据生效——数据目录不受影响。

绑定地址: 默认 127.0.0.1 让存储只在本机可达。--listen 0.0.0.0 (或 pg.yaml 里的 listen 键)会把它暴露到网络上。能访问该端口的任何人 都可尝试 root 凭据,所以要么放在防火墙后、要么开 TLS(--tls)。

TLS(--tls)

--tls 让 rustfs 提供 HTTPS。pgcli 用标准库生成一个自签 CA 与一张叶子证书 ——SAN 覆盖 loopback 名称、localhost 以及主机的每张网卡 IP——写入 <base_dir>/tls/rustfs/<name>/:rustfs_cert.pem / rustfs_key.pem(rustfs 要求的名字,同时另存一份 public.crt / private.key 便于 CA 工具复用)与用 于分发的 ca.crt。证书目录以只读挂载、并如权限与属主所述被 拷进容器;端点 URL 变成 https://。

为什么需要它:pgBackRest 对 S3 仓库强制 HTTPS,所以一个要接收 Patroni archive-push 的 rustfs 必须支持 TLS。backup.repo.s3.ca_file 指向 ca.crt,pgBackRest 就会带完整证书校验连接——见备份 → S3 对象存储仓 库。该页关于 MinIO 仓库的一切同样适用于 rustfs:S3 契约完全一致(path-style、repo1-s3-uri-style=path)。

把 CA 送到远端主机——无需 scp。 服务端证书以 leaf+CA 链的形式被提供, 所以另一台机器上的消费方可以直接从一次 TLS 握手里抽出根证书:

pg backup fetch-ca <store-host>:9000
#   [OK] CA fetched from <store-host>:9000
#        saved:    ~/.pgcli/backup/repo-ca/ca-<store-host>-9000.crt
#        SHA-256:  c0f0…fe2e
pg backup setup --s3-ca-file ~/.pgcli/backup/repo-ca/ca-<store-host>-9000.crt

fetch 是 trust-on-first-use——信任前先把打印出的 SHA-256 与存储主机上的 sha256sum ~/.pgcli/tls/rustfs/<name>/ca.crt 比对一下。

自带证书(--tls-cert / --tls-key)

--tls 只会服务 pgcli 自家的自签证书对。若要服务你已经持有的证书,传 --tls-cert <leaf(+chain).pem> --tls-key <key.pem>。配合 --tls(隐含)这 会取代生成的证书:两个文件以只读挂载,并按 rustfs 要求的名字被拷进容器 (/opt/rustfs/certs/rustfs_cert.pem、…/rustfs_key.pem);pgcli 既不重新 属主、也不写它们的源目录。

pg addon install rustfs --name store \
  --tls-cert /etc/ssl/wildcard.example.com.crt \
  --tls-key  /etc/ssl/wildcard.example.com.key

还没有证书?pg cert 能按你给定的 SAN 铸一张自签 leaf,rustfs 原样 serve 它(真机验证过:rustfs 实际呈现的证书与 pg cert 输出在实时握手上吻合, pg mc 对它完成完整的 put/get 往返)。签发的参数、客户端信任锚、续期,见 自签证书。

BYO 模式的其余一切——为什么续期需要 --force(单文件挂载会钉住宿主源 inode)、客户端如何挑选信任锚、pg cert 作为测试证书铸造——与 MinIO 插件完 全一致,见 MinIO → 自带证书。关掉 BYO: 从 pg.yaml 里删 掉这个插件下的 cert_file/key_file,再 pg addon install rustfs --name store --force 重建。

部署模式

rustfs 有三种布局——没有多节点单盘模式:

模式 形态 适用场景
SNSD(单节点单盘) 一个节点、一个数据目录——不给 --endpoint/--drive 时的默认 dev、test、demo
SNMD(单节点多盘) 一个节点、多块盘——每块一个 --drive,见下文 SNMD 单主机上抵抗磁盘故障
MNMD(多节点多盘) 多节点、每节点多盘——本节点的 --drive + --endpoint 列表 同时抵抗磁盘故障与节点故障

rustfs 有意没有 MNSD(多节点单盘)。只给 --endpoint 不给 --drive 会在 install 期被拒:rustfs 自己派生每盘 volume 范围,所以一个分布式节点必须声明 自己有几块盘。

rustfs 严格要求物理盘互斥。 在 SNMD/MNMD 下,每个 --drive 必须位于自 己的块设备上。若两块盘共享一个设备(同一 st_dev),rustfs 进程启动即 [FATAL] 退出。pgcli 以分离方式启动容器,所以 pg addon install 仍返回 0,故障表现为一个永远变不健康的容器——若多盘 install 起不来,查 pg logs addon rustfs --name store。(这比 MinIO/silo 更严,后者只告警。)

单节点多盘(SNMD)

一个 rustfs 进程、若干主机目录、在它们之间纠删码。用 --drive 一次一块盘替 代 --data-dir:

pg addon install rustfs --name store \
  --drive /mnt/rustfs/disk1 --drive /mnt/rustfs/disk2 \
  --drive /mnt/rustfs/disk3 --drive /mnt/rustfs/disk4 --tls

每个 --drive 是位于独立设备上的一个主机目录;第 N 块盘(0-indexed)被 bind 挂到容器路径 /data/rustfsN,RUSTFS_VOLUMES 被设为花括号范围 /data/rustfs{0...3},由镜像的 /entrypoint.sh 展开成各个独立目录。 --drive 与 --data-dir 互斥;把 --drive 和 --endpoint 组合就是 MNMD。

pg addon remove rustfs --name store --clean-data 会删除每个 drive 目录 ——但会拒删仍然处于挂载状态的盘,这样一次手滑的 --clean-data 绝不会穿过 活挂载点 rm -rf 到下面的真实磁盘。若确实要清数据,先卸载。

分布式 / 集群模式(MNMD)

每个节点跑各自的 pgcli 与各自的 pg.yaml;每份 pg.yaml 带同一份完整 的 endpoint 列表与同一套 root 凭据。对 rustfs 而言,--endpoint 列表是 每节点一条 scheme://host:port(不带 path——rustfs 从 --drive 自己派生 /data/rustfsN volume 范围),每节点再各传各的盘:

# node 1(10.0.0.11),四块独立数据盘:
pg addon install rustfs --name store \
  --listen 0.0.0.0 \
  --root-password '<shared-secret>' \
  --drive /mnt/rustfs/d1 --drive /mnt/rustfs/d2 --drive /mnt/rustfs/d3 --drive /mnt/rustfs/d4 \
  --endpoint http://10.0.0.11:9000 --endpoint http://10.0.0.12:9000 \
  --endpoint http://10.0.0.20:9000 --endpoint http://10.0.0.21:9000

# node 2-4:同样的命令、自己的 --drive,外加相同的 --endpoint 列表
# 与相同的 --root-password 值。

除了一份份逐字节相同的配置,还有两点关键:--listen 必须是 0.0.0.0(或本节点 自己的可达地址)而不是默认的 loopback——podman host 网络下端点广播的是各节点真实 IP,只监听 127.0.0.1 的节点永远进不了集群。而且所有节点的 endpoint 列表与 root 凭据必须完全一致:rustfs 据此派生出同一个纠删码集合,一旦对不上就退化成彼此独立 的单实例。

--endpoint 传的是不带 path 的 scheme://host:port,每节点重复一条(写成逗号串 --endpoint a,b,c 也等效——它是 string-slice flag);pgcli 会给每条拼上本节点的 /data/rustfs{0...N-1} 盘范围,再用空格把四条 URL 连成单个 RUSTFS_VOLUMES。 (RUSTFS_VOLUMES 本身必须是空格连接、不能是逗号连接:rustfs 二进制会把逗号串错误 切分,把 http:// 塌缩成 http:/,节点因此解析不出自己的盘——列表中第一个节点以 VolumeNotFound 退出,其余节点卡在 waiting for storage_quorum,始终凑不出可写集 群。空格分隔的字面 URL 与官方文档的紧凑花括号形式 http://node{1...4}:9000/data/rustfs{0...3} 解析结果完全一致,却能直接用各节点的 普通 IP,不需要 /etc/hosts 或 DNS。)

跨主机 MNMD 已在真机 4 节点 × 4 盘集群上验证(Linux,rootful podman):四台全部 /health 返回 200,且写入一次的对象从每一台读回都逐字节一致——纠删码分片确实 散布在全部 16 块盘上。

使用 mc 客户端

pg mc 在一次性容器里跑 MinIO 的 mc 客户端,对 rustfs 的 S3 API 与其它 存储一样可用:

pg mc alias set store http://127.0.0.1:9000 admin <password>    # Linux
pg mc mb store/backups
pg mc ls store
pg mc cp ./dump.pglz store/backups/

S3 数据面(PUT/GET)经 pgBackRest 与一次基础 mc 往返对 rustfs 已端到端证 实,其中包括一段完整的 PITR 流程——全量 base backup、WAL 的 archive-push、 以及一次 --time 恢复:从 rustfs 取回归档 WAL 回放并精确停在目标时间 (Linux,rootless podman 实测)。如实标注:管理面 的 mc 命令(alias set 校验、bucket policy、admin 系列)尚未对 rustfs 逐一验证。把它当作纯对象 put/get 用,理解某些 MinIO 专属的管理命令未必在 rustfs 上有对应。

端口

每个实例从 minio_start_port(默认 9000)起的同一个池里取两个连续端 口:先 S3 API、后 console。这个池在 minio、silo、rustfs 之间共享——三 者由同一个游标分配,故在同一主机并存不冲突(先按名字排 minio,再 silo,再 rustfs):

pg addon install minio  --name store   # 9000 / 9001
pg addon install silo   --name lake    # 9002 / 9003
pg addon install rustfs --name archive # 9004 / 9005

配置

实例位于 pg.yaml 顶层的 addons.rustfs 映射里:

namespace: default
minio_start_port: 9000       # 共享池:minio、silo 与 rustfs 都从这里取
addons:
  rustfs:
    store:
      container_name: pgcli-rustfs-default-store
      name: store
      image_tag: ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0
      # data_dir: /srv/rustfs    # 省略则为 <base-dir>/addon/rustfs/store/data
      # drives:                  # 多盘(SNMD/MNMD):每块一个主机目录,
      #   - /mnt/rustfs/d1       # 挂到 /data/rustfs0../rustfsN
      #   - /mnt/rustfs/d2
      listen: 127.0.0.1
      api_port: 9000
      console_port: 9001
      root_user: admin
      root_password: <generated>   # 首次 install 写入
      autostart: false             # pg autostart enable --rustfs --name store
      # tls: true                  # 提供 HTTPS(自签 CA,或下方的 BYO)
      # cert_file: /etc/ssl/rustfs.test.crt   # BYO leaf(+chain),隐含 tls
      # key_file:  /etc/ssl/rustfs.test.key   # BYO 私钥,须与 cert_file 配对
      # endpoints:                 # 单节点省略;MNMD 每节点一条:
      #   - http://10.0.0.11:9000
      #   - http://10.0.0.12:9000

改动 listen、端口、root_user、root_password、image_tag、data_dir、 tls/cert_file/key_file 或 endpoints,需在下一次 pg addon install rustfs --name store --force 后生效。

列表

pg addon list
Infra add-ons (rustfs):
  rustfs (name: store)
    Status:      running
    Listen:      127.0.0.1
    API port:    9000
    Console port: 9001
    Console URL: http://127.0.0.1:9001/
    Data:        ~/pg/addon/rustfs/store/data
    Root user:   admin
    Image:       ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0
    Container:   pgcli-rustfs-default-store

pg addon list 从不打印密码——去 pg.yaml 读。健康端点是 GET /health (返回 200),不同于 MinIO 的 /minio/health/live。

启停

pg addon start rustfs --name store
pg addon stop  rustfs --name store

install 会跳过仍在的容器(停着的则启动);start 只启动已存在的容器(并 在状态异常时从配置重建以自愈)。在 TLS 生成模式下 start 还会重新校验并按 需重签叶子证书。

开机自启

容器带 --restart unless-stopped 策略(崩溃重启,而非开机自启)。要在主机 重启后拉起实例:

pg autostart enable --rustfs --name store

rustfs 的固定容器 uid 在 pgcli 的 wrapper 镜像内部处理,所以开机时的启动除 了运行 podman 本身不需要任何特殊主机权限。开机是只启动;rustfs 独立于 PostgreSQL 栈,故最后启动。

移除

pg addon remove rustfs --name store            # 容器移除,数据保留
pg addon remove rustfs --name store --clean-data   # 同时删除数据目录

数据目录就是对象存储——丢了它就丢了里面所有 bucket——所以 remove 默 认保留它。--clean-data 才删除(并清理默认布局下已空的父目录)。在 rootless podman 下,对象归一个附属 uid 所有、普通 rm 无法 unlink,因此 --clean-data 会透明地回退到 podman unshare rm 来回收它们。

日志

pg logs addon rustfs --name store      # 最近 50 行
pg logs addon rustfs --name store -f   # 跟踪

故障排除

  • install 时 pulling rustfs image ... : ...。 wrapper tag ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0 不可达——检查到 ghcr.io 的网络 访问,或用 podman pull 预拉。
  • 多盘 install 起不来(永不健康)。 两块盘共享一个物理设备时 rustfs [FATAL]。查 pg logs addon rustfs --name store 看磁盘检查是否触发,并确 保每个 --drive 在独立设备上(独立磁盘或各自的 loop 挂载)。
  • rootless podman 写不进某个盘。 该盘的挂载点归 root 所有、落在命名空间 uid 范围之外,容器无法认领。把挂载点 chown 给运行 pg 的用户。
  • 手工 --api-port 撞端口。 池在 minio/silo/rustfs 之间共享,一个显式端 口必须被三者的自动分配器都视为已占用。每个存储需要一对连续端口。
  • --clean-data 落了个目录没删。 rootless podman 下的附属 uid 目录树经 podman unshare rm 回退删除;若该路径不可用,自己动手 podman unshare rm -rf <dir>。

6 - HAProxy

以 pgcli 插件方式运行 HAProxy——Patroni 集群前的 TCP 负载均衡,支持读写一体(unified)与读写分离(split)两种模式

HAProxy 是 Patroni 官方文档推荐放在集群前面的负载 均衡器:客户端只连一个稳定地址,HAProxy 对各成员的 REST API 做健康检查,把写 请求路由到当前 leader(读写分离模式下,读请求路由到 replica)。pgcli 将其作为 独立的顶层插件运行——共享的路由基础设施,而非实例级 sidecar——镜像固定为 官方 haproxy:3.2.23-alpine。

平台支持: HAProxy 插件目前仅支持 Linux。它通过主机网络访问 Patroni 成员,而 macOS 的 podman machine 不会把主机网络暴露给容器。macOS 上 NewHAProxyManager 会快速失败并给出明确提示;pg addon list 仍可显示已安装 实例,但没有实时状态。

工作原理

路由方式遵循 Patroni 官方 haproxy.cfg 的模式。每个后端就是一个 Patroni 成员,每个成员有两个关键端口:

  • PostgreSQL 端口 —— HAProxy 实际转发连接的目标端口;
  • REST API 端口 —— HAProxy 发送 httpchk 健康检查的端口。

Patroni 的 REST API 对下列 GET 请求的应答(这些 GET 按设计无需认证):

端点 返回 200 的节点
GET / 仅 leader
GET /replica(可加 ?lag=<上限>) 仅 replica,且延迟在限制之内

于是写监听器里唯一 UP 的服务器就是 leader,读监听器里唯一 UP 的就是各 replica——健康检查状态一变,故障切换自动生效。pgcli 把上述配置渲染到 <base-dir>/addon/haproxy/<name>/haproxy.cfg,并以只读方式挂载进容器。

两种模式

用 --mode 选择:

  • unified(默认)——单个监听器把所有流量发给当前 leader。简单, 每条连接都可读可写。leader 切换时,写监听器用 on-marked-down shutdown-sessions 掐断旧连接,让客户端重连到新 leader。
  • split —— 读写分离:_<name>_rw 监听器把写请求发给 leader,另一个 _<name>_ro 监听器把读请求轮询分发到各 replica(可按 lag 过滤)。客户端 端口从 1 个变成 2 个。

统计页

每个实例还会得到一个 HTTP stats 监听器,可在浏览器里观察服务器 up/down 状态:http://<listen>:<stats-port>/。

安装

后端成员用两种方式之一提供(互斥):

  • --node NAME=HOST:PGPORT:RESTPORT(可重复)——显式列出;
  • --ha <scope> —— 自动从 pg ha 管理的 Patroni scope 中推导所有本机 成员,直接使用各成员的 PostgreSQL 与 REST API 端口。

--node 各字段的含义,以 node1=10.0.0.11:35532:8008 为例:

字段 含义
node1 成员名——成为 haproxy.cfg 中的 server 名,也是 stats 页里的标签
10.0.0.11 本机可访问到该成员的地址(成员的 advertise-host;同机可写 127.0.0.1)
35532 成员的 PostgreSQL 端口——HAProxy 实际转发连接的目标
8008 成员的 Patroni REST API 端口——健康检查打这个端口

用 --ha 时这四个值都来自 pg.yaml:Patroni 成员名、其 advertise-host (默认 127.0.0.1)、以及 host_port / restapi_port。

# 读写一体:单端口全走 leader,后端从 scope "app" 自动推导
pg addon install haproxy --name lb --ha app

# 读写分离:rw + ro 两个监听器,读副本 lag 上限 1MB
pg addon install haproxy --name lb --mode split --ha app --max-lag 1MB

# 显式指定后端(成员在别的机器上,或 scope 不由 pgcli 管理)
pg addon install haproxy --name lb --mode split \
  --node node1=10.0.0.11:35532:8008 \
  --node node2=10.0.0.11:35533:8009 \
  --node node3=10.0.0.11:35534:8010

安装输出会报告分配到的端口和现成的连接串:

✓ haproxy installed: "lb"
  Container:    pgcli-haproxy-default-lb
  Image:        docker.io/library/haproxy:3.2.23-alpine
  Mode:         split
  Patroni:      app
  Backends:     3
  Read-write:   127.0.0.1:5000
  Read-only:    127.0.0.1:5001 (lag ≤ 1MB)
  Stats:        http://127.0.0.1:5002/
  Config:       ~/pg/addon/haproxy/lb/haproxy.cfg

  Connect via HAProxy:
    postgres://<user>@127.0.0.1:5000/<database>

之后增删成员

后端列表跟随 pg.yaml 中该 scope 的本机成员,因此任何拓扑变化——增员或 移除——都通过重跑同一条 install 命令同步:它重新推导完整后端列表并重建 容器。

用 pg ha create <scope> --member <m> 扩容后,新成员还不在运行中的 HAProxy 配置里:

pg ha create app --member node3 --etcd m1
pg addon install haproxy --name lb --mode split --ha app --max-lag 1MB
# Backends: 2 -> 3,haproxy.cfg 新增 server node3 127.0.0.1:35534 ... check port 8011

对称地,pg ha remove <scope> --member <m> 之后,被移除的成员会作为过期后端 残留在配置里——重跑 install 即可将其剔除:

pg ha remove app --member node3
pg addon install haproxy --name lb --mode split --ha app --max-lag 1MB
# Backends: 3 -> 2,server node3 一行消失

显式 --node 安装时没有自动推导:直接改 --node 列表本身(增加或删除某个 spec)再重跑——目标列表每次整体替换,命令里写的就是最终全集。

跨主机集群

--ha 只从本机的 pg.yaml 推导后端,而它只记录本机 pg ha create 管理的 成员。位于其他主机的成员从来就不是本机的后端——在那边增删成员,本机 无需重新同步(也不会有残留)。若想用一个 HAProxy 前置整个集群,请改用显式 的 --node 列表,逐个填上各成员的 advertise 主机地址:

pg addon install haproxy --name lb --mode split \
  --node node1=10.0.0.11:35532:8008 \
  --node node2=10.0.0.12:35532:8008 \
  --node node3=10.0.0.13:35532:8008
# 之后:再加(或删)一个 --node 再重跑——列表整体替换

注意:在单台主机上用 --ha 安装的实例,写流量只会路由到本机的 leader—— 一旦故障切换把 leader 提到了远程成员,rw 监听器就没有 UP 的服务器了,直到 leader 迁回来。对于 leader 可能漂移的跨主机集群,优先用显式的、列出全部成员的 --node 列表(或每台主机各跑一个 HAProxy,再用上层的 VIP / DNS 前置它)。

读写分离使用示例

用 --mode split 安装后,会暴露两个客户端端口:

端口 流量 后端
write_port(默认 5000) 读写 仅 leader
read_port(默认 5001) 只读 各 replica(轮询,lag ≤ max-lag)
stats_port(默认 5002) HTTP 统计页 —
# 写(始终打到当前 leader)
psql "host=127.0.0.1 port=5000 user=postgres dbname=postgres"

# 读(负载均衡到各 replica)
psql "host=127.0.0.1 port=5001 user=postgres dbname=postgres"

故障切换后 HAProxy 通过健康检查自动识别新 leader —— 无需改连接串。浏览器打开 http://127.0.0.1:5002/ 可实时查看后端状态。

端口

端口从主机级端口池自动分配:每个实例连续取 3 个空闲端口(rw,split 模式下再 加 ro,最后 stats),起始值为 haproxy_start_port(默认 5000)。也可以显式 指定:

pg addon install haproxy --name lb --ha app \
  --rw-port 6000 --ro-port 6001 --stats-port 6002

端口池会探测占用并记录所有 HAProxy 实例已分配的端口,因此同一台机器上可以跑 多个实例而不会冲突。split 实例占 3 个端口(rw / ro / stats),unified 实例占 2 个(rw / stats):

pg addon install haproxy --name lb1 --mode split --ha app     # 5000 / 5001 / 5002
pg addon install haproxy --name lb2 --ha report              # 5003 / 5004

绑定地址默认 127.0.0.1;如需暴露到网络,用 --listen 0.0.0.0(或在 pg.yaml 里设 listen)。除非后端确有需要,请保持 loopback——这里 PostgreSQL 前面没有任何认证。

配置

全部配置位于 pg.yaml 顶层 addons.haproxy 映射中,以实例名为 key:

namespace: default
haproxy_start_port: 5000
addons:
  haproxy:
    lb:
      mode: split            # unified | split
      ha_scope: app          # 后端来源的 scope(供 --ha 重新推导)
      listen: 127.0.0.1
      write_port: 5000
      read_port: 5001
      stats_port: 5002
      replica_max_lag: 1MB   # 仅 split 模式
      autostart: false       # pg autostart enable --haproxy --name lb
      image_tag: docker.io/library/haproxy:3.2.23-alpine
      targets:
        - { name: node1, host: 127.0.0.1, pg_port: 35532, rest_port: 8008 }
        - { name: node2, host: 127.0.0.1, pg_port: 35533, rest_port: 8009 }

targets 由 install 渲染并重新推导;mode、端口、replica_max_lag、 image_tag 可在这里手调,然后 pg addon install haproxy --name lb ... 生效。 渲染出的 haproxy.cfg 不含任何密钥——只有主机、端口和健康检查 URI。

查看列表

pg addon list
Infra add-ons (haproxy):
  haproxy (name: lb)
    Status:      running
    Mode:        split
    Patroni:     app
    Read-write:  127.0.0.1:5000
    Read-only:   127.0.0.1:5001
    Stats:       http://127.0.0.1:5002/
    Backends:    3
      - node1        127.0.0.1:35532 (check :8009)
      - node2        127.0.0.1:35533 (check :8010)
      - node3        127.0.0.1:35534 (check :8011)

启动与停止

宿主机重启后,无需重新渲染配置即可拉起实例:

pg addon start haproxy --name lb
pg addon stop  haproxy --name lb

install 总是重建容器以保证配置生效;start 只启动已有容器(状态不当时会 用磁盘上的 haproxy.cfg 自愈重建)。

开机自启

容器带有 --restart unless-stopped 策略(应对崩溃,而非重启)。要在宿主机 重启后拉起实例:

pg autostart enable --haproxy --name lb

这会设置 autostart: true 并安装 / 刷新开机服务(见开机自启)。 开机时是仅启动语义:它启动已存在的容器、读取磁盘上已有的 haproxy.cfg, 不会重新渲染配置——请先安装实例。开机服务会在 Patroni 成员之后启动 HAProxy,因此其后端也已在拉起中。pg autostart status 会列出每个目标的状态。

移除

pg addon remove haproxy --name lb

停止并删除容器,同时删除其配置目录。

日志

pg logs addon haproxy --name lb       # 最近 50 行
pg logs addon haproxy --name lb -f    # 持续跟踪

容器日志走 stdout(log stdout format raw local0 info),健康检查状态变化与 连接事件都会出现在这里。

故障排除

  • stats 页所有后端都 DOWN。 健康检查打的是各成员的 REST API 端口。先确认 其可达:curl http://<host>:<rest_port>/ 在 leader 上返回 200、其余 503; /replica 正相反。返回 000 说明 REST API 挂了或端口写错。
  • --ha 找不到成员。 --ha 只推导 pg ha 在 pg.yaml 中管理的本机 成员。远程成员或 pgcli 不管理的 scope,请用 --node 显式列出。
  • 手动 --rw-port 撞端口。 请选端口池(haproxy_start_port 起)之外的 端口,否则会被自动分配器视为已占用。
  • macOS。 暂不支持——HAProxy 需要主机网络访问 Patroni 成员,podman machine 虚拟机不提供该能力。

7 - Silo

以 pgcli 插件方式运行 silo(Pigsty 的 MinIO 分支)——单机或跨主机分布式纠删码集群的 S3 兼容对象存储(含 Web 控制台)

silo 是 Pigsty 社区维护的 MinIO 分支:S3 兼容对象 存储、内置 Web 控制台,并完整保留了 MinIO 的对外契约——S3 API、MINIO_* 环境变量、server /data --address :9000 --console-address :9001 命令行、 --certs-dir 目录布局、以及纠删码集群模式。pgcli 将其作为独立的顶层插件 运行,功能面与 minio 插件 完全一致——install、TLS、BYO 证书、 分布式模式、logs、autostart 一一对应。如果你跟随 Pigsty 的发布节奏就选 silo; 两者可以在同一台主机并存(共用一个端口池,统一游标分配,不会冲突)。

平台支持: silo 插件两个平台都支持。Linux 上通过主机网络提供服务; macOS 上加入 pgcli-net bridge 网络并发布两个端口,Mac 用 127.0.0.1:<port> 即可访问 API 与控制台。公开镜像为双架构(amd64 + arm64)。两个客户端——pg mcli(silo 自带)与 pg mc(MinIO 的)——也都双平台可用,且都能 直连 silo 存储。

工作原理

一个容器、一个目录:silo server /data --address <listen>:<api-port> --console-address <listen>:<console-port>,其中 /data 是实例主机数据目录 (默认 <base-dir>/addon/silo/<name>/data)的 bind mount。silo 存储的一切 都在这个目录里,因此它的生命周期长于容器。

镜像是公开的官方 tag docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z—— 双架构、控制台内置、并且连 mcli 客户端一起打包——所以 pg addon install silo 只负责拉取;pgcli 运行时从不构建镜像(pgcli-minio 是自建 tag,因为 MinIO 官方镜像移除了控制台,silo 没有这个问题)。pgcli 通过显式 --entrypoint silo 直接驱动 silo 二进制,绕开镜像自带的 entrypoint 包装 脚本,与驱动 MinIO 的方式一致。

凭据的处理方式与 Patroni 相同:

  • root_user 默认 admin;
  • root_password 首次 install 时自动生成(也可以用 --root-password 显式指定),存入 pg.yaml (addons.silo.<name>.root_password),并在安装摘要中打印一次方便记录。

二者就是 silo root 的 access key / secret key——经由 silo 从 MinIO 继承的 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD 环境变量传入——所有 S3 客户端(包括 pg mcli alias set <名称> <URL> <root_user> <root_password>)所说的 access key 与 secret key 就是它们。

容器按 MinIO 官方部署建议(契约原样沿用)带上 --ulimit nofile=1048576:1048576 和 --stop-timeout 60;macOS 上 podman machine 虚拟机的 RLIMIT_NOFILE 上限更低,因此该平台改用 65536——单机 dev/test 足够。root 凭据通过 -e 传入会出现在 podman inspect 里——与手工运行容器的 暴露程度相同;对 rootless 单机部署(本插件的定位)可以接受。

安装

# 默认实例名 "silo",端口从共享池分配(基址 9000),只绑回环
pg addon install silo

# 命名实例 + 显式数据目录
pg addon install silo --name store --data-dir /srv/silo

# 固定端口、指定 root 用户
pg addon install silo --name store --api-port 9000 --console-port 9001 --root-user admin

# 绑定到所有网卡,暴露到网络
pg addon install silo --name store --listen 0.0.0.0

# 用 pgcli 自签 CA 提供 HTTPS(pgBackRest S3 仓库的前提)
pg addon install silo --name store --tls

# 用你已有的证书提供 HTTPS(公共 CA 或私有 CA 签发的域名证书)
pg addon install silo --name store --tls-cert /etc/ssl/silo.test.crt --tls-key /etc/ssl/silo.test.key

输出会报告端点与 root 凭据:

✓ silo installed: "store"
  Container:    pgcli-silo-default-store
  Image:        docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z
  Data:         ~/pg/addon/silo/store/data
  S3 API:       http://127.0.0.1:9000
  Console:      http://127.0.0.1:9001

  Root user:     admin
  Root password: <generated>

用打印出的 root 用户和密码登录 Console: URL 的控制台。S3 客户端(包括 pgBackRest)指向 S3 API: URL,或在终端里用下文的 pg mcli 操作。

对存活实例重复执行 install 是 no-op:容器不会被重建(已停止的会直接启动 并给出提示),命令行参数会合并进已存储的配置,root 密码保持不变。想改端口、 监听地址、endpoint 列表或凭据,用 --force 重建容器——数据目录不会丢。

绑定地址: 默认 127.0.0.1 让存储保持本地可见。--listen 0.0.0.0 (或 pg.yaml 里的 listen 键)会把它暴露到网络。届时任何能连到端口的 人都可以尝试 root 凭据,所以只在防火墙后面、或配合 TLS(下述 --tls) 使用。

TLS(--tls)

--tls 让 silo 以 HTTPS 提供服务。pgcli 用标准库生成自签 CA 与叶子证书—— SAN 覆盖回环名、localhost 和主机的所有网卡 IP——写入 <base_dir>/tls/silo/<name>/:public.crt / private.key 供 silo 的 --certs-dir 使用,ca.crt 用于分发。证书目录以只读方式挂载,端点 URL 变为 https://。

为什么需要它:pgBackRest 对 S3 仓库强制 HTTPS(明文是上游明确拒绝的 选项),所以用于接收 Patroni archive-push 的 silo 必须讲 TLS。把 backup.repo.s3.ca_file 指向 ca.crt,pgBackRest 就会做完整证书验证——见 备份 → S3 对象存储仓库。该页 关于 MinIO 仓库的一切对 silo 同样逐字适用:客户端契约完全相同。

关于存储与集群配对的一点:backup.repo.s3 每个配置文件只有一个全局仓库, 所以第一个接入的 TLS 存储(无论 minio 还是 silo)会接收该环境下所有 stanza。确实需要两个目的地时,请再维护一份配置(见 备份 → 共享备份容器,以及完整示例 HA 集群 + 自签 CA MinIO)。

pg mcli 在命令通过回环 alias 访问 TLS 存储时自动加 --insecure(mcli 不会按 alias 持久化 CA 信任;同机回环下这样可接受)。指向局域网 IP 的 alias 不算回环——自己补 -- --insecure,或正规配置 CA。外部 https:// 端点保持 完整验证。对已有实例切换 --tls 需要 --force 才生效。

有效期与其他主机的访问。 CA 与叶子证书都是长生命周期;pgcli 在 --force 时、或主机地址变化时重新签发叶子(silo 监视证书文件并热加载重新 签名的一对,部署因此无需重建即可拿到新链),所以把 ca.crt 交给客户端是 每个存储一次性的动作。TLS 客户端用自己拨出的地址校验叶子证书的 SAN—— 客户端自身的地址无关紧要。远程主机因此把 backup.repo.s3.endpoint 指向存储 主机的某个 IP(回环名与所有网卡 IP、含虚拟网桥,都在 SAN 里;裸主机名不在 ——除非设置 MINIO_SERVER_URL,其 host 会被加入)。

把 CA 拿到远程主机上——不需要 scp。 服务端证书以叶子+CA 链的形式提供, 另一台机器上的消费方可以直接从 TLS 握手中取回根证书:

pg backup fetch-ca <store-host>:9002
#   [OK] CA fetched from <store-host>:9002
#        saved:    ~/.pgcli/backup/repo-ca/ca-<store-host>-9002.crt
#        SHA-256:  c0f0…fe2e
pg backup setup --s3-ca-file ~/.pgcli/backup/repo-ca/ca-<store-host>-9002.crt

此取回是 trust-on-first-use——先与存储主机上 sha256sum ~/.pgcli/tls/silo/<name>/ca.crt 比对指纹再信任。随后一次 setup 会把 CA 重新发布进 Patroni 集群的 etcd 注册表,其他集群主机零手工步骤即可获得 (见备份 → S3 对象存储仓库)。

自带证书(--tls-cert / --tls-key)

--tls 永远只服务 pgcli 自签的一对。要服务你已有的证书——公共 CA 为真实 域名签发的、或你的私有 CA 签发的——改用 --tls-cert <leaf(+chain).pem> --tls-key <key.pem>。它隐含 --tls(无需重复传),取代生成的证书:两个文件 以只读方式直接挂到 silo --certs-dir 要求的文件名上 (/opt/silo/certs/public.crt、/opt/silo/certs/private.key),pgcli 从不 复制或重签——私钥始终只在你放置的那一个位置。

pg addon install silo --name store \
  --tls-cert /etc/ssl/wildcard.example.com.crt \
  --tls-key  /etc/ssl/wildcard.example.com.key

BYO 模式的其余一切——install 校验什么、为什么续期必须 --force(单文件 挂载锁定源 inode)、客户端如何选取信任锚、以及用 pg cert 生成测试证书—— 与 MinIO 插件完全相同,见 MinIO → 使用自带证书。pg cert 示例 改写如下:

pg cert --host "silo.test,127.0.0.1,10.0.0.9" \
  --cert-file silo.crt --key-file silo.key
pg addon install silo --name store --tls-cert silo.crt --tls-key silo.key

关闭 BYO。 没有 off 开关——跨重跑的 config 合并是单向的,与 --tls 本身一致。要回到生成模式,删除 pg.yaml 里该插件下的 cert_file/key_file,再 pg addon install silo --name store --force 重建。

部署形态

silo/MinIO 对自身的部署布局有明确分类;本插件四种全部支持:

形态 结构 适用场景
SNSD(单机单盘) 单节点、单个数据目录——不给 --endpoint 也不给 --drive 时的默认 开发、测试、演示
SNMD(单机多盘) 单节点、多块盘——每个 --drive 传一块盘,见下文 SNMD 单机部署要扛住坏盘,又不想额外搭文件系统层
MNSD(多机单盘) 多节点、每节点一块数据盘——即下文的分布式模式 紧凑的高可用部署
MNMD(多机多盘) 多节点、每节点多块盘——--drive 传本节点的盘,--endpoint 传全集群矩阵 既要扛坏盘、又要扛坏机,且不额外搭文件系统层

pg addon install silo 开箱即是 SNSD。要得到 MNSD,传入集群的 endpoint 列表(至少四节点)——见下文分布式 / 集群模式。

MNMD 把两个 flag 组合起来:--drive 传本节点的盘(与 SNMD 完全一致), --endpoint 传整个集群的 host×drive 端点矩阵(每台每个盘各一条 URL)—— 见下文多机多盘(MNMD)。SNSD/MNSD 下的磁盘冗余另一条路仍然 可用——把 ZFS 池垫在 --data-dir 底下——它能让底层布局随时可换,空间利用上 也往往更省。S3 存储高可用方案 对比了原生 MNMD 与 ZFS 两条路,也覆盖"4 主机、每主机多块盘"这个既能扛坏盘又能扛坏机的 混合形态。

单机多盘(SNMD)

一个 silo 进程、多个宿主目录、盘之间做纠删码。每个 --drive 传一块盘,替代 --data-dir:

pg addon install silo --name store \
  --drive /mnt/minio/disk1 --drive /mnt/minio/disk2 \
  --drive /mnt/minio/disk3 --drive /mnt/minio/disk4 --tls

每个 --drive 是一块独立设备上的宿主目录;第 N 块盘挂到容器路径 /dataN,服务进程以 silo server /data1 /data2 ... /dataN 启动。--drive 与 --data-dir 互斥——多盘模式的数据位置只由 --drive 决定;--drive 再加 上 --endpoint 就是 MNMD,即多盘模式的多机版本,见下文多机多盘 (MNMD)。和 silo/MinIO 所有盘一样,与宿主根设备共享的盘会 在启动时被拒绝;pgcli 会列出越界的盘、在 install 时给出警告。

EC 换来什么(在活的 4 盘 silo 集上实测):4 盘集默认 2 片校验——可容忍 2 块盘故障。坏 1 块盘时读写都照常;坏 2 块盘时读仍成功、写被拒——这就是 quorum 边界,和 MNSD 用的是同一套算术,只不过成员是盘而不是节点。可用容量约 为原始总量的一半。回来的盘由 silo 自己 heal,pgcli 无需介入。校验片默认值随 盘数变化——本文只实测了 4 盘这一种形状。

pg addon remove silo --name store --clean-data 会删除每一块盘的目录——但 拒删仍处于挂载状态的盘,所以一次误操作的 --clean-data 绝不会穿透挂载点把 底下的盘 rm -rf 掉。真要丢弃下面的数据,先 umount。

分布式 / 集群模式

silo 的纠删码(EC)集群模式与 MinIO 完全一致——命令形状相同,规则也相同。 它要求至少四个互不相同的 host:port endpoint,且与其他插件不同,没有 中心协调者:每个节点各自运行 pgcli 和各自的 pg.yaml,每一份 pg.yaml 携带相同的完整 endpoint 列表与相同的 root 凭据。pgcli 只为本节点的容器 执行启动——组环的握手由 silo 自己跨主机完成。

# 节点 1(10.0.0.11),独立数据盘挂载于 /data:
pg addon install silo --name store \
  --listen 10.0.0.11 \
  --data-dir /data \
  --root-password '<shared-secret>' \
  --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,以及 SAME 的 endpoint 列表和 SAME
# 的 root 密码。

三条硬性规则(endpoint 必须可路由且互不相同、数据目录必须位于与根文件系统 分离的磁盘——否则 pgcli 在 install 时告警、集群模式下不设置逐节点的 MINIO_SERVER_URL)以及 quorum 计算,与 MinIO → 分布式 / 集群模式相同;silo 因为继承 了这些检查,执行得同样严格。

多机多盘(MNMD)

MNMD 与 MinIO 下的做法完全一致:用 --drive 传本节点的盘,用 --endpoint 传整个集群的 host×drive 端点矩阵——每台、每个盘各一条 URL。每个端点必须指 名该节点的一个 /data1../dataN 盘槽,矩阵长度必须是每节点盘数的整数倍且至少 包含一台远端节点,--tls 节点的端点必须全部是 https://——这四项 pgcli 都会 在启动容器前检查。完整步骤见 MinIO → 多机多盘(MNMD)。

在活体的 4 节点 × 4 盘 silo 集(16 × 2 GiB)上实测到的形状与 MinIO 一致: 16 块盘在线,EC:4,位于单个 stripe 大小为 16 的纠删码集合里;损失一整台 节点(12/16 在线)时读和写都照常工作,64 MiB 往返逐字节一致;再损失第二台 节点(8/16 在线)时写被拒(Resource requested is unwritable)、读也失败, 节点重启后集合自愈回 16/16。16 盘 EC:4 保留原始字节的 12/16。

TLS 集群的推荐与 MinIO 下一条完全一致——一张 pg cert 叶子、SAN 覆盖所有节 点地址、每个节点 --tls-cert/--tls-key 同一对文件——并在 silo 上按与 minio 相同的方式验证过:grid 直接成环,没有逐节点 CA 需要对齐。配法见 MinIO → 多机多盘(MNMD)。

使用 mcli 客户端

pg mcli 从 pgsty/silo 镜像(经 --entrypoint mcli 选择——镜像默认 entrypoint 起的是服务端)在一次性容器里运行 silo 自带的 mcli 客户端——本机 零安装:

# 按平台选择 URL——见 mc 页面的"按平台区分的端点":
pg mcli alias set store http://127.0.0.1:9000 admin <password>    # Linux,本机插件
pg mcli alias set store http://host.containers.internal:9000 admin <password>  # macOS,本机插件
pg mcli mb store/backups
pg mcli ls store
pg mcli cp ./dump.pglz store/backups/

mcli 与 mc 的 alias 契约相同,因此互通——对 silo 存储、MinIO 存储或任何 S3 端点都可用。pgcli 有意把两个客户端指向同一个文件:alias 持久化在主机 ~/.mc/config.json(mc 的原生默认路径),所以 pg mc 注册的 alias 对 pg mcli 同样可见,反之亦然——只设一次,不必每个客户端各设一次(无状态的 MC_HOST_<name> 形式两边也都识别)。原生 mcli 若按自身默认运行则读 ~/.mcli/config.json,除非把 MC_CONFIG_DIR 指过去,否则看不到这个共享文件。

alias set 在写入前会对端点校验凭据,密码错误时配置文件保持不变。cp / mirror / diff 的本地路径参数会按真实绝对路径解析并挂载,与 pg mc 行为 一致(macOS 上请保持在 home 目录内)。会被 pg 自身解析器拒绝的 mcli 参数放到 -- 之后:

pg mcli ls store -- --all
MC_HOST_store="http://admin:<password>@127.0.0.1:9000" pg mcli ls store

两个客户端共用 MinIO 页面记录的命令表—— 常用命令——把 pg mc 换成 pg mcli 即可。

端口

每个实例从一个池中取两个连续端口,起点为 minio_start_port(默认 9000):先 S3 API,后控制台。该池与 minio 插件共享——minio 与 silo 实例在同一游标上分配,同机并存不冲突;先 minio 表(按名称),后 silo 表:

pg addon install minio --name store    # 9000 / 9001
pg addon install silo  --name lake     # 9002 / 9003

配置

实例位于 pg.yaml 顶层 addons.silo 映射,以实例名为键:

namespace: default
minio_start_port: 9000       # 共享池:minio 与 silo 都从这里取
addons:
  silo:
    store:
      container_name: pgcli-silo-default-store
      name: store
      image_tag: docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z
      # data_dir: /srv/silo     # 省略则为 <base-dir>/addon/silo/store/data
      # drives:                  # 多盘(SNMD/MNMD):每块盘一个宿主目录,
      #   - /mnt/minio/disk1     # 各自挂到自己的 /dataN —— 见下文 SNMD/MNMD
      #   - /mnt/minio/disk2
      listen: 127.0.0.1
      api_port: 9000
      console_port: 9001
      root_user: admin
      root_password: <generated>   # 首次 install 时写入
      autostart: false             # pg autostart enable --silo --name store
      # tls: true                  # HTTPS(自签 CA,或下面的 BYO)
      # cert_file: /etc/ssl/silo.test.crt   # BYO 叶子(+链),隐含 tls
      # key_file:  /etc/ssl/silo.test.key   # BYO 私钥,必须与 cert_file 配对
      # endpoints:                 # 省略为单机;见"分布式 / 集群模式"
      #   - http://10.0.0.11:9000/data
      #   - http://10.0.0.12:9000/data
      #   - http://10.0.0.20:9000/data
      #   - http://10.0.0.21:9000/data
      #   (同时设置 drives 即为 MNMD —— 每个节点的每块盘对应一条 /dataN 端点,
      #    且各节点的列表完全一致;见"多机多盘(MNMD)")

对 listen、端口、root_user、root_password、image_tag、data_dir、 tls/cert_file/key_file 或 endpoints 的修改,在下一次 pg addon install silo --name store --force 后生效——普通 install 会跳过仍 存在的容器,--force 重建它(数据目录永不触碰)。

查看列表

pg addon list
Infra add-ons (silo):
  silo (name: store)
    Status:      running
    Listen:      127.0.0.1
    API port:    9000
    Console port: 9001
    Console URL: http://127.0.0.1:9001/
    Data:        ~/pg/addon/silo/store/data
    Root user:   admin
    Image:       docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z
    Container:   pgcli-silo-default-store

pg addon list 永不打印密码——从 pg.yaml 读取。

启停

主机重启后,不重新应用配置地拉回实例:

pg addon start silo --name store
pg addon stop  silo --name store

install 跳过仍存在的容器(已停止的则启动);start 只启动已存在的容器 (状态异常时依配置重建自愈)。TLS 生成模式下 start 还会重新校验、必要时 重签叶子证书(silo 热加载挂载的证书)。

开机自启

容器带有 --restart unless-stopped 策略(管崩溃,不管重启)。要在主机重启 后拉起实例:

pg autostart enable --silo --name store

这会设置 autostart: true 并安装/刷新 boot service(见 开机自启)。boot 是只启动的。silo 独立于 PostgreSQL 栈,因此排在最后启动;无顺序约束。pg autostart status 列出所有目标的状态。

删除

pg addon remove silo --name store            # 容器删除,数据保留
pg addon remove silo --name store --clean-data   # 同时删除数据目录

数据目录就是对象存储——丢了它等于丢掉其中所有 bucket——所以 remove 默认保留并打印其位置。--clean-data 删除它(并清理默认为空的默认布局父 目录;data_dir 覆盖路径及其父目录永不触碰)。

日志

pg logs addon silo --name store      # 最后 50 行
pg logs addon silo --name store -f   # 跟踪

silo 输出到 stdout:启动行(API:/Console: 地址、Documentation:)与请求 错误。API: http://... 块确认监听已就绪。

故障排查

  • install 报 pulling silo image ... : ...。 公开 tag docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z 不可达——检查到 docker.io 的网络/registry 访问,或用 podman pull 预先拉取。
  • 手工 --api-port 撞端口。 选择自动池(minio_start_port 及以上) 之外的端口,否则自动分配器会视其为已占用;该池与 minio 插件共享,所以 silo 实例也要避开 minio 实例的显式端口,反之亦然。每个存储都需要一对 连续端口。
  • 主机重启后 pg addon start 无效 / 失败。 看 pg logs addon silo --name store -f——多半是数据目录被删(--clean-data 或手工),silo 拒绝在曾格式化过的空目录上启动;或绑定端口变了。
  • 控制台可访问但 S3 客户端超时。 MINIO_SERVER_URL 由 listen + API 端口构成;绑 127.0.0.1 却从其他主机访问时,客户端会被重定向到回环 URL。把 listen 设成客户端真正可达的地址。(集群模式完全不设 MINIO_SERVER_URL。)
  • 用 pg mc 设的 alias 在 pg mcli 里看不到(或反之)。 两者按设计共 享 ~/.mc/config.json,出现这种情况说明 alias 确实没写进去——pg mc alias list(同一文件)核对一下,或用环境变量传 MC_HOST_<name>。原生 mcli 读 的是 ~/.mcli,不指 MC_CONFIG_DIR 就看不到这个共享文件。
  • macOS。 受支持:插件在 pgcli-net bridge 上服务并发布两个端口,Mac 的 127.0.0.1:<port> 即可访问(容器内部绑 0.0.0.0, MINIO_SERVER_URL 声明 Mac 使用的回环地址)。改端口/凭据后 --force 重建容器。pg mcli 的容器不能用 127.0.0.1 alias——见 MinIO mc 页的 端点说明。

8 - MinIO

以 pgcli 插件方式运行 MinIO——单机或跨主机分布式纠删码集群的 S3 兼容对象存储(含 Web 控制台)

MinIO 是 S3 兼容的对象存储。pgcli 将其作为独立的顶层插件 运行——共享基础设施,而非实例级 sidecar——带 Web 控制台。默认采用单机 (single-node)模式,也支持跨主机的分布式集群模式(见下文 分布式 / 集群模式)。两种模式下它都是通用对象存储。 silo——Pigsty 的 MinIO 分支,保持线路兼容——作为同级插件提供,功能面 完全一致;两者共用一个端口池,可在同一主机并存。

平台支持: MinIO 插件两个平台都支持。Linux 上通过主机网络提供服务; macOS 上加入 pgcli-net bridge 网络并发布两个端口,Mac 用 127.0.0.1:<port> 即可访问 API 与控制台,与其他插件一致。公开镜像为双架构 (amd64 + arm64),任意主机架构均可运行。pg mc 客户端同样两个平台可用。

工作原理

一个容器、一个目录:minio server /data --address <listen>:<api-port> --console-address <listen>:<console-port>,其中 /data 是实例主机数据目录 (默认 <base-dir>/addon/minio/<name>/data)的 bind mount。MinIO 存储的一切 都在这个目录里,因此它的生命周期长于容器。

镜像是公开的预构建 tag ghcr.io/mars-base/pgcli/pgcli-minio:20250422221226—— 上游静态二进制(双架构 amd64 + arm64,来自 minio/minio 的 GitHub release)跑在 Alpine 上,因为 MinIO 官方镜像移除了内置的 Web 控制台。pg addon install 只负责拉取;pgcli 运行时从不构建镜像。

凭据的处理方式与 Patroni 相同:

  • root_user 默认 admin;
  • root_password 首次 install 时自动生成(也可以用 --root-password 显式指定),存入 pg.yaml (addons.minio.<name>.root_password),并在安装摘要中打印一次方便记录。

二者就是 MinIO root 的 access key / secret key——所有 S3 客户端(包括 pg mc alias set <名称> <URL> <root_user> <root_password>)所说的 access key 与 secret key 就是它们。

容器按 MinIO 官方部署建议带上 --ulimit nofile=1048576:1048576 和 --stop-timeout 60;macOS 上 podman machine 虚拟机的 RLIMIT_NOFILE 上限更 低,因此该平台改用 65536——单机 dev/test 足够。注意 root 凭据通过 -e 传 入,会出现在 podman inspect 里——与手工运行容器的暴露程度相同;对 rootless 单机部署(本插件的定位)可以接受。

安装

# 默认实例名 "minio",端口从池中分配(基址 9000),只绑回环
pg addon install minio

# 命名实例 + 显式数据目录
pg addon install minio --name store --data-dir /srv/minio

# 固定端口、指定 root 用户
pg addon install minio --name store --api-port 9000 --console-port 9001 --root-user admin

# 绑定到所有网卡,暴露到网络
pg addon install minio --name store --listen 0.0.0.0

# HTTPS(pgBackRest S3 仓库的前提):pgcli 自签 CA + 原生 TLS
pg addon install minio --name store --tls

# 用你自己已有的证书对外提供 HTTPS(公共 CA 或私有 CA 签发的域证书)
pg addon install minio --name store --tls-cert /etc/ssl/minio.test.crt --tls-key /etc/ssl/minio.test.key

输出会报告端点与 root 凭据:

✓ minio installed: "store"
  Container:    pgcli-minio-default-store
  Image:        ghcr.io/mars-base/pgcli/pgcli-minio:20250422221226
  Data:         ~/pg/addon/minio/store/data
  S3 API:       http://127.0.0.1:9000
  Console:      http://127.0.0.1:9001

  Root user:     admin
  Root password: <generated>

用打印出的 root 用户与密码登录 Console: 地址的控制台。S3 客户端 (包括 pgBackRest)指向 S3 API: 地址即可,也可以在终端用下文的 pg mc直接操作。

对已存在的实例重复执行 install 是无损的:不会重建容器(处于停止状态的会 直接启动并给出提示),命令行参数会合并进已存配置,已有的 root 密码保持 不变。要让改过的端口、监听地址或凭据生效,加 --force 重建容器(数据目录不受 影响)。

绑定地址: 默认 127.0.0.1,存储仅本机可见。--listen 0.0.0.0(或 pg.yaml 里的 listen 键)会把它暴露到网络上——能访问该端口的任何人都可尝试 root 凭据,因此只应在防火墙后或 TLS 终结代理之后这样暴露。

TLS(--tls)

--tls 让 MinIO 以 HTTPS 提供服务:pgcli 用标准库生成一把自签 CA 和一张叶证 书(SAN 覆盖回环名、localhost 与本机所有网卡 IP),落在 <base_dir>/tls/minio/<name>/(public.crt / private.key 供 MinIO --certs-dir 用,ca.crt 供分发),随后把证书目录只读挂进容器。端点 URL 随 之变为 https://。

之所以需要它:pgBackRest 对 S3 仓库强制 HTTPS(明文被上游明确拒绝),所以 一个要接收 Patroni archive-push 的 MinIO 必须说 TLS。把 ca.crt 路径填进 backup.repo.s3.ca_file,pgBackRest 就会以完整证书校验连接它——见 备份 → S3 对象存储仓库。

关于"store 与集群怎么配对"的一点说明:backup.repo.s3 是一份配置文件里唯一 的全局仓库,所以最先接上的那台 TLS MinIO 会收到该环境里的所有 stanza—— 再装一台 MinIO 并不会让集群们多出一个可按集群选择的 store。确需两个目的地 时,跑第二份配置(见备份 → 共享备份容器,以及完 整示例示例:HA 集群 + 自签 CA 的 MinIO)。

pg mc 在命令通过回环别名访问 TLS 存储时会自动附加 --insecure(mc 无法 按别名持久化 CA 信任;同机 127.0.0.1 回环可接受)。指向局域网 IP 的别名不算 回环——需自己加 -- --insecure,或正确配置 CA 信任。外部 https:// 端点仍 做完整校验。给已存在的实例开关 --tls 需要 --force 重建容器才生效。

有效期与跨主机访问。 CA 与叶证书均有效期 100 年(公共 CA 需遵守的 825 天 上限是浏览器策略,Go 校验器对私有根 CA 并不强制)。pgcli 会在 --force 时、 或主机地址变化时重签叶证书——因此对每个存储,分发 ca.crt 只需做一次。TLS 客户端校验的是它所拨号的目标地址是否匹配叶证书的 SAN,客户端自身地址从不 出现在任何证书里。所以远端主机(虚拟机里的 Patroni 成员、另一台机器上的备份容器)把 backup.repo.s3.endpoint 指向存储主机的 某个 IP 即可(回环名与本机所有网卡 IP——含虚拟网桥——都在 SAN 里;裸主机名 不在,除非设置了 MINIO_SERVER_URL,其 host 会被追加进 SAN)。

远端主机取 CA——无需 scp。 服务端证书以"叶 + CA"链形式下发,所以另一台机器 上的消费方可以直接从 TLS 握手里把根证书拉回来:

pg backup fetch-ca <存储主机>:9002
#   [OK] CA fetched from <存储主机>:9002
#        saved:    ~/.pgcli/backup/repo-ca/ca-<存储主机>-9002.crt
#        SHA-256:  c0f0…fe2e
pg backup setup --s3-ca-file ~/.pgcli/backup/repo-ca/ca-<存储主机>-9002.crt

这次拉取本质是"首次使用即信任"(TOFU)——在你拿到 CA 之前,没有任何东西能 验证它——所以信任前请把打印出的 SHA-256 与存储主机上 sha256sum ~/.pgcli/tls/minio/<name>/ca.crt 的结果对拍(如同核对 SSH 主机指 纹)。之后一次 setup 会把 CA 发布进 Patroni 集群的 etcd 注册表,集群里其余 主机自动拉取、零手工步骤(见备份 → S3 对象存储仓库)。手工拷 贝 ca.crt 仍是可用的兜底路径,也是存储主机跑的是"链下发"之前的旧 pgcli 时的 唯一选择——fetch-ca 的报错会明确指出这一点,在存储主机上重跑 pg addon install minio --tls 即可升级(证书热重载,无需重启)。pgBackRest stanza 从不要求 MinIO 与它同机。

使用自带证书(--tls-cert / --tls-key)

--tls 只会下发 pgcli 自签的那对证书。如果你想对外提供自己已有的证书——某 个公共 CA 为真实域名签发的,或你的私有 CA 签发的——改用 --tls-cert <leaf(+链).pem> --tls-key <key.pem>。它与 --tls(会被隐式打开, 无需重复传)配合,取代生成的证书:两个文件被只读挂载到 MinIO --certs-dir 要求的文件名上(/opt/minio/certs/public.crt、/opt/minio/certs/private.key), pgcli 从不复制或重签它们——私钥只留在你放置的那一个地方。

pg addon install minio --name store \
  --tls-cert /etc/ssl/wildcard.hi.163.com.crt \
  --tls-key  /etc/ssl/wildcard.hi.163.com.key

安装时校验什么、不校验什么。 --tls-cert/--tls-key 会在任何容器启动前, 通过 Go 自身的 TLS 加载器配对校验,所以密钥与证书不匹配、证书其实只是一张 CA、证书已过期、或证书被限定用于服务端以外的用途——都会在安装时直接失败,而 不会延后成一个反复崩溃重启的容器。证书 SAN 是否覆盖所配置的 --listen 地址 也会被检查,但只作告警——域证书常常是通过 DNS 或负载均衡器背后的名字被访问 的,与它运行在哪台主机无关,所以这里的不匹配是提示、不是致命错误。

续期。 替换证书/密钥文件后重建容器:

pg addon install minio --name store --tls-cert <new.crt> --tls-key <new.key> --force

重建是必需、而非可选的:文件以只读方式 bind 进容器后,运行中的 MinIO 不会 感知被替换的证书——既不会感知"写新文件再覆盖旧文件"(那会换掉单文件挂载所钉 住的 inode),甚至连原地重写同一个文件也不感知。已实测:改写宿主文件后,容器 仍继续下发旧的证书对,直到 --force 重建为止。pgcli 从不重签自带证书——证书 生命周期由运维方掌握——所以没有可依赖的自动刷新。

客户端。 用受公共 CA 签发的证书时,S3 客户端(mc 系、aws CLI、 pgBackRest 经 repo*-s3-ca-file)完全不需要额外信任材料——信任链本就锚定在 它们已信任的 CA 里。其余证书则把信任锚交给客户端的 backup.repo.s3.ca_file / --s3-ca-file:私有 CA 证书交签发 CA(或完整的 叶+中间证书链);自签证书既然自身即锚,pg cert 造的那张就直接交被服务的 .crt 本身。从没见过这个文件的远端主机,可以不用 scp,直接从 TLS 握手里自动 把它拉回来——fetch-ca 会识别自签叶证书并替你保存:

pg backup fetch-ca <存储主机>:9010
#   [OK] CA fetched from <存储主机>:9010
#        saved:    ~/.pgcli/backup/repo-ca/ca-<存储主机>-9010.crt
#        SHA-256:  d4df…81e2
pg backup setup --s3-ca-file ~/.pgcli/backup/repo-ca/ca-<存储主机>-9010.crt

请把打印的 SHA-256 与存储主机上对你交给 --tls-cert 的那个 .crt 跑 sha256sum 的结果对拍(首次使用即信任,与上面取回生成 CA 的用法同理)。公共 CA 证书则仍然用不上这条命令——没有需要去取回、且尚未被信任的东西。

关闭自带证书模式。 没有 --tls-cert=/关闭 这类 flag——跨重跑的配置合并是 单向的,与 --tls 本身的行为一致。要回到生成证书模式,从 pg.yaml 里该实例 下删掉 cert_file/key_file,再用 pg addon install minio --name store --force 重建即可。

生成测试证书:pg cert

尝试自带证书模式不必先有一套真的 CA——pg cert 签发一张自签证书,SAN 覆盖你 要求的任意域名 / IP 组合:

# 一张同时对一个主机名和两个 IP 生效的叶证书,ECDSA P-256(默认),
# 825 天有效期(默认)——这正是多数自带证书场景要的形态:
pg cert --host "minio.test,127.0.0.1,10.0.0.9" \
  --cert-file minio.crt --key-file minio.key

# 然后对外提供它:
pg addon install minio --name store --tls-cert minio.crt --tls-key minio.key

它产出的任何东西都不碰 pg.yaml、也不碰容器——只是往你指定的路径写两个 PEM 文件并打印 SAN。flag 一览:

flag 默认值 含义
--host 127.0.0.1 逗号分隔的域名和/或 IP,编码进 SAN(可重复传)。条目会自动识别是域名还是 IP,"minio.test,10.0.0.9" 不需要特殊语法;*.wild.test 会作为通配符域名条目。
--cert-file cert.pem 写出证书 PEM 的路径。
--key-file key.pem 写出私钥 PEM(PKCS8)的路径。
--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)——用于你想拿它当私有根再去签别的证书,而非常规的 --tls-cert 用法。

同一套签发逻辑也编成了一个可脱离 pg 使用的独立二进制:make gencert → bin/gencert,flag 完全相同、只是用单横线形式(-host、-cert-file……)。两 者背后是同一个 internal/certgen 包,产出的证书类型逐字节一致。

pg cert 写出的是单张自签叶证书,不是一条链。 --cert-file 里的 PEM 恰好 只有一块 CERTIFICATE——这张证书自我签名(IsCA: false、serverAuth EKU、 你要求的 SAN)。它有意不是"叶+中间+根"的打包链:它上面没有签发 CA,所以没有 东西可以拼进链;ValidateBYOCert 只看文件里的第一块证书,并拒绝 CA 证书 (leaf.IsCA)——这也正是 --ca 产出的那张证书不能拿去喂 --tls-cert 的原因:它是信任锚本身,不是服务端证书。

既然产出是自签证书,在客户端侧就把生成的 minio.crt 当作 pgcli 自己为 --tls 生成的那张 ca.crt 同样使用——被下发的那张叶证书本身就是自己的 信任锚,所以 backup.repo.s3.ca_file / pg backup setup --s3-ca-file 直接 指向同一个文件即可(见 备份 → S3 对象存储仓库)。这不是 pgBackRest 勉强容忍的旁门做法:OpenSSL 的信任库把通过 -CAfile/SSL_CTX 交给它的任何 证书都当作锚点,并不要求 CA:TRUE,而 pgBackRest 的 S3 TLS 路径(curl 走 OpenSSL)用的正是这一套机制——直接实测过:对 pg cert 产出的自签、CA:FALSE 叶证书跑 openssl verify -CAfile <证书> <证书>,返回 OK。

部署形态

MinIO/silo 对自身的部署布局有明确分类;本插件四种全部支持:

形态 结构 适用场景
SNSD(单机单盘) 单节点、单个数据目录——不给 --endpoint 也不给 --drive 时的默认 开发、测试、演示
SNMD(单机多盘) 单节点、多块盘——每个 --drive 传一块盘,见下文 SNMD 单机部署要扛住坏盘,又不想额外搭文件系统层
MNSD(多机单盘) 多节点、每节点一块数据盘——即下文的分布式模式 紧凑的高可用部署
MNMD(多机多盘) 多节点、每节点多块盘——--drive 传本节点的盘,--endpoint 传全集群矩阵 既要扛坏盘、又要扛坏机,且不额外搭文件系统层

pg addon install minio 开箱即是 SNSD。要得到 MNSD,传入集群的 endpoint 列表(至少四节点)——见下文分布式 / 集群模式。

MNMD 把两个 flag 组合起来:--drive 传本节点的盘(与 SNMD 完全一致), --endpoint 传整个集群的 host×drive 端点矩阵(每台每个盘各一条 URL)—— 见下文多机多盘(MNMD)。SNSD/MNSD 下的磁盘冗余另一条路仍然 可用——把 ZFS 池垫在 --data-dir 底下——它能让底层布局随时可换,空间利用上 也往往更省。S3 存储高可用方案 对比了原生 MNMD 与 ZFS 两条路,也覆盖"4 主机、每主机多块盘"这个既能扛坏盘又能扛坏机的 混合形态。

单机多盘(SNMD)

一个 MinIO 进程、多个宿主目录、盘之间做纠删码。每个 --drive 传一块盘,替 代 --data-dir:

pg addon install minio --name store \
  --drive /mnt/minio/disk1 --drive /mnt/minio/disk2 \
  --drive /mnt/minio/disk3 --drive /mnt/minio/disk4 --tls

每个 --drive 是一块独立设备上的宿主目录;第 N 块盘挂到容器路径 /dataN,服务进程以 minio server /data1 /data2 ... /dataN 启动。--drive 与 --data-dir 互斥——多盘模式的数据位置只由 --drive 决定;--drive 再加 上 --endpoint 就是 MNMD,即多盘模式的多机版本,见下文多机多盘 (MNMD)。和 MinIO 所有盘一样,与宿主根设备共享的盘会被 MinIO 在启动时拒绝;pgcli 会列出越界的盘、在 install 时给出警告。

EC 换来什么(在活的 4 盘集上实测):MinIO 把每个对象切成数据片 + 校验 片,4 盘集默认 2 片校验——可容忍 2 块盘故障。坏 1 块盘时读写都照常;坏 2 块 盘时读仍成功、写被拒——这就是 quorum 边界,和 MNSD 用的是同一套算术,只不过 成员是盘而不是节点。可用容量约为原始总量的一半(4 × 2 GiB 盘 → mc admin info 报 3.6 GiB 可用、EC:2)。回来的盘由 MinIO 自己 heal,pgcli 无需介入。校验片默认值随盘数变化——本文只实测了 4 盘这一种形状。

pg addon remove minio --name store --clean-data 会删除每一块盘的目录——但 拒删仍处于挂载状态的盘,所以一次误操作的 --clean-data 绝不会穿透挂载点把 底下的盘 rm -rf 掉。真要丢弃下面的数据,先 umount。

分布式 / 集群模式

也支持 MinIO 的纠删码(EC)集群模式。它要求至少 4 个互不相同的 host:port 端点,并且与其他插件不同,没有中心协调者:每台主机各自跑 一份 pgcli、各自一份 pg.yaml,而每一份 pg.yaml 都完整写入同一份端点 列表和同一套 root 凭据。pgcli 只负责拉起本机对应的那一个容器——跨主机的 组环握手由 MinIO 自己完成。

端点列表就是模式开关:为空即单机(行为不变),非空则以 minio server <ep1> <ep2> ... 进入分布式模式。

每个端点的形式是 http://<host>:<port><路径>。host:port 是节点之间互相 访问、完成组环握手的地址。尾部那段 <路径> 不是 HTTP 路由——客户端永远 看不到它——它是 export path(导出路径),即该节点把自己那一份纠删码分片 数据存放在容器内的哪个目录。MNSD 下(不给 --drive)pgcli 把 --data-dir 挂载到容器的 /data,所以这段路径必须从 /data 开始——最简单就直接写 /data。四个端点里这段路径字符串要保持一致。(MNMD 下节点也会传 --drive, 此时每个端点改为指名该节点的一个 /data1../dataN 盘槽——见下文 MNMD。)

注意端点里的路径是容器内路径,与宿主目录结构无关:如果 /data 是一块 共享盘、还想在上面放别的东西,把 --data-dir 指到它的子目录即可—— --data-dir /data/minio 让本集群的数据落在宿主的 /data/minio,而端点仍然 写 http://<host>:9000/data(端点无法指名这个子目录,它看到的是挂载根)。 只有当你有意往卷内部再扩展一层(比如 /data/mystore)时,数据才会在宿主上 多落一层到 <data-dir>/mystore——避免把 --data-dir /data/minio 和端点 .../data/minio 搭配使用,那会嵌套成 /data/minio/minio。

# 节点 1(10.0.0.11),宿主上独立数据盘已挂载到 /data:
pg addon install minio --name store \
  --listen 10.0.0.11 \
  --data-dir /data \
  --root-password '<共享密码>' \
  --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 列表,以及
# 完全相同的 --root-password 值。

--root-password 是可选的:不传的话首次 install 会自动生成一个(打印一次、 存入 pg.yaml)。集群模式下每台传同一个值,就能保证全集群凭据一致,不需要 任何手工复制。

安装摘要会列出成员并提醒一致性要求:

✓ minio installed: "store"
  ...
  Distributed mode: 4 endpoints
    - http://10.0.0.11:9000/data
    - http://10.0.0.12:9000/data
    - http://10.0.0.20:9000/data
    - http://10.0.0.21:9000/data
  NOTE: every node's pg.yaml must carry the identical endpoint list AND
  identical root credentials, or the cluster will not form.

集群模式有三条硬性约束,都是实测踩出来的:

  • 端点必须是可路由、互不相同的主机。 同一主机会折叠成"单机多盘"并被 拒绝(use path style endpoint for single node setup);127.0.0.0/8 回环段直接被拒(resolves to localhost)。请用各节点真实的局域网地址。
  • 数据目录必须位于与根文件系统不同的磁盘上。 MinIO 拒绝与系统盘同设备 的驱动器(drive is part of root drive, will not be used)。pgcli 会用 数据目录与 / 的 stat 做检测,在 --data-dir 落在根设备时于 install 时给出警告——警告是提示性的、不阻断;请把 --data-dir 指向独立挂载的 数据盘。
  • 集群模式下不下发 MINIO_SERVER_URL。 它在各节点本就不同(各自宣告 自己的 IP),而 MinIO 要求所有 MINIO_* 环境变量逐节点字节一致,否则每台 都会卡在 Waiting for at least 1 remote servers with valid configuration。 地址由端点列表决定。单机模式仍按 listen 设置 MINIO_SERVER_URL。

Quorum(EC): 写需要 ⌈N/2⌉+1 个节点在线,读需要 ⌈N/2⌉。因此 4 节点 集群在挂掉 2 台时仍可读、但拒绝写;重启下线的节点后集群自愈。MNMD 下被计数的 成员是盘而不是节点——活体 4×4 集群上实测的盘级边界见下文 MNMD。

仅跨主机。 这是真正的分布式部署——每台主机由你自己跑 pgcli。pgcli 不会 在节点间 SSH、也不做成员注册;保持 N 份 pg.yaml 一致是运维的职责。

多机多盘(MNMD)

MNMD 就是每个节点贡献多块盘的 MNSD:MinIO 在整个 host×drive 矩阵上做纠删 码,因此这套集合不需要任何文件系统层就能同时扛住单块盘故障和整台节点故障。 用 --drive 传本节点的盘(与单机多盘(SNMD)完全一致),用 --endpoint 传整个集群的端点列表——每台、每个盘各一条 URL,不只是本机。

与 MNSD 唯一的区别是导出路径。每个节点把自己的盘挂到 /data1../dataN,所以 每个端点必须指名其中一个槽:http://<host>:<port>/data<k>。pgcli 不解析 URL——它把整个矩阵原样交给 minio server,与 MinIO 官方文档的写法一致——因 此每个节点都要携带同一份完整列表。列表长度必须是每节点盘数的整数倍(MinIO 要 求每个节点贡献相同数量的盘),且必须至少包含一台远端节点;会折叠到单台主机的 矩阵会在容器启动前就被拒绝,--tls 节点搭配明文 http:// 端点同样会被拒。

用 TLS 服务整个 grid——一份证书,而不是每节点一张 CA。 让每个节点各自跑 裸 --tls,每台主机就会签出自己的私有 CA,于是第一次跨节点握手死在 x509: certificate signed by unknown authority,剩下的活儿只能靠手工把某一张 CA 种进 每个节点的证书目录。干净的做法是整个 grid 共用一份证书:pg cert 写出一张 SAN 覆盖所有节点地址的自签叶子,把这一份 .crt/.key 逐字节相同地复制到所有 节点,用 --tls-cert / --tls-key 服务它(根本没有逐节点 CA 需要对齐)。这 就是推荐的 TLS 配法——反正 pgBackRest 对 S3 仓库强制 HTTPS,一个备份 store 本 就是一个 TLS grid。端点矩阵届时必须全部是 https://,且节点拨出的每个地址都 要在叶子的 SAN 里。(同一份共享叶子服务 MNSD 集群同样合适——握手的还是那几台 服务器。)

# 任意一台上执行一次——整个 grid 共用一张叶子(列出所有节点地址;客户端还会拨的
# VIP 或主机名也一并加上):
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),四块数据盘已挂载并用 --drive 传入:
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

# 节点 2-4:同样的命令、同一份 grid.crt/grid.key、各自的 --drive 路径,以及
# 完全相同的 16 条端点矩阵和完全相同的 --root-password 值。

--listen 0.0.0.0 是有意为之:每个节点都要在其他 15 条端点所指名的地址上应答。 --root-password 必须处处一致,与 MNSD 的要求相同。

在活体的 4 节点 × 4 盘集群(16 × 2 GiB)上——正是用上面这套共享 pg cert 的 TLS 配法成环,Network: 4/4 OK,没有逐节点 CA 需要对齐——用 mc admin info 实测到的形状:

  • 16 块盘在线,EC:4,位于单个 stripe 大小为 16 的纠删码集合里——该集合报 4 片校验,因此 16 块盘里坏 4 块仍然健康。
  • 损失一整台节点(4 块盘 → 12/16 在线)时读和写都照常工作;64 MiB 上传 /下载往返逐字节一致。
  • 再损失第二台节点(8/16 在线):写被拒(Resource requested is unwritable),读也失败——该集合已不再安全可服务。重启节点后自愈回 16/16。
  • 可用容量随校验比例而定:16 盘 EC:4 保留原始字节的 12/16。--clean-data 会逐盘删除目录,但拒删仍在挂载的盘;回来的盘由 MinIO 自己 heal。

所有节点的 pg.yaml 都携带同一份 16 行矩阵和一个共享的 root 密码;保持它们 一致,与 MNSD 一样,是运维的职责。

使用 mc 客户端

pg mc 在一次性容器里运行 MinIO 自家的 mc 客户端——不用本地安装,也不用 手敲 podman run:

# 按平台选 URL —— 见下文"各平台的端点":
pg mc alias set store http://127.0.0.1:9000 admin <密码>    # Linux,本机 addon
pg mc alias set store http://host.containers.internal:9000 admin <密码>  # macOS,本机 addon
pg mc alias set store http://10.0.5.7:9000 admin <密码>     # 两者皆可,远端存储
pg mc mb store/backups
pg mc ls store
pg mc cp ./dump.pglz store/backups/

别名持久化保存在宿主的 ~/.mc/config.json——这是 mc 自己的默认路径, Linux 和 macOS 都一样——alias set 一次,之后每次 pg mc(以及 Linux 上 原生安装的 mc)都能看到同一批别名。pg mc alias list 读回列表, pg mc alias remove 删除单个别名。alias set 会先拿凭据向端点校验,通过才 写文件——密码打错不会在配置里留下一个用不了的别名。pg mc 底层做的事只是把 你的 ~/.mc 挂载进容器,然后运行 ghcr.io/mars-base/pgcli/pgcli-mc(静态上 游二进制 + scratch,首次使用时自动拉取);没有别的魔法。

cp / mirror / diff 的本地文件参数同样可用:pg mc 会把每个本地路径按 realpath 解析,并把该绝对路径原样挂载进容器,因此

pg mc cp ./dump.pglz store/backups/        # 从当前目录上传
pg mc cp store/backups/dump.pglz ./        # 下载到当前目录
pg mc mirror ./repo store/backups/         # 或镜像整棵目录树

所见即所得。下载到尚不存在的路径(新文件名、或还没建的 ./newdir/)也没问 题——挂载的是它最近的已存在父目录,mc 写出的文件会落回宿主。macOS 上路径必须 位于家目录之下——那是 podman machine 虚拟机唯一共享的目录树;家目录之外的 路径 pg mc 会明确提示并跳过。

任何 pg 自己解析器会拒绝的 mc 标志都要放到 -- 之后,与 pg etcdctl 的 约定完全一致。mc 的长标志(--all、--json、--recursive、--force、 --newer-than 等)pg 一个都不认识,因此全部放 -- 之后:

pg mc ls store -- --all
pg mc cp ./dir store/backups/ -- --recursive
pg mc rm store/old -- --recursive --force

放在 -- 之前会在 pg 这一层就被拦下,报错形如 unknown flag: --recursive ——看到它就说明分隔符漏了。

原生 mc 有个与本地挂载相关的注意点值得知道:在文件类命令(cp / mirror / diff)里,首段不是已知别名的参数一律按本地路径处理。所以别名拼错时不会报 错,而是安静地上传/下载到当前目录下一个以拼错名字命名的本地目录——pg mc 只是忠实挂载了 mc 判定要读的路径,这是原生行为而非 pg mc 的怪癖。执行文件 操作前先用 pg mc alias list 确认别名存在。

MC_HOST_<name> 环境变量形式的别名——mc 的无状态用法,不碰配置文件——同样 适用于 pg mc,因为该变量会被转发进容器:

MC_HOST_store="http://admin:<密码>@127.0.0.1:9000" pg mc ls store

各平台的端点: Linux 上 pg mc 走主机网络,127.0.0.1 别名可直达本机 addon 实例。macOS 上容器位于 bridge 网络,127.0.0.1 是容器自己的回环—— 指向 Mac 本机 addon 的别名请用 http://admin:<密码>@host.containers.internal:9000,远端存储则用其可路由地 址。pg mc 本身在两个平台上行为一致。

Mac 浏览器访问控制台走的是宿主机侧的 127.0.0.1:<console-port>(插件已发布 该端口),这与 pg mc 容器能不能用 127.0.0.1 无关——那只是宿主机自己的回环。

常用命令

命令 用途
pg mc ls store / pg mc ls store/backups 列出桶 / 对象
pg mc mb store/backups 创建桶
pg mc cp ./file store/backups/ 上传(目录树用 pg mc mirror ./dir store/backups/,或给 cp 加 -- --recursive)
pg mc cp store/backups/file ./ 下载
pg mc find store -- --name '*.pglz' 按名字搜索对象
pg mc du store 统计各桶大小
pg mc rm store/backups/file 删除单个对象
pg mc rm store/prefix -- --recursive 批量删除对象
pg mc rb store/bucket -- --force 连对象一起删桶

同一套 root 凭据适用于任何 S3 SDK——包括 pgBackRest 的 repo1-type=s3 仓库(见备份、恢复)。

端口

每个实例从同一个端口池取两个连续端口,基址为 minio_start_port(默认 9000):先 S3 API、后控制台。与其他自动分配池一样,会跳过已占用端口与同级 实例的显式端口:

pg addon install minio --name store    # 9000 / 9001
pg addon install minio --name archive  # 9002 / 9003

配置

实例存放在 pg.yaml 顶层 addons.minio 映射中,以实例名为键:

namespace: default
minio_start_port: 9000
addons:
  minio:
    store:
      container_name: pgcli-minio-default-store
      name: store
      image_tag: ghcr.io/mars-base/pgcli/pgcli-minio:20250422221226
      # data_dir: /srv/minio     # 省略则为 <base-dir>/addon/minio/store/data
      # drives:                  # 多盘(SNMD/MNMD):每块盘一个宿主目录,
      #   - /mnt/minio/disk1     # 各自挂到自己的 /dataN —— 见下文 SNMD/MNMD
      #   - /mnt/minio/disk2
      listen: 127.0.0.1
      api_port: 9000
      console_port: 9001
      root_user: admin
      root_password: <generated>   # 首次 install 时写入
      autostart: false             # pg autostart enable --minio --name store
      # tls: true                  # 以 HTTPS 提供服务(自签 CA,或下面的自带证书)
      # cert_file: /etc/ssl/minio.test.crt   # 自带叶证书(+链),隐含 tls;见"使用自带证书"
      # key_file:  /etc/ssl/minio.test.key   # 自带私钥,须与 cert_file 配对
      # endpoints:                 # 省略即单机;见"分布式 / 集群模式"
      #   - http://10.0.0.11:9000/data
      #   - http://10.0.0.12:9000/data
      #   - http://10.0.0.20:9000/data
      #   - http://10.0.0.21:9000/data
      #   (同时设置 drives 即为 MNMD —— 每个节点的每块盘对应一条 /dataN 端点,
      #    且各节点的列表完全一致;见"多机多盘(MNMD)")

修改 listen、端口、root_user、root_password、image_tag、data_dir、 tls/cert_file/key_file 或 endpoints 后,执行 pg addon install minio --name store --force 即生效——普通 install 会跳过已存在 的容器,--force 会重建它(数据目录永不受影响)。

查看列表

pg addon list
Infra add-ons (minio):
  minio (name: store)
    Status:      running
    Listen:      127.0.0.1
    API port:    9000
    Console port: 9001
    Console URL: http://127.0.0.1:9001/
    Data:        ~/pg/addon/minio/store/data
    Root user:   admin
    Image:       ghcr.io/mars-base/pgcli/pgcli-minio:20250422221226
    Container:   pgcli-minio-default-store

pg addon list 从不打印密码——请到 pg.yaml 里查看。

启动与停止

主机重启后,不重新下发配置即可拉起实例:

pg addon start minio --name store
pg addon stop  minio --name store

install 会跳过已存在的容器(处于停止状态的直接启动);start 只启动已有 容器(状态异常时依据配置自动重建)。

开机自启

容器带 --restart unless-stopped 策略(管崩溃、不管重启)。主机重启后自动拉起 实例:

pg autostart enable --minio --name store

这会置 autostart: true 并安装/刷新 boot service(见 开机自启)。开机路径是只启动语义。MinIO 与 PostgreSQL 栈相互独立,因此排在最后启动,无顺序依赖。pg autostart status 列出所有目标 的状态。

移除

pg addon remove minio --name store               # 只删容器,数据保留
pg addon remove minio --name store --clean-data  # 连数据目录一起删除

数据目录就是对象存储本身——丢了它等于丢掉里面所有 bucket——因此 remove 默认保留它并打印其位置。--clean-data 会删除它(并清理默认布局下已清空的父 目录;显式 data_dir 及其父目录永不受影响)。

日志

pg logs addon minio --name store      # 最近 50 行
pg logs addon minio --name store -f   # 持续跟踪

MinIO 日志走 stdout:启动行(API:/Console: 地址、Documentation:)与请求 错误。看到 API: http://... 块即确认监听已就绪。

故障排除

  • install 报 pulling minio image ... : ...。 公开 tag ghcr.io/mars-base/pgcli/pgcli-minio:20250422221226 拉不到——检查网络/registry 可达性,或先 podman pull 手动拉取。
  • 手动 --api-port 撞端口。 请选端口池(minio_start_port 起)之外的端口, 否则会被自动分配器视为已占用;另注意 MinIO 需要的是一对连续端口。
  • 主机重启后 pg addon start 无效果/失败。 看 pg logs addon minio --name store -f——多半是数据目录被删了(--clean-data 或人为),MinIO 拒绝在曾格式化过的空目录上启动;或者绑定端口变了。
  • 控制台能打开,但 S3 客户端超时。 单机模式下 MINIO_SERVER_URL 由 listen + API 端口拼成;若绑在 127.0.0.1 却从其他主机访问,客户端会被 重定向到回环地址。把 listen 设为客户端真正可达的地址。(集群模式完全 不下发 MINIO_SERVER_URL——见分布式 / 集群模式。)
  • 集群卡在 Waiting for at least 1 remote servers with valid configuration。 节点之间配置不一致。逐台用 podman inspect pgcli-minio-<ns>-<name> --format '{{json .Config.Env}}' 核对每份 pg.yaml 的 endpoints 列表与 root_password 是否字节级一致,并确认 没有残留的每节点 MINIO_SERVER_URL。
  • macOS。 已支持:插件加入 pgcli-net bridge 并发布两个端口,Mac 用 127.0.0.1:<port> 即可访问(容器内部绑 0.0.0.0,MINIO_SERVER_URL 宣告 Mac 使用的回环地址)。改过端口或凭据后用 --force 重建容器。用 pg mc 时 注意容器内 127.0.0.1 别名不可用——见上文端点说明。