跳转到主要内容

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

返回本页常规视图.

文档

了解如何使用 pgcli 管理 PostgreSQL 数据库。

使用 pgcli 管理 PostgreSQL 数据库的完整指南。

PostgreSQL 18 — 可用于生产环境。

  • 快速开始 — 安装与基本用法
  • 备份 — 快照与 pgBackRest 管理
  • 恢复 — 时间点恢复(PITR)
  • 复制 — 只读副本与故障转移
  • 扩展 — 安装和管理 PostgreSQL 扩展
  • 开机自启 — 重启后自动启动实例和服务
  • 工具箱 — 独立工具,包括 ZFS 存储管理

1 - 平台支持

pgcli 各组件与功能在 Linux、macOS 上的支持情况

pgcli 在 Linux 和 macOS 上都是驱动 Podman 容器运行的。 两者的根本区别在于容器如何获得网络:

  • Linux 原生运行 Podman,容器共享宿主网络栈(--network host),彼此通过 127.0.0.1 互访。这是零开销路径,也是唯一支持跨主机拓扑的路径。
  • macOS 把 Podman 跑在 podman machine 虚拟机里。此处的 host 网络绑定的是 虚拟机的回环,Mac 看不到;于是 pgcli 改为加入共享 bridge 网络 (pgcli-net)并发布各端口 —— Mac 通过 gvproxy 用 127.0.0.1:<port> 访问,容器之间则通过 bridge 上的容器名互访。这适用于单主机 (开发/测试),不是跨主机 HA 路径。

下表是当前支持矩阵。“macOS(单主机)“表示单机完全可用;那些必须向其它机器 广播可路由地址、或依赖 Linux 专属容器内部机制的功能,仍是 仅 Linux。

支持矩阵

组件 / 功能 Linux macOS
实例生命周期 —— pg create / start / stop / restart / destroy / status ✅ ✅ 单主机
pg psql / pg exec ✅ ✅
SQL 命令参考(pg exec、管理查询) ✅ ✅
日志 —— pg logs ✅ ✅
命名空间隔离(pg.yaml 的 namespace) ✅ ✅
数据导入 / 导出 —— pg import / pg export ✅ ✅
克隆 —— pg clone(流式 pg_dump | pg_restore) ✅ ✅
扩展 —— pg extension install / list ✅ ✅
备份 / 恢复 —— pg backup / pg restore(pgBackRest) ✅ ✅ 单主机
物理副本 —— pg replica ✅ ✅ 单主机
故障切换 —— pg failover(副本提升) ✅ ✅ 单主机
开机自启 —— pg autostart ✅ systemd user unit ✅ launchd(登录后)
插件 —— PgBouncer ✅ 含跨主机连接池 ✅ 单主机 dev/test
插件 —— PgDog ✅ ✅ 单主机 dev/test
插件 —— etcd ✅ 含跨主机集群 ❌ 仅 Linux
插件 —— HAProxy ✅ ❌ 仅 Linux
插件 —— MinIO ✅ host 网络 ✅ bridge,发布端口
插件 —— silo ✅ host 网络 ✅ bridge,发布端口
插件 —— rustfs ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Redis ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Predixy ✅ host 网络(单机与跨机集群皆已验证) ❌ 仅 Linux
插件 —— Garage ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— SeaweedFS ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— pgAdmin 4 ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Grafana ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Prometheus ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Alertmanager ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— Gitea ✅ host 网络 ⚠️ bridge 路径已实现但未实测 —— 暂按仅 Linux 对待
插件 —— nginx ✅ host 网络 ✅ bridge,发布端口(轻度实测)
客户端 —— pg mc(MinIO 客户端) ✅ host 网络 ✅ bridge(见下)
高可用 —— Patroni(pg ha) ✅ 含跨主机 ❌ 仅 Linux

图例:✅ 支持 · ✅ 备注 支持但有所述限制 · ⚠️ 该平台上已实现但未实测 · ❌ 不支持。

实例类功能

所有构建在受管 PostgreSQL 实例之上的功能在两个平台都可用 —— 生命周期命令、 pg psql / pg exec、日志、命名空间、导入/导出、克隆、扩展、备份/恢复 (pgBackRest)、物理副本、故障切换。macOS 的 bridge 路径(发布端口 + 容器名 DNS)最早由实例、副本/备份路径验证;代理插件复用的正是这一套。

macOS 上这些都是单主机:跨主机副本、跨多机的连接池属于 Linux 的 --network host + LAN 地址能力。

开机自启

两个平台都支持,但机制不同:

  • Linux —— systemd 的 user unit。
  • macOS —— launchd 的 LaunchAgent。LaunchAgent 在用户登录时运行,而 非系统启动时 —— 因为 rootless podman 无法在有人登录前启动。请预期容器在你 登录后才拉起。见开机自启。

插件

各组件的网络模型不同:

  • PgBouncer 与 PgDog —— 纯代理,macOS 上支持单主机 dev/test。macOS 下它们加入 pgcli-net 并发布端 口;客户端连接仍是 127.0.0.1:<port>。后端地址的注意事项(remote/后端主机 必须是从 Mac 可达的地址,而不是 127.0.0.1)见各自页面的"平台支持"一 节。
  • etcd —— 仅 Linux。成员以 host 网络运行,并把 client/peer URL 作为永久的集群状态写进 raft 成员列表;要移植 macOS 的 bridge + 容器名模型就得重写这份状态,在完成该设计之前保持仅 Linux。
  • Patroni(pg ha)—— 仅 Linux。成员依赖 podman 的 host 网络;root 和 rootless podman 均可使用。podman machine 虚拟机的 uid 映射与 host 网络模型不吻合。这些命令在 macOS 上会快速失 败并给出清晰提示。见 Patroni 高可用。
  • HAProxy —— 仅 Linux。它通过主机网络代理 Patroni 成员,而 podman machine 虚拟机不提供该能力;manager 在 macOS 上快速 失败。
  • MinIO —— 两个平台都支持。对象存储,单机或跨 主机分布式纠删码集群皆可。Linux 上经主机网络提供服务;macOS 上加入 pgcli-net 并发布 API 与控制台端 口,Mac 通过 127.0.0.1:<port> 即可访问(与代理插件同一条路径)。公开镜像 为双架构(amd64 + arm64)。
  • silo —— 两个平台都支持。Pigsty 的 MinIO 分支, 网络模型与 MinIO 完全一致:Linux 走 host 网络,macOS 加入 pgcli-net 并发布 端口。公开镜像为双架构(amd64 + arm64)。
  • rustfs —— 实际上仅 Linux。pgcli 运行一个定制 wrapper 镜像(ghcr.io/mars-base/pgcli/pgcli-rustfs),其 entrypoint 在容器 内部先把 bind 挂载的数据/证书目录 chown 成 rustfs 固定的 uid 10001,再降权到 非特权用户。wrapper 镜像为双架构构建,Go manager 也有与 MinIO/silo 相同的 macOS bridge 路径(发布端口),故 macOS 预期可用 —— 但本插件的 e2e 覆盖只在 Linux 上跑过,在 Mac 上实测之前请按仅 Linux 对待。
  • Redis —— Linux 已实测;macOS 预期可用。纯上游 docker.io/library/redis 镜像,没有 wrapper、也没有固定容器 uid,因此不需要 rustfs 那套属主机制。Linux 上走 host 网络(端口按 listen 落在全部网卡或仅 回环);macOS 上加入 pgcli-net 并发布端口,pg redis-cli 拨 host.containers.internal —— 与对象存储相同的 bridge 路径,代码完备但尚未在 Mac 上跑过,e2e 覆盖目前仅限 Linux。
  • Predixy —— 仅 Linux。 与 HAProxy 一样,它需要 host 网络才能拨通后端的集群节点(单机与跨机集群都已验证)。wrapper 镜像只有 amd64 —— 上游只发 amd64 二进制 —— 因此 arm64 主机需要 --platform linux/amd64 模拟运行,而 macOS 的 podman machine 路径根本不存在。
  • pgAdmin 4 —— Linux 已实测;macOS 预期可用。纯 上游 docker.io/dpage/pgadmin4 镜像,没有 wrapper —— 镜像自己的 entrypoint 把 /var/lib/pgadmin chown 给 uid 5050 并降权,所以 pgcli 传 --user 0、从不在 宿主上 chown(与 rustfs 不同)。Linux 上走 host 网络,端口取自 pgadmin_start_port(默认仅回环);macOS 上加入 pgcli-net 并发布端口、 listen 放宽到 0.0.0.0 —— 与 redis/对象存储相同的 bridge 路径,代码完备但 尚未在 Mac 上跑过,e2e 覆盖目前仅限 Linux。
  • Grafana —— Linux 已实测;macOS 预期可用。纯上 游 docker.io/grafana/grafana 镜像,没有 wrapper。镜像固化 USER 472,且 entrypoint 完全不做任何属主管理,所以 pgcli 在创建容器前自己把宿主数据目录 chown 成 472:0 —— rootless podman 下经 podman unshare chown,pgcli 以 root 跑在 rootful 宿主上则直接递归 chown。Linux 上走 host 网络,端口取自 grafana_start_port(默认开放全部网 卡);macOS 上加入 pgcli-net 并发布端口、listen 放宽到 0.0.0.0 —— 与 pgAdmin/redis 相同的 bridge 路径,代码完备但尚未在 Mac 上跑过,e2e 覆盖目前 仅限 Linux。
  • Prometheus —— Linux 已实测;macOS 预期可用。 纯上游 docker.io/prom/prometheus 镜像,没有 wrapper,属主形态与 Grafana 一致(固化 USER nobody,uid/gid 65534,entrypoint 不做任何属主管理)—— pgcli 在创建容器前先把宿主 TSDB 目录 chown 过去:rootless podman 下经 podman unshare chown 65534:65534,pgcli 以 root 跑在 rootful 宿主上则直接 递归 chown。与 Grafana 不同,它靠一份 bind mount 的 prometheus.yml(渲染 而来,或从 --conf-file 拷贝)加镜像名之后的 CLI flag 配置,而非环境变量。 Linux 上走 host 网络,端口取自 prometheus_start_port(默认开放全部网卡)—— 且该默认背后没有任何鉴权:不像 Grafana 的 0.0.0.0 有密码兜底,这里的 暴露面只靠网络边界。macOS 上加入 pgcli-net 并发布端口 —— 与 Grafana 相同的 bridge 路径,代码完备但尚未在 Mac 上跑过,e2e 覆盖目前仅限 Linux。
  • Alertmanager —— Linux;macOS 代码完备、未 实测。Prometheus 的下游告警路由端,也是它的近乎完全同构体:纯上游 docker.io/prom/alertmanager 镜像,属主形态一致(固化 USER nobody,uid/gid 65534,entrypoint 不做属主管理——pgcli 预 chown 宿主状态目录),靠一份 bind mount 的 alertmanager.yml(渲染而来,或从 --conf-file 拷贝)加镜像名之后的 CLI flag 配置。两个 Alertmanager 特有的事实:--cluster.listen-address= 始终以 空串传入,所以单实例永不开启 HA gossip 端口(TCP+UDP 9094);而与 Prometheus 不同,镜像里那条无鉴权的 POST /-/reload 路由无法关闭——pgcli 用 SIGHUP 重载、从而永远不依赖那条路由,但路由本身照样敞开。这叠在一个比 Prometheus 更 尖锐的暴露面上:Listen 默认 0.0.0.0 且完全无鉴权,能连到端口即可创建 silence——悄悄压掉 oncall 期待的每一条通知。macOS 上加入 pgcli-net 并发布 端口 —— 同一条 bridge 路径,代码完备但尚未在 Mac 上跑过。
  • Gitea —— Linux;macOS 代码完备、未实测。自托管 Git 服务,跑纯上游 docker.io/gitea/gitea 镜像(rootful 构建——rootless 变体的 数据布局互不兼容,pgcli 永不选它)。属主形态与 Grafana 同构只是数字不同:守护 进程 su-exec 降到固化的 git 用户,uid/gid 1000(经 USER_UID/USER_GID 钉死,宿主预 chown 无从漂移),不带 --user flag;pgcli 同样预 chown 宿主 /data 目录。配置只经环境变量(GITEA__* 每次启动折叠进持久化的 app.ini)——这也正是 SECRET_KEY/INTERNAL_TOKEN 绝不注入的原因:重新生成的 secret 会在重装时静默重设幸存数据库的密钥。安装免向导(INSTALL_LOCK),admin 随后经容器内 CLI 创建。单端口惯例之外有两张可选面孔:--dsn 把元数据搬到外置 PostgreSQL(pgcli 从不建库也从不删库);git-over-ssh 跑在镜像自带的 OpenSSH sshd 上,而这个 sshd 在每个实例上都无条件在跑(s6 服务关不掉)。 --ssh-port N 让它变成 N 端口上的活克隆端点;默认关时它只是 parked 在池端口上 ——但无论如何都得给它 park 个地方:宿主网络下若留在 22 就会永远跟宿主 sshd 死磕。Listen 默认 0.0.0.0(Git 服务天然要给别的机器克隆),由自动生成的 admin 密码与关闭自助 注册兜底。macOS 上加入 pgcli-net 并发布 Web 端口、以及启用 SSH 时的 SSH 端口;代码完备但尚未在 Mac 上跑过。
  • BeeGFS —— 仅 Linux;macOS 上直接拒绝安装。 首个多容器插件:一条配置 = 一个单机收敛文件系统——三个容器(mgmtd + meta + storage)按依赖顺序启动、以集群为单位管理。它是唯一没有 bridge 路径、也没有 bridge 兜底的插件:BeeGFS 说的是自家 TCP/UDP 协议(不是 HTTP),守护进程跑 --network host、直接绑协议原生端口——而 8.4 的 meta(8005)与 storage(8003)根本没有监听口覆盖手段,所以每台宿主只容得下 一个文件系统(守卫拦截,而非端口池)。macOS 没有可 bridge 过去的等价形态: 没有 host 网络就没有正确的 BeeGFS 拓扑,所以 Mac 上 pg addon install beegfs 快速失败而不是半可用。属主形态与 Gitea/Grafana 相反:镜像以 root 运行并 自属 bind 挂载目录,pgcli 从不 chown 任何东西。客户端侧是内核模块(DKMS), 无法容器化——pgcli 刻意从不安装、从不挂载,改用 pg addon beegfs mount-hint 输出逐字的客户端配置。
  • nginx —— 两个平台都支持(macOS 轻度实测)。HTTP 反向代理,Linux 上走 host 网络;macOS 上加入 pgcli-net 并发布端口,与对象 存储同一条 bridge 路径 —— 已跑通过但未经负载考验。上游镜像多架构 (amd64 + arm64)。
  • pg mc(MinIO 客户端)—— 两个平台都支持。它在一次性容器里运行 mc:宿主的 ~/.mc/config.json 按其原生默认路径挂载,cp/mirror/diff 的本地文件参数也会按真实路径动态 挂载 —— macOS 上要求位于家目录之下(podman machine 只共享家目录)。Linux 上走 host 网络,127.0.0.1 别名可直达本机 addon 实例;macOS 上容器在 bridge 网络里,127.0.0.1 是容器自己的回环 —— 本机 addon 请用 host.containers.internal:<port>,远端存储用可路由地址。

确认当前平台

pg start 会在横幅里打印检测到的平台,便于确认当前走的是哪条路径:

=== pg start ===
Platform: linux        # 或:macOS

2 - 快速开始

安装 pgcli 并创建你的第一个 PostgreSQL 实例

几分钟内让 pgcli 运行起来。

安装

使用单条命令安装 pgcli:

curl -fsSL https://raw.githubusercontent.com/mars-base/pgcli/main/scripts/install.sh | bash

此脚本将:

  • 为你的平台下载最新的 pgcli 二进制文件
  • 安装到 /usr/local/bin(如果没有 sudo 则安装到 ~/.local/bin)
  • 将 pgcli 添加到你的 PATH

初始化配置

# 创建基础目录
mkdir -p ~/pg

# 初始化配置并创建默认实例
pg config init --add default --base-dir ~/pg

# 将数据和备份分离到不同磁盘(生产环境推荐)
pg config init --base-dir /data/pg --backup-dir /mnt/backup/pg --add default

这会在 ~/.pgcli/pg.yaml 创建带有合理默认值的配置,包括:

  • 名为 default 的默认实例
  • 数据目录 ~/pg/default
  • 从 35432 开始自动分配的端口

--backup-dir 可选但生产环境推荐:它将备份数据和日志放在与实例数据不同的 路径上,防止数据盘故障连累备份丢失。

启动实例

# 启动默认实例
pg start

# 查看状态和连接信息
pg status

输出会显示连接 URL、管理员密码和备份状态。

连接到数据库

# 使用 pgcli 内置的 psql 封装
pg psql

# 或使用 pg status 中显示的连接字符串直接连接
psql postgres://admin:<password>@localhost:35432/admin_db

# 直接执行 SQL
pg exec "SELECT version()"

基本操作

# 列出所有实例
pg list

# 停止实例
pg stop

# 停止所有实例、备份容器和 PgBouncer
pg stop --all

# 启动实例
pg start

# 查看实例状态
pg status

# 执行 SQL
pg exec "SELECT version();"

多实例管理

# 创建额外实例
pg create -i proj01 --base-dir /data/pg
pg create -i proj02 --base-dir /data/pg

# 列出所有实例
pg list

# 启动所有实例
pg start --all

多配置文件(隔离环境)

为同一主机上的隔离测试环境或每个项目一个配置, 使用不同的 --namespace 和不重叠的端口范围生成独立的配置文件。

# 环境 "t1":容器前缀 pgcli-pg-t1-*,PG 端口从 38000 开始
pg config init -o ~/.pgcli-t1/pg.yaml --namespace t1 --pg-start-port 38000 --pg-ssh-port 43000 --add proj1

# 环境 "t2":不同的命名空间、端口,以及独立的备份磁盘
pg config init -o ~/.pgcli-t2/pg.yaml --namespace t2 --base-dir /data/t2 --backup-dir /mnt/backup/t2 --pg-start-port 38100 --pg-ssh-port 43100 --add proj2

# 使用 -c 管理各环境
pg -c ~/.pgcli-t1/pg.yaml start -i proj1
pg -c ~/.pgcli-t2/pg.yaml list
参数 默认值 含义
--namespace default 容器名称前缀:pgcli-pg-<namespace>-<instance>
--base-dir ~/.pgcli 所有数据路径的基础目录
--backup-dir (在 --base-dir 下) 备份数据/日志的独立基础目录
--pg-start-port 35432 第一个 PG 主机端口;实例按顺序分配
--pg-ssh-port 42201 第一个 SSH 主机端口;按顺序分配

规划建议:

  • 始终使用显式的 --namespace 以避免容器名称冲突。
  • 同一主机上的端口范围不能重叠。
  • 命名空间在创建时固化到容器名称中。 后期更改需要 pg destroy 后重新初始化。

交互式 psql 会话

# 打开交互式 psql(默认实例)
pg psql

# 为指定实例打开 psql
pg psql -i proj01

# 从 stdin 读取 SQL(非交互,用于脚本)
echo "SELECT version();" | pg psql

# 执行单条 SQL 命令
pg psql -- -c "SHOW work_mem"

# 连接到不同数据库
pg psql -- -d postgres

# 使用 psql 元命令
pg psql -- -c "\dt"     # 列出表
pg psql -- -c "\du"     # 列出用户
pg psql -- -c "\l"      # 列出数据库

# 通过连接字符串连接远程数据库
pg psql --dsn postgres://user:pass@host:5432/db

在交互式 psql 中你可以使用:

  • 带历史记录和 tab 补全的 SQL 查询
  • psql 元命令(\dt、\du、\l 等)
  • \q 退出

容器 Shell

# 在容器中打开 bash(默认实例)
pg shell

# 为指定实例打开 shell
pg shell -i proj01

# 直接运行命令
pg shell -- -c "ls -la /var/lib/postgresql/data"

# 查看日志
pg shell -- -c "tail -f /var/log/postgresql/postgresql-*.log"

Shell 以 root 身份在容器内运行,可以完全访问:

  • PostgreSQL 数据目录(/var/lib/postgresql/data)
  • 配置文件
  • 日志文件(/var/log/postgresql/)
  • 所有系统工具和实用程序

下一步

3 - 命名空间隔离

使用命名空间在单台主机上创建隔离的环境

命名空间允许你在单台主机上创建完全隔离的环境,非常适合将生产、开发和测试环境分开,避免容器名称冲突或端口冲突。

什么是命名空间?

命名空间是应用于配置文件中所有容器名称的前缀。这种隔离机制确保:

  • 容器名称不冲突:每个命名空间有自己的容器前缀
  • 端口范围分离:每个配置文件从自己的范围分配端口
  • 备份容器隔离:每个命名空间有自己的 pgBackRest 容器
  • 配置文件独立:每个环境使用单独的配置文件

使用场景:生产和开发环境

一个常见场景是在同一台服务器上运行生产和开发环境:

# 创建生产环境
pg config init \
  --namespace prod \
  --pg-start-port 35432 \
  --pg-ssh-port 42201 \
  --add app-db \
  -o ~/.pgcli-prod/pg.yaml

# 创建开发环境
pg config init \
  --namespace dev \
  --pg-start-port 38000 \
  --pg-ssh-port 43000 \
  --add app-db \
  -o ~/.pgcli-dev/pg.yaml

这会创建两个完全隔离的环境:

环境 配置文件 容器前缀 PG 端口范围 SSH 端口范围
生产 ~/.pgcli-prod/pg.yaml pgcli-pg-prod-* 35432+ 42201+
开发 ~/.pgcli-dev/pg.yaml pgcli-pg-dev-* 38000+ 43000+

管理多个环境

使用 -c 标志指定要使用的配置文件:

# 启动生产数据库
pg -c ~/.pgcli-prod/pg.yaml start -i app-db

# 启动开发数据库
pg -c ~/.pgcli-dev/pg.yaml start -i app-db

# 列出生产环境实例
pg -c ~/.pgcli-prod/pg.yaml list

# 列出开发环境实例
pg -c ~/.pgcli-dev/pg.yaml list

工作原理

容器命名

命名空间为 prod,实例为 app-db:

  • 实例容器:pgcli-pg-prod-app-db
  • 备份容器:pgcli-backup-prod
  • 网络:pgcli-net-prod(如果使用独立网络)

没有命名空间(或 --namespace ""):

  • 实例容器:pgcli-pg-default-app-db
  • 备份容器:pgcli-backup-default

端口分配

配置文件中的每个实例获得顺序端口:

  • 第一个实例:pg_start_port(例如 35432)
  • 第二个实例:pg_start_port + 1(例如 35433)
  • 依此类推…

SSH 端口同理。

配置持久化

命名空间和端口范围保存在配置文件中:

namespace: prod
pg_start_port: 35432
pg_ssh_port: 42201

最佳实践

1. 始终使用显式命名空间

在一台主机上运行多个配置时,永远不要依赖默认命名空间:

# 错误:两个配置都会使用 "default" 命名空间并冲突
pg config init --add app -o ~/.pgcli-prod/pg.yaml
pg config init --add app -o ~/.pgcli-dev/pg.yaml  # 冲突!

# 正确:显式命名空间
pg config init --namespace prod --add app -o ~/.pgcli-prod/pg.yaml
pg config init --namespace dev --add app -o ~/.pgcli-dev/pg.yaml

2. 使用不重叠的端口范围

确保配置之间的端口范围不重叠:

# 生产:35432-35999, 42201-42999
pg config init --namespace prod \
  --pg-start-port 35432 \
  --pg-ssh-port 42201 \
  --add app -o ~/.pgcli-prod/pg.yaml

# 开发:38000-38999, 43000-43999
pg config init --namespace dev \
  --pg-start-port 38000 \
  --pg-ssh-port 43000 \
  --add app -o ~/.pgcli-dev/pg.yaml

# 测试:40000-40999, 44000-44999
pg config init --namespace test \
  --pg-start-port 40000 \
  --pg-ssh-port 44000 \
  --add app -o ~/.pgcli-test/pg.yaml

为每个环境中的多个实例预留足够的端口空间。

3. 命名空间在创建时固化

命名空间在创建时嵌入容器名称。之后更改会破坏关联:

# 使用命名空间 "prod" 创建
pg -c ~/.pgcli-prod/pg.yaml start -i app-db
# 容器:pgcli-pg-prod-app-db

# 编辑配置将命名空间改为 "production"
# 这样不行 - 容器名称不匹配!

# 正确做法:销毁并重建
pg -c ~/.pgcli-prod/pg.yaml destroy -i app-db --clean-data
pg -c ~/.pgcli-prod/pg.yaml create -i app-db
pg -c ~/.pgcli-prod/pg.yaml start -i app-db

4. 使用 Shell 别名简化操作

创建别名避免重复输入 -c:

# 添加到 ~/.bashrc 或 ~/.zshrc
alias pg-prod='pg -c ~/.pgcli-prod/pg.yaml'
alias pg-dev='pg -c ~/.pgcli-dev/pg.yaml'
alias pg-test='pg -c ~/.pgcli-test/pg.yaml'

# 然后使用:
pg-prod start -i app-db
pg-dev list
pg-test destroy -i test-db --force

高级用法:带副本的多环境

你甚至可以设置隔离的复制环境:

# 生产:主实例 + 副本
pg -c ~/.pgcli-prod/pg.yaml create -i primary --base-dir /data/prod
pg -c ~/.pgcli-prod/pg.yaml replica create replica -i primary

# 开发:独立的主实例 + 副本
pg -c ~/.pgcli-dev/pg.yaml create -i primary --base-dir /data/dev
pg -c ~/.pgcli-dev/pg.yaml replica create replica -i primary

每个环境维护自己的复制槽、备份 stanza 和数据目录。

故障排除

容器名称冲突

Error: container "pgcli-pg-default-app-db" already exists

原因:两个配置使用了相同的命名空间。

解决:使用不同的命名空间,或先销毁冲突的实例。

端口已被占用

Error: listen tcp 0.0.0.0:35432: bind: address already in use

原因:配置之间的端口范围重叠。

解决:使用不重叠的端口范围,并预留足够间距。

更改命名空间后找不到实例

Error: instance "app-db" exists in config but container not found

原因:创建实例后更改了配置文件中的命名空间。

解决:销毁并重建实例,或恢复命名空间更改。

总结

命名空间为单台主机上的多环境提供完全隔离:

  • 独立配置:每个环境有自己的 pg.yaml
  • 不同命名空间:防止容器名称冲突
  • 不重叠端口:避免端口冲突
  • 独立操作:每个环境使用 -c 单独管理

非常适合在同一台服务器上运行生产、开发、测试和预发布环境,互不干扰。

4 - SQL 命令

pgcli SQL 命令指南

两种对实例运行 SQL 的方式:pg exec 用于一次性 SQL 或容器命令,pg psql 用于交互式会话,pg shell 用于在容器内打开交互式 bash shell。

pg exec

SQL 模式(默认)

没有 -- 的参数作为 SQL 通过 psql 执行,使用实例配置的用户和数据库。

pg exec "SELECT version()"
pg exec -i proj01 "SELECT count(*) FROM users"
pg exec "CREATE TABLE test (id serial PRIMARY KEY, msg text)"

容器命令模式(– 之后)

-- 之后的参数直接在容器内运行(以 root 身份)。

pg exec -- pg_isready
pg exec -- ls -la /var/lib/postgresql/data
pg exec -- bash -c "cat /var/lib/postgresql/data/postgresql.conf"
pg exec -- tail -f /var/log/postgresql/postgresql-*.log

远程数据库(–dsn)

通过连接字符串对任何可达数据库执行 SQL,使用临时容器。--dsn 仅支持 SQL 模式;容器命令需要本地实例。

pg exec --dsn postgres://user:pass@host:5432/db "SELECT count(*) FROM users"

pg psql

交互式会话

pg psql                          # 默认实例
pg psql -i proj01                # 特定实例

在 shell 内你获得完整的 psql 功能:带历史记录和 tab 补全的 SQL、元命令(\dt、\du、\l),以及 \q 退出。

非交互式(脚本)

echo "SELECT version();" | pg psql        # 来自 stdin 的 SQL
pg psql -- -c "SHOW work_mem"             # 单条命令
pg psql -- -d other_db                    # 连接到不同数据库
pg psql -- -U other_user                  # 以不同用户连接

切换到 postgres 超级用户

某些管理任务(例如创建某些扩展、修改系统级设置)需要 postgres 超级用户权限。使用 -- 传递 psql 参数并切换用户:

pg psql -i pg01 -- -U postgres -d postgres

推荐方法:日常操作使用实例默认用户(admin),仅在需要超级用户权限时切换到 postgres。这比修改配置文件或重启容器更安全更方便。

示例场景:

# 创建需要超级用户权限的扩展
pg psql -i pg01 -- -U postgres -d postgres -c "CREATE EXTENSION pg_cron"

# 配置 cron.database_name(pg_cron 特定参数)
pg psql -i pg01 -- -U postgres -d postgres -c "ALTER SYSTEM SET cron.database_name = 'pg01_db'"

# 查看系统级配置
pg psql -i pg01 -- -U postgres -d postgres -c "SHOW shared_preload_libraries"

远程数据库(–dsn)

pg psql --dsn postgres://user:pass@host:5432/db

pg shell

在 PostgreSQL 容器内打开交互式 bash shell。

pg shell                             # 交互式 bash
pg shell -i proj01                   # 特定实例
pg shell -- -c "ls -la /var/lib/postgresql/data"
pg shell -- -c "cat /etc/postgresql/postgresql.conf"

使用场景:

  • 检查 PostgreSQL 数据目录和日志文件
  • 运行容器内部命令(如 psql、pg_isready、pg_repack)
  • 调试扩展安装和配置文件

规则

  • --dsn 和 --instance 互斥:连接字符串确定主机、端口和数据库,因此 -i 被拒绝以避免静默误用。
  • 使用 --dsn 时,数据库是 URL 的路径部分:postgres://user:pass@host:5432/mydb 连接到 mydb。要使用另一个数据库,更改路径。
  • 使用本地实例时,-- 传递原始 psql 参数(包括 -d/-U),覆盖实例默认值。

5 - 备份

pgcli 备份指南

快照

# 创建快照(完整备份)
pg snapshot create -i proj01

# 创建差异备份(推荐)
pg snapshot create --type diff -i proj01

# 快照期间流式输出备份容器日志
pg snapshot create --tail-logs -i proj01

# 列出快照
pg snapshot list -i proj01

# 限制显示的快照数量
pg snapshot list --limit 5 -i proj01

# 删除快照
pg snapshot delete 20260826-073712F -i proj01

快照类型:

  • full — 完整备份(默认,自包含)
  • diff — 自上次完整备份以来的更改
  • incr — 自上次备份以来的更改

共享备份容器

所有实例共享单个 pgbackrest 容器;每个实例在存储库中都有自己的 stanza。

一份配置一个仓库。 这个容器只服务一个仓库——data_dir 下的本地目 录,或配置了 backup.repo.s3 时那唯一的一个 S3 端点——环境里的每个 stanza (普通实例与 Patroni 集群一视同仁)都推给它。不存在按实例或按集群选仓库: 于是一个 pgcli 环境就只备份到一个 store。若想把第二个 store(比如再 pg addon install minio 一个)只喂给部分集群,就跑第二份配置 (pg config init --namespace …)、配上它自己的备份容器——完整走法见 示例:HA 集群 + 自签 CA 的 MinIO。

# 初始化共享 pgbackrest 容器(构建镜像、创建目录、生成配置)
pg backup setup

# 使用自定义基础目录存储备份数据和日志
pg backup setup --base-dir /mnt/backup

# 启动 / 停止备份容器
pg backup start
pg backup stop

# 删除备份容器及其生成的配置(pgbackrest.conf、ssh_config)。备份数据与
# SSH 密钥对会保留,之后 `pg backup setup` 可直接重新接上同一个仓库;
# --clean-data 则连本地仓库、日志与凭据一起删除。
pg backup remove
pg backup remove --clean-data

# 显示备份容器状态
pg backup status

# 列出 backup 容器管理的全部 stanza(每个 PITR 实例一个、每个 Patroni 集群
# 一个)。下面的 stanza-upgrade 用的就是这些名字——它们是仓库里的备份对象
# 标识,不是容器名。
pg backup list-stanza

# 数据目录被重建后(restore、recreate、reinit)刷新 stanza 元数据。此后
# 备份会报 "[051] system-id ... do not match stanza";stanza-upgrade 重新
# 同步它而不删除已有备份。必须显式给出 stanza 名(有意不提供全量升级)。
pg backup stanza-upgrade pgcli_default
pg backup stanza-upgrade pgcli_default pgcli_app-default

备份基础设施(网络、镜像、目录、配置、容器)在 pg start 时自动准备;手动运行 pg backup setup 重新初始化,例如更改基础目录之后。

S3 对象存储仓库

Patroni 集群的备份与 WAL 归档可以推送到任意 S3 兼容存储(MinIO、silo、rustfs、 garage、AWS S3……)。配置写在 pg.yaml 的 backup.repo.s3 下:

backup:
  repo:
    s3:
      endpoint: 10.0.0.9:9000     # host:port,不带 scheme
      bucket: pgbackrest
      access_key: admin
      secret_key: <密码>           # 与其它 pgcli 密码一样存于 pg.yaml
      ca_file: /home/you/.pgcli/tls/minio/store/ca.crt   # 私有 CA 时必填

然后(重)跑 setup,它会自动完成归档编排:

pg backup setup
  • HTTPS 是硬性要求。 pgBackRest 拒绝明文 S3(上游明确不实现),端点必须 提供 TLS。pgcli 自带的 MinIO 用 pg addon install minio --tls 开启原生 HTTPS —— pgcli 会生成自签 CA,并把 ca.crt 路径填进上面的 ca_file。 外部端点(真实 AWS S3 等公签证书)留空 ca_file 即可;实在无法提供 CA 时 可用 verify_tls: false 逃生(不做证书校验)。
  • 远端存储主机取 CA——无需 scp。 MinIO 在另一台机器时,用一次 TLS 握手就 能取回信任锚,不必拷贝文件:pg backup fetch-ca <存储主机>:9002 从端点下发 的证书里取出信任锚,存到 <base-dir>/backup/repo-ca/ 下并打印 SHA-256 指纹——这本质是"首次使用即信任"(TOFU),所以信任前先把指纹与存储主机对拍 一次,再把文件交给 pg backup setup --s3-ca-file <路径>。它覆盖 pgcli 会提供 的两种 TLS 形态:开 --tls 的 MinIO(下发的是生成好的"叶+CA"链,取出的是 签名根);以及喂了 pg cert 自签叶证书的 MinIO(叶本身就是信任锚,原样保存 即可)。公共 CA 签发的端点根本不需要 ca_file,此命令对它们没有意义。
  • 跨机 HA 免手工分发。 Patroni 集群分散在多台主机时,ca_file 与备份 SSH 公钥只需在一台主机配好:pg backup setup 把 CA 发布进集群的 etcd 注册表, ca_file 留空的加入者自动拉取;各主机的备份公钥在 pg ha create 时发布, 由 setup 合并进每个成员的 authorized_keys。注册表里只放公开材料(CA 证书、SSH 公钥),绝不含私钥或 S3 secret_key。详见 HA 文档"备份跨机 leader"一节。
  • 归档自动化。 setup 检测到 Patroni 成员的归档配置过期时,会 pause 集群、 按"副本先、leader 后"重建成员容器(recreate 即重启,archive_mode 是 postmaster 级参数)、resume。每个成员的 patroni.yml 会注入同一份 archive_command(每个集群一个 stanza,成员互为 pg*-host,pgBackRest 自动定位 primary),WAL 从此持续 push 到 S3。
  • 作用范围。 S3 仓库只作用于 Patroni 集群的 stanza;pg 直接管理的普通 实例备份仍走本地仓库,互不影响。

等价的一次性 flag 写法(secret_key 建议手编 pg.yaml,避免落入 shell 历史):

pg backup setup --s3-endpoint 10.0.0.9:9000 --s3-bucket pgbackrest \
    --s3-access-key admin --s3-ca-file ~/.pgcli/tls/minio/store/ca.crt

6 - 副本(只读备用)

pgcli 副本(只读备用)指南

创建现有实例的只读物理副本。副本通过 PostgreSQL 物理复制从其主实例持续流式传输 WAL,并提供只读查询服务——适用于读写分离、报告或作为热备。

# 创建默认实例的副本
pg replica create ro1

# 创建特定实例的副本
pg replica create ro1 -i proj01

# 列出副本和复制延迟
pg replica list

发生了什么

  1. 预检 — 主实例必须正在运行(在任何配置写入之前验证;停止的主实例会失败但没有副作用)
  2. 注册 — 添加新实例条目,使用主实例的数据库名称和密码(参见注释),禁用 PITR,replica_of 设置为主实例
  3. 复制设置 — 在主实例上:
    • pg_hba.conf 为回环地址和 RFC1918 范围添加 host replication 条目(幂等)
    • 创建物理复制槽 pgcli_r_<name>,保留 WAL 以确保副本不会落后于 WAL 回收
  4. 基础备份 — pg_basebackup -R 将主实例的数据目录复制到副本的数据目录,写入 primary_conninfo(带密码)和 standby.signal,使副本以备用模式启动
  5. 启动 — 副本容器启动并持续流式传输 WAL

验证

# 只读备用?
pg exec -i ro1 "SELECT pg_is_in_recovery()"      # t

# 写入被拒绝
pg exec -i ro1 "INSERT INTO t VALUES (1)"         # 只读事务错误

# 流式传输活跃
pg exec -i ro1 "SELECT pg_is_in_recovery(), now() - pg_last_xact_replay_timestamp()"

# 主实例上的槽是活跃的
pg exec -i primary "SELECT slot_name, active FROM pg_replication_slots"

# 概览
pg list                              # ROLE/PRIMARY 列
pg replica list                      # NAME/PRIMARY/STATUS/LAG
pg status -i ro1                     # Role: standby (replica of ...)

销毁

销毁副本是两步操作:

# 步骤 1:销毁副本实例(移除容器 + 数据 + 配置条目)
pg destroy -i ro1 --clean-data --force

# 步骤 2:在主实例上删除复制槽(如果已不存在则为无操作)
pg replica drop ro1 -i <primary>

步骤 1 必须在步骤 2 之前运行:PostgreSQL 拒绝删除仍在流式传输的槽(replication slot is active),因此必须先销毁副本以关闭其流式连接。

注意: 如果主实例是本地 pgcli 管理的实例(同一主机),可以跳过步骤 2 — destroy 会自动清理槽。当从副本主机无法访问主实例时需要步骤 2。

跨网络副本

上述同主机流程假设主实例和副本共享一个服务器(一个 podman 守护进程,一个网络)。对于在另一台主机上的副本,pgcli 在每一侧运行一个命令——无需 SSH,唯一的跨机器信息就是你作为参数传递的内容:

# ---- 在主实例主机上:准备主实例(首先运行)----
pg replica create ro1 -i pg01 --replica-host 10.0.0.21

# ---- 在副本主机上:复制数据并启动副本(其次运行)----
pg replica create ro1 --primary-dsn "postgres://admin:<password>@10.0.0.20:35432/pg01_db" --primary-name pg01

获取主实例 DSN

如果主实例是 pgcli 管理的实例,使用 pg status 获取其连接信息:

pg status -i pg01
# ...
#   Connection: postgres://admin:<password>@127.0.0.1:35432/pg01_db

然后将 127.0.0.1 替换为从副本主机看到的主实例主机 IP(例如 10.0.0.20)——用户、密码和数据库按原样使用。注意主实例主机必须接受从副本主机到该端口的 TCP 连接(防火墙/安全组)。

每一侧的作用:

  • 主实例侧(--replica-host <ip|hostname>):仅准备主实例——本地不创建任何内容。它将 host replication all <addr> 条目追加到 pg_hba.conf 并创建物理槽 pgcli_r_<name>,然后打印要在副本主机上运行的确切 --primary-dsn 命令。IP 地址获得 /32(IPv6 为 /128)掩码;主机名按原样写入。已在托管 RFC1918 范围(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)内的 IP 会被跳过去重。幂等——重新运行不会添加重复行。
  • 副本侧(--primary-dsn):首先验证主实例上存在槽(验证连接性和顺序——在主实例侧之前运行会失败并带有可操作的错误消息且没有副作用),然后注册实例,通过网络从 DSN 运行 pg_basebackup(主机网络),并启动备用实例。

副本侧仅在副本主机上运行:副本实例的用户、数据库和密码来自 DSN(物理复制复制 pg_authid,因此本地密码必须等于主实例的密码)。主实例名称使用 --primary-name 给出并记录为 replica_of;它不必存在于副本主机的配置中,-i 不用于远程主实例——如果给出,它保持其严格含义并且必须引用真实的本地实例。

销毁是对称的,每个主机一个命令——按此顺序:

# 1. 在副本主机上:移除容器 + 配置,还通过 DSN 删除远程主实例上的槽
#    (如果主实例可达则自动)
pg destroy -i ro1

# 2. 在主实例主机上(仅当步骤 1 的 DSN 连接失败时):
#    手动删除槽
pg replica drop ro1 -i pg01

步骤 1 必须在步骤 2 之前运行:PostgreSQL 拒绝删除仍在流式传输的槽(replication slot is active),因此必须先销毁副本以关闭其流式连接。

自动槽清理: 当跨主机副本设置了 PrimaryDSN 时,destroy 会自动尝试通过 DSN 删除远程主实例上的复制槽。如果主实例可达,则无需手动步骤 2。replica drop 是幂等的——当槽已不存在时重新运行会作为无操作成功。

非 pgcli 主实例

主实例不必由 pgcli 管理——只要主实例侧已手动准备(槽检查仅验证槽是否存在,不验证谁创建了它),副本侧可以对任何 PostgreSQL 服务器工作:

  1. 在 pg_hba.conf 中允许从副本主机复制,然后重新加载(SELECT pg_reload_conf()):
    host replication <replica user> <replica ip>/32 scram-sha-256
  2. 使用确切名称 pgcli_r_<replica-name> 创建物理槽(副本侧检查此名称):
    SELECT pg_create_physical_replication_slot('pgcli_r_ro1');
    需要 wal_level = replica(或 logical)和具有 REPLICATION 权限的用户——DSN 用户。

然后副本侧命令不变:

pg replica create ro1 --primary-dsn "postgres://<user>:<pass>@<primary ip>:5432/<db>" --primary-name pg01

销毁时,主实例侧没有 pgcli——在 pg destroy -i ro1 之后,手动删除槽:

SELECT pg_drop_replication_slot('pgcli_r_ro1');

如果基础备份失败(例如网络中断),销毁副本并重新运行副本侧命令——主实例侧的槽和 hba 条目保持有效。

注释

  • 只读 — 副本拒绝所有写入(cannot execute INSERT in a read-only transaction)。要使其可写你需要提升它(pg_ctl promote),这尚未作为 pgcli 命令暴露
  • 相同数据,相同密码 — 物理复制是主实例的逐字节复制,包括 pg_authid。因此副本的管理员密码和数据库名称与主实例相同;只有容器名称、端口和数据目录不同。使用 --dsn 风格的连接时使用副本的端口
  • 副本上禁用 PITR — 备用实例不归档任何内容,也不在 pgBackRest 备份容器中注册;备份在主实例上运行
  • 主实例必须正在运行 — 初始创建(pg_basebackup)和持续流式传输都需要;如果主实例重启,副本会自动重新连接(槽显示为 active)
  • 延迟显示 — replica list 延迟(now() - pg_last_xact_replay_timestamp())在主实例空闲时会增长;它会在下一个复制的事务时降回零。这是预期的空闲行为,不是漂移
  • 幂等启动 — 重复的 pg start -i ro1 在数据目录已初始化时跳过基础备份

7 - 故障转移:副本提升

pgcli 故障转移:副本提升指南

当当前主实例失败时,将副本提升为新的主实例。pgcli 提供 3 步手动故障转移工作流——每个步骤在其各自的主机上运行,不自动检测同主机与跨主机拓扑。

概述

                    ┌─────────────┐
                    │   primary   │  ← 崩溃 / 变得不可用
                    │  (pg01)     │
                    └──────┬──────┘
                           │ WAL 流式传输
               ┌───────────┼───────────┐
               ▼           ▼           ▼
          ┌─────────┐ ┌─────────┐ ┌─────────┐
          │  ro1    │ │  ro2    │ │  ro3    │
          │ replica │ │ replica │ │ replica │
          └─────────┘ └─────────┘ └─────────┘

故障转移后(提升 ro1 → 新主实例):

                    ┌─────────────┐
                    │   ro1       │  ← 新主实例(已提升)
                    │  (primary)  │
                    └──────┬──────┘
                           │ WAL 流式传输
               ┌───────────┼───────────┐
               ▼           ▼           ▼
          ┌─────────┐ ┌─────────┐ ┌─────────┐
          │  ro2    │ │  ro3    │ │  pg01   │
          │ replica │ │ replica │ │ replica │  ← 降级的旧主实例
          └─────────┘ └─────────┘ └─────────┘

3 步故障转移

每个步骤都是独立的命令。按顺序在各自的主机上运行它们。

# 步骤 1:在被提升的副本上
pg replica promote ro1

# 步骤 2:在旧主实例主机上(当它恢复时)
pg replica drop ro1 -i pg01

# 步骤 3:在每个剩余副本主机上
pg replica repoint ro2 --primary-dsn "postgres://admin:<pw>@<new-primary-ip>:<port>/<db>" --primary-name ro1
pg replica repoint ro3 --primary-dsn "postgres://admin:<pw>@<new-primary-ip>:<port>/<db>" --primary-name ro1

步骤 1:pg replica promote <name>

在被提升为主实例的副本主机上运行。

pg replica promote ro1

发生了什么:

  1. 验证实例是副本(ReplicaOf 已设置)且容器正在运行
  2. 调用 pg_promote()(PostgreSQL 12+ 原生提升——无需容器重启)
  3. 等待恢复结束(通常亚秒级)
  4. 通过 ALTER SYSTEM RESET 从 postgresql.auto.conf 清理 primary_conninfo
  5. 更新配置:清除 ReplicaOf 和 PrimaryDSN,启用 PITR
  6. 自动初始化 PITR:
    • pgBackRest stanza 创建
    • archive_mode / archive_command 配置
    • PostgreSQL 重启以应用 postmaster 级别参数
  7. 打印下一步指令

幂等: 如果副本已被提升(例如通过手动 pg_ctl promote),命令会跳到配置更新。

步骤 2:pg replica drop <name> -i <old-primary>

在旧主实例主机上运行以清理复制槽。此步骤仅在特定场景中需要。

pg replica drop ro1 -i pg01

这会在旧主实例上删除物理复制槽 pgcli_r_ro1。没有清理,槽会无限期保留 WAL,直到主实例磁盘空间耗尽。

何时运行:

场景 操作 原因
旧主实例永久丢失 跳过 槽随服务器一起消失
计划将旧主实例降级为副本 跳过 repoint 销毁数据目录(包括 pg_replslot/),所有槽被隐式移除
旧主实例已恢复,继续作为独立主实例运行 必须运行 槽无限期保留 WAL;没有清理磁盘最终会填满
旧主实例已恢复但将被关闭 可选 如果实例不再运行,跳过无害

保持旧主实例不变? 如果你想保留旧主实例及其原始数据(例如用于取证分析或作为只读归档),你可以简单地不理会它——不要在其上运行 drop 或 repoint。旧主实例继续作为具有陈旧数据的独立实例运行。只是要注意被提升副本的复制槽仍然存在并会累积 WAL;你可能想要仅删除那个特定的槽(pg replica drop ro1 -i pg01)同时保持其他一切不变。

步骤 3:pg replica repoint <name> --primary-dsn <dsn> --primary-name <name>

在每个剩余副本主机上运行以将其重新指向新主实例。

pg replica repoint ro2 \
  --primary-dsn "postgres://admin:<password>@10.0.0.11:35439/pg01_db" \
  --primary-name ro1

发生了什么:

  1. 通过 DSN 查询新主实例的扩展(pg_extension 目录)
  2. 如果存在非内置扩展(例如 pg_cron、timescaledb),构建本地 -ext 镜像并匹配包
  3. 停止旧副本容器并销毁其数据目录
  4. 通过 DSN 在新主实例上创建复制槽
  5. 更新配置:ReplicaOf、PrimaryDSN、ImageTag、Extensions,禁用 PITR
  6. 通过 pg_basebackup -R 从新主实例重新初始化
  7. 以备用模式启动副本容器

为什么销毁 + 重建而不是 ALTER SYSTEM SET?

提升后,新主实例进入新时间线。旧时间线上的其他副本不能简单地更改 primary_conninfo——PostgreSQL 会拒绝连接:

FATAL: requested starting point on timeline 1 is not in this server's history

唯一安全的方法是从新主实例进行完整的 pg_basebackup。

获取主实例 DSN

从被提升副本主机上的 pg status 获取新主实例的连接字符串:

pg status -i ro1
# Connection: postgres://admin:<password>@127.0.0.1:35439/pg01_db

将 127.0.0.1 替换为从副本主机可达的新主实例主机 IP(例如 10.0.0.11)。

降级旧主实例

当旧主实例恢复时,你可以使用相同的 repoint 命令将其作为新主实例的副本重新加入:

# 在旧主实例主机上
pg replica repoint pg01 \
  --primary-dsn "postgres://admin:<password>@10.0.0.11:35439/pg01_db" \
  --primary-name ro1

即使 pg01 曾是主实例(未设置 ReplicaOf)这也有效。该命令:

  1. 停止 pg01 并销毁其数据(包括旧的 PITR stanza)
  2. 在新主实例上为 pg01 创建复制槽
  3. 设置 ReplicaOf = "ro1",PITR.Enabled = false
  4. 通过 pg_basebackup 从新主实例重新初始化

重新指向后,pg01 作为只读副本从新主实例流式传输 WAL——没有 WAL 归档,没有备份。

扩展同步

当副本被重新指向新主实例时,pgcli 会自动同步扩展:

  1. 查询 — 通过 DSN 连接到新主实例并查询 pg_extension 获取已安装的扩展
  2. 过滤 — 识别非内置扩展(需要外部包的扩展,例如 pg_cron、pgmq、timescaledb)
  3. 构建 — 如果存在非内置扩展,构建本地 -ext 镜像:
    • 如果本地 -ext 镜像已存在,在其上安装缺失的包(重用 Pigsty 仓库——快速)
    • 如果没有 -ext 镜像,从基础镜像构建并设置 Pigsty 仓库
    • apt-get install 是幂等的——安装已存在的包是无操作
  4. 应用 — 副本启动时,ApplyExtensions 将 shared_preload_libraries 写入 postgresql.conf
  5. 跳过 CREATE EXTENSION — 副本是只读的;扩展通过 pg_basebackup + WAL 流式传输从主实例复制

这确保副本容器具有 postgresql.auto.conf 中引用的所需共享库(例如 pg_cron)。

同主机 vs 跨主机

pgcli 不自动检测拓扑。你选择在每个命令运行的位置:

场景 步骤 1 步骤 2 步骤 3
全部在同一主机 pg replica promote ro1 pg replica drop ro1 -i pg01 pg replica repoint ro2 --primary-dsn "postgres://...@127.0.0.1:..." --primary-name ro1
主实例 + 副本分散在多个主机 在副本主机 在旧主实例主机 在每个副本主机上使用新主实例的网络 IP
混合 在各自的主机 在旧主实例主机 在每个副本主机

--primary-dsn 必须使用从 repoint 运行所在主机可达的 IP/主机名。

完整示例

# ── 初始设置:pg01(主实例)+ ro1, ro2(副本)在同一主机 ──

$ pg list
NAME    ROLE      PRIMARY   STATUS
pg01    primary   -         Up 2 hours
ro1     replica   pg01      Up 1 hour
ro2     replica   pg01      Up 1 hour

# ── pg01 崩溃 ──

# 步骤 1:提升 ro1
$ pg replica promote ro1
  [OK] pg_promote() signaled
  [OK] recovery ended, instance is now read-write
  [OK] primary_conninfo removed from postgresql.auto.conf
  [OK] config updated (ReplicaOf/PrimaryDSN cleared, PITR re-enabled)
-> Initializing PITR (backup stanza + archive_mode)...
  [OK] pgBackRest stanza created
-> archive_command configured
  [OK] WAL archiver caught up
✓ Replica "ro1" promoted to primary

# 步骤 2:在旧主实例上清理(如果 pg01 永久丢失则跳过)
$ pg replica drop ro1 -i pg01
  [OK] replication slot "pgcli_r_ro1" removed from primary "pg01"

# 步骤 3:将 ro2 重新指向新主实例
$ pg replica repoint ro2 \
    --primary-dsn "postgres://admin:<password>@127.0.0.1:35437/pg01_db" \
    --primary-name ro1
  [OK] extension image built with pg_cron, pgmq, timescaledb
  [OK] replication slot "pgcli_r_ro2" created on new primary
  [OK] config updated (ReplicaOf = "ro1")
✓ Replica "ro2" re-pointed to "ro1"

# ── pg01 恢复,降级为副本 ──
$ pg replica repoint pg01 \
    --primary-dsn "postgres://admin:<password>@127.0.0.1:35437/pg01_db" \
    --primary-name ro1
  [OK] backup stanza removed: pgcli_pg01
  [OK] replication slot "pgcli_r_pg01" created on new primary
  [OK] config updated (ReplicaOf = "ro1", PITR disabled)
✓ Replica "pg01" re-pointed to "ro1"

# ── 最终状态 ──
$ pg list
NAME    ROLE      PRIMARY   STATUS
pg01    replica   ro1       Up 30 seconds    # 已降级
ro1     primary   -         Up 10 minutes    # 新主实例
ro2     replica   ro1       Up 5 minutes

跨主机示例

# ── 设置:ra3(主实例,主机 A)+ ra2(副本,主机 A)+ ro2(副本,主机 B)──

# ra3 崩溃。在主机 A 上,提升 ra2:
$ pg replica promote ra2
  [OK] pg_promote() signaled
  [OK] recovery ended
  [OK] PITR initialized (stanza + archive_mode)
✓ Replica "ra2" promoted to primary

# 在主机 B(10.0.0.12)上,将 ro2 重新指向新主实例 ra2(主机 A = 10.0.0.11):
$ pg replica repoint ro2 \
    --primary-dsn "postgres://admin:<password>@10.0.0.11:35438/pg01_db" \
    --primary-name ra2
-> New primary has 3 non-builtin extension(s): pg_cron, pgmq, timescaledb
-> Extension image already has all required packages
  [OK] replication slot "pgcli_r_ro2" created on new primary
  [OK] config updated (ReplicaOf = "ra2", image = ...-ext)
✓ Replica "ro2" re-pointed to "ra2"

# 验证跨主机复制:
$ pg exec -i ra2 "INSERT INTO test(msg) VALUES ('after failover')"
$ pg exec -i ro2 "SELECT * FROM test ORDER BY id DESC LIMIT 1"
   msg: after failover

级联复制

副本本身可以作为下游副本的主实例,形成级联链。这减少主实例的负载并启用分层拓扑。

primary (ra3)
  ├─→ replica (ra2) ← ra2_ro1 的上游
  │     └─→ replica (ra2_ro1)
  └─→ replica (pg01)

工作原理

  1. 创建副本的副本:使用副本作为 -i 目标

    # ra2 是 ra3 的副本,创建 ra2_ro1 作为 ra2 的副本
    pg replica create ra2_ro1 -i ra2
  2. WAL 传播:

    • ra2 从 ra3 流式传输 WAL
    • ra2_ro1 从 ra2 流式传输 WAL
    • 数据流:ra3 → ra2 → ra2_ro1
  3. 复制槽:每个链接维护自己的槽

    • ra3 有槽 pgcli_r_ra2
    • ra2 有槽 pgcli_r_ra2_ro1

优势

  • 减少主实例负载:只有直接副本连接到主实例
  • 地理分布:主实例 → 区域副本 → 本地副本
  • 网络效率:本地副本可以共享区域上游

限制

  • 增加延迟:每一跳增加复制延迟
  • 级联故障:如果 ra2 失败,ra2_ro1 失去其上游
  • 提升复杂性:提升 ra2_ro1 需要将其重新指向新主实例

验证级联

# 检查 ra2 的下游副本
pg exec -i ra2 "SELECT client_addr, state FROM pg_stat_replication"

# 检查 ra2_ro1 的上游
pg exec -i ra2_ro1 "SELECT conninfo FROM pg_stat_wal_receiver"

级联故障转移

如果 ra2(中间节点)失败:

# 选项 1:将 ra2_ro1 直接重新指向 ra3
pg replica repoint ra2_ro1 \
  --primary-dsn "postgres://admin:password@ra3-host:5432/pg01_db" \
  --primary-name ra3

# 选项 2:等待 ra2 恢复(一旦 ra2 重新连接到 ra3 自动进行)

如果 ra3(主实例)失败且 ra2 被提升:

# 步骤 1:提升 ra2
pg replica promote ra2

# 步骤 2:ra2_ro1 自动跟随(它已经从 ra2 复制)
# ra2_ro1 无需操作

# 步骤 3:将其他副本重新指向新主实例
pg replica repoint pg01 \
  --primary-dsn "postgres://admin:password@ra2-host:5432/pg01_db" \
  --primary-name ra2

注释

  • pg_promote() — PostgreSQL 12+ 原生函数,无需容器重启。实例就地退出恢复并立即变为可读写
  • 时间线分歧 — 提升后,新主实例在新时间线上。其他副本不能用 ALTER SYSTEM SET primary_conninfo 重新指向——它们必须通过 pg_basebackup 重建
  • 被提升副本上的 PITR — 提升后,pgBackRest stanza 和 WAL 归档自动初始化(无需手动执行 pg start)。被提升的副本没有先前的备份历史
  • 复制槽 — 旧主实例为被提升副本的槽在提升后变得陈旧。pg replica drop 清理它。如果旧主实例被降级为副本,repoint 销毁旧数据且陈旧的槽不再被引用
  • 扩展 — 副本容器通过 postgresql.auto.conf 从主实例继承 shared_preload_libraries。repoint 命令确保本地镜像在重建副本之前具有所需的扩展包
  • 跳过 CREATE EXTENSION — 副本是只读的;pg_basebackup 从主实例复制扩展元数据,因此不需要 CREATE EXTENSION(且会失败 “cannot execute CREATE EXTENSION in a read-only transaction”)

8 - 管理

pgcli 管理指南

Shell 补全

为命令、标志和实例名称启用 tab 补全。

Bash

# Linux
pg completion bash > /etc/bash_completion.d/pg

# macOS(使用 Homebrew bash-completion)
pg completion bash > $(brew --prefix)/etc/bash_completion.d/pg

# 或在当前会话中加载
source <(pg completion bash)

Zsh

# 启用补全系统(一次)
echo "autoload -U compinit; compinit" >> ~/.zshrc

# 安装补全
pg completion zsh > "${fpath[1]}/_pg"

Fish

pg completion fish > ~/.config/fish/completions/pg.fish

PowerShell

pg completion powershell > pg.ps1
# 从你的 PowerShell 配置文件加载

PostgreSQL 配置

通过 pg exec 使用 ALTER SYSTEM 修改 PostgreSQL 运行时参数,然后重新加载:

# 更改参数
pg exec "ALTER SYSTEM SET work_mem = '256MB'"
pg exec "SELECT pg_reload_conf()"

# 针对特定实例
pg exec -i proj01 "ALTER SYSTEM SET effective_cache_size = '4GB'"
pg exec -i proj01 "SELECT pg_reload_conf()"

注意: 某些参数(例如 shared_buffers、max_connections)需要重启而不是重新加载。使用 pg stop && pg start 应用这些更改。

配置文件管理

检查或验证配置文件(默认为 ~/.pgcli/pg.yaml,使用 -c 覆盖)。

# 显示当前配置(YAML)
pg config show

# 以 JSON 格式显示配置
pg config show --json

# 验证配置文件结构
pg config validate

Init 生成默认配置;--add 在同一文件中创建命名实例,-o 写入自定义路径:

pg config init --add default --base-dir /data/pg
pg config init --add proj01 --base-dir /data/pg -o ./my-pg.yaml

# 将数据和备份分离到不同磁盘
pg config init --base-dir /data/pg --backup-dir /mnt/backup/pg --add default

--backup-dir 将备份数据和日志放在与实例数据不同的路径上。这是典型的生产环境 配置:PG 数据在一块磁盘,备份在另一块,防止数据盘故障连累备份丢失。

在一个主机上运行多个配置的隔离参数(参见快速开始进行规划):

pg config init --namespace t1 --pg-start-port 38000 --pg-ssh-port 43000 --add proj01 -o ~/.pgcli-t1/pg.yaml
参数 默认值 含义
--namespace default 容器名称前缀:pgcli-pg-<namespace>-<instance>,备份容器 pgcli-backup-<namespace>。传递 --namespace "" 保持旧式名称不带前缀
--base-dir ~/.pgcli 所有数据路径的基础目录(实例数据、插件配置、备份)
--backup-dir (在 --base-dir 下) 备份数据/日志的独立基础目录。覆盖 --base-dir 设置的备份路径
--pg-start-port 35432 分配范围中的第一个 PG 主机端口
--pg-ssh-port 42201 分配范围中的第一个 SSH 主机端口

所有都保存到配置文件中(namespace、pg_start_port、pg_ssh_port);跨配置使用不重叠的端口范围,这样分配永远不会冲突。

9 - 克隆

pgcli 克隆指南

创建新实例,其数据从现有实例复制,直接流式传输——磁盘上无临时文件。

# 克隆默认实例
pg clone test02

# 克隆特定实例
pg clone test02 -i proj01

# 通过连接字符串克隆远程数据库
pg clone test02 --dsn postgres://user:pass@host:5432/db

# 新实例的自定义数据目录
pg clone test02 -i proj01 --base-dir /data/pg

发生了什么

  1. 预检 — 在创建任何内容之前验证源:
    • 本地实例:容器必须正在运行
    • --dsn:认证的 SELECT 1 必须成功(捕获错误密码、不可达主机)
  2. 创建 — 新实例条目添加到配置,带有随机密码、自己的容器名称、数据目录和自动分配的端口
  3. 启动 — 新实例启动(与 pg start 相同的工作流)
  4. 流式传输 — 源数据管道传输到目标,每秒显示一次实时传输进度

注释

  • 源实例必须正在运行(或 --dsn 目标可达);错误的源会立即失败且无副作用
  • 新实例名称在配置中不能已存在
  • --dsn 和 --instance 互斥:使用 --dsn 时连接字符串确定主机、端口和数据库
  • 新实例获得新的随机密码——在克隆输出或 pg status -i <name> 中查找
  • 仅逻辑复制(模式 + 数据);对于大型数据库,物理方法可能更快

10 - 扩展

pgcli PostgreSQL 扩展指南

pgcli 支持从 Pigsty DEB 仓库安装和管理 PostgreSQL 扩展。

工作原理

扩展被烘焙到派生容器镜像中:

  1. pg extension install 构建新镜像(基于当前镜像 + Pigsty 仓库 + 扩展包)
  2. 停止并移除旧容器
  3. 从新镜像重新创建容器(主机数据卷被保留)
  4. 更新配置文件中的 image_tag

这种方法的优势:

  • 扩展在容器重建后仍然存在(烘焙到镜像层)
  • pg start 不需要在每次启动时运行 apt-get install
  • 扩展文件是持久的并与容器生命周期解耦

命令

安装扩展

# 安装单个扩展
pg extension install pg_stat_statements

# 安装多个扩展(单次镜像构建,逗号分隔)
pg extension install pgmq,uuid-ossp,pg_stat_statements

# 针对特定实例
pg extension install pg_stat_statements -i pg01

重启确认: 需要 shared_preload_libraries 的扩展(例如 pg_stat_statements、pg_cron)需要 PostgreSQL 重启。默认情况下,会提示你确认:

# 交互式确认(默认)
pg extension install pg_stat_statements
# 输出:
# Installing extensions that require shared_preload_libraries will cause a PostgreSQL restart.
# Extensions to be installed: [pg_stat_statements]
# This will cause a brief interruption to database connections.
# Restart PostgreSQL now? [y/N]:

# 跳过确认并自动重启
pg extension install pg_stat_statements --auto-restart

如果你拒绝重启,可以稍后应用更改:

pg stop -i pg01
pg start -i pg01

列出已安装扩展

pg extension list -i pg01

示例输出:

Installed extensions in "pg01":
  pg_stat_statements (managed)
  uuid-ossp (managed)
  plpgsql (unmanaged)
  • managed:由 pgcli 跟踪(记录在配置中,包含在镜像中)
  • unmanaged:手动安装的扩展(不在配置中跟踪)

移除扩展

pg extension remove pgmq -i pg01

工作流:

  1. DROP EXTENSION IF EXISTS pgmq
  2. 更新配置和 shared_preload_libraries
  3. 无镜像重建 — -ext 镜像在实例间共享,包永远不会被卸载

重启确认: 如果移除需要 shared_preload_libraries 的扩展(例如 pg_stat_statements、pg_cron),会在重启前提示你确认:

# 交互式确认(默认)
pg extension remove pg_stat_statements -i pg01
# 输出:
# Removing extensions that require shared_preload_libraries will cause a PostgreSQL restart.
# Extensions to be removed: [pg_stat_statements]
# This will cause a brief interruption to database connections.
# Restart PostgreSQL now? [y/N]:

# 跳过确认并自动重启
pg extension remove pg_stat_statements -i pg01 --auto-restart

如果你拒绝重启,可以稍后应用更改:

pg stop -i pg01
pg start -i pg01

查看可用扩展

pg extension available

列出所有 440 个已知扩展:

  • 45 个内置(contrib,已在基础镜像中——无需镜像构建)
  • 395 个 Pigsty 目录(来自 Pigsty DEB 仓库,需要镜像构建)

扩展目录

参见扩展目录获取 45 个内置扩展完整列表(无需安装)。

参见常用扩展获取常用扩展详细使用示例。

目录外的扩展

只有目录中的扩展(builtin + Pigsty)可以通过 pg extension install 安装。未知扩展名称会在构建开始前被拒绝:

  [X] Unknown extension(s): [nonexistent_ext]

      These extensions are not in the Pigsty catalog or builtin contrib list.
      Check available extensions: pg extension available
      Full Pigsty catalog: https://pigsty.cc/ext/list/

完整目录:https://pigsty.cc/ext/list/

配置

安装扩展后,配置会被更新:

instances:
  pg01:
    extensions:
      - pg_stat_statements
      - uuid-ossp
      - pgmq
    podman:
      image_tag: ghcr.io/mars-base/pgcli/pgcli-pg:18-2.58.0-ext

image_tag 指向包含所有已安装扩展的派生镜像。

共享预加载库

需要 shared_preload_libraries 的扩展会在 postgresql.conf 中自动配置:

# === pgcli extensions (managed — do not edit) ===
shared_preload_libraries = 'pg_stat_statements,pg_cron'
# === end pgcli extensions ===

必须最先加载的扩展(如 citus、timescaledb)会自动放在位置 0,不管安装 顺序如何 —— 如果它们出现在其他位置,PostgreSQL 会 FATAL。

这是 postmaster 级别参数;更改后必须重启 PostgreSQL。

故障排除

扩展安装失败

  [X] Unknown extension(s): [nonexistent_ext]

原因:扩展名称不在内置 contrib 列表或 Pigsty 目录中。

解决方法:

  • 验证扩展名称:pg extension available
  • 检查 Pigsty 目录:https://pigsty.cc/ext/list/
  • 注意确切的 SQL 扩展名称(例如 vector 而不是 pgvector)

CREATE EXTENSION 失败

ERROR: extension "pgmq" already exists

扩展已安装但未在配置中跟踪。你可以安全地忽略这个,或手动将其添加到配置:

extensions:
  - pgmq

共享预加载库冲突

如果 shared_preload_libraries 在 postgresql.conf 中被手动编辑,pgcli 的标记块会覆盖它。

解决方法:删除手动配置并让 pgcli 管理它。

注释

  • 扩展数量:45 个内置(contrib)+ 395 个 Pigsty 目录 = 440 个已知扩展
  • 镜像大小:每个扩展增加 10-50MB 到镜像,但 Pigsty 包已优化
  • 构建时间:首次扩展安装需要 1-3 分钟(下载 + 构建);后续安装更快(缓存命中)
  • 副本行为:副本可以安装扩展,但 CREATE EXTENSION 会被拒绝(只读)。在主实例上安装;副本通过物理复制同步
  • 扩展升级:ALTER EXTENSION ... UPDATE TO ... 尚不支持;通过 pg exec 手动运行

10.1 - 扩展目录

基础镜像中内置的 PostgreSQL 扩展列表

本页列出基础镜像中所有内置扩展。使用示例请参见常用扩展。

内置扩展(45 个)

这些扩展已包含在 PostgreSQL 基础镜像中,无需镜像构建或 pg extension install。直接运行 CREATE EXTENSION 即可启用。

需要 shared_preload_libraries

这些扩展启用后需要重启 PostgreSQL。通过 pg extension install 安装时,pgcli 会自动处理重启。

扩展 描述
pg_stat_statements SQL 性能分析——跟踪查询执行次数、时间和资源使用
pg_cron 数据库内置 Cron 定时任务调度器
pg_hint_plan 查询提示,影响查询计划器
pg_stat_monitor 高级性能监控,包含查询计划
pg_qualstats 查询谓词统计,用于索引推荐
pg_stat_kcache 内核级 CPU/IO 性能统计
pg_wait_sampling 等待事件采样,用于性能分析
pg_track_settings 跟踪配置变更历史
timescaledb 时序数据库扩展,支持超级表
pg_prewarm 重启后将表数据预加载到共享缓冲区

无需重启

这些扩展可以通过 CREATE EXTENSION 立即启用,无需重启。

扩展 描述
uuid-ossp UUID 生成函数(v1/v3/v4/v5)
hstore 单列键值对存储
pgcrypto 哈希(bcrypt/sha256)、加密/解密、随机数生成
pg_trgm 三元组相似性匹配,用于模糊搜索
unaccent 去除字符重音,用于国际化文本搜索
fuzzystrmatch 模糊字符串匹配(Levenshtein、Soundex、Metaphone)
intarray 整数数组操作(去重、排序、交集)
isn ISBN/ISSN/EAN 标准数字类型及验证
pg_repack 在线表重组,无需排他锁
pg_squeeze 表空间回收
pg_partman 自动分区管理
pgvector (Pigsty) 向量相似性搜索,用于 AI 应用
postgis (Pigsty) 地理空间数据支持
pgmq (Pigsty) 轻量级消息队列
tablefunc 交叉表/透视表函数
btree_gist GiST 索引的 B-tree 支持
btree_gin GIN 索引的 B-tree 支持
citext 不区分大小写的文本类型
cube 多维立方体数据类型
ltree 层次化标签树数据类型
seg 浮点区间数据类型
earthdistance 地球表面大圆距离计算
postgres_fdw 将外部 PostgreSQL 服务器作为本地表查询
file_fdw 将服务器文件作为外部表读取
dblink 连接其他 PostgreSQL 数据库
amcheck 验证 B-tree 索引完整性
pageinspect 底层页面检查,用于调试
pg_buffercache 查看共享缓冲区内容
pg_freespacemap 查看表的空闲空间映射
pg_visibility 查看表的可见性映射
pg_walinspect 检查 WAL 记录
pgstattuple 表级元组统计
pgrowlocks 显示行级锁信息
pg_surgery 底层堆元组操作
xml2 XML 解析和 XPath 函数
dict_int 整数文本搜索字典
dict_xsyn 扩展同义词文本搜索字典
bloom 布隆过滤器索引访问方法
autoinc 自增触发器函数
insert_username 插入用户名触发器函数
moddatetime 自动更新时间戳触发器
refint 引用完整性触发器函数
tcn 触发器变更通知
sslinfo SSL 连接信息函数
lo 大对象类型支持
intagg 整数数组聚合/枚举
tsm_system_rows 按行数进行表采样
tsm_system_time 按时间进行表采样
pg_logicalinspect 逻辑复制槽检查

10.2 - 常用扩展

最常用 PostgreSQL 扩展的使用示例

最常用 PostgreSQL 扩展的使用示例,已在 PG 18 + pgcli 环境下测试通过。

性能与监控

扩展 安装 描述
pg_stat_statements pg extension install pg_stat_statements SQL 性能分析。生产环境必备。
pg_repack pg extension install pg_repack 在线表重组,回收膨胀空间。
pg_prewarm pg extension install pg_prewarm 重启后预加载表数据。
# 一次性安装所有扩展
pg extension install pg_stat_statements,pg_repack,pg_prewarm --auto-restart

# 查找 top 5 慢查询
pg exec "SELECT query, calls, total_exec_time::numeric(10,2) AS ms
         FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"

# 重组膨胀表
pg exec -- pg_repack -U admin -d default_db -t my_table --no-superuser-check

# 预热表
pg exec "SELECT pg_prewarm('my_table');"

数据类型与标识符

扩展 安装 描述
uuid-ossp 内置 UUID 生成(v1/v3/v5)。PG 18 已内置 gen_random_uuid() (v4) 和 uuidv7() (v7)——无需安装扩展。
hstore 内置 键值对存储。
citext 内置 不区分大小写的文本。Foo = foo。
# UUID 生成(PG 18 内置——无需安装扩展)
pg exec "SELECT gen_random_uuid();"  # v4 随机 UUID(PG 13 起内置)
pg exec "SELECT uuidv7();"           # v7 时间有序 UUID(PG 18 新增)

# uuid-ossp 扩展——仅在需要 v1/v3/v5 UUID 时安装
pg exec "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";"
pg exec "SELECT uuid_generate_v1();"  # v1 时间戳 + MAC 地址
pg exec "SELECT uuid_generate_v3(uuid_ns_url(), 'https://example.com');"  # v3 基于 MD5
pg exec "SELECT uuid_generate_v5(uuid_ns_url(), 'https://example.com');"  # v5 基于 SHA-1

# 键值存储
pg exec "SELECT 'theme => dark, lang => en'::hstore -> 'theme';"
# 返回: dark

# 不区分大小写匹配
pg exec "CREATE TABLE users (email citext);
         INSERT INTO users VALUES ('Alice@Example.COM');
         SELECT * FROM users WHERE email = 'alice@example.com';"

搜索与文本

扩展 安装 描述
pg_trgm 内置 三元组相似性,用于模糊搜索和自动补全。
unaccent 内置 去除重音,用于灵活的国际化文本搜索。
# 模糊搜索
pg exec "SELECT name, similarity(name, 'John') FROM users
         ORDER BY similarity(name, 'John') DESC LIMIT 5;"

# 创建三元组索引加速模糊搜索
pg exec "CREATE INDEX users_name_trgm_idx ON users USING gin (name gin_trgm_ops);"

# 去除重音
pg exec "SELECT unaccent('Crème Brûlée');"  -- 返回 'Creme Brulee'

# 组合使用实现不区分重音的模糊搜索
pg exec "SELECT name FROM users WHERE name % unaccent('Creme Brulee');"

安全与加密

扩展 安装 描述
pgcrypto 内置 哈希、加密/解密、随机数生成。
pgaudit pg extension install pgaudit 审计日志。合规(GDPR、HIPAA、SOX)必需。
# 使用 bcrypt 哈希密码
pg exec "SELECT crypt('my_password', gen_salt('bf'));"

# 验证密码
pg exec "SELECT (crypt('my_password', stored_hash) = stored_hash);"

# 对称加密/解密
pg exec "SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'key'), 'key');"

# 启用特定操作的审计日志
pg exec "SET pgaudit.log = 'read, ddl';"

地理空间

扩展 安装 描述
postgis pg extension install postgis 完整空间数据库:几何类型、空间索引、距离/面积计算。
pg extension install postgis --auto-restart

# 创建包含空间数据的表
pg exec "CREATE TABLE places (id serial, name text, geom geometry(Point, 4326));"
pg exec "INSERT INTO places (name, geom) VALUES
         ('San Francisco', ST_MakePoint(-122.4, 37.8)),
         ('London', ST_MakePoint(-0.1276, 51.5074));"

# 查找 5km 范围内的地点
pg exec "SELECT name FROM places
         WHERE ST_DWithin(geom, ST_MakePoint(-122.4, 37.8)::geography, 5000);"

# 计算两个城市间的距离(米)
pg exec "SELECT ST_Distance(
           ST_MakePoint(-74.006, 40.7128)::geography,
           ST_MakePoint(-0.1276, 51.5074)::geography);"

AI 与向量搜索

扩展 安装 描述
pgvector pg extension install vector 存储和搜索嵌入向量。AI 应用标准。
pg extension install vector --auto-restart

# 创建带向量列的表
pg exec "CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3));"
pg exec "INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]'), ('[7,8,9]');"

# 查找最近邻(欧几里得距离)
pg exec "SELECT id FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;"

# 创建 IVFFlat 索引加速近似搜索
pg exec "CREATE INDEX items_embedding_idx ON items USING ivfflat (embedding vector_l2_ops);"

时序数据

扩展 安装 描述
timescaledb pg extension install timescaledb 优化的时序存储,支持超级表。
pg extension install timescaledb --auto-restart

# 创建超级表
pg exec "CREATE TABLE sensor (time timestamptz NOT NULL, value float);
         SELECT create_hypertable('sensor', 'time');"

# 时间桶聚合
pg exec "SELECT time_bucket('1 hour', time) AS bucket, avg(value)
         FROM sensor GROUP BY bucket ORDER BY bucket;"

定时任务与分布式

扩展 安装 描述
pg_cron pg extension install pg_cron Cron 定时任务调度器。pgcli 自动配置。
citus pg extension install citus 分布式 PostgreSQL,支持分片。
pg extension install pg_cron --auto-restart

# 调度每日清理任务(午夜运行)
pg exec "SELECT cron.schedule('cleanup', '0 0 * * *',
         'DELETE FROM logs WHERE created_at < now() - interval ''30 days''');"

# 列出已调度的任务
pg exec "SELECT jobid, schedule, command FROM cron.job;"

# 取消调度
pg exec "SELECT cron.unschedule('cleanup');"

外部数据

扩展 安装 描述
postgres_fdw 内置 将外部 PostgreSQL 服务器作为本地表查询。
pg_duckdb pg extension install pg_duckdb 嵌入式 DuckDB 分析引擎。读取 Parquet/CSV,OLAP 查询加速。
# 创建外部服务器
pg exec "CREATE SERVER remote FOREIGN DATA WRAPPER postgres_fdw
         OPTIONS (host '10.0.0.2', port '5432', dbname 'analytics');"

# 创建用户映射
pg exec "CREATE USER MAPPING FOR current_user SERVER remote
         OPTIONS (user 'reader', password 'secret');"

# 导入远程 schema 作为外部表
pg exec "IMPORT FOREIGN SCHEMA public FROM SERVER remote INTO remote_schema;"

# 像查询本地表一样查询远程数据
pg exec "SELECT * FROM remote_schema.events LIMIT 10;"
pg extension install pg_duckdb --auto-restart

# 在 DuckDB 中执行 SQL —— duckdb.query 是表函数(必须在 FROM 子句中使用)
pg exec "SELECT * FROM duckdb.query(\$\$ SELECT 42 AS answer, 'hello' AS msg \$\$);"

# 将表导出为 Parquet 文件
pg exec "COPY my_table TO '/tmp/my_table.parquet' (FORMAT 'parquet');"

# 直接读取 Parquet/CSV 文件
pg exec "SELECT * FROM duckdb.query(\$\$
         SELECT * FROM read_parquet('/tmp/my_table.parquet') WHERE id > 2 \$\$);"

# 将整个查询下推到 DuckDB 执行(OLAP 加速)
pg exec "SET duckdb.force_execution = true;
         SELECT category, sum(amount) FROM sales GROUP BY category;"

# 纯 DuckDB SQL(完全绕过 PostgreSQL 规划器)
pg exec "SELECT * FROM duckdb.raw_query(\$\$ SELECT range AS n FROM range(1, 6) \$\$);"

# --- 从 S3 / MinIO 读取文件(无需复制文件) ---

# 先创建 S3 secret(use_ssl 是文本类型:'true'/'false')
pg exec "SELECT duckdb.create_simple_secret('S3', '<access_key>', '<secret_key>',
         '', 'us-east-1', 'path', '', 'minio.example.com:9000',
         's3://bucket', '', 'false');"

# 直接查询 bucket 中的 CSV
pg exec "SELECT * FROM duckdb.query(\$\$
         SELECT category, count(*) AS cnt, sum(sales) AS total_sales
         FROM read_csv('s3://bucket/products.csv', auto_detect=true)
         GROUP BY category ORDER BY total_sales DESC \$\$);"

兼容性说明

扩展 PG 18 状态 备注
pgml 不可用 仅支持 PG 14-17(无 PG 18 包)
uuid-ossp 部分需要 PG 18 内置:gen_random_uuid() (v4)、uuidv7() (v7)。扩展仅在需要 v1/v3/v5 时安装

11 - 数据导入/导出

pgcli 数据导入/导出指南

导出和导入数据库到转储文件。支持自定义格式(推荐)和纯 SQL,带有自动 gzip 压缩。还支持在实例间管道传输。

# 导出为自定义格式(推荐,最快恢复)
pg export -i proj01 -o backup.dump

# 导出为 SQL 格式(人类可读)
pg export -i proj01 -o backup.sql

# 导出时使用 gzip 压缩(从 .gz 扩展名自动检测)
pg export -i proj01 -o backup.dump.gz
pg export -i proj01 -o backup.sql.gz

# 导出特定数据库
pg export -i proj01 -d mydb -o backup.dump

# 导出时指定压缩级别(0-9)
pg export -i proj01 -o backup.sql.gz --compress=9

# 导出时显示详细输出(显示进度)
pg export -i proj01 -o backup.dump -v

# 从自定义格式导入
pg import -i proj02 backup.dump

# 从 SQL 格式导入
pg import -i proj02 backup.sql

# 导入压缩文件(从 .gz 扩展名自动检测)
pg import -i proj02 backup.dump.gz

# 导入到特定数据库
pg import -i proj02 -d mydb backup.dump

# 导入时清理(恢复前删除现有对象)
pg import -i proj02 --clean backup.dump

# 导入时显示详细输出
pg import -i proj02 backup.dump -v

# 在实例间管道传输(无临时文件)
pg export -i proj01 | pg import -i proj02
pg export -i proj01 -d mydb | pg import -i proj02 -d mydb --clean

# 通过 SSH 跨主机管道传输
pg export -i proj01 | ssh user@remote "pg import -i proj02"
ssh user@remote "pg export -i proj01" | pg import -i proj02
ssh user@host1 "pg export -i proj01" | ssh user@host2 "pg import -i proj02"

# 通过连接字符串使用远程数据库(--dsn)
pg export --dsn postgres://user:pass@host:5432/mydb -o backup.dump
pg import --dsn postgres://user:pass@host:5432/mydb backup.dump --clean
pg export -i proj01 | pg import --dsn postgres://user:pass@host:5432/mydb
pg export --dsn postgres://user:pass@host1:5432/db1 | pg import --dsn postgres://user:pass@host2:5432/db2

# DSN 也可用于本地实例(当端口与默认值不同时很有用)
pg export --dsn postgres://admin:pass@127.0.0.1:35432/mydb | pg import --dsn postgres://admin:pass@127.0.0.1:35433/mydb --clean

格式比较:

特性 自定义(.dump) SQL(.sql)
导入速度 更快(二进制格式) 更慢(文本格式)
文件大小 更小(压缩) 更大(纯文本)
人类可读 否 是
选择性恢复 是(特定表) 否
最适合 迁移、备份、大型数据库 版本控制、CI 种子数据、手动编辑

格式检测: 使用魔数(基于内容)配合扩展名回退。

  • 以 PGDMP 开头的文件 → 自定义格式
  • .sql 或 .sql.gz → 纯 SQL 格式
  • .gz 扩展名或 gzip 魔数(0x1f 0x8b) → 自动解压
  • 如果内容检测失败则使用扩展名作为回退

远程数据库(–dsn): 使用连接字符串连接到任何 PostgreSQL 实例。

  • 无需本地 PostgreSQL 安装
  • 适用于本地到远程、远程到本地和远程到远程迁移
  • 支持与本地实例相同的所有标志(-o、-d、--clean、-v、--compress)

关于现有数据的说明: 导入到包含现有表的数据库会失败,除非你使用 --clean 标志,它会在恢复前删除对象。导入到已包含数据的数据库时使用 --clean。

用例:

  • 在实例间迁移数据:pg export -i proj01 | pg import -i proj02
  • 跨主机迁移:pg export -i proj01 | pg import --dsn postgres://user:pass@remote:5432/db
  • 与团队共享数据库:pg export -i proj01 -o dump.dump.gz(压缩,更小文件)
  • 重大更改前备份:pg export -i proj01 -o pre-migration.sql.gz
  • CI/CD 管道:导出测试数据,导入到新的测试数据库

12 - 恢复

pgcli 时间点恢复(PITR)指南

时间点恢复(PITR)

恢复到首次备份后的任意时间点。

# 恢复(只读,提交前检查)
pg restore --time "2026-08-26 15:30:00+00"

# 预览将要恢复的内容而不执行(试运行)
pg restore --time "2026-08-26 15:30:00+00" --dry-run

# 恢复期间流式输出恢复容器日志
pg restore --time "2026-08-26 15:30:00+00" --tail-logs

# 如果需要可以尝试不同的时间
pg restore --time "2026-08-26 15:25:00+00"

# 提升为读写(切换时间线)
pg restore --time "2026-08-26 15:30:00+00" --promote

# 跳过确认
pg restore --time "2026-08-26 15:30:00+00" --promote --force

时间格式:

  • 2026-08-26 15:30:00+08:00 — 带时区偏移
  • 2026-08-26 15:30:00+08 — 仅时区小时
  • 2026-08-26 15:30:00Z — UTC
  • 2026-08-26 15:30:00 — 假定为 UTC

恢复工作流: 停止 → 恢复 → 启动 → WAL 重放到目标时间

注意: --promote 之后,在进行进一步的 PITR 之前需要创建新的完整快照。

Patroni 集群

Patroni HA 集群用 pg ha restore 恢复,是上面单实例命令的集群版对应物。它接受 相同的 --time(全部格式),并支持 --dry-run、--tail-logs、--force。

# 预览恢复计划,不触碰集群
pg ha restore app --time "2026-08-26 15:30:00+00" --dry-run

# 恢复,同时流式输出承载 bootstrap 成员的恢复日志
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

# 跳过确认提示
pg ha restore app --time "2026-08-26 15:30:00+00" --force

原理: 先 pause 集群(冻结 failover,免得本机停掉 leader 时远端副本被 promote 抢走 leadership),再移除集群的 DCS 身份,然后让一个本机成员通过 Patroni 的自定义 bootstrap 方法,从 pgBackRest 仓库把(已清空的)数据目录恢复到目标 时间点。该成员在新时间线上启动并自动 promote 成可写 leader;其余成员——本机的、 跨主机的——都经 DCS 重新加入它。前提是该 stanza 已配好 WAL 归档,由带 S3 仓库的 pg backup setup 预置(见 HA 备份插件)。

与单实例恢复的差异:

  • 总是 promote。 没有"挂起只读、检查、再换个时间重试"的两步——集群在目标点 直接以可写状态起来。执行前用 --dry-run 确认目标时间。
  • 必须在拥有成员的主机上运行,且动手前先检测 leader。 pg ha restore 在执行 任何破坏性操作之前,会先从 DCS 读取当前 leader,判断它是否是本机的可控成员。 真实执行时若 leader 不在本机(远端成员,或本机视图里根本没有的跨主机成员),会 直接拒绝;--dry-run 则只是告警。原因:破坏性路径会清掉 DCS 身份并让本机 成员重新 bootstrap,若本机停不掉当前 leader,它会在 旧时间线 上抢先重新抢回 DCS,与 bootstrap 竞态。先把 leader 切到本机(pg ha switchover/failover), 或到 leader 所在主机执行。leader 读不到/为空时不阻断(覆盖首次 bootstrap 的场景)。
  • 没有 --promote flag —— promote 是自动的。

恢复工作流: 预检(leader 必须是本机成员)→ pause 集群 → 停本机成员 → 清 DCS → 清空并用仓库 bootstrap 一个成员 → 它 promote 成新时间线的 leader → 其余成员作为副本 重新加入。

恢复之后: 建一个新的完整快照以给新时间线重新定位—— pg ha snapshot create app --type full。这步通常直接成功:同一 stanza 的 pgBackRest 恢复在时间线切换时保持 system-id 不变,所以首个快照不会报 [051] system-id ... do not match stanza,也不需要 stanza-upgrade。若有副本没有 自动重新加入,用 pg ha ctl app -- reinit app-<ns> <member> --force 重建它。

13 - 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"

相关

13.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。

13.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

13.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

13.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 到远程主机。

参考

13.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 一次全量,把快照基线对齐到 健康的拓扑。

13.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 不会遇到这个。

13.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。

13.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 属于生产环境——上面所有操作都碰不到它,这正是隔离的用意。

相关

13.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

相关

13.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 签发的自 带证书)行为一致——见生成证书。

相关

14 - 自签证书

用 pg cert 签发自签 TLS 证书,并让 store 插件加载它

为什么要自签

三个以 TLS 对外提供 S3 的插件——minio、 silo、rustfs——可以用两种方式加载证书:

  • 自动生成(--tls):pgcli 替你签一张自签 CA + leaf,托管证书目录,重启时 刷新。零配置,SAN 固定。
  • 自带证书(BYO)(--tls-cert / --tls-key):你提供这一对。公 CA 签发的证书、 你已有的通配符证书、需要自定义 SAN 的证书,都走这条。

pg cert 就是为第二条准备的签发工具:它生成一张自签证书,SAN 恰好覆盖你需要的 主机名和 IP,让你不必有真正的 CA 也能体验 BYO TLS——而且它只写出一对独立文件, 不注册进 pgcli:文件落到你指定的路径,pg.yaml 不动,也不会拉起任何容器。你得自己 用 --tls-cert/--tls-key 把它交给 store。

快速上手

# 放证书对的目录(任何可写路径都行;别放数据目录里)。pgcli 的 base dir 本就在
# ~/.pgcli 下:
mkdir -p ~/.pgcli/certs

# 一张对「一个域名 + 两个 IP」有效的 leaf,ECDSA P-256、825 天(均默认):
pg cert --host "rustfs.test,127.0.0.1,10.0.0.9" \
  --cert-file ~/.pgcli/certs/rustfs.crt \
  --key-file  ~/.pgcli/certs/rustfs.key

# 交给 store 加载(三个之一都行),写全路径:
pg addon install rustfs --name store \
  --tls-cert ~/.pgcli/certs/rustfs.crt \
  --tls-key  ~/.pgcli/certs/rustfs.key

pg cert 只打印它写入的 SAN,别的什么都不做——它是一个纯粹的文件生成器,不是 插件命令。

参数

参数 默认 含义
--host 127.0.0.1 逗号分隔的 DNS 名和/或 IP,编进 SAN(可重复)。每项自动判定是 IP 还是域名,所以 "rustfs.test,10.0.0.9" 无需特殊写法;*.wild.test 作为通配符 DNS 项有效。
--cert-file cert.pem 写证书 PEM 的路径。
--key-file key.pem 写私钥 PEM(PKCS8,权限 0600)的路径。
--valid-duration 825 天 证书有效期,例如 --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, 参数完全一致、只是单横线形式(-host、-cert-file …)。两者背后是同一个包,产出 的证书逐字节同类。

pg cert 产出什么

一张自签 leaf,不是链。 --cert-file 里的 PEM 恰好只有一个 CERTIFICATE 块—— 证书自己签自己(CA:FALSE、serverAuth EKU、你的 SAN)。刻意没有 leaf+中间+根 的捆绑:它上面没有签发 CA,也就无链可拼。

这也是为什么 --ca 的产物不该喂给 --tls-cert:pg addon install … --tls-cert 会跑 ValidateBYOCert,它检查第一张证书并拒绝一张 CA。--ca 证书是你拿去签 别人的信任锚,不是用来对外 serve 的服务器证书。

把它当作客户端的信任锚

因为结果自签,把同一个 .crt 作为 CA 文件交给每个 TLS 客户端——被 serve 的 leaf 就是它自己的信任锚。对 pgBackRest 的 S3 仓库,这意味着把 --s3-ca-file 直接指向 pg cert 的输出:

pg backup setup --s3-endpoint <store-host>:<api-port> \
  --s3-ca-file ~/.pgcli/certs/rustfs.crt

这不是 pgBackRest 勉强容忍的变通:OpenSSL 把你交给它的任何证书都当信任锚(不要求 CA:TRUE),pgBackRest 的 S3 TLS 路径就是这套机制。直接验证过:对自签的 CA:FALSE leaf 跑 openssl verify -CAfile <cert> <cert> 返回 OK。

若要把证书放到拨号的那台机器上而不搬文件,用 pg backup fetch-ca——它直接从实时 TLS 握手里抽出 签发证书。

交给 store 加载

三个 S3 store 各自把你的这一对挂到其二进制要求的文件名下,所以你的文件名随意—— pg cert --cert-file anything.crt 对三个都成立:

# MinIO / silo 读 public.crt + private.key:
pg addon install minio --name store --tls-cert anything.crt --tls-key anything.key
pg addon install silo  --name store --tls-cert anything.crt --tls-key anything.key

# rustfs 读 rustfs_cert.pem + rustfs_key.pem:
pg addon install rustfs --name store --tls-cert anything.crt --tls-key anything.key

pgcli 从不重新属主、也不写你的源文件;在 rustfs 的情形下这一对以只读挂载、再拷进 容器自己的证书目录(见 rustfs → 权限与属主)。

在 rustfs 上实测过

真机安装验证:rustfs serve 的 pg cert leaf 经实时 TLS 握手确认(握手实际呈现的 证书 subject == issuer,不只看 pg.yaml 记了什么),容器以 HTTPS 起来且健康, pg mc 对它完成 alias set → mb → put → ls → get 全链路,取回的对象与上传时 逐字节一致。

续期一张 BYO 证书

单文件 bind 挂载会钉住宿主源 inode,所以就地替换文件对运行中的容器无效。要续期, 重新铸一对(或覆盖文件),再重建:

pg cert --host "rustfs.test,127.0.0.1" \
  --cert-file ~/.pgcli/certs/rustfs.crt --key-file ~/.pgcli/certs/rustfs.key
pg addon install rustfs --name store \
  --tls-cert ~/.pgcli/certs/rustfs.crt \
  --tls-key  ~/.pgcli/certs/rustfs.key --force

关掉 BYO(退回自动生成,或纯 HTTP):从 pg.yaml 里删掉该插件的 cert_file/key_file 行,再 --force 重建。

另见

15 - 开机自启

配置 pgcli 在主机重启后自动启动 PostgreSQL 实例和服务

pgcli 可以在主机重启后自动启动 PostgreSQL 实例、备份容器、PgBouncer 服务、etcd 成员以及其他受支持的 addon。此功能使用系统服务管理器:

  • Linux: systemd 用户单元
  • macOS: launchd LaunchAgents

工作原理

启用开机自启后,pgcli 会创建一个系统服务,在开机时(或用户登录时)运行 pg start --autostart。--autostart 标志仅启动配置中标记为 autostart: true 的实例和服务。

开机自启是配置驱动的:如果你对某个实例执行了 pg stop(或 pg stop --all),它仍然会在下次开机时自动启动。要阻止自动启动,请使用 pg autostart disable。

默认行为:备份容器在新配置中默认 autostart: true。实例、PgBouncer 和 etcd 成员默认 autostart: false,需要手动启用。

启用开机自启

单个实例

pg autostart enable -i <实例名>

示例:

pg autostart enable -i default

这会在实例配置中添加 autostart: true 并创建/更新开机服务。

备份容器

pg autostart enable --backup

启用共享 pgBackRest 备份容器的开机自启。

PgBouncer

启用与特定实例关联的 PgBouncer 的开机自启:

pg autostart enable --pgbouncer -i <实例名>

或为远程 PgBouncer 启用:

pg autostart enable --pgbouncer --pg-name <远程名称>

etcd 成员

为通过 pg addon install etcd 安装的 etcd 成员启用开机自启:

pg autostart enable --etcd --name <成员名>

省略 --name 时成员名默认为 etcd:

pg autostart enable --etcd

集群的每个成员都要分别启用一次(m1、m2、m3……)——自启是按成员配置的。 跨主机集群时,在各自主机上为该主机的成员执行此命令。

开机只做启动(start-only)。 开机时 pgcli 只把成员已存在的容器拉起 (podman start,仅当容器缺失或卡在 improper 状态时才按配置重建容器), 绝不重新注册 membership——data-dir 已初始化的 etcd 成员会从磁盘加载集群状态, 普通 start 即可重新加入,所以该成员必须事先在集群可达时安装好。成员启动后、 quorum 尚未形成前会短暂报 etcdserver: no leader,属正常现象,足够多成员 起来后即恢复。

Kafka 节点

为通过 pg addon install kafka 安装的 Kafka 实例启用开机自启:

pg autostart enable --kafka --name <实例名>

省略 --name 时默认为 kafka。开机时 pgcli 会先按 pg.yaml 重新渲染 server.properties,再启动已有容器并等待 broker API 响应。已经格式化过的 数据目录不会被重新格式化——cluster id 与 node id 从 meta.properties 读取, 原有日志直接复用。

SQLite 数据库

为受管的 SQLite 数据库启用开机自启(仅 Linux):

pg autostart enable --sqlite --name <数据库名>

--name 必填——SQLite 没有默认数据库名。复制必须已经启用,且 litestream.yml 必须存在:

pg sqlite replication enable appdb --s3-bucket sqlite-backups ...
pg autostart enable --sqlite --name appdb

开机时 pgcli 重启该数据库的 litestream replicate 进程——没有容器,数据库 文件本身也没有可启动的服务。该进程在对象存储 addon 之后启动,因此同机的 MinIO/silo/rustfs/garage/SeaweedFS 已经先开始监听。如果数据库文件丢失,该 数据库的自启会报错(其余自启目标继续执行);请恢复文件或禁用该目标。

pg sqlite replication disable 会同时清除该数据库的 autostart 标志,因为 没有复制配置时 Litestream 无法运行。

禁用开机自启

pg autostart disable -i <实例名>
pg autostart disable --backup
pg autostart disable --pgbouncer -i <实例名>
pg autostart disable --pgbouncer --pg-name <远程名称>
pg autostart disable --etcd --name <成员名>
pg autostart disable --sqlite --name <数据库名>

当所有开机自启目标都被禁用后,开机服务会被自动移除。

查看状态

pg autostart status

显示:

  • 哪些实例和服务已启用开机自启
  • 开机服务单元名称和状态
  • Linger 状态(Linux)—— rootless podman 在开机时启动服务所需

平台特定行为

Linux (systemd)

开机服务以 systemd 用户单元创建:pgcli-autostart-<hash>.service

重要:Rootless podman 需要 loginctl enable-linger 才能在开机时(用户登录前)启动容器。pgcli 会自动尝试此操作,但如果失败会打印提示。

没有 linger,服务会在用户登录时启动,而不是在系统开机时。

Systemd Unit 配置

pgcli 生成的 systemd unit 包含以下关键配置:

[Service]
Type=oneshot
Delegate=yes
RemainAfterExit=yes
  • Delegate=yes - 将 cgroup 控制权委托给服务进程,允许 rootless podman 正确管理容器的 cgroup 层级
  • RemainAfterExit=yes - 服务执行完成后保持 active 状态,而不是立即变为 inactive
  • Type=oneshot - 一次性执行 pg start --autostart,然后退出

Cgroup 隔离与自动回退

问题:当容器由 systemd 用户单元启动时,从 SSH 会话或其他上下文访问容器时可能会遇到 cgroup 权限错误:

Error: crun: writing file `/sys/fs/cgroup/.../cgroup.procs`: Permission denied: OCI permission denied

原因:systemd 用户单元启动的容器位于 user@1000.service/app.slice cgroup 子树,而 SSH 会话位于 session-N.scope,两者在不同的 cgroup 作用域。

解决方案:pgcli 会自动检测 cgroup 权限错误,并通过 systemd-run --user --scope 重新执行命令。这会创建一个临时 scope,使其能够加入容器的 cgroup 子树。

受影响的命令(自动回退):

  • pg exec - 执行 SQL 或容器命令
  • pg psql - 交互式 psql 会话
  • pg status - 检查容器状态
  • pg backup - 备份操作
  • pg extension - 扩展管理
  • 所有其他 podman exec 操作

不受影响的命令:

  • pg start - 启动容器(使用 systemd-run 本身)
  • pg stop - 停止容器
  • pg restart - 重启容器

查看服务状态

pg autostart status 现在会显示详细的 systemctl 信息:

pg autostart status

输出包括:

  • 所有开机自启目标的启用状态
  • systemd unit 名称、加载状态、活跃状态
  • 服务日志和最近执行记录
  • Linger 状态

示例输出:

=== Auto-start targets ===
  instance default         enabled
  backup                 enabled
  etcd m1                  enabled

=== Boot service ===
  Unit:      pgcli-autostart-554d14ed
  Installed: yes
  Enabled:   enabled
  Running:   active
  Linger:    yes

=== Service status ===
     Loaded: loaded (/home/user/.config/systemd/user/pgcli-autostart-554d14ed.service; enabled)
     Active: active (exited) since Mon 2026-09-07 14:30:00 CST; 2h ago
    Process: 1234 pg -c /home/user/.config/pgcli/pg.yaml start --autostart (code=exited, status=0/SUCCESS)
   Main PID: 1234 (code=exited, status=0/SUCCESS)

macOS (launchd)

开机服务以 LaunchAgent 创建:com.pgcli.autostart-<hash>.plist

注意:macOS LaunchAgents 在用户登录时运行,而不是在系统开机时。这是 macOS 的限制 —— rootless podman 无法在用户登录前启动服务。

配置文件

autostart: true 标志会出现在你的 pgcli 配置文件中:

instances:
  default:
    # ... 其他设置 ...
    autostart: true

backup:
  # ... 其他设置 ...
  autostart: true

addons:
  # 对于 PgBouncer
  pgbouncer:
    default:
      # ... 其他设置 ...
      autostart: true
  # 对于 etcd 成员
  etcd:
    m1:
      # ... 其他设置 ...
      autostart: true
  # 对于 SQLite 数据库(需要先启用复制)
  sqlite:
    appdb:
      # ... 其他设置 ...
      autostart: true

开机服务行为

开机服务运行 pg start --autostart,它会:

  1. 启动所有 autostart: true 的实例
  2. 如果 backup.autostart: true,启动备份容器
  3. 启动 autostart: true 的 PgBouncer 服务
  4. 启动 autostart: true 的 etcd 成员(只做启动;见上文)
  5. 启动其他 autostart: true 的 addon;SQLite 数据库在对象存储 addon 之后 重启其 Litestream 复制

如果没有配置任何开机自启目标,服务会正常退出(无错误)。

每个 manager(PostgreSQL、备份、PgBouncer、etcd)在启动时都会自愈 rootless podman 的过期状态——主机重启后 pause 进程已死,服务会在触碰容器前透明地执行 podman system migrate。因此即使只给 addon(没有 PG 实例)配置了自启,开机 也能正常工作。

故障排查

服务启动失败

查看开机服务日志:

Linux (systemd):

journalctl --user -u pgcli-autostart-<hash>.service

macOS (launchd):

cat ~/Library/Logs/pgcli-autostart-<hash>.log

Linux: “XDG_RUNTIME_DIR is not set”

此错误表示 systemd 用户单元不可用。确保你有正确的用户会话:

  • SSH 会话默认支持 systemd –user
  • 桌面环境中的终端会话支持 systemd –user
  • Cron 任务和其他非交互式上下文不支持 systemd –user

Linux: 开机自启仅在登录后生效

运行 loginctl enable-linger <你的用户名> 以允许 rootless podman 在开机时启动服务。

macOS: 开机自启仅在登录后生效

这是预期行为。由于 rootless podman 的限制,macOS LaunchAgents 无法在用户登录前运行。

示例

为完整的生产环境启用开机自启:

# 启用 default 实例
pg autostart enable -i default

# 启用备份容器
pg autostart enable --backup

# 为 default 实例启用 PgBouncer
pg autostart enable --pgbouncer -i default

# 为 etcd 成员 m1、m2 启用(成员名来自 pg addon list / pg.yaml)
pg autostart enable --etcd --name m1
pg autostart enable --etcd --name m2

# 为 SQLite 数据库重启 Litestream(需先启用复制)
pg autostart enable --sqlite --name appdb

# 检查状态
pg autostart status

下次重启后,这些服务都会自动启动。

16 - 插件 (Addons)

pgcli 插件管理指南

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

支持的插件

目前支持以下插件:

插件 说明
pgbouncer 连接池管理器,提供事务级连接池化
etcd 分布式键值存储——独立运行、可组集群,用于 HA / DCS
pgdog Postgres 代理——连接池化、负载均衡与分片
postgrest 将 PostgreSQL schema 暴露为 REST API——单容器无状态,本地或远程(任意 PG 端点)
haproxy Patroni 集群前的 TCP 负载均衡——读写一体或读写分离,也可用自己的 haproxy.cfg 做任意原生代理(仅 Linux)
minio S3 兼容对象存储,含 Web 控制台——单机或跨主机分布式纠删码集群(Linux 与 macOS)
silo S3 兼容对象存储(Pigsty 的 MinIO 分支)——功能面与 minio 插件一致,共用同一端口池;自带 mcli 客户端(Linux 与 macOS)
rustfs S3 兼容对象存储(Rust 重新实现)——SNSD/SNMD/MNMD 纠删码布局,固定的容器 uid 由 pgcli 定制镜像在内部消化,共用同一端口池(仅 Linux)
garage S3 兼容对象存储(Deuxfleurs 的 Rust 实现)——v1 单节点,渲染 garage.toml,两个数据目录,四个端口取自自己的池,仅明文 HTTP(TLS 需 nginx 前置)(Linux;macOS 代码完备、未实测)
seaweedfs S3 兼容对象存储(Go 实现,以 volume 粒度管盘)——单节点 weed mini,或跨主机 weed server 集群(filer 元数据放共享 PostgreSQL),全集群共用同一对凭据,原生 S3 TLS;独立端口池(Linux;macOS 代码完备、未实测)
redis 独立键值存储,用于缓存/会话/排行榜/原子计数——大版本 7/8 可选,自动生成 requirepass,RDB 持久化,读副本,--cluster 原生集群分片,独立的端口池(Linux;macOS 代码完备、未实测)
predixy Redis 协议代理——把原生集群变成一个普通的 redis:// 端点,客户端完全无需感知集群;独立端口池(仅 Linux)
pgadmin pgAdmin 4——PostgreSQL 官方 Web 管理界面,单容器直接跑上游镜像;Web 登录自动生成,可选经 --dsn/--pg-name 一次性预置服务器,持久化会话存储;独立端口池(Linux;macOS 代码完备、未实测)
nginx HTTP 反向代理,前置多个 Web 服务——按声明的后端做基于路径的路由,可选 TLS 终止,渲染 nginx.conf 或用自己的 --conf-file,配置变更后 pg addon nginx reload;独立端口池(Linux 与 macOS)
grafana Grafana——开源可观测性面板,单容器直接跑官方上游镜像;Web 登录自动生成,Listen 默认 0.0.0.0(关闭自助注册),v1 只经环境变量(GF_*)配置、无 provisioning;独立端口池(Linux;macOS 代码完备、未实测)
prometheus Prometheus——Grafana 读取的指标采集后端,单容器直接跑官方 docker.io/prom/prometheus 镜像;渲染 prometheus.yml(自带自监控 job + 每个 --scrape JOB 一个 job,该 job 声明的 target 全部并入其中;--scrape-append 增量合并;--alertmanager-url 转发已触发告警并附带对应的抓取 job;--rule-file 拷贝并加载告警规则)或用自己的 --conf-file,pg addon prometheus reload 经 SIGHUP 热加载;无 Web 登录——Listen 默认 0.0.0.0 仅靠网络边界保护(仅本机访问用 --listen 127.0.0.1);独立端口池(Linux;macOS 代码完备、未实测)
alertmanager Alertmanager——Prometheus 体系的告警去重/分组/路由端,单容器直接跑官方 docker.io/prom/alertmanager 镜像;渲染 alertmanager.yml(一条最小路由指向 no-op 或 --webhook-url 的 web.hook receiver)或用自己的 --conf-file,pg addon alertmanager reload 经 SIGHUP 热加载;HA 集群 gossip 已禁用(9094 永不开);无 Web 登录——Listen 默认 0.0.0.0 仅靠网络边界保护,而可触达即意味着任何人能创建 silence(静音 on-call),仅本机访问用 --listen 127.0.0.1;与 Prometheus 的衔接在 Prometheus 侧完成(pg addon install prometheus --alertmanager-url);独立端口池(Linux;macOS 代码完备、未实测)
victoriametrics VictoriaMetrics——高性能、兼容 Prometheus 的时序数据库,单容器直接跑官方 docker.io/victoriametrics/victoria-metrics 镜像;无需配置文件(一切经 CLI flag:-storageDataPath / -httpListenAddr / -retentionPeriod / --extra-arg),无热重载(变更需重建容器),镜像以 root 运行(无需预 chown);无 Web 登录——Listen 默认 0.0.0.0 仅靠网络边界保护(仅本机访问用 --listen 127.0.0.1);独立端口池(Linux;macOS 代码完备、未实测)
nodeexporter node_exporter——Prometheus 官方宿主机指标导出器,单容器直接跑官方 docker.io/prom/node-exporter 镜像、无状态;它是唯一一个必须写死三条只读宿主挂载(/proc→/host/proc、/sys→/host/sys、/→/rootfs)加 --path.* 重定向的插件,否则会静默导出容器自身的数字而非宿主的;无需配置文件(一切经 CLI flag:--web.listen-address / --path.* / --collector.filesystem.mount-points-exclude / --extra-arg),无热重载(变更需重建容器),无数据目录(remove 绝不删任何东西);无鉴权、无 TLS——Listen 默认 0.0.0.0,因为抓取它的 Prometheus 通常在另一台主机,远程暴露的实例仅靠网络边界保护;用 pg addon install prometheus --scrape node-exporter=<host>:9100 从 pgcli 的 Prometheus 抓它;独立端口池,默认 9100(Linux;macOS 代码完备、未实测)
pgexporter pg_exporter——Pigsty 的声明式 PostgreSQL 数据库指标导出器,单容器直接跑官方 docker.io/pgsty/pg_exporter 镜像、无状态;它是 node_exporter(描述宿主)的搭档,导出约 460 个 pg_* 指标(活动、锁、复制、膨胀、WAL、按库统计),其标志性能力是统一采集:目标要么是 pgcli 纳管的实例(--pg-name,admin DSN 每次启动现推),要么是任意 pgcli 并不托管、可达的 PostgreSQL——远程目标只用一个互斥 URL 给出:--admin-dsn(一个 admin URL,只用于建监控角色后即丢弃——只存其不含凭据的地址,导出器以监控角色连)或 --dsn(原样拨号:pgcli 绝不跑 SQL,导出器就用这个 URL 自带的凭据——适合角色已存在、或不想让 pgcli 碰库)——意义在于用一种导出器形状同时采集 pgcli 内外的数据库;默认 pgcli 在目标上创建一个低权限 pg_monitor 监控角色,故导出器从不用 admin 连库——本地 admin 凭据自动派生,远程目标用 --admin-dsn,它只用于那一批 CREATE ROLE/GRANT、绝不落盘,生成的监控密码经 pg addon password pgexporter 取回;无需配置文件(默认采集器集内建在二进制里、按版本/角色/扩展在运行时规划;可选的 --config-file 采集器 YAML 只读挂载并经 pg addon pgexporter reload SIGHUP 热重载);/up /primary /replica 健康探针可兼作 HAProxy 式路由检查;无鉴权、无 TLS,且管理端点 /reload /explain /stat 在同端口——Listen 默认 0.0.0.0 仅靠网络边界保护;用 pg addon install prometheus --scrape pg=<host>:9630 从 pgcli 的 Prometheus 抓它;独立端口池,默认 9630(Linux 宿主网络已实测;macOS bridge 够不到本地实例的回环 DSN——那里用远程 URL(--dsn 或 --admin-dsn))
victorialogs VictoriaLogs——VictoriaMetrics 家族的日志数据库,单容器直接跑官方 docker.io/victoriametrics/victoria-logs 镜像;单 HTTP 端口同时提供摄入(JSON Lines / Elasticsearch bulk / syslog / OTLP)、LogsQL 查询与内置 UI;无需配置文件(一切经 CLI flag:-storageDataPath / -httpListenAddr / -retentionPeriod / --extra-arg),--retention 接受时长(30d、3months 等),默认省略以保持二进制自身默认;无 Web 登录——Listen 默认 0.0.0.0 仅靠网络边界保护(仅本机访问用 --listen 127.0.0.1);独立端口池,默认 9428(Linux;macOS 代码完备、未实测)
influxdb InfluxDB——经典 1.x 时序数据库(TSM 存储、InfluxQL),单容器直接跑 pgcli 自建的 ghcr.io/mars-base/pgcli/pgcli-influxdb 镜像(官方 1.13.1 linux/amd64 发行版二进制 + Alpine);认证始终开启(安装时生成 admin 密码,pg addon password influxdb 取回),最小化渲染的 influxdb.conf 只覆盖路径/监听/认证,其余全部沿用上游默认值,任意持久化覆盖经 --config-option 或 pg addon influxdb config set/unset;一个容器加每个实例唯一的仅回环备份/恢复 RPC 端口(上游 RPC 无认证);在线 portable backup/restore(--newdb 可并存恢复)、离线 export(influx_inspect,会先停容器,除非 --no-start 否则结束后重启)、import 自动改写 # CONTEXT-DATABASE:;顶层 pg influx 透传用一次性容器运行 influx CLI,可操作本地 addon 或任意远程 InfluxDB 1.x(--host/--dsn、TLS flags、INFLUX_USERNAME/INFLUX_PASSWORD,-import -path FILE 会挂载进容器);没有 HTTPS 监听(用 nginx 前置)、无 HA、v1 处于维护模式;独立端口池(仅 Linux/amd64)
influx2tsdb influx2tsdb——在 PostgreSQL + TimescaleDB 之上提供 InfluxDB 1.x HTTP API(Line Protocol + InfluxQL)的协议适配器,单容器、无状态,镜像为 pgcli 自建的 ghcr.io/mars-base/pgcli/pgcli-influx2tsdb(上游 1.3.2 + Debian);每个 InfluxDB 数据库对应一个 PostgreSQL schema,每个 measurement 对应一个 hypertable;用 --pg-name 指向本地实例(每次启动重新推导 DSN),或用 --dsn 接任意远程 PostgreSQL/TimescaleDB;无认证、无 TLS——需用 nginx 前置或隔离端口;保留与压缩以 TimescaleDB 后台任务形式下发(add_retention_policy / add_compression_policy,每 5 分钟重新同步)——不需要 pg_cron,且代理停机期间清理照常进行;数据全在 PostgreSQL,remove 从不删数据,备份/PITR/复制/HA 都走 PostgreSQL;InfluxQL 子集、纳秒时间戳、性能低于原生 InfluxDB;独立端口池(仅 Linux/amd64)
vlagent vlagent——VictoriaLogs 官方日志 agent,单容器直接跑官方 docker.io/victoriametrics/vlagent 镜像;跟踪宿主日志文件(每个 --log-path 以只读方式挂载到容器内相同绝对路径),用 vlagent 原生 glob(--include/--exclude,不是正则)选择文件,并通过一个或多个以 /insert/native 结尾的 --remote-write-url 转发给 VictoriaLogs;不给路径时仅做 HTTP 转发;无配置文件、无鉴权;独立端口池,默认 9429(Linux;macOS 代码完备、未实测)
vmcluster VMCluster——VictoriaMetrics 集群版,三个组件(vminsert + vmselect + vmstorage)各跑一个容器,镜像为官方 docker.io/victoriametrics/*:*-cluster;--roles 选择本机运行哪些组件(默认全部 = 单机收敛),--storage-node 列出所有 vmstorage 地址以支持跨主机分片/副本;只有 vmstorage 有持久数据;无配置文件、无鉴权;每实例 5 个端口,基址 vmcluster_start_port(默认 8480-8484)(仅 Linux)
vlcluster VLCluster——VictoriaLogs 集群版,三个组件(vlinsert + vlselect + vlstorage)共用同一个官方 docker.io/victoriametrics/victoria-logs 镜像,仅靠 flag 区分角色;--roles 选择本机运行哪些组件(默认全部 = 单机收敛),--storage-node host:storagePort 列出所有 vlstorage 地址以支持跨主机分片(无副本——任一 vlstorage 下线时查询返回 502);只有 vlstorage 有持久数据;无配置文件、无鉴权;每实例 3 个端口,基址 vlcluster_start_port(默认 9430-9432)(仅 Linux)
gitea Gitea——自托管 Git 服务,单容器直接跑官方 docker.io/gitea/gitea 镜像(rootful 构建,以 uid 1000 运行);免向导安装(INSTALL_LOCK)+ 自动创建的 admin,其 Web 登录自动生成、打印一次、可用 pg addon password gitea 取回;默认内嵌 SQLite,或用 --dsn 接外置 PostgreSQL(Gitea 自建 schema,pgcli 从不建库/删库);可选经 --ssh-port 启用 ssh:// 克隆(独立第二端口池;默认关闭——只走 HTTP clone。镜像的 OpenSSH sshd 在每个实例上都无条件在跑,--ssh-port 的作用是把 parked sshd 提升为活的克隆端点);加密 secret(SECRET_KEY/INTERNAL_TOKEN)刻意永不注入——它们归持久 volume 所有,以免重装静默重设密钥锁死幸存数据库;Listen 默认 0.0.0.0(注册默认关闭,--allow-registration 打开;建号走 pg addon gitea user create),配置只经环境变量(GITEA__*)交付;独立端口池(Linux;macOS 代码完备、未实测)
beegfs BeeGFS——并行文件系统,pgcli 的首个多容器插件:一条配置 = 一个单机收敛文件系统 = 三个容器(mgmtd + meta + storage,官方 ghcr.io/thinkparq/beegfs-* 镜像),按依赖顺序启动、以集群为单位管理(集群粒度的安装复用、启停、autostart、pg logs addon beegfs --service);仅 host 网络——只有 Linux、没有端口池:协议口即原生固定口(8008/8010 可用两枚 --beegfs-mgmtd-* flag 移动,meta 8005 + storage 8003 在 8.4 无任何覆盖手段),这正是 v1 每宿主仅一个文件系统(有守卫)的原因;连接认证密钥首装自动生成(pg addon password beegfs),mgmtd TLS 关闭(--tls-disable,客户端必须同样关闭);客户端是内核模块——pgcli 从不安装、从不挂载,pg addon beegfs mount-hint 输出逐字的客户端配置;license 永不注入;pg addon beegfs check 靠守护进程日志行判健康;v1 单机,v2 = 跨主机集群
juicefs JuiceFS——S3 后端的 POSIX 文件系统,pgcli 的首个宿主二进制插件(无容器):二进制从 GitHub releases 下载、sha256 校验(PATH 上已有则复用);两端全部显式、无同主机自动识别——对象存储是外部 S3 服务(--s3-endpoint http(s)://host[:port] + --access-key/--secret-key,format 前先探活),元数据引擎是一个显式 --dsn URL(postgres:///redis:///sqlite3://,pgcli 从不创建或删除该库);建桶仍是手动 pg mc 的活——pgcli 把逐字命令打印给你;挂载为普通宿主目录(不进 PG 容器);`pg addon juicefs mount
zerofs ZeroFS——加密、自维护的 S3 文件系统(自有 LSM 跑在桶上,无外部元数据引擎)的宿主二进制形态:pgcli 写 config.toml 并起两个宿主进程——仅 loopback 的 9P server(9P 无认证,非 loopback --listen 直接拒绝;每实例一个端口)+ FUSE client;对象存储是外部 S3 服务、全显式给出(--s3-endpoint + --access-key/--secret-key,可选 --s3-region——无同主机 addon 自动解析);生成的加密口令包裹桶的数据密钥(pg addon password zerofs;remove 保留桶后要用原口令 --encryption-password 重接,否则 server 报 Invalid password or corrupted key);fencing 需 conditional put——--conditional-put auto(MinIO 2022-08+/真 S3)或给缺此能力的存储走 redis(必须显式 --redis-dsn),none 仅限单写者;`pg addon zerofs start
duckdb DuckDB——嵌入式 OLAP 分析数据库(“SQLite for analytics”)的宿主二进制形态(无容器、无守护进程):C++ 列式引擎,向量化执行,直接查询 Parquet/CSV/JSON/Arrow;从 GitHub releases 下载、sha256 校验(PATH 上已有 duckdb 则直接复用);常用扩展(httpfs、parquet、json)自动安装;--version 指定版本(默认 1.5.6 LTS,也支持 2.0.0);`pg addon duckdb status
clickhouse ClickHouse——单机列式 OLAP 数据库,直接跑官方 docker.io/clickhouse/clickhouse-server 镜像(默认固定 26.8 LTS 26.8.36.3,amd64/arm64;host 网络,数据/日志/配置/备份目录均绑定挂载);认证始终开启,安装时生成 admin 账号(pg addon password clickhouse 取回密码);默认 HTTP 8123 + 原生 TCP 9000,取自两个独立端口池(TCP 与 S3 API 池共享);Listen 默认 127.0.0.1;无 TLS 监听(用 nginx 前置),单节点、无复制/HA;`pg addon clickhouse status
kafka Kafka——KRaft broker(无需 ZooKeeper),官方 docker.io/apache/kafka 镜像(固定 4.4.0,amd64/arm64;host 网络、渲染 server.properties、数据目录绑定挂载);单机 broker+controller combined 模式,或跨主机静态 controller quorum 中的一个节点(每台主机用 --cluster/--node-id/--peer 各装一次;推导 cluster id,成员不可在线变更);默认 broker 9092、controller 19093(各自端口池,controller 避开 Alertmanager 的 9093);Listen 默认 127.0.0.1(集群要求 0.0.0.0);默认 PLAINTEXT,可选 TLS 监听(--tls,SSL 端口取自 9093 池;仅加密,不认证客户端;集群客户端需拼接 CA bundle);RF=min(3, voters) / minISR=min(2, RF);pg addon kafka status|shell|topic create/list/describe/delete/retention|group list/describe|cluster status;--config-option 覆盖项持久化在 pg.yaml;仅 Linux
rabbitmq RabbitMQ——消息 broker,官方 docker.io/library/rabbitmq management 镜像(固定 4.4.0,amd64/arm64;host 网络、渲染 rabbitmq.conf、数据目录绑定挂载、enabled_plugins 固定为 management,集群模式另加 etcd 发现插件);单节点,或通过 etcd 自动发现组建多机集群的一个成员(--cluster、--node-name、--etcd-endpoints 或 --etcd,共享密钥用 pg addon rabbitmq secret generate + --secrets-from-etcd,或 --erlang-cookie-file/--admin-password-file 手动分发);默认 AMQP 5672 与 management Web UI/API 15672,另有 EPMD 4369 与 Erlang distribution 25672(单机为端口池分配且仅 loopback,集群成员固定且需网络可达,一台主机一个集群实例);Listen 单机默认 127.0.0.1,集群默认 0.0.0.0;认证始终开启(自动生成管理员密码,pg addon password rabbitmq 取回,每次启动对账);可选增量 TLS(--tls:AMQPS 5671 + management HTTPS 15671,自签 CA 或自带 --tls-cert/--tls-key;仅加密,不认证客户端,distribution 流量仍不加密);pg addon rabbitmq status|shell|queue ...——declare/info/publish/get/bind/unbind/bindings 走 management API,list/purge/delete 走 rabbitmqctl;集群 declare 默认 quorum 队列(--type classic 不复制);pg addon rabbitmq cluster status|forget NODE;不启用 Prometheus 插件;仅 Linux
rocketmq RocketMQ——Apache RocketMQ 5.x 消息中间件,官方 docker.io/apache/rocketmq 镜像(host 网络,渲染 namesrv.conf/broker.conf/rmq-proxy.json,broker/controller store 与日志绑定挂载);每个实例三个容器(NameServer + broker + proxy,按 namesrv → broker → proxy 启动);单机为一个无副本 async master,或跨主机集群的一个成员——每个 NameServer 内嵌 DLedger controller,broker 以 controller 模式运行并自动主备切换(--cluster/--controller-id/--advertise-host/--peer ID@HOST,每台主机传完整对端列表,一台主机一个成员,固定端口 9876/9877/10911/10912/8080/8081);NameServer 9876、controller 9877、broker 10911(HA 10912、VIP 10909)、proxy remoting 8080 与 gRPC 8081;无认证、无 TLS、无 Web UI;pg addon rocketmq status|shell|topic create/list/describe/delete|send|cluster status;--config-option 的 broker.conf 覆盖项持久化在 pg.yaml;controller/broker store 有状态,3 个以上成员才有 quorum 容错,安装后身份不可变;仅 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 · Garage · SeaweedFS · Redis · Predixy · pgAdmin 4 · nginx · Grafana · Prometheus · Alertmanager · VictoriaMetrics · node_exporter · pg_exporter · VictoriaLogs · vlagent · InfluxDB · influx2tsdb · ClickHouse · Kafka · RabbitMQ · RocketMQ · VMCluster · VLCluster · Gitea · BeeGFS · JuiceFS · ZeroFS · DuckDB。Patroni 相关页面见 HA 集群。

16.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 模式

16.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。

16.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 管理的实例,一个代理可横跨多个数据库 / 分片。

16.4 - Garage

以 pgcli 插件方式运行 Garage(Deuxfleurs 的 Rust S3 兼容对象存储)——单节点或多节点跨主机集群,明文 HTTP,TLS 由 nginx 插件前置来终结

Garage 是 Deuxfleurs 出品的 Rust、AGPLv3 S3 兼容对象存储重实现,为小规模自建和异地分布式集群设 计。pgcli 将其作为独立的顶层插件运行,CLI 表面与 minio、silo、rustfs 一致 (install、list、logs、autostart、password),但其运行时模型在四个方面与三者 都不同,这也决定了本页的写法:

  1. 渲染 TOML 配置文件,而不是 argv/env。 Garage 从 /etc/garage.toml 读 配置。pgcli 在实例目录下渲染该文件并以只读方式 bind 挂载;minio/silo/rustfs 插件则传 argv 或 env。
  2. 两类目录——且自 Garage v0.9.0 起多盘是原生支持的。 metadata_dir(LMDB)与 data_dir(数据块)是各自独立的 bind 挂载,且 metadata_dir 永远只有一个。data_dir 既可以是一个路径,也可以(上游原生 地)是 { path = …, capacity = … } 的列表——一块盘一项,节点因此自己就能把 数据块铺在多块盘上,而不必在底下垫主机 LVM/RAID 拼一个大文件系统。pgcli 用 可重复的 --drive 暴露这个列表;如今 minio、silo、rustfs、seaweedfs、 garage 全都接受每节点一串盘——见多盘数据目录与容量与尺寸 计算。
  3. 四个端口,来自自己的池。 S3 API、节点间 RPC、s3_web 静态站点、 admin/metrics API——每实例一起从 garage_start_port(默认 3900)分配, 与共享的 minio/silo/rustfs 池是两条独立游标。
  4. 无原生 TLS——仅明文 HTTP。 pgBackRest 的 S3 仓库强制 HTTPS,因此一个要 接收 archive-push 的 Garage 存储必须由 nginx 插件前置——见用 nginx 前置实现 TLS。

和 rustfs 一样,在开源 MinIO 仓库已归档之后,Garage 是一条很好的替代路线。pgcli 默认按单节点运行(replication_factor = 1、garage server --single-node), 再加上几个集群 flag 即可跑成多节点跨主机集群——见多节点集 群。

平台支持: 已在 Linux(host 网络)上验证。macOS 的 bridge 路径会发布全 部四个端口、代码完整,遵循与其它对象存储一致的形态,但尚未在真机上跑过。

工作原理

一个实例一个容器。pgcli 直接拉取上游 docker.io/dxflrs/garage 镜像 (仅 pull)——不像 rustfs 需要自建 wrapper 镜像,因为 Garage 在自己的容器里以 root 运行、且不带 uid-drop,所以不存在主机侧属主处理要消化。

数据存放在长于容器生命周期的 bind 挂载主机目录里:

  • <base-dir>/addon/garage/<name>/data → /var/lib/garage/data(数据块),
  • <base-dir>/addon/garage/<name>/meta → /var/lib/garage/meta(LMDB),

外加渲染出的 <base-dir>/addon/garage/<name>/garage.toml → /etc/garage.toml,以只读方式挂载(Garage 从不改写自身配置)。用 --drive(多盘)时,那个单一数据目录被"每盘一个主机目录"取代,各自挂在编号的 容器槽位 /var/lib/garage/data1..N 上——meta 目录与渲染出的配置仍然各只有一个; 见多盘数据目录。

凭据的处理与其它存储类似,但 Garage 用的是 access key / secret key 对,而不 是 root user / password:

  • root_user 是 access key id,首次安装时生成,带 Garage 自家的 GK 前缀形态(GK + 32 位十六进制,即上游 quick-start 里 openssl rand -hex 16 产出的形状),或用 --root-user 指定。
  • root_password 是 secret key,首次安装时生成(64 位十六进制),或用 --root-password 指定。
  • 第三个秘密 rpc_secret(集群 RPC 签名密钥)以同样方式生成并一并存储。单节点 下它从不出现在 CLI 表面;集群 bootstrap 会把它连同加入命令一起打印一次, 由操作员搬运到下一台主机(pgcli 从不在远端执行任何东西)。

三者都落进 pg.yaml(addons.garage.<name>.*),因此重建容器是稳定的。单节 点首次启动时容器被传入 --single-node --default-bucket 以及 GARAGE_DEFAULT_ACCESS_KEY / GARAGE_DEFAULT_SECRET_KEY / GARAGE_DEFAULT_BUCKET env,于是 Garage 自己创建这个 access key 和一个桶(以实 例名命名、归属该 key)——无需安装后再 exec。key 与桶都存在 metadata_dir 里, 因此能挺过容器重建。集群成员不带这些 flag 启动,key 与桶改由安装尾部显式播种 (见下文)。

安装

# 默认实例名 "garage",四个端口取自 garage 池,回环绑定
pg addon install garage

# 指定名字与显式数据目录的实例
pg addon install garage --name store --data-dir /srv/garage

# 把本节点的数据块铺在多块盘上(见下文"多盘数据目录")
pg addon install garage --name store --drive /mnt/d1/garage --drive /mnt/d2/garage

# 固定 S3 API 端口(rpc/web/admin 取随后三个空闲端口)
pg addon install garage --name store --api-port 3900

# 把存储暴露到网络而非仅回环
pg addon install garage --name store --listen 0.0.0.0

# 自带 access key / secret key,而不是自动生成
#   受 Garage 自家限制:access key ≥ 8 字符,secret key ≥ 16 字符
pg addon install garage --name store --root-user GK<...> --root-password <secret>

# 加入一个跨主机集群(见下文"多节点集群")
pg addon install garage --name store --cluster c1 --advertise-host 10.0.0.11 \
  --zone z1 --capacity 15 --replication-factor 3 \
  [--peer <id@addr> --rpc-secret <64hex>]   # joiner 两个都要带

自带凭据: 这两个 flag 只在单节点安装时生效——集群成员从不读它们, 其 key 来自 quorum 达成那次安装的 garage key create。取值原样交给 Garage,由 Garage 执行限制:access key id 短于 8 字符、或 secret key 短于 16 字符,会在容器首启时被拒(Invalid default access key: … should be at least N characters long)——又因容器是后台起的,install 本身报成功、节点却 在 --restart unless-stopped 下后台崩溃重启。若一次自带 key 的安装起来后无 法服务,查 podman logs(或干脆让 pg mc alias set 报错)。生成出的形状 (GK + 32 hex / 64 hex)远远越过这两个下界;mc 另外要求 access key 为 [A-Za-z0-9],所以保持字母数字。(pgcli 现在也会在安装前就按同样下界拦一道, 见下。)

输出会报告端点和播种的凭据:

✓ garage installed: "store"
  Container:     pgcli-garage-default-store
  Image:         docker.io/dxflrs/garage:v2.4.1
  Data:          ~/pg/addon/garage/store/data
  Metadata:      ~/pg/addon/garage/store/meta
  Config:        ~/pg/addon/garage/store/garage.toml
  S3 API:        http://127.0.0.1:3900
  RPC/Web/Admin: 3901/3902/3903

  Access key:    GK<generated>
  Secret key:    <generated>
  Bucket:        store (created on first boot, owned by the access key)

  NOTE: Garage serves plaintext HTTP only. pgBackRest requires HTTPS — front it
        with the nginx addon via --conf-file (template --upstream mode strips
        the port from Host and breaks S3 signing; see docs/addon/garage):
          backend 127.0.0.1:3900, listen ssl, proxy_set_header Host $http_host
  Try it:        pg mc alias set store http://127.0.0.1:3900 GK<...> <secret> && pg mc ls store

对存活实例重跑 install 是 no-op:容器不会被重建(处于停止的会被启动,并给出 提示),flag 会合并进已存配置,已有凭据保持不变。传 --force 会重建容器,从而让 改动过的端口、监听地址或凭据生效——但不丢数据目录。

监听地址: 默认 127.0.0.1 让存储留在本地。--listen 0.0.0.0(或 pg.yaml 里的 listen 键)把它暴露到网络。由于 Garage 服务的是明文 HTTP,任何能触达端口的人都能在传输中嗅到凭据——所以只在防火墙后公开绑定, 实际做法则是放在nginx TLS 前置之后。

--tls 对 garage 会被拒绝。 Garage 没有可启用的 TLS 监听器;传 --tls/--tls-cert/--tls-key 会直接报错并把你指向 nginx 前置,而不是默默 忽略这个 flag。

用 nginx 前置实现 TLS

由于 pgcli 不把两个插件耦合在一起,在一个 Garage 存储前面终结 TLS 是显式的两步: 先装 garage,再用 nginx 前置它。

用 --conf-file,不要用 --upstream 模板。 S3 请求用 SigV4 对含端口 的 Host 头签名。模板模式转发 Host $host,而 nginx 渲染它时不带端口 ——于是 Garage 重新算出的 canonical request 不同,每个写操作都以 Invalid signature 失败(已实测:经模板前置 pg mc mb 被拒;经下面 --conf-file 前置 则完整往返)。自定义配置还能设 client_max_body_size 0;(nginx 默认的 1 MiB 上限会拒掉任何真实对象上传)以及很长的代理超时。

pg addon install garage --name store

# front.conf —— 经 TLS 的 path-style S3,保留 host+port 以供签名:
cat > front.conf <<'EOF'
worker_processes auto;
pid /tmp/nginx.pid;
events { worker_connections 1024; }
http {
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
    server {
        listen 0.0.0.0:8081 ssl;          # 0.0.0.0 让远端客户端也能访问;pgcli 分配给前置的 HTTPS 端口
        ssl_certificate     /etc/nginx/certs/public.crt;
        ssl_certificate_key /etc/nginx/certs/private.key;
        client_max_body_size 0;          # PUT 不限大小
        proxy_read_timeout   3600s;
        proxy_send_timeout   3600s;
        location / {
            proxy_pass http://127.0.0.1:3900;   # garage 的 S3 API 端口
            proxy_set_header Host $http_host;   # 保留端口 —— SigV4 需要
            proxy_request_buffering off;
            proxy_http_version 1.1;
        }
    }
}
EOF

pg addon install nginx --name gtls --tls --conf-file front.conf --https-port 8081

--tls 让 pgcli 生成自签 CA + leaf(public.crt / private.key,挂在 /etc/nginx/certs),并起一个 HTTPS 监听器。把 S3 客户端指向 https://<host>:8081,并把前置的 CA <base-dir>/addon/nginx/gtls/tls/ca.crt 交给消费方。

前置绑 0.0.0.0,Garage 留在回环。 上面那条 listen 绑所有网卡,让远端 主机(如要推归档的 Patroni 节点)能访问这个 TLS 端点——TLS 是在 nginx 处 终结的,所以这样做是安全的,而直接暴露 Garage 的明文端口则不是。proxy_pass 仍指向 127.0.0.1:<garage-api-port>:nginx 容器跑在 host 网络上,从它那里能达 到 Garage 的回环绑定,明文那一跳从不出机器。若前置只想本机可用,改回 listen 127.0.0.1:<port> ssl;。

为什么 listen 与 proxy_pass 两行要手写死。 --conf-file 模式下 pgcli 原样挂载你的配置,所以里面的端口必须与两个插件被分配到的端口对上——garage 的 S3 API: 端口取自它的安装输出,nginx 的 HTTPS: 端口取自 pg addon list。 ($http_host 和 client_max_body_size 两行,正是模板前置对 S3 会搞错的地 方。)

把那个 CA 弄到远程 Patroni 主机上而无需手动 scp,正是 pg backup fetch-ca 的用 武之地——它拨通 TLS 前置,直接从握手里取出根证书:

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

这个拉取是 trust-on-first-use——在信任之前,把打印出的 SHA-256 与前置主机上的 sha256sum ~/.pgcli/addon/nginx/gtls/tls/ca.crt 对一遍。

前置一个集群:一台,还是本地几台

集群并不需要把所有成员都挂到前置后面。S3 的复制是数据层的事:打到任意一 个成员的请求都是完整的,该成员自己会按 replication_factor 复制。所以上面那个 单后端前置对集群同样成立——把 proxy_pass 指到一个成员的 S3 端口即可。

只有当你想把"被前置的那台成员"从接入层单点里拿掉时,才多挂几台。upstream 里只保留前置所在主机可达的成员(通常是本机房——见异地分布拓 扑),每个机房各自跑自己的前置:

http {
    upstream garage_s3 {
        server 10.0.0.11:3904;   # 成员 A,本机房
        server 10.0.0.12:3900;   # 成员 B,本机房
        # 不放远端成员:它是数据副本,不进请求路径
    }
    server {
        listen 0.0.0.0:8081 ssl;
        ssl_certificate     /etc/nginx/certs/public.crt;
        ssl_certificate_key /etc/nginx/certs/private.key;
        client_max_body_size 0;
        proxy_read_timeout   3600s;
        proxy_send_timeout   3600s;
        location / {
            proxy_pass http://garage_s3;
            proxy_set_header Host $http_host;   # 保留端口 —— SigV4 需要
            proxy_request_buffering off;
            proxy_http_version 1.1;
        }
    }
}

相比单后端示例,这带来两个新约束:

  • 成员的 S3 API 必须离开回环。 集群模式只会自动放宽 RPC bind;--listen 默认仍是 127.0.0.1,远端前置够不到别的主机的成员。被前置的成员要用 --listen 0.0.0.0 安装(放在防火墙后——讲 TLS 的是前置),或者接受"upstream 里只有同主机成员可用"。
  • 各主机端口号不同——和 RPC 端口是同一个坑:每个成员的 S3 API 端口取自它自己 的安装输出 / pg addon list(S3 URL:),未必是 3900。

这仍然是 --conf-file 配置:--upstream 模板对 S3 的毛病不变——Host $host 不带端口会让每个 SigV4 写失败,而且它没有 client_max_body_size 0(见上面的注 意)。多 upstream 服务器时默认的轮询就够用;max_fails/fail_timeout 按需调。 (已在 3 节点集群实测:pg mc 经这样一套前置完整往返;停掉 upstream 中的一台 成员后,前置经另一台照常服务。)

把 pgBackRest 接到前置后的 Garage 存储

一旦有了 TLS 端点,一个 Garage 支撑的 S3 仓库与 MinIO 的完全一样——S3 契约相 同(path-style、repo1-s3-uri-style=path):

pg backup setup \
  --s3-endpoint <front-host>:<https-port> \
  --s3-bucket store \
  --s3-access-key GK<...> \
  --s3-secret-key <secret> \
  --s3-ca-file <front>/addon/nginx/gtls/tls/ca.crt

完整流程见备份 → S3 对象存储仓 库。pg addon password garage --name store 会 为脚本打印 secret key。

多盘数据目录

Garage 节点不必在自己底下垫一个大文件系统。从 v0.9.0 起 Garage 原生接受一组 数据目录,并自己在其中均衡数据块;pgcli 用一个可重复的 --drive 暴露它:

pg addon install garage --name store \
  --drive /mnt/d1/garage --drive /mnt/d2/garage --drive /mnt/d3/garage

产出的结果(在真盘上验证过:一个 3 节点集群,每台各带两块盘):

  • 每块盘 bind 挂载到自己独立的容器槽位——按 --drive 顺序为 /var/lib/garage/data1、data2……——渲染出的 garage.toml 用 Garage 的数组 形态列出它们:

    data_dir = [
      { path = "/var/lib/garage/data1", capacity = "4G" },
      { path = "/var/lib/garage/data2", capacity = "4G" },
    ]
  • 每项的 capacity 是必填的——v2.4 会拒绝一个既不带 capacity 也不带 read_only = true 的项(invalid data_dir config: data directories in data_dir should have a capacity value or be marked read_only)。pgcli 不为它 设旋钮,而是填入每块盘自己文件系统的可用容量(GiB,向下取整、下限 1)—— 这正是"每块独立挂载的盘给一个目录"这种布局想要的,也正是让 Garage 把铺开权重 倾向更大那块盘的机制。--capacity 仍是节点级的那个数,不受此影响。

  • metadata_dir 从不加入这个集合——它仍是与实例数据路径同级的那一个 meta 目 录(LMDB 按上游设计就是一个目录)。

  • --drive 与 --data-dir 互斥:盘列表整体取代解析出的数据目录。对一个多 盘实例不带 --drive 重跑 install,已存的盘会保留(和其它键一样是合并语义)。

  • 落在主机根设备上的盘只警告、照样使用(与 seaweedfs 一致;MinIO/silo 直接 拒绝,rustfs 硬性要求独立设备)——多盘只有在独立挂载的盘上才买得到东西。

  • pg addon list 会显示成 Drives: N (multi-disk data_dir),下面每盘一行 容器槽位 <- 主机目录;安装摘要打印同样的映射,形如 Drive 1: … -> /var/lib/garage/data1。

  • remove --clean-data 会逐个处理每一块盘目录(外加 meta 目录与渲染出的配 置),每个仍然拒绝删除处于挂载状态的点。

  • 数据块确实落在两块盘上:往一个两盘节点上传一个 48 MiB 对象后,两个主机目录里 都有数据;一个 64 MiB 对象经 3 节点 rf = 3 集群从三台主机分别读取 往返,sha256sum 一致。stop/start 保留对象;重建的容器读到的是同样的盘。

这套做法给不了你的是:单个孤立节点上的盘级冗余——见容量与尺寸计算。 日后新增一块盘时 Garage 自己的均衡规则(已有数据块不会被重排;只读项会被机会 式地、或经显式 repair 主动地排空)属于上游 Multi-HDD support 的范畴:pgcli 负责写配置,搬运由 Garage 完成。

多节点集群

Garage 的真正价值在于跨主机集群:不同 zone 的节点、CRDT 复制的 layout、quorum 读写。pgcli 用同一条 pg addon install garage 驱动它——没有新入口——只是多 了几个集群 flag。集群是逐台主机组装的(Patroni 模式):pgcli 从不在远端主机 上执行任何东西。第一台bootstrap,打印一条现成的加入命令(内含共享的 rpc_secret 和本节点的 id@addr),你把这条命令(为下一台改好参数)自己在下一 台上执行。

角色

安装角色由你传的 flag 决定:

角色 何时 install 做什么
single 没有 --cluster v1 路径,逐字节不变(--single-node --default-bucket)
bootstrap 有 --cluster、无 --peer 生成 rpc_secret,起裸 garage server,staged 本机 layout 角色,打印加入命令
join --cluster + --peer <id@addr> 带 --rpc-secret,connect 该 peer,staged 本机角色,凑齐 quorum 的那次 apply 并提交

三主机走一遍

主机 1——bootstrap。 给它 --zone、--capacity 和最终的 --replication-factor。此时集群里只有一个节点,Garage 会拒绝 apply layout (它需要 replication_factor 个有正容量的节点),pgcli 把这变成警告而不是错误 ——staged 角色是集群复制的,会一直等到最后一台提交:

# 在 10.0.0.11 上
pg addon install garage --name store --cluster c1 \
  --advertise-host 10.0.0.11 --zone z1 --capacity 15 --replication-factor 3

集群成员和其它 Garage 节点一样,也可以带 --drive 用它自己的盘(见多盘数据目 录)——已在一个 3 主机集群上验证,每台各带两块盘。

摘要会打印 secret 和一份可复制的加入模板:

  Cluster:       c1 (role: bootstrapping cluster, replication factor 3)
  Advertise:     10.0.0.11:3905
  Zone/Capacity: z1 / 15 GiB
  Node ID:       1c0597cc…200aeb@10.0.0.11:3905
  [i] layout staged but not applied yet — Garage needs replication-factor assigned nodes to commit (later joins will).

  To add ANOTHER member, run on the next host (carrying --rpc-secret yourself;
  pgcli never executes remotely). ...
    pg addon install garage --name store --cluster c1 \
      --advertise-host <next-host-ip> --rpc-secret <64hex> \
      --peer 1c0597cc…200aeb@10.0.0.11:3905 --zone <zone> --capacity <GiB> --replication-factor 3

主机 2——join。 复制打印出的命令,把 --advertise-host 改成本机 IP、 --zone 换一个:

# 在 10.0.0.12 上
pg addon install garage --name store --cluster c1 \
  --advertise-host 10.0.0.12 --rpc-secret <64hex> \
  --peer 1c0597cc…200aeb@10.0.0.11:3905 --zone z2 --capacity 15 --replication-factor 3

现在有两个节点 assigned,仍少于 rf = 3,所以再次警告。peer 连接是可传递 的:主机 2 只需要一个 --peer(主机 1);主机 3 指向任意一台已存在的节点都行。

主机 3——凑齐 quorum 的那次安装。 第三台能看到完整的 staged 角色集(assign 是集群复制的),于是它的 layout apply 把版本 1 提交——并且就是这一次安装 完成 access key 和 bucket 的播种:

# 在 10.0.0.14 上
pg addon install garage --name store --cluster c1 \
  --advertise-host 10.0.0.14 --rpc-secret <64hex> \
  --peer 5b370435…1c1c9187@10.0.0.12:3901 --zone z3 --capacity 15 --replication-factor 3
  Access key:    GKc57063…6147b0
  Secret key:    <printed once, here>
  Bucket:        store (cluster key + bucket seeded on this install)

每台安装都幂等地跑播种步骤,所以最后加入的那台天然就是执行播种的——不需要 主机 1 回填。key 名就是实例名(store),桶同名,key 被授予该桶的 read/write/owner。

播种与 secret

集群里单节点的 --default-bucket / GARAGE_DEFAULT_* 快速通道不可用,所以 pgcli 用 key create + bucket create + bucket allow 显式播种。两个推论:

  • garage key create 的 secret 只打印一次,且不幂等(重跑会铸造第二个同名 key)。pgcli 因此先查 key list,缺席才创建。执行播种的那台主机把 access/secret 对写进它自己的 pg.yaml;其它主机只持有公开的 key id。
  • 非播种主机上的 pg addon password garage 无 secret 可打印,会明确告知—— secret 在完成 quorum 的那台主机的 pg.yaml 里。去那台取回,或自己 garage key create 铸一把新 key。

版本冲突与重跑

layout apply --version N 要求恰好等于 当前版本 + 1——Garage 的乐观锁。两台 主机并发安装时,慢的一台 apply 失败;pgcli 报"layout changed concurrently — re-run the same install command"。assign 幂等、重新 stage 无害,所以重跑只是重新 读版本再 apply。对已在线成员重跑安装其它步骤都是 no-op(容器在跑→ skipping creation;已 connect→视为成功)。

zone 与 replication-factor 建议

  • --replication-factor 必须 ≤ 你打算加入的节点数;在 rf 个节点拿到正容量 角色之前,集群无法播种。
  • 每台主机给一个互不相同的 --zone,以承受整个故障域丢失;Garage 会尽量把 副本放进不同 zone。
  • --capacity 单位 GiB,是 Garage 划给数据块的额度;capacity 为 0 的节点被视为 不承载数据。
  • --advertise-host 对每个成员都是必需的——它成为 rpc_public_addr,peer 拨号 的地址。缺了它,集群成员不可达。(--listen 为回环的集群成员仍会把 RPC bind 放宽到 0.0.0.0;只有 S3/web/admin 端口留在回环上。)

容量与尺寸计算

Garage 的冗余单位是节点,不是盘——一个复制对象的每一块都会被放到不同节点 上(尽量放到不同 zone),所以只要还有 replication_factor 份副本活着,单块盘 或单台节点的故障都不会丢数据。一个节点如何处置自己的盘是另一个维度的事,而 Garage 从 v0.9.0 起原生处理它:一个节点可以给定好几个数据目录,按各自声明的容 量比例把数据块均衡在这些目录里(见多盘数据目录)。MinIO 与 rustfs 是靠节点内纠删码把盘聚成一个池、以此扛单盘损坏;Garage 的多盘模式是 聚合但没有冗余——丢一块盘就丢了该节点持有的那部分数据块,对象转而从其它 节点上的副本修复回来。若你要让一个孤零零的单节点也扛得住丢盘、又没有别的节点可 用来修复,那就该在 data_dir 底下垫 LVM/RAID。MinIO/rustfs 把盘管成一个池; Garage 管的是节点,盘只用来铺。

--capacity 就是交给这个模型的那个数:本节点可存放的 GiB 数 (layout assign -c <N>GiB)。有两个性质必须内化:

  • 它是放置权重,不是配额。容量用满时 Garage 也从不拒写——真正的剩余空间是 garage status 里 DataAvail 报的值。在一块 20 GiB 的文件系统上设 --capacity 100,只会让 Garage 把盘填满(然后写坏)。把它保持在后端文件系统 可用大小以内(或更低)——多盘模式下,是各盘可用大小的总和。
  • 改它是一次 layout 变更:用新的 --capacity 重跑同一条 pg addon install(assign 幂等,apply 会把 layout 版本加一),Garage 就把 数据块重新均衡到扩出来的空间上。先把后端文件系统扩开,才是真正买到容量。

注意一个多盘节点上有两个容量数,它们不是同一个旋钮:--capacity 是本节点在 集群 layout 里的角色(这个节点被信任存放多少,跨它所有盘共享),而每个 data_dir 项的 capacity 告诉 Garage 那块盘有多大、以便按它加权地铺。pgcli 自动填每盘的数字(见下文);只有节点级的 --capacity 归你选。

可用容量是所有节点容量之和除以副本因子——每个对象都保留 rf 份:

节点数 单节点(--capacity) rf 原始总量 可用
3 15 GiB 3 45 GiB 15 GiB
4 15 GiB 3 60 GiB 20 GiB
5 15 GiB 3 75 GiB 25 GiB

一般规则:可用 = Σ容量 ÷ rf。同一套数学也解释了为什么 rf = 2 把存储开销降一 半、代价是只能容忍一台节点丢失;rf = 3 是 3 倍开销,但一次写入只要有 3 份中 的 2 份 ack 就提交,所以 3 节点的 rf = 3 集群写入时可容忍 1 台宕,5 节点的可 容忍 2 台(剩余成员里任意 2 台能 ack)。因此加第 4、第 5 台同时买到容错和可用 容量,这跟单纯镜像不一样。

停一台后的 quorum 行为

停掉任意一台,其余两台照常读写(rf = 3 时的 2/3 quorum):

# 主机 3 上
pg addon stop garage --name store
# 主机 1 上——照常读写
pg mc cat c1/store/v.txt

pg addon start 会以同一个 Node ID 重新入集群(密钥对在 metadata_dir 里,不属于容器),garage status 重新显示它健康。 start/stop/remove/autostart/logs 永远只管本机容器——Patroni 语义;没有远程编 排。

异地分布拓扑:2 本地 + 1 远端

Garage 生来为此的生产形态:本机房放两个成员(各占一个机架/zone,z1 + z2),第三个成员放在远端机房(z3)。这个形状和写延迟的数学很合拍: rf = 3 的写只需 2/3 quorum 提交——本地两副本 ack 即算完成,远端副本异步 跟进,WAN 链路慢、甚至临时断开都不拖慢写入。nginx TLS 前置只指向两个本地成员 (见前置一个集群);远端成员只承载数据、不进 请求路径,通常自己也起一个前置,供本地读修复/灾难时使用。

对远端成员"不是什么",有两个诚实的注脚:

  • 它是副本,不是备份。 CRDT 复制同步一切,包括删除——误操作的 mc rb --force 或 pgBackRest expire 都会传播到远端机房。要拿"免疫误删"的 副本得用 Garage 自己的跨集群 bucket replication(Garage CLI 功能,本文档不代 管:garage bucket replication);2+1 集群给你的是节点级与机房级的存活能力加 一份人工抢救副本,仅此而已。
  • 整机房故障不会自动切换。 本地两台一起挂,只剩远台一台低于 quorum(1 < 2),集群不可写。数据在那台节点上是完好的,Garage 也有文档化的人工恢复流程 (从存活节点重新 commit 一个缩减 layout 拉起集群),但 pgcli 不编排这一步—— 那是"操作员确认机房真的没了"之后手敲 garage layout 的事。

所以准确的定位是:2 本地 + 1 远端 = 带机房级抢救副本的 HA,写入保持本地速 度;“备份”(免疫误删义)仍然是 pgBackRest(或跨集群 replication)的职责。

拿到集群的 AK/SK

播种的 access key 是集群级凭据(任何成员都能验证),但它的 secret 只存在于一 台主机——凑齐 quorum 的那次安装打印过一次,并写进了那台主机的 pg.yaml:

# 只能在种子主机上:
pg addon password garage --name store              # 打印 secret key
pg addon password garage --name store --file ./ak  # 0600 写文件,便于搬运
pg addon list --show-password                      # access key + secret 一起看
  • 任何主机都能看公开 id(pg addon list 的 Access key: 行);非种子主机上 pg addon password 会报错并点名持有 secret 的主机——信息准确,secret 就是不 在那台机器上。

  • Secret 丢了,或想要一套自己的凭据?在任意成员上铸一把新 key 并授权桶—— key 是 CRDT 状态,新凭据即刻全集群可用:

    pg garage --name store key create app-key
    pg garage --name store bucket allow store \
      --key GK<新id> --read --write --owner

    这是推荐的 day-2 姿势:应用和 pgBackRest 各用各的 key(独立轮换/吊销),播种 那把留作 break-glass 管理员凭据。pg garage 是 Garage 自家控制面 CLI 的通用 透传入口——完整运维菜单见用 pg garage 做管理。

v2.0 不做:把节点从 layout 移除(layout remove + 数据迁移)、revert 已提 交的 layout、多 zone 故障切换编排。这些仍是手敲 garage CLI 的事——用 pg garage 跑。

使用 mc 客户端

pg mc 从一个临时容器里跑 MinIO 自家的 mc 客户端——无需本地安装、无需手动 podman run——并像对其它存储一样讲 Garage 的 S3 API。用 access key 顶替 user、secret key 顶替 password:

# 按平台选 URL——见下面"按平台的端点":
pg mc alias set store http://127.0.0.1:3900 GK<...> <secret>       # Linux,本地插件
pg mc alias set store http://host.containers.internal:3900 GK<...> <secret>  # macOS,本地插件
pg mc alias set store http://10.0.5.7:3900 GK<...> <secret>        # 二者皆可,远程存储
pg mc mb store/backups
pg mc ls store                       # 播种的 "store" 桶已经在这里
pg mc cp ./dump.pglz store/backups/

alias 持久化在主机上的 ~/.mc/config.json——mc 自己的默认路径,Linux 和 macOS 上一致——所以 alias set 一次,之后每次 pg mc 调用(以及 Linux 上的原生 mc)都看到同样的 alias。pg mc alias list 读回,pg mc alias remove 删 除。alias set 在写入前先对端点校验凭据,所以写错的 key 不会污染 ~/.mc/config.json。底层上 pg mc 把你的 ~/.mc 挂进容器并跑 ghcr.io/mars-base/pgcli/pgcli-mc(scratch 上的静态上游二进制);此外没有任何 魔法。

任何会被 pg 自身 flag 解析器拒绝的 mc flag 都放在 -- 之后,与其它存储完全 一样——--recursive、--force、--all、--json 等等:

pg mc cp ./dir store/backups/ -- --recursive
pg mc rb store/scratch -- --force

按平台的端点: 在 Linux 上 pg mc 跑在 host 网络里,所以 127.0.0.1 的 alias 能直达本地插件实例。在 macOS 上容器坐在 bridge 网络里,那里的 127.0.0.1 是容器自己的回环——把本地 Mac 插件的 alias 指向 http://GK<...>:<secret>@host.containers.internal:3900,或把远程存储指向其可 路由地址。要走 TLS,就把 alias 设成 nginx 前置的 https:// 端点(自签 leaf 再加 -- --insecure)。

常用命令

命令 用途
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 连对象一起删桶

同一对 access/secret key 适用于任何 S3 SDK——包括对 repo1-type=s3 仓库的 pgBackRest(经 nginx TLS 前置)。

用 pg garage 做管理

pg garage 把 Garage 自家控制面 CLI 的任意子命令转发进实例的运行中容器——手写 podman exec pgcli-garage-<ns>-<name> /garage … 的替代品。--name 选实例 (默认按名字排第一的那个);子命令词之后的一切逐字到达 /garage,所以 Garage 自家的 flag 不需要 -- 分隔。单节点实例和集群成员上用法完全一致;集群里问任意 运行中的成员即可——status、layout、key 都是 CRDT 复制状态,全集群一致。

任意成员都能应答任意 pg garage 命令——读和写都一样。 每台主机都跑着 自己的 garage 节点,而 bucket、key、layout 是不属于任何单机的 CRDT 状态,所 以 bucket create、key create、bucket allow、layout show——以及对应 的 delete/deny——指向哪个运行中的成员结果都一样,改动在其余成员上立即可见。 (三节点集群实测:在主机 2 上建的桶,主机 1、3 的 bucket list 都能马上看 到;从主机 1 发起的删除,在主机 2 的列表里立刻消失。)唯一跟成员挂钩的是 取 secret:播种 key 的 secret 只存在于铸出它那台主机的 pg.yaml 里, pg addon password 只在那台好用、在别处会报错点名它(见拿到集群的 AK/SK)——这是 pgcli 的本地配置边界,不是 garage 的限 制,也不妨碍你从任意成员铸新 key。

健康与状态

pg garage --name store health            # 集群健康时退出码才是 0
pg garage --name store status            # 每节点:zone、容量、DataAvail、版本
pg garage --name store layout show       # 已提交角色 + 暂存变更 + layout 版本
pg garage --name store stats             # 逐分区的复制明细
curl -s http://127.0.0.1:3903/health     # admin 端口的存活检查行

health 是对监控最友好的一条:打印简短汇总(已知/已连/存储节点数、分区 quorum),并且会设置退出码,所以 cron 或巡检脚本可以直接用 pg garage --name store health > /dev/null。status 是宽表——盯住 DataAvail 列,那才是真实剩余空间(见容量与尺寸计算)。

key 与 bucket

pg garage --name store key list                    # 谁能访问这个存储
pg garage --name store key create app-key          # secret 只打印一次
pg garage --name store bucket allow store --key GK... --read --write
pg garage --name store bucket deny  store --key GK... --write
pg garage --name store bucket list
pg garage --name store bucket info store           # 大小、对象数、逐 key 授权
pg garage --name store key delete GK...            # 吊销一把应用 key

先铸 key 再 allow,就是 day-2 凭据姿势:应用各用各的 key,播种那把留作 break-glass(见拿到集群的 AK/SK)。

建桶走的是同一条透传——bucket create 起名字,随后必须 bucket allow 决定谁能用(新桶在授权之前任何 key 都够不着)。安装时播种只产出以实例命名的那 一个桶;后续的桶在这里加:

pg garage --name store bucket create backups        # 全局 alias = "backups"
pg garage --name store bucket allow backups --key GK... --read --write --owner
pg garage --name store bucket list                  # 能看到新的 "backups" alias
pg garage --name store bucket delete backups --yes  # 必须带 --yes;仅对空桶有效

桶的全局 alias 就是它的 S3 名字,全集群唯一;问任意运行中的成员即可—— 从哪个成员建/删都一样(见本节开头的"任意成员"说明)。

layout 变更(集群)

加节点走 pg addon install(assign/apply 自动完成);裸 CLI 用于审计,以及 pgcli 刻意不编排的手动边缘场景:

pg garage --name store layout history              # 哪个版本、何时、由哪台提交
pg garage --name store layout show                 # 有没有 STAGED ROLE CHANGES 悬着
pg garage --name store node connect <id@addr>      # (重新)peer 一个节点

layout remove、layout revert 与多 zone 故障切换仍是手动 garage 操作—— v2.0 限制清单在拿到集群的 AK/SK一节末尾。

日常维护

pg garage --name store bucket cleanup-incomplete-uploads   # 中止陈旧的 multipart 上传
pg garage --name store key delete-expired                  # 清理过期 key
pg garage --name store bucket inspect-object store/key     # 这个对象的块都在哪些节点

端口

每个实例从 Garage 自己的池取四个连续端口,起点是 garage_start_port(默认 3900):先是 S3 API,然后 RPC、s3_web、admin/metrics。这个池与共享的 minio/silo/rustfs 池(那个从 minio_start_port、默认 9000 起)分开,所以一个 garage、一个 minio、一个 rustfs 实例永不冲突:

pg addon install garage --name store    # 3900 / 3901 / 3902 / 3903
pg addon install garage --name lake     # 3904 / 3905 / 3906 / 3907
pg addon install minio  --name archive  # 9000 / 9001  (共享池)

只有 S3 API 端口是旋钮(--api-port);另外三个被分配其后的空闲端口。RPC 端口 是节点间流量,对单节点实例会留在回环上;admin/metrics 端口(3903)服务 Garage 的状态 API。集群成员的 RPC 端口是 peer 拨号的目标(rpc_public_addr),所以 即便 --listen 为回环,pgcli 也会把它的 RPC bind 放宽到 0.0.0.0——这条通道由 rpc_secret 签名。注意端口号也随 --advertise-host/--peer 一起走,所以在一台 已跑着另一个 garage 实例的主机上,集群的 RPC 可能落在不同于 3901 的端口(例如 10.0.0.11:3905)。

没有 Web 控制台——用 CLI 与 admin API 管理

与 MinIO(自带跑在第二个端口上的 Web Console)不同,Garage 没有内置 Web UI。它的管理面是三样东西:

  • garage CLI,通过 pg garage——检视与改动状态的实际手段。pg garage 把命令跑进实例的运行中容器(它经由本机 RPC 端口通信),从此不用手敲 podman exec:

    pg garage --name store status         # 节点、zone、容量、DataAvail
    pg garage --name store layout show    # 已提交的 layout + 版本
    pg garage --name store bucket list
    pg garage --name store key list

    --name 之后的内容逐字转发给 /garage,所以 Garage 自家的 flag (--read --write、-z、-c)不需要 -- 分隔——且必须放在子命令词之后, 因为 pgcli 只读它前面的自有 flag。只装了一个实例时 --name 可省;装了多个 时,省略则命中按名字排第一的那个(stderr 会播报)。它需要一个运行中的成 员——不同于 pg mc/pg etcdctl 会拉起临时客户端容器,pg garage 是在节点 自己容器里 exec。完整运维菜单见用 pg garage 做管理。

  • admin API,在 admin 端口(3903)——一个 JSON API。从 Garage v2 起端点形如 /v2/<Operation>(如 /v2/GetClusterHealth),且必须在 Authorization 头里 带 Bearer token;裸 curl 会拿到 403 AccessDenied。只有少数端点无需 token:

    curl -s http://127.0.0.1:3903/health          # "Garage is fully operational"
    curl -s http://127.0.0.1:3903/metrics         # Prometheus 指标
    curl -s http://127.0.0.1:3903/v2/GetClusterHealth \
      -H "Authorization: Bearer <admin-token>"   # 需要 token
  • s3_web,在 web 端口(3902)——这不是控制台。它只为开启了网站托管的 bucket 提供静态站点内容(按 bucket 的 vhost);访问根路径返回 404。

社区有第三方 Web UI(基于 admin API 构建的看板等),但它们不属于 Garage、也不由 pgcli 管理;接一个这样的界面意味着先签发一个 admin API token,再把它指向 admin 端口。pgcli 分配的 web_port(3902)是留给 bucket 网站托管的,所以第三方控制台 应该对接 3903(admin API)或用 pg garage——而不是 3902。

配置

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

namespace: default
garage_start_port: 3900    # garage 自己的池(API/RPC/web/admin,每个占四个)
addons:
  garage:
    store:
      container_name: pgcli-garage-default-store
      name: store
      image_tag: docker.io/dxflrs/garage:v2.4.1
      # data_dir: /srv/garage      # 省略则为 <base-dir>/addon/garage/store/data
      #                             # (meta 目录和 garage.toml 与它同级)
      # drives:                    # 多盘:取代 data_dir,每块盘一个目录
      #   - /mnt/d1/garage         # (与 data_dir 互斥;每块盘挂在
      #   - /mnt/d2/garage         #  /var/lib/garage/data<N> 上)
      # config_file: /etc/garage.toml   # 自带配置:原样挂载,永不改写
      listen: 127.0.0.1
      api_port: 3900
      rpc_port: 3901
      web_port: 3902
      admin_port: 3903
      root_user: GK<generated>        # access key id,首次安装时写入
      root_password: <generated>      # secret key,首次安装时写入
      rpc_secret: <generated>         # 集群 RPC 签名密钥,首次安装时写入
      region: garage                  # s3_region
      replication_factor: 1           # 单节点必须为 1;集群才设 >1
      autostart: false                # pg autostart enable --garage --name store
      # --- 多节点集群键(单节点请全部省略)---
      # cluster: c1                   # 非空即集群模式;把成员归到一组
      # advertise_host: 10.0.0.11 # rpc_public_addr 主机;集群模式必填
      # zone: z1                      # layout assign -z(故障域)
      # capacity: 15                  # layout assign -c,单位 GiB;0 表示不自动 assign
      # peers:                        # 已知 id@addr,逐个 `garage node connect`
      #   - 1c0597cc…200aeb@10.0.0.11:3905
      # node_id: 1c0597cc…200aeb@10.0.0.11:3905  # 首启后读回并缓存

replication_factor 默认 1。没有 cluster 却把它设 > 1 会在安装时被拒绝 ——单节点凑不出多节点 quorum。上面的集群键由集群安装路径写入;也可手写后重装。 zone 与 capacity 是 layout 状态、不进 TOML,所以只出现在这里。

对 listen、端口、root_user、root_password、image_tag、data_dir、 drives 或 config_file 的修改,在下一次 pg addon install garage --name store --force 后生效。

列表

pg addon list
Infra add-ons (garage):
  garage (name: store)
    Status:      running
    Cluster:     c1 (rf 3)
    Advertise:   10.0.0.11:3905
    Zone/Cap:    z1 / 15 GiB
    Node ID:     1c0597cc…200aeb@10.0.0.11:3905
    Nodes:       3 assigned (healthy)
    Listen:      127.0.0.1
    API port:    3904
    RPC/Web/Admin: 3905/3906/3907
    S3 URL:      http://127.0.0.1:3904
    Data:        ~/pg/addon/garage/store/data
    Metadata:    ~/pg/addon/garage/store/meta
    Access key:  GK<generated>
    Bucket:      store
    Image:       docker.io/dxflrs/garage:v2.4.1
    Container:   pgcli-garage-default-store
    NOTE:        plaintext HTTP only — front with the nginx addon for TLS

集群成员多出 Cluster / Advertise / Zone/Cap / Node ID / Nodes 几行; Nodes: 是一次实时的 garage status 读取(尽力而为——成员不可达时省略)。多盘 实例把 Data: 行换成 Drives:,其下每盘一行缩进的"容器槽位/主机目录"(见多 盘数据目录)。集群里,未执行播种的那几台主机上 Access key 只 有公开 id(没有 secret);见播种与 secret。

pg addon list 从不打印 secret key。要刻意揭示它用 pg addon list --show-password(追加一行 Secret key:),或为脚本只打印这个秘 密:pg addon password garage --name store(secret key,与上面 Access key: 配 对;--file <path> 以 0600 写出)。

启停

pg addon start garage --name store
pg addon stop  garage --name store

install 跳过仍存在的容器(停止的会被启动);start 只启动已存在的容器(并通 过从配置重建来自愈一个处于 improper 状态的容器,同时重新渲染 garage.toml)。

开机自启

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

pg autostart enable --garage --name store

garage 自启会重新渲染 garage.toml 并启动已存在的容器。Garage 在上游镜像里以 root 运行,所以开机启动除运行 podman 本身外不需要特殊主机权限。开机是仅启 动;garage 独立于 PostgreSQL 栈,所以与其它对象存储一起被启动。

移除

pg addon remove garage --name store            # 容器消失,数据保留
pg addon remove garage --name store --clean-data   # 同时删除所有数据目录 + meta 目录 + 配置

这些数据目录就是对象存储——丢了它们等于丢了里面每个桶——所以 remove 默认 保留。--clean-data 删除数据目录(多盘模式下是每一块盘的目录)、meta 目录和 pgcli 渲染的 garage.toml(你自带 config_file 指向的文件从不被碰),并清理已空 的默认布局父目录。每个目录在仍是活动挂载点时都会被跳过——以一句拒绝、而不是悄悄 放过。与 rustfs 不同,Garage 以 root 写、pgcli 在受管主机上以 root 跑 podman,所以 普通删除即可——没有 podman unshare 兜底。

日志

pg logs addon garage --name store      # 最后 50 行
pg logs addon garage --name store -f   # 跟随

故障排除

  • install 时 pulling garage image ... : ...。 tag docker.io/dxflrs/garage:v2.4.1 不可达——检查到 Docker Hub 的网络,或先用 podman pull 预拉。
  • pg mc alias set 报连接错误。 默认 --listen 127.0.0.1 把存储绑在回环 上;在 macOS 上用 host.containers.internal 而非 127.0.0.1。用 pg addon list 确认实时 API 端口。
  • 经 nginx 前置的写操作报 Invalid signature。 前置把 Host 重写成不带端口 的形式;用 proxy_set_header Host $http_host;(conf-file 配方就是这么写的)——--upstream 模板模式写死 $host,不适合 S3。
  • 经 nginx 前置的 PUT 在 ~1 MiB 处失败。 nginx 默认 client_max_body_size 为 1m;conf-file 配方把它设成 0(不限)。
  • pgBackRest 拒绝端点(HTTP / 证书)。 pgBackRest 对 S3 仓库强制 HTTPS。用 nginx 插件(--tls)前置 Garage,并把 backup.repo.s3.ca_file 指向前置的 ca.crt。
  • 单节点的 replication_factor 必须为 1。 没有 cluster 却手改 replication_factor: 2 会在安装时被拒——单节点凑不出多节点 quorum。要跑多节点 就设 cluster(或用 --cluster 安装),见多节点集群。

16.5 - 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 拓扑。

16.6 - SeaweedFS

以 pgcli 插件方式运行 SeaweedFS(Go 语言、Apache-2.0 的 S3 兼容对象存储)——单节点 “weed mini”,或跨主机 “weed server” 集群(filer 元数据放在共享 PostgreSQL 里),原生 S3 TLS,全集群共用同一对凭据

SeaweedFS 是一个 Go 语言、Apache-2.0 的 S3 兼容对象存储,其标志性特征是以 volume 粒度管理磁盘:数据被切成固定大 小的 volume(.vol 文件),由 master 在各 volume 服务器之间放置,从而获得 O(1) 读取和廉价的小文件存储。pgcli 将其作为独立的顶层插件运行,CLI 表面与 garage、minio、silo、 rustfs 一致(install、list、logs、autostart、password、 pg mc),但其运行时模型在四个方面与四者都不同,这也决定了本页的写法:

  1. 一个进程,全部角色。 单节点跑 weed mini——master、volume 服务器、 filer、S3 网关四合一(官方认可的单机生产形态)。集群成员跑 weed server -filer -s3,每台主机一个,同样是全部角色一起——绝不拆成四个 容器。
  2. 三个渲染出的文件,其余全走 argv。 pgcli 在实例目录下渲染 s3.json (S3 身份)以及集群场景下的 filer.toml(PostgreSQL 元数据存储),以只读方 式 bind 挂载;其它配置都是 weed 的命令行 flag。
  3. 集群元数据放在共享 PostgreSQL 里。 集群每个节点都对外提供 filer + S3, 所以 filer 命名空间必须是一个数据库,而不是各节点各自的 leveldb2——pgcli 让 所有成员指向同一个 --dsn PostgreSQL。这正落在 pgcli 的主场:对象存储可以 紧挨着(甚至就在)一个 pgcli 管理的实例旁边。
  4. 原生 S3 TLS——--tls(自签)或 --tls-cert/--tls-key(自带证书,例如 pg cert 的产物)——是 MinIO 的姿态,不是 Garage 的 nginx 前置。开启 TLS 后,池化的 S3 端口只服务 HTTPS;明文监听器完全不 绑定。

平台支持: 已在 Linux(host 网络)上验证。macOS 的 bridge 路径代码完整、 遵循与其它对象存储一致的形态,但尚未在真机上跑过。

工作原理

一个实例一个容器,来自上游 docker.io/chrislusf/seaweedfs:4.48_large_disk 镜像(仅 pull)。pgcli 覆盖镜像 自带的 ENTRYPOINT,直接运行 /usr/bin/weed -logtostderr=true <subcommand> …: 官方 entrypoint 每次启动都会 chown -R /data 再降权到 uid 1000——在大目录上正 是我们想避开的"静默变慢"形态;而不带 -logtostderr 的话 weed 会把日志写进容器 内的文件,pg logs 根本看不到。

主机侧状态存放在 <base-dir>/addon/seaweedfs/<name>/ 下:

  • data/ → /data(volume,单机时还有 filer 的 leveldb2 元数据)——多盘模式下 被每盘一个 bind 挂载(/data1…/dataN)整体取代,见多盘,
  • s3.json → /etc/seaweedfs/s3.json:ro(S3 身份;宿主侧权限 0600),
  • filer.toml → /etc/seaweedfs/filer.toml:ro(仅集群 / --dsn;渲染元数据 DSN 的 [postgres2] 段),
  • 开启 TLS 时,cert.pem/key.pem 挂到 /etc/seaweedfs/ 下。

凭据是一对 access key / secret key,渲染进 s3.json(SeaweedFS 的静态身份 机制——AWS_* env 优先级最低、会被本地 store 覆盖,pgcli 从不用它):

  • root_user 是 access key id——单节点安装时首次自动生成 (SW + 28 位 hex),或由 --root-user 指定。
  • root_password 是 secret key(64 位 hex),自动生成或由 --root-password 指定。
  • 在 --cluster 下两者都是必填,且每个节点必须完全一致——见多节点集 群。

身份渲染时带裸 Admin action——在 SeaweedFS 的权限模型里它无条件短路成全权 限。所有值都落进 pg.yaml(addons.seaweedfs.<name>.*),重建才稳定。安装还会 渲染出一个桶——以实例名命名(--bucket 可覆盖):单节点由 weed mini -bucket 启动时创建;集群在该节点健康门变绿后,通过 S3 API 用 mc mb --ignore-existing 播种。

安装

# 默认实例名 "seaweedfs",四个端口取自 seaweedfs 池,loopback 绑定
pg addon install seaweedfs

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

# 把单节点的 volume 铺到多块独立挂载的盘上
pg addon install seaweedfs --name store --drive /mnt/sw-disk1 --drive /mnt/sw-disk2

# 固定 master 端口(volume/filer/s3 顺取后三个;gRPC 孪生口 +10000)
pg addon install seaweedfs --name store --port 8400

# 把存储暴露到网络,而不是仅 loopback
pg addon install seaweedfs --name store --listen 0.0.0.0

# 自带 access key / secret key(seaweedfs 下限:key ≥ 8,secret ≥ 16 字符)
pg addon install seaweedfs --name store --root-user SW<...> --root-password <secret>

# 原生 TLS(自签)——S3 口变 HTTPS-only
pg addon install seaweedfs --name store --tls

# 加入跨主机集群(见下文多节点集群)——凭据在这里是强制的
pg addon install seaweedfs --name store --cluster c1 --advertise-host 10.0.0.11 \
  --replication 001 --root-user SW<...> --root-password <secret> \
  --dsn "postgres://admin:<pw>@10.0.0.11:35432/seaweed_store" \
  --peer 10.0.0.11:8400 --peer 10.0.0.12:8400 --peer 10.0.0.14:8400

单节点输出的摘要报告端点和铸造的凭据:

  [OK] cluster leader: 127.0.0.1:8400.18400

✓ seaweedfs installed: "store"
  Container:     pgcli-seaweedfs-default-store
  Image:         docker.io/chrislusf/seaweedfs:4.48_large_disk
  Data:          /root/.pgcli/addon/seaweedfs/store/data
  S3 endpoint:   http://127.0.0.1:8403
  Ports:         master=8400 volume=8401 filer=8402 s3=8403 (gRPC twins +10000)
  Bucket:        store

  Access key:    SW<generated>
  Secret key:    <generated>
  Try it:        pg mc alias set store http://127.0.0.1:8403 SW<...> <secret> && pg mc ls store

安装以健康门收尾:pgcli 轮询 master 的 JSON /cluster/status API 最多 30 秒 才宣布成功——一个 detach + --restart unless-stopped 的容器否则会把错误的 argv 吞成一次崩溃循环、却显示安装成功。只有健康门绿了才进行桶播种。

对存活实例重跑 install 是 no-op:容器不重建(停止的会被启动并给提示), flag 合并进存的配置,已有凭据保留。传 --force 才会重建容器,让改动的端口 / 监听地址 / 凭据生效——数据目录和元数据库都不会丢。

绑定地址: 默认 127.0.0.1 保持存储本地;--listen 0.0.0.0(或 pg.yaml 里的 listen 键)把它暴露到网络。集群成员无论 --listen 是什么 都绑 0.0.0.0——peer 必须能连到它的 gRPC 孪生口(port + 10000)做 Raft—— 而 -ip 仍固定注册为那一个地址供 peer 拨号。

-ip 从不留给自动探测。 weed 会把它的 volume 注册到它自己猜出来的地址 下;filer/S3 的内部数据面随后去拨这个猜测地址,在一个"看起来健康"的容器上 上传就会 500。pgcli 永远显式传 -ip(单机取 --listen 值,集群取 --advertise-host)——在一台测试主机上吃过这个坑才确认的。

TLS

SeaweedFS 在 S3 口原生服务 HTTPS;没有 nginx 前置,也没有 web-console 那一路 需要代理。两种形态,同 minio/rustfs:

# 自签:pgcli 在实例目录下生成 cert+key(30 天叶子证书,SAN 覆盖
# bind/advertise 地址、127.0.0.1 和 localhost)
pg addon install seaweedfs --name store --tls

# 自带证书:例如 `pg cert` 签发的证书对(CA 证书会被拒绝)
pg addon install seaweedfs --name store --tls-cert fullchain.pem --tls-key privkey.pem

TLS 在底层改了什么(已实测):明文监听器关掉(-s3.port=0——该池端口直接 拒绝 TCP 连接),同一个池化 S3 端口变成 -s3.port.https。所以安装摘要里打印的 端点是 https://…:8403,不是一个新端口。pg mc 照常指向它即可——loopback 别名会自动带 --insecure;真实 CA 或生成的 CA 则要交给 S3 客户端做校验。

多盘

单节点可以用多块盘:weed 自己的 -dir 本就是一个逗号列表 (dir[,dir]…),由同一个 volume 服务器把 volume 铺在这些目录上;pgcli 把 它暴露成可重复的 --drive——每块盘一个宿主目录,各自挂到自己的槽位:

# 每个 --drive 都应是一块独立挂载的盘;drives 完全取代 --data-dir(两者同时给是错误)
pg addon install seaweedfs --name store \
  --drive /mnt/sw-disk1 --drive /mnt/sw-disk2 --drive /mnt/sw-disk3

摘要和 pg addon list 会打印映射,取代单独那行 Data::

  Drive 1:       /mnt/sw-disk1 -> /data1
  Drive 2:       /mnt/sw-disk2 -> /data2
  Drive 3:       /mnt/sw-disk3 -> /data3

容器因此以 -dir=/data1,/data2,/data3 运行(已实测:volume 服务器的 /status 把每个宿主目录报成独立的 DiskStatuses 条目、各自记账;remove --clean-data 也会逐个遍历 drives,并拒绝删除仍是活动挂载点的那一个)。

两点语义值得知道:

  • 这是放置,不是 RAID。 是一个 volume 服务器把 .vol 文件分散到自己的目录 ——OS 层面既没有条带也没有镜像,各盘之间也没有纠删码(集群副本是另一件 正交的事,见多节点集群)。一块盘消失,它上面的 volume 就没了; 这是否造成数据丢失取决于 placement code,而不是本地布局。
  • 设备检查是建议性的。 install 会对任何与宿主根文件系统同设备的 drive 发 出警告——多盘只有配独立挂载的盘才有意义。与 MinIO(会在启动时直接拒绝这种 盘)不同,weed 照样会用它们,所以 pgcli 只警告、不阻断。请先自行格式化并挂 载每块盘;pgcli 从不做分区或格式化。

drives 是逐节点的开关,并且与集群正交组合:在每台成员的 install 命令上照常 传 --drive,每个节点就在共享集群背后跑自己的多盘 volume 服务器(已在三主机 weed server 集群上实测——每台各挂两个 drive:每台进程都显示 -dir=/data1,/data2 且各报两个独立 DiskStatuses 条目;同一个 8MB volume 的 001 副本一份落在某台的 实体盘上、另一份落在副本台的另一块 drive 上,即各节点的 volume 服务器在自己的 目录之间选择,而放置决策在节点之间进行)。各节点不需要相同的 drive 数量;每台只 看得到自己 -dir 列表里的目录。stop/start 与 --force 重建会像处理其它配置变更 一样处理 drive 集合——挂载与 -dir 都从 pg.yaml 重建。

多节点集群

集群是 MinIO-MNMD 形态,不是 Garage 的 bootstrap/join:没有"第一个节点"的 概念、没有携带集群密钥的 join 命令、也没有 layout 要 assign。你在每台主机上 跑同一条 install 命令——Raft 成员资格就是每个节点都传的完整 --peer 列表, quorum 未齐就位的节点只是等待(其健康门报 warn 而非失败;补齐 quorum 的那次 安装完成桶播种)。

与"更大的单节点"相比有两点不同,而且这两点天然要求全部一致——每台成员的 命令都要原样携带:

  1. 全集群一对凭据。 每个网关只针对自己那份渲染出的 s3.json 认证; SeaweedFS 没有独立 IAM server 就没有共享凭据后端(超出 pgcli v1 范围),所以 逐节点自动生成会让三个网关各自接受一份不同的身份。pgcli 因此在 --cluster 下拒绝自动生成:每台节点传同一对 --root-user 和 --root-password。先跑完第一个节点的 install,从打印的摘要里复制这对值, 原样带到其余节点(pgcli 从不远程执行任何东西)。只传一半(两个里只给一 个)是错误;把一个单节点配置加 --cluster 重装同样是错误。
  2. 共享 filer 元数据库。 --dsn postgres://user:pass@host:port/db 是必填, 且必须从每台主机都可拨通——所以用宿主发布端口 / LAN 地址,绝不要容器内 部的 127.0.0.1:5432——数据库本身也必须已经存在:pgcli 只探测可达性,从不 创建(也从不删除)它不拥有的数据库。元数据表不需要任何准备:weed 的 postgres2 store 在首次使用时自动在其内建 .system、filemeta 和每桶一张 表(已实测)。filer.toml 里渲染的是 sslmode=disable,所以能连通 + pg_hba 放行就够了。

三主机实操

先在一个所有节点都能到的地方准备元数据库(这里用主机 1 上一个 pgcli 管理的实 例,宿主端口 35432):

pg exec -i demo "CREATE DATABASE seaweed_store"

然后在每台主机上跑同一条命令,只换这台主机自己的 --advertise-host(下面的 key 是占位符——所有节点用同一对值):

# on 10.0.0.11 — 打印出的摘要会交给你用于其它主机的命令
pg addon install seaweedfs --name store --cluster c1 \
  --advertise-host 10.0.0.11 --replication 001 \
  --root-user SW<...> --root-password <secret> \
  --dsn "postgres://admin:<pw>@10.0.0.11:35432/seaweed_store" \
  --peer 10.0.0.11:8400 --peer 10.0.0.12:8400 --peer 10.0.0.14:8400

# on 10.0.0.12 — 除 --advertise-host 外完全相同
# on 10.0.0.14 — 除 --advertise-host 外完全相同

想让每台成员跑自己的多盘 volume 服务器,就往同一条命令上再加 --drive (--drive /mnt/sw-disk1 --drive /mnt/sw-disk2——见多盘);它是逐节 点的 flag,各台可以带不同的 drive 集合。已在三主机集群上实测。

摘要末尾会带一段可复制的、正好是这个形态的块,用于其余主机(凭据和 DSN 从本 节点配置里填好)。

集群还在拼装中时,先装的节点会说什么,都是正常的:

  • 主机 1 起来时就能看到 leader(3 节点列表下,落单 master 立刻赢下自己的选举 ——这是实测到的行为),但桶播种可能以 connection refused 失败:集群的 S3 侧 还不可服务。pgcli 打印 [!] bucket seeding incomplete … re-run this install command once the cluster has quorum,并且退出码 0。
  • 主机 2(或 3)补齐 quorum;同样幂等的播种在那里成功(Bucket created successfully)。事后重跑主机 1 的命令会安静复检,mb --ignore-existing 保持 no-op。

三台都装完后,任意成员展示整个集群:

$ pg weed --name store --shell "cluster.status"

cluster:
	id:       topo
	status:   unlocked
	nodes:    3
	topology: 1 DC, 3 disks on 1 rack
…

经任意节点读写

因为凭据共享、filer 命名空间是一个数据库,任意网关都服务任意对象:经主机 2 的 S3 口写入、经主机 3 用同一把 key 读回——在三主机测试集群上端到端验证,包 括故意用错 secret 时返回 The request signature we calculated does not match (它不是每节点各自的本地信任库)。--replication 001 给每个 volume 存一份远端 副本(volume.list 显示 ReplicaPlacement:001,每个 volume 落在两个数据节点 上)。

停掉一个成员,集群仍然服务(Raft quorum 是多数;3 个 master 时 2 个继续选举和 写入):

# on host 3
pg addon stop seaweedfs --name store
# on host 1 — 经主机 2 的网关读取仍然成功
pg mc cat store2/store/roundtrip.bin | sha256sum

pg addon start 让节点重新加入;它的 volume 以同一个 -ip 重新注册。成员变 更不被编排:--peer 是静态 Raft 配置——增删成员意味着重建集群(v1 局限,见 局限)。

取集群的 AK/SK

这对凭据按设计存在于每台节点的 pg.yaml 里(是你带过去的),所以不像 garage 的 CRDT key——其 secret 恰好只在一台主机上—— pg addon password seaweedfs --name store 在任意成员上都能用。如果打印出的 secret 在所有主机上都丢了,就挑一对新值,在每台节点上用 --force 重装(渲染出 的 s3.json 会被重写;数据和元数据不受影响)。

用 mc 客户端

pg mc 从一次性容器里跑 MinIO 的 mc——无需本地安装——像对其它存储一样说 SeaweedFS 的 S3 API。把 access key 当 user、secret key 当 password 用:

pg mc alias set store http://127.0.0.1:8403 SW<...> <secret>   # Linux,本地
pg mc mb store/backups
pg mc cp ./dump.pglz store/backups/
pg mc ls store

别名持久化在 ~/.mc/config.json;pg 的解析器会误吞的 flag 放到 -- 之后 (pg mc rm store/scratch -- --force)。macOS 上把别名指到 host.containers.internal:<s3-port> 而不是 127.0.0.1。开 TLS 就用 https:// 端点(loopback 别名自动 --insecure)。

用 pg weed 运维

pg weed 在一个运行中成员的容器里跑 SeaweedFS 自己的 weed 控制工具——替代 手写 podman exec pgcli-seaweedfs-<ns>-<name> …。--name 选实例(默认:按名第 一个);pgcli 自己的 flag 必须放在子命令词之前,weed 的 flag 原样透传。两种 形态:

pg weed <args>...           # 原样:容器内 /usr/bin/weed <args>
pg weed --shell <line>...   # 每一行喂进 `weed shell`,已通过 loopback 预先
                            # 接好本实例的 master 和 filer

weed shell 菜单是 day-2 的表面(命令名已对 4.48 校验——shell 的 help 列得更 多):

pg weed --shell "cluster.status"                     # 拓扑、节点、volume
pg weed --shell "volume.list"                        # 每个 volume、它的副本、大小
pg weed --shell "s3.bucket.list"                     # filer 视角下的桶
pg weed --shell "fs.ls /"                            # filer 命名空间,根
pg weed --name store --shell "volume.list" "s3.bucket.list"   # 一次多行
pg weed flag.lookup 0,1,2                            # 原样子命令
pg weed shell                                        # 交互式(需要 TTY)

weed 类 CLI 可能打印完结果还带着打开的 gRPC channel 不退出;pgcli 在其输出安 静 3 秒后(总上限 10 分钟)把这种命令切断,于是挂起暴露成一次响亮的超时——重跑 即可。

端口

每实例从 SeaweedFS 自己的池里取四个连号端口,起于 seaweedfs_start_port (默认 8400):master、volume、filer、s3——并且每个角色还开一个 http 端口 + 10000 的 gRPC 孪生口(8400 → 18400,……),pgcli 也把这些在 池里预留了。所以第二个实例落在 8404-8407 / 18404-18407,一个 seaweedfs、一个 garage、一个 minio 实例永不冲突(各自独立池:8400 / 3900 / 9000):

pg addon install seaweedfs --name store   # 8400-8403 + 18400-18403
pg addon install seaweedfs --name lake    # 8404-8407 + 18404-18407

--port 只设 master 锚点;其余随之。四个里只有 S3 口是客户端端点;master 8400 还服务 /cluster/status JSON API 和运维 UI 页面(admin UI 和 webdav 在 argv 里 被关掉,iceberg/lance 旁路监听器钉到 0,所以 weed 不开额外的洞)。

配置

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

namespace: default
seaweedfs_start_port: 8400    # seaweedfs 自己的池 (master/volume/filer/s3, 各 4 连号, gRPC 孪生口预留)
addons:
  seaweedfs:
    store:
      container_name: pgcli-seaweedfs-default-store
      name: store
      image_tag: docker.io/chrislusf/seaweedfs:4.48_large_disk
      # data_dir: /srv/seaweedfs    # 省略则 <base-dir>/addon/seaweedfs/store/data
      # --- 多盘(取代 data_dir;与其互斥)---
      # drives:                      # 每盘一个宿主目录,挂到 /data1../dataN,
      #   - /mnt/sw-disk1            #   并把它们逗号拼接后交给 -dir
      #   - /mnt/sw-disk2
      listen: 127.0.0.1
      port: 8400                    # master 锚点;volume/filer/s3 = 后三个
      volume_port: 8401
      filer_port: 8402
      s3_port: 8403
      root_user: SW<generated>         # s3.json access key
      root_password: <generated>       # s3.json secret key
      bucket: store                    # 首次启动播种(默认 = name)
      volume_size_mb: 1024             # -master.volumeSizeLimitMB(weed 自带的 128 太小)
      autostart: false                 # pg autostart enable --seaweedfs --name store
      # --- 多节点集群键(单节点省略)---
      # cluster: c1                    # 非空 ⇒ 集群模式(weed server)
      # advertise_host: 10.0.0.11  # peer 拨号的 -ip;集群必填
      # replication: "001"             # placement 码 XYZ(X 远端副本数,
      #                                #   Y 机架数, Z 数据中心数),集群内不可变
      # peers:                         # 全量 Raft 列表,每台都写
      #   - 10.0.0.11:8400
      #   - 10.0.0.12:8400
      #   - 10.0.0.14:8400
      # filer_dsn: postgres://admin:<pw>@10.0.0.11:35432/seaweed_store  # 集群必填
      # --- TLS ---
      # tls: true                      # 自签(或 --tls-cert/--tls-key 自带)
      # cert_file: …/cert.pem
      # key_file: …/key.pem

对 listen、端口、root_user、root_password、image_tag、data_dir、 drives、filer_dsn 或 TLS 材料的改动,在下一次 pg addon install seaweedfs --name store --force 后生效。集群里的这次 force-recreate 必须在每台成员上跑(每台主机各持有自己的 pg.yaml 和容器)。

列出

pg addon list
Infra add-ons (seaweedfs):
  seaweedfs (name: store)
    Status:      running
    Cluster:     c1 (replication 001, 3 peers configured)
    Peers:       10.0.0.11:8400,10.0.0.12:8400,10.0.0.14:8400
    Leader:      10.0.0.11:8400.18400
    Filer store: postgres 10.0.0.11:35432/seaweed_store
    Listen:      127.0.0.1
    Ports:       master=8400 volume=8401 filer=8402 s3=8403
    S3 URL:      http://127.0.0.1:8403
    Data:        /root/.pgcli/addon/seaweedfs/store/data
    Access key:  SW<generated>
    Bucket:      store
    Image:       docker.io/chrislusf/seaweedfs:4.48_large_disk
    Container:   pgcli-seaweedfs-default-store

集群成员多出 Cluster / Peers / Leader / Filer store 四行;Leader: 是一 次实时的 /cluster/status 读(尽力而为——成员没跑就省略,集群还在拼装、尚无 leader 时打印成 (none yet — still electing / no quorum))。单节点成员这四行都 没有。多盘实例则以 Drives 块(每个 drive 一行 - /dataN <- 宿主路径)取代单 独那行 Data:——见多盘。开 TLS 时末尾追加一行 TLS: native S3 HTTPS on 8403 (cleartext listener off)。pg addon list 从不打印 secret key;要显式暴露用 pg addon list --show-password(追加一行 Secret key:),或 pg addon password seaweedfs --name store(--file <path> 以 0600 权限写入)。 Filer store 行是 host:port/db,密码永远隐藏。

启停

pg addon start seaweedfs --name store
pg addon stop  seaweedfs --name store

install 跳过还在的容器(停止的会被启动);start 只启动已存在的容器——并且 先重新渲染 s3.json/filer.toml,所以对存的 config 手工编辑会在重启后生效。

开机自启

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

pg autostart enable --seaweedfs --name store

自启是只启动——渲染文件和容器都已存在。一个比 peers 先起的集群成员只是像任 何一次冷启动一样等 quorum。SeaweedFS 在上游镜像里以 root 运行,所以开机启动除了 运行 podman 本身不需要额外主机特权。

移除

pg addon remove seaweedfs --name store               # 容器没了,数据留着
pg addon remove seaweedfs --name store --clean-data  # 连数据目录 + 渲染文件一起删

数据目录就是对象存储——丢了它等于丢了里面所有 volume——所以 remove 默认 保留它。--clean-data 删除 data 目录、s3.json,以及存在时的 filer.toml / TLS 目录(每个都要先 stat——缺失的可选项什么都不打印),并拒绝碰任何还是活动 挂载点的东西。filer 元数据库永不删除,在任何主机上、任何时候都是:移除时会 打印

  [i] The filer metadata database (10.0.0.11:35432/seaweed_store) was KEPT — pgcli never drops databases; drop it yourself if the data is disposable.

于是你清楚真要清干净时该 DROP DATABASE 什么(每台成员的 remove 都打同样的提 示;数据库比它们所有容器都活得久)。

日志

pg logs addon seaweedfs --name store      # 最后 50 行
pg logs addon seaweedfs --name store -f   # 跟随

局限

v1 有意不做的事:

  • 集群成员变更(增/删/替换节点)不被编排——--peer 是静态全列表;改它意味 着重建集群。
  • EC 纠删码、分层(tiering)、cloud-sync(向其它 S3 复制)、FUSE 挂载,以及 独立 IAM server(动态 s3.accesskey.* 用户)都不被管理——全部仍是手工 pg weed 的领地,尤其 IAM 是 SeaweedFS 本身动态共享凭据的唯一途径,这里超出 范围。
  • 凭据轮换是一次带 --force 在每台成员上的重装;pgcli 从不就地编辑一个活的 s3.json。
  • 只播种一个桶(实例名);后续桶就是普通 pg mc mb 的事——它们自动落进同 一个 filer 数据库。
  • filer.toml 里的 DSN 渲染 sslmode=disable;如果你的 PostgreSQL 在这一 条上强制 TLS,pgcli 的渲染不适用(自带 TLS 的 --dsn 不是 v1 表面)。

故障排查

  • --cluster 安装报 requires explicit shared credentials。 这是设计上预 期的——用显式的 --root-user/--root-password 对跑第一个节点,并把同样的值 带到每个其余主机(安装摘要会打印填好它们的现成命令)。单节点安装可以省略它 们、拿到自动生成的 key。
  • requires a shared filer metadata database: pass --dsn …。 没有 --dsn 的集群无法在各网关间维持一个命名空间。预建好数据库,传一个主机可达的 URI(发布端口 / LAN 地址——容器内的 127.0.0.1:5432 不行)。
  • [!] no leader yet — normal while the cluster is still assembling。 健康 门的 quorum 警告,不是失败:把其余成员的 install 跑完(或等它们起来后重跑这条), 它就绿了。
  • [!] bucket seeding incomplete … re-run this install command。 晚一步的同 一种姿态:这个节点拿到了 leader 但它的网关还不可服务。最后一台加入后重跑同一 条 install;mb --ignore-existing 让它在桶已落地时是一个干净的 no-op。
  • 单机健康外观下上传 500。 历史上就是缺 -ip 的签名(volume 注册了一个自动 猜的地址,数据面拨不通);pgcli 永远传 -ip,若你还看到,检查有没有被手工改 过的 argv 或一个很旧的二进制。
  • mc: … you already own it。 对一个已存在的桶跑了裸 mc mb;pgcli 自己的 播种用 --ignore-existing,所以只会从手工 mc 里看到这个——加上那个 flag。
  • 错 secret 的困惑:The request signature we calculated does not match。 这 对值不匹配该节点渲染的 s3.json——在集群里,这意味着各节点装的时候用了不同 的凭据;用同一对值 + --force 在每台重跑 install。
  • install 时 pulling seaweedfs image … 失败。 docker.io/chrislusf/seaweedfs:4.48_large_disk 不可达——检查到 Docker Hub 的 网络,或先 podman pull 一把。

16.7 - 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 从不打印密码。要有意揭示它,加 --show-password(追加一行 Root password:),或只把密钥原样打到 stdout 供脚本使用:pg addon password rustfs --name store(即 access-key 密钥,与上面的 Root user: 配对; --file <path> 以 0600 权限写入文件)。健康端点是 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>。

16.8 - HAProxy

以 pgcli 插件方式运行 HAProxy——Patroni 集群前的 TCP 负载均衡(读写一体或读写分离),或用 –conf-file 提供自己的 haproxy.cfg 实现任意原生代理

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 个。

两种配置模式

在上面两种路由模式之上,还有一个谁来写 haproxy.cfg 的选择:

  • 渲染模式(默认)——pgcli 根据 --node/--ha 加 --mode、端口和 --max-lag 生成配置,每次安装都会重写。就是上面说的 Patroni 读写代理。
  • 自定义模式(--conf-file <path>)——你提供完整的 haproxy.cfg,pgcli 原样运行它,用于任意原生 HAProxy 代理场景(普通 TCP 中继、HTTP frontend、任意数量的 frontend/backend/listen 段——HAProxy 能做的都 行),不再限于 Patroni 集群。pgcli 把文件复制到自己的配置目录、以只读方式 挂载该副本,(重)启容器前先用 haproxy -c 校验,且永不改写、不删除你的 原文件。改完文件后,pg addon haproxy reload 会重新校验并热加载运行中的 实例、不断流(仅 conf-file 实例——见热加载)。所有监听器都来自 文件本身;--mode、--rw-port/--ro-port/--stats-port、--max-lag、 --listen 与 --node/--ha 在这里没有意义,会被拒绝或忽略——见 自定义配置文件。

统计页

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

安装

后端成员用两种方式之一提供(互斥),或者用 --conf-file 完全绕过这套生成逻辑 (见自定义配置文件):

  • --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 前置它)。

自定义配置文件

上面的渲染模式是 pgcli 对 Patroni 路由的一种固定写法。用 --conf-file 就完全 跳过生成,直接跑你自己的 haproxy.cfg——任意原生 HAProxy:普通 TCP 中继、 HTTP frontend、任意数量的 frontend/backend/listen 段,upstream 软件支持 什么就能做什么。pgcli 只保留容器生命周期(拉取、创建、启停、remove、autostart) 和配置校验门:

pg addon install haproxy --name custom --conf-file /root/haproxy/lb.cfg

--conf-file 与 --node/--ha 互斥(同时给出会以报错拒绝安装),文件必须存在 且非空。一个把原始 TCP 中继和不相干 HTTP 监听器混在一起的典型文件:

global
    maxconn 1000
    log stdout format raw local0 info

defaults
    log global
    mode tcp
    timeout client 30m
    timeout connect 4s
    timeout server 30m

# 到某个 PostgreSQL 实例的普通 TCP 中继(pgcli 在这里不推导任何东西)
frontend pg_relay
    bind 10.0.0.11:7000
    default_backend pg

backend pg
    server demo 127.0.0.1:35432 check

# 一个用于不相干用途的 HTTP 模式监听器
listen webstats
    mode http
    bind 127.0.0.1:7001
    http-request return status 200 content-type text/plain lf-string "haproxy custom proxy alive\n"

安装摘要随后报告自定义模式,而不再报告端口(没有端口可报——它们在你的文件里):

✓ haproxy installed: "custom"
  Container:    pgcli-haproxy-default-custom
  Image:        docker.io/library/haproxy:3.2.23-alpine
  Mode:         custom (user config — all listeners come from the file)
  Config:       /root/.pgcli/addon/haproxy/custom/haproxy.cfg (from /root/haproxy/lb.cfg)

这个模式的具体行为,逐条说明(以下每一条都是真实主机上安装实测的结果):

  • 你的文件是唯一事实来源;跑起来的是 pgcli 的副本。 install 把它复制到 <base-dir>/addon/haproxy/<name>/haproxy.cfg,只读挂载这份副本。你的原文件 永不重写,pg addon remove 也只删 pgcli 这份副本——你的文件留在原地。
  • 改配置靠 reload(无损)或重跑 install(有损)。 改完文件后,要么 pg addon haproxy reload 把它热应用到运行中的容器、不断流,要么重跑同一条 install 重建容器。两者都会先把文件重新复制一份并用 haproxy -c 校验。渲染 模式的旋钮在这里改了没用——--mode、--rw-port/--ro-port/--stats-port、 --max-lag、--listen 描述的是生成器,不是你的文件。见热加载。
  • 动容器之前全部经过校验。 每次 install 都先把挂载的副本扔进一次性容器跑 haproxy -c(仅检查)。坏文件会以 HAProxy 自己的诊断信息让 install 失败、 退出码非零,并回滚到上一份好配置——运行中的容器和磁盘上的配置原样不动, 之后的重启或 pg addon start 不可能复活一份被拒绝的配置。(warning——比如 缺 timeout——放行;只有真正的 error 才拒。)
  • 端口由你自己选。 端口池跳过 conf-file 实例:既不分配,也不把你文件里的 端口登记为已用。监听端口请自己规划,避开渲染模式实例自动分配的端口和主机上 其他服务——撞了的话 HAProxy 启动时以 cannot bind socket 失败。
  • 容器仍走 host 网络,与渲染模式一致。绑 127.0.0.1 的 frontend 只有主机 本机能访问;要让别的容器/主机进来,绑主机的地址(上面 pg_relay 就是)。
  • 代理的健康状况由你的配置负责。 pg logs <name> 显示 HAProxy 自己的 stdout(前提是文件里写了 log stdout,如示例);stats 页只可能是你自己写的 stats 段——pgcli 不会替你加一个。
  • 切回渲染模式只需一条不带 --conf-file 的 install:重新给 --node/--ha, 实例就回到渲染模式(磁盘上的 conf-file 副本会被生成的配置覆盖)。

示例:带健康检查的 HTTP 负载均衡

conf-file 实例就是一个完整的 HAProxy,所以 Patroni 那套并非它唯一能做的事—— 下面是一个对多台 app 服务器做 HTTP 轮询、并带主动健康检查的配置(就是这份配置 实跑过,pg logs 里观察到了 DOWN/UP 行为):

global
    maxconn 4096
    log stdout format raw local0 info

defaults
    log global
    mode http
    retries 2
    timeout client 30m
    timeout connect 4s
    timeout server 30m
    timeout http-request 30s

frontend http_in
    bind 127.0.0.1:8180
    default_backend http_pool

backend http_pool
    # 在各 app 服务器间轮询;用 GET / 做健康检查
    option httpchk GET /
    http-check expect status 200
    default-server inter 3s fall 3 rise 2
    server app1 127.0.0.1:8081 check
    server app2 127.0.0.1:8181 check
    # 再加:server app3 127.0.0.1:<port> check

每部分的作用,以及实测观察到的现象:

  • 多条 server 行——HAProxy 在新连接间轮询分发到各 UP 服务器(每台加一 行;列表顺序即初始轮转顺序)。
  • option httpchk GET / + http-check expect status 200——健康探测是 HTTP GET /,只有 200 算 UP。方法和服务实际应答的要一致:对 HEAD 返 405、对 GET 返 200 的服务,得用 GET 形式,而不是默认的 HEAD /。
  • default-server inter 3s fall 3 rise 2——每 3s 探测一次,连续 3 次失败标 DOWN,连续 2 次成功恢复。
  • 每台 server 上的 check 为该 server 开启探测。

某后端不可达时,其探测失败,fall 3 后 HAProxy 把它从轮转中摘掉并记日志—— pg logs <name> 里看到:

Server http_pool/app2 is DOWN, reason: Layer4 connection problem, info: "Connection refused", check duration: 0ms. 1 active and 0 backup servers left. ...

而所有前端流量仍稳定返回 200——只流向存活的 app1。等 app2 的端口重新应答, 过了 rise 2 它就自动加回轮转,无需 reload。要增删 server 行,改完 文件跑 pg addon haproxy reload(见热加载):它先用 haproxy -c 校验,再热换 worker、不断开在途连接。后端成员的增减仍以走上面的健康检查那条 最干净(你停掉的 server 无需任何 reload 就被摘出轮转);reload 是给你改动 server 列表本身用的。

并发调优(线程数)

HAProxy 的模型是单进程、多线程——没有 nginx 那种 worker_processes。在 conf-file 模式下,你在自己文件的 global 段里调它(渲染模式不设这些):

  • nbthread N——worker 线程数。对镜像验证过:haproxy -c 接受 nbthread 4。
  • 不设置时的默认——在多处理器主机上,HAProxy 会自动按 CPU 数铺开线程(其 原话:“Threads will automatically be used on multi-processor machines if available”),所以只有要钉死具体数量时才需要 nbthread。
  • cpu-map auto:1-N 0-M——可选,把线程 1…N 绑定到 CPU 核 0…M(已验证)。
  • nbproc 已不存在。 自 HAProxy 2.5 起它被直接拒绝(haproxy -c 报 “nbproc is not supported any more since HAProxy 2.5”)。用线程,不用多进程。
global
    nbthread 4
    cpu-map auto:1-4 0-3
    maxconn 4096
    log stdout format raw local0 info

改它意味着编辑文件后跑 pg addon haproxy reload——master 重新读配置、按新的 线程数起一个新 worker(实测:nbthread 2 → 4 热应用,在途连接不断),因此 不像重跑 install 那样掐流量。这是你文件里的配置项,不是 pgcli 的 flag。

热加载

--conf-file 实例支持无损热加载配置——HAProxy 版的 pg addon nginx reload:

pg addon haproxy reload --name custom

官方镜像本就以 master-worker 模式(-W)运行 HAProxy:pid 1 是 master, 子进程是 worker。reload 给 master 发 SIGUSR2,于是 master 用磁盘上的配置 起一个新 worker、把监听套接字交接过去,再让老 worker 处理完在途连接后才退出 ——不断流,不同于 install 那次有损的容器重建。(这是实测过的行为,不是 纸上推演:一个运行中的实例的 worker 被整段换掉,期间始终返回 200。)

在发任何信号之前,reload 做的正是 install 那道校验门——它不是一个盲目 的信号:

  1. 把你的 --conf-file 重新(原样)复制到挂载的 haproxy.cfg;
  2. 把副本扔进一次性容器跑 haproxy -c(仅检查);
  3. 只有校验通过才发 SIGUSR2。校验不过的文件会带着 HAProxy 自己的诊断 信息中止 reload(退出码非零)、把上一份好配置写回磁盘、运行中的容器 原样不动——所以一份被拒的编辑,之后的重启也绝不会把它捡起来。

前提与范围:

  • 仅 --conf-file 实例。 渲染实例的配置每次 install 都从 --node/--ha 重建、不接受手改,因此它的更新路径仍是 pg addon install haproxy。对它跑 reload 会报这条提示并拒绝。
  • 容器必须在运行。 reload 打的是活的 master;实例停着的话先 pg addon start haproxy --name custom(一次全新的 install 也会把改好的文件 带起来)。
  • reload 是给就地改运行中配置用的。仅仅是维护时临时下线某后端仍完全不需 reload——健康检查会把它摘出轮转;reload 是改 server 列表 / global 旋钮 本身时用的。

同样的热加载也可以通过 install 的 --reload 参数触发:对一个正在运行的 conf-file 实例,install 会校验并热加载而非重建容器,一条命令既(重)配置又应用 、不断流:

pg addon install haproxy --name custom --conf-file /root/haproxy/lb.cfg --reload

不加 --reload 时,install 保持默认的容器重建行为(有损)。对已停止或渲染模式 的实例加 --reload 只打印一条提示、按正常 install 走。

读写分离使用示例

用 --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

conf-file 实例(见自定义配置文件)不消耗端口池: 池既不给它分配,也不登记它文件里的端口。

绑定地址默认 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
      # conf_file: /root/haproxy/lb.cfg   # 自定义模式(见上文;跳过 targets/mode/端口)
      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)

conf-file 实例显示的是来源文件,而不是端口和后端:

  haproxy (name: custom)
    Status:      running
    Mode:        custom (user config)
    Config:      /root/haproxy/lb.cfg

启动与停止

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

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

install 总是重建容器以保证配置生效;start 只启动已有容器(状态不当时会 用磁盘上的 haproxy.cfg 自愈重建)。对 --conf-file 实例, pg addon haproxy reload 能把改好的文件热应用到运行中的容器、免去这次重建 ——见热加载。

开机自启

容器带有 --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 起)之外的 端口,否则会被自动分配器视为已占用。
  • conf-file 安装没过 haproxy -c,但旧配置还在跑。 这是设计行为:被拒的 副本会回滚,容器不动。按 HAProxy 报错指出的行修文件,再重装。若容器确实起来 后又死了(cannot bind socket、配置引用的文件缺失),看 pg logs <name>。
  • reload 报"容器未在运行"。 reload 打的是活的 master,实例必须先起来—— pg addon start haproxy --name <name>(或一次全新的 install,也会把改好的 文件带起来)。
  • reload 因 haproxy -c 报错而拒绝文件。 与 install 同一道门:reload 中止、 上一份好配置写回、什么都没重载——按 HAProxy 指出的行修好再 reload。
  • macOS。 暂不支持——HAProxy 需要主机网络访问 Patroni 成员,podman machine 虚拟机不提供该能力。

16.9 - 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 永不打印密码。要有意揭示它,加 --show-password(追加一行 Root password:),或只把密钥原样打到 stdout 供脚本使用:

pg addon password silo --name store

输出的值即 access-key 密钥,与上面的 Root user: 配对使用。--file <path> 以 0600 权限写入文件而非终端,避免落入 shell 历史与回滚缓冲。

启停

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

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 页的 端点说明。

16.10 - 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 从不打印密码。要有意揭示它,加 --show-password(追加一行 Root password:),或只把密钥原样打到 stdout 供脚本使用:

pg addon password minio --name store
mc alias set local http://127.0.0.1:9000 admin "$(pg addon password minio --name store)"

输出的值即 access-key 密钥,与上面的 Root user: 配对使用。--file <path> 以 0600 权限写入文件而非终端,避免落入 shell 历史与回滚缓冲。

启动与停止

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

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 别名不可用——见上文端点说明。

16.11 - Redis

以 pgcli 插件方式运行 Redis——用于缓存/会话/排行榜/原子计数的独立键值存储,大版本 7 / 8 可选

Redis 是应用需要缓存、会话后端、排行榜(有序集合)或 原子计数时最常选的内存键值存储。pgcli 将其作为独立的顶层插件运行,CLI 表面与对象存储一致(install / start / stop / logs / autostart / remove), 全部通过 pg.yaml 管理。

它是整个插件族里刻意做得最简单的一个:

  • 一个容器、一个端口、一个密码。 requirepass 在首次安装时自动生成并 存入 pg.yaml——不存在"未认证的 Redis"这种可达状态。
  • 版本选择。 首个支持版本选择的插件:--version 7|8 通过内置映射表解析 到固定的 docker.io/library/redis 标签(7 → 7.4.11,8 → 8.10.2,默认 8)。--image 仍可绕过映射表直接指定任意标签。
  • 可调的持久化。 默认数据集是落在 bind 挂载主机目录里的 RDB 快照——重启 后仍在,pg addon remove(不带 --clean-data)后也仍在,重装同名实例即可 复活数据。--aof 在其上叠加写前日志,用于崩溃安全、不驱逐的存储; --save no 可彻底关闭快照(见参数设置)。

在 PostgreSQL 实例旁边做缓存/会话/排行榜/计数就选 Redis;它与 PG 栈无关,可 与任何插件并存,同一主机也能跑多个实例(甚至跨大版本)。

范围之外: Sentinel(自动故障转移)不由 pgcli 管理。Redis 的原生集 群是支持的——--cluster 把实例变成 cluster-enabled 成员,组建由你自己跑 一次 pg redis-cli -- --cluster create(默认纯主分片,用 --cluster-replicas N 可给每个主加从属,见原生集 群)。读副本也支持——--replica-of 把一个实例变成对另一个 主节点的只读跟随(见读副本与已知限制)。

参数设置

pgcli 刻意把 Redis 的参数面做得很小:一个可调项对应一个 flag,最终都落到 redis-server 的原生选项上——经镜像自带 entrypoint 用 argv 覆盖,不生成 redis.conf;一个实例一个容器,直接用纯上游镜像(pgcli 不为 Redis 构建 wrapper,这点与 rustfs 不同)。下表列出每个可调项及其默认值:

Flag / pg.yaml 键 默认值 Redis 选项 含义
--version / version 8 —(决定镜像 tag) 安装的大版本(见版本选择)
--image / image_tag 由版本映射表解析 — 直接指定任意 tag
--port / port 从 redis_start_port 自动分配(基 6379) --port 宿主端口
--listen / listen 0.0.0.0 --bind 监听地址(127.0.0.1 收紧)
--password / password 自动生成 20 字符 --requirepass 认证;始终必须存在(副本的必须与主节点相同)
--maxmemory / maxmemory 不设(无上限) --maxmemory 数据集上限,开启驱逐
--maxmemory-policy / maxmemory_policy allkeys-lru --maxmemory-policy 驱逐策略,仅在设了上限时有意义
--aof / aof 关 --appendonly yes 在快照之上叠加写前日志
--appendfsync / appendfsync everysec(Redis 自身默认) --appendfsync AOF 刷盘强度,仅在开 --aof 时有意义
--save / save Redis 默认计划 --save RDB 快照计划;no 关闭快照
--replica-of / replica_host+replica_port 不设(主节点) --replicaof + --masterauth 成为本机主实例的只读副本(host/port/密码都从配置里取)
--replica-of-host / replica_host — --replicaof 跨机副本的主节点地址(与 --replica-of-port + --password 搭配)
--replica-of-port / replica_port — --replicaof 主节点端口,与 --replica-of-host 搭配
--cluster / cluster 不设(独立实例) --cluster-enabled yes + --masterauth 原生集群成员:非空值即组名——同名归同一集群,且同组成员共用首成员的密码(--cluster create 用一个密码认证全部操作数节点,与"副本密码必须等于主节点"同理)。每个成员还会带上 --masterauth,以便 Redis 在组建时被提升为从属的节点能向主节点认证。与 --replica-of/--replica-of-host 互斥——一个实例要么是集群成员,要么是读副本。pgcli 只装节点,组建由你自己做——见原生集群
--cluster-replicas / cluster_replicas 0(纯主分片) —(仅回显进建议的 --cluster create) 组建步骤里每个主的从属数:摘要按 3*(1+N) 个节点计数,打印的命令以 --cluster-replicas N 结尾。哪些成员成为从属是 Redis 在 --cluster create 时的决定,而非安装时。它是与密码同类的组属性——首成员设定,后续成员继承,显式不一致会被拒绝
--advertise-host / advertise_host 不设(127.0.0.1,单机) --cluster-announce-ip/-port/-bus-port 本成员向对等节点宣告的地址——填局域网 IP,跨机集群必备(Redis 集群总线不支持 NAT/端口重映射)
--data-dir / data_dir <base-dir>/addon/redis/<name>/data --dir /data(主机目录 bind 挂载到 /data) 数据落盘位置
— autostart: false — 开机自启(pg autostart enable --redis)

各旋钮的组合方式:

  • requirepass 始终开启——即使手工往 pg.yaml 里留空 password,也不 存在"未认证的 Redis"这种可达状态。
  • 持久化默认只有 RDB 快照——沿用 Redis 自身的 save 计划,外加停机时补 一份最终快照(--stop-timeout 30 给足时间)与启动时从 dump.rdb 回放。
  • --aof 在快照之上叠加写前日志(--appendonly yes)——耐久、不驱逐的 存储形态。--appendfsync 调节刷盘强度:always(耐久)/ everysec(默 认)/ no(吞吐)。AOF 文件在数据目录的 appendonlydir/ 下,启动时先于 快照回放,两者同开时以 AOF 为准。
  • --save no 关闭快照——与 --aof 搭配即 AOF-only(安装摘要显示 aof only);单独使用则是纯内存实例,重启丢数据(install 会警告)。
  • --maxmemory 隐含驱逐,默认 allkeys-lru——上限的语义是"缓存,超了 就驱逐"。传 --maxmemory-policy noeviction 变成硬顶(超上限写失败而非驱逐 键),或干脆不设 --maxmemory 表示无上限。policy 依赖 --maxmemory、 --appendfsync 依赖 --aof——CLI 在拉镜像/建容器之前就拒绝这类组合。
  • --replica-of 造只读副本——见读副本。持久化/驱逐旋钮与角色 正交:副本也能设 --maxmemory 或开 --aof,跟主节点一模一样。
  • --cluster 造集群成员,与 --replica-of/--replica-of-host 互斥—— 一个实例只有一个角色。同 --cluster 组成员自动共用首成员的密码(后续成员 带不同的 --password 会在拉镜像/建容器之前被拒绝);每主从属数由 --cluster-replicas N 同样按组继承。见原生集群。

Redis/Valkey 的选项空间远比这张表大,pgcli 有意不包装多节点控制面 (Sentinel、自动故障转移),集群编排也一样:pgcli 装 cluster-enabled 节 点,但从不替你跑 --cluster create/add-node/resharding/缩容——见原生集 群与已知限制。读副本是支持的(读副本), pgcli 留下的只是编排那一层:主节点挂了不自动提升副本。引擎本身倒是可以用 --image 变通:pg addon install redis --image docker.io/valkey/valkey:8 跑的 是 Valkey(Redis 的分支),大版本反解析为 8。

版本选择

--version 选大版本;映射表把每个大版本钉到一个固定的补丁标签:

大版本 默认镜像标签 选择方式
7 docker.io/library/redis:7.4.11 pg addon install redis --name cache --version 7
8 docker.io/library/redis:8.10.2 pg addon install redis --name cache(默认 8)

解析规则:

  • --version 7(或 8)→ 表中 tag 存入 image_tag,大版本存入 version;
  • --image <tag> → 以你的 tag 为准;大版本从 tag 反解析出来仅供展示 (redis:7.2.4 → 7),解析不出则留空(展示时原样显示 tag);
  • 两者都不给 → 大版本 8;
  • 未知的 --version → 报错并列出可用大版本。

两个大版本可在同一主机并存——tag 不同、端口各自从同一池分配。表中 tag 跟随 各大版本的最新补丁,与 docs/images.md 的条目一起升级。

安装

# 默认:大版本 8,实例名 "redis",端口从 6379 自动分配
pg addon install redis

# 命名的大版本 7 缓存实例,带内存上限(到上限即驱逐)
pg addon install redis --name legacy --version 7 --maxmemory 256mb

# 收紧为仅回环监听(默认是全网卡)
pg addon install redis --name cache --listen 127.0.0.1

# 固定密码(跨重装保持稳定)与固定端口
pg addon install redis --name sessions --password 'S3ssions!' --port 6379

# 本机 "sessions" 的只读副本(借用它的大版本、密码与端点)
pg addon install redis --name sessions-ro --replica-of sessions

安装摘要打印客户端所需的一切:

✓ redis installed: "cache"
  Container:  pgcli-redis-default-cache
  Version:    8
  Image:      docker.io/library/redis:8.10.2
  Data:       ~/pg/addon/redis/cache/data
  Address:    0.0.0.0:6379
  Role:       master
  Persistence: rdb snapshots (default save schedule)

  Password:    <generated>

  Client:      pg redis-cli ping
  Raw DSN:     redis://:<password>@0.0.0.0:6379/0
  NOTE: listening on every interface; the password above is the only gate.
        loopback-only: pg addon install redis --name cache --listen 127.0.0.1 --force

安全模型。 listen 默认 0.0.0.0——Linux(host 网络)下端口发布在 所有网卡上,requirepass 是唯一的闸门。这对"应用跑在另一台机器"很方便,但 也意味着密码是分量的:用 --listen 127.0.0.1 可把实例收紧为仅回环。 pg addon list 与日志从不打印密码;需要时显式揭示——pg addon password redis --name cache 或 pg addon list --show-password(也可直接读 pg.yaml)。

重复执行 install 是幂等的:运行中的容器原样不动,停止的容器会被拉起。要让 改动的端口/监听/密码/镜像生效,加 --force(容器按配置重建;数据目录不受影 响)。持久化类旋钮(--aof/--appendfsync/--save/--maxmemory-policy)同 样靠 --force 生效。

使用 Redis

pg redis-cli

pg redis-cli 用该实例镜像起一个一次性容器跑 Redis 官方客户端——宿主机不 用装任何东西,也不用输密码:

pg redis-cli ping                              # PONG
pg redis-cli set session:42 '{"user":"alice"}' # 认证自动注入
pg redis-cli get session:42
pg redis-cli lrange queue 0 -1                 # 负数索引原样透传
pg redis-cli --name legacy zrevrange leaderboard 0 9

本地目标是 --name <实例>,省略时取按名字排序的第一个 redis 实例(装了多个时, 实际选中的目标会提示在 stderr)。pgcli 自己的 flag 只在命令词之前解析;命令 词之后一切原样转发给 redis-cli——负数索引、经 -- 的选项 flag 都靠这个。

连接远程端点 —— --host/--port 覆盖客户端连去哪里,无需把远程注册成 插件:

# 复用本地实例的镜像 + 密码,但连到远程主机(如副本)
pg redis-cli --host 10.0.0.7 ping
pg redis-cli --host 10.0.0.7 --port 6380 --name cache info

# 本机没有任何实例:用默认大版本镜像,密码走 REDISCLI_AUTH
REDISCLI_AUTH=s3cret pg redis-cli --host 10.0.0.7 dbsize

要连远程用这两个,而不是 redis-cli 自己的 -h/-p:后者落在命令词之后,会 被当成命令参数(ping -h 10.0.0.7 报错),而连接用的 -h/-p 由 pgcli 负 责注入。REDISCLI_AUTH(redis-cli 原生环境变量)若已设置,任何模式下都覆盖 存储的密码:

REDISCLI_AUTH=otherpass pg redis-cli dbsize

应用连接

任何 Redis 客户端都可用;安装时打印的 DSN 形如 redis://:<password>@<host>:<port>/0。需要 TLS 则要在前面架 stunnel 之类 ——见已知限制。

读副本

--replica-of 把新实例变成已有主实例的只读副本:它持续拉取主节点的写入 并提供读,对它的写会被 READONLY 拒绝。这是读扩展与数据冗余,不是高可 用——pgcli 不跑 Sentinel,主节点挂掉不会自动提升副本(见[已知限制](#已知限 制))。

# 主节点(可写)
pg addon install redis --name cache

# 同机副本:借用主节点的大版本、密码与端点
pg addon install redis --name cache-ro --replica-of cache

本机的 --replica-of 一切从配置解析:副本采用主节点的 version(跨大版本复制 不受支持——不一致会立刻报错)、它的密码,以及端点 127.0.0.1:<主节点端口>。 这一个密码随后同时服务 requirepass(副本自己的客户端用它认证)与 --masterauth(副本向主节点认证),所以 --password/--maxmemory/--aof 等与主节点完全一致。存储下来的 replica_host/replica_port 意味着即使主节点 已从配置中删除,pg addon start 也能重建出同样的 --replicaof argv。

跨机主节点用显式形式——pgcli 查不到远程主节点的密码,得由你给(必须与主 节点相同):

pg addon install redis --name cache-r2 \
    --replica-of-host 10.0.0.7 --replica-of-port 6379 --password <master-password>

pg addon list 与安装摘要都会打印 Role: 行——master,或 replica of <host>:<port> (read-only);pg redis-cli --name cache-ro info replication 在(携带 RDB 的)首轮同步完成后显示 role:replica 且 master_link_status:up。读流量指向副本、写流量指向主节点,例如用各自的 DSN:

# 读流量 -> 副本
redis://:<password>@127.0.0.1:<副本端口>/0

要在运行时把一个副本提升为独立主节点(这类手工故障转移正是 pgcli 有意留给你的 部分),先解除复制,再——若希望重启后依然成立——把角色从配置里去掉:

pg redis-cli --name cache-ro replicaof no one   # 停止跟随主节点
pg addon remove redis --name cache-ro          # 保留数据,忘掉角色
pg addon install redis --name cache-ro         # 作为普通主节点重装

原生集群

--cluster 把实例变成 Redis 原生集群 的成员——按 16384 个哈希槽分片,而 非对某个主节点做只读复制。这是 pgcli 唯一支持的多节点拓扑;[Sentinel](#已知 限制) 不支持。

pgcli 在这件事上的角色刻意很窄:装 cluster-enabled 的节点,但从不替你组 建。这本来就是 Redis 自己的两步流程,不是 pgcli 发明的花活——集群 = 若干 cluster-enabled yes 的服务器 + 一条对它们整体跑一次的 redis-cli --cluster create。把这两步拆开,pgcli 就不必去猜"这是不是我等着的最后一个成员、现在 该不该把集群拉起来"(那种 etcd 式的自动 bootstrap 状态机);组建永远是你 的决定,跑一次,跟你对着裸 redis-server 做的事一模一样。

# 1. 装成员——一个 --cluster <组名> 把每台变成 cluster-enabled 节点;同名意味着
#    "同一个集群"。最少 3 个主节点(Redis 自己的法定人数规则)。纯主分片是默认
#    (装 3 个,--cluster-replicas 0);要每台主带一个从属,就装 3*(1+N) 个成员,
#    在第一个安装时传 --cluster-replicas N(其余自动继承)——见第 2 步。
pg addon install redis --name n1 --cluster app --maxmemory 10mb
pg addon install redis --name n2 --cluster app --maxmemory 10mb
pg addon install redis --name n3 --cluster app --maxmemory 10mb

同组成员自动共用首成员的密码:--cluster create 用一个 -a(这里是 pg redis-cli 注入的那一个 REDISCLI_AUTH)认证操作数里的每个节点,所以 必须一个密码打通全组——和"读副本的密码必须等于主节点"是同一条 规则。后续成员带不同的 --password 会在拉镜像/建容器之前被拒绝;不带 --password 则自动继承组内的值。每个成员还会带上 --masterauth(与 requirepass 相同的共享密码):--cluster create 时 Redis 可以把任意节点提升 为从属,从属需要 --masterauth 才能向主节点认证——没有它节点就会陷入永久 的重连风暴(master_link_status:down,CPU 满载)。--cluster 与 --replica-of/--replica-of-host 在一次安装里互斥——一个实例要么是集群成员, 要么是读副本,绝不兼得。

# 2. 一次组建,走 pg redis-cli 的透传(pgcli 从不替你跑这一步)
pg redis-cli --name n1 -- --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 --cluster-replicas 0

三个 addr:port 操作数就是每个成员的 PeerAddr:port——单机集群里是 127.0.0.1:<端口>,跨机集群里是每个成员的 --advertise-host:<端口>(见下 文)。安装摘要已经会打印出这条命令,按当下配置好的 --cluster app 成员现 场拼装(比如装到 n3 时看到 pg redis-cli --name n3 -- --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 --cluster-replicas 0),复制 粘贴即可用,不需要手工拼:

-> NOTE: native-cluster member of "app" — it comes up cluster-enabled but INCOMPLETE
   until you assemble the group once (see the "Cluster:" line below). pgcli does not
   run --cluster create for you.
...
  Cluster:     "app" — 3 masters configured. Assemble once (pgcli does not run this):
               pg redis-cli --name n3 -- --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 --cluster-replicas 0
               (bus port = client+10000; for cross-host peers open it on the firewall, and give each member --advertise-host)

(安装摘要/列表的输出与英文文档格式一致——pg CLI 的输出本身是英文,两种 语言下字节相同。)

--cluster-replicas 0(默认)就是"纯主分片"——每个成员各管一段槽,且都没有 从属。要让每台主带从属(这样某台主挂掉时它的从属能自动顶上,槽区间不至于随 之失联),在第一个安装时传 --cluster-replicas N 并装够成员数:每台主需要它 自己加它的 N 个从属,所以总数是 3*(1+N)——N=1 时是 6 个成员:

pg addon install redis --name n1 --cluster app --cluster-replicas 1 --maxmemory 10mb
pg addon install redis --name n2 --cluster app --maxmemory 10mb   # 继承 replicas=1
# …装到 6 个成员,然后:
pg redis-cli --name n1 -- --cluster create \
    127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 \
    127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1

哪些成员被 Redis 选为主、哪些选为从,是它在 --cluster create 时的决定(能 跨主机均衡时它会尽量均衡),所以 pgcli 的实例命名刻意是角色中立的;从属重启 后会从自己的 nodes.conf 重新接上主——角色与 masterauth 配对都能跨重启保留, 无需重新 create。上面的安装摘要在引入从属后会从"3 masters configured"切换为 按节点计数(“6 nodes configured”),并把组内的 --cluster-replicas N 回显 进建议命令;它绝不会把从属所在组的节点叫成 master。

组建之后

连到 cluster 成员的 pg redis-cli 自动是集群感知客户端:pgcli 会注入 redis-cli 自己的 -c,get/set 落到"错的那个节点"时会跟着 MOVED 重定 向跑到真正持槽的那台——不用你记 -c,随便指哪个 --name 都一样:

pg redis-cli --name n1 set foo bar      # 可能实际由 n3 应答(MOVED 跟随)
pg redis-cli --name n2 get foo          # 同一个 key,再跟一次重定向
pg redis-cli --name n1 -- --cluster check 127.0.0.1:6379   # 管理型子命令,不归 -c 管

最后一行是例外,不是规则:命令是 redis-cli 自己的 --cluster <子命令> 时 (这类子命令会按操作数自己拨到目标节点),pgcli 会抑制 -c 注入——它是 组建/管理面,不是数据命令。

上面的自动 -c 依赖 pgcli 从 pg.yaml 读到一个本地集群成员(即 --name 路径)。当你从一台没配该 addon 的机器用 --host/--port 远程连集群时, pgcli 无从知道目标是不是集群节点,于是什么都不注入——一个未跟随重定向的 get/set 落到错误的节点,只会返回 MOVED ...。这时你自己透传 redis-cli 的 -c(它是 redis-cli 的 flag,所以放在 -- 透传之后、命令字之前):

export REDISCLI_AUTH=<组密码>
pg redis-cli --host 10.10.0.158 --port 6379 -- -c get foo   # 替你跟随 MOVED
pg redis-cli --host 10.10.0.158 --port 6379 -- -c set foo bar

-c 对写和读一样重要:不带它,指向非持槽节点的 set 会返回 MOVED 且不会 生效,那个 key 根本不存在。写的时候也带上 -c,任意单个节点即可服务整个 键空间。

也可以不用环境变量、改用 redis-cli 自己的 -a 传密码——它同样是连接选项, 所以放在同一个透传位置、命令字之前:

pg redis-cli --host 10.10.0.158 --port 6379 -- -a <组密码> -c get foo

与 REDISCLI_AUTH 相比有两点区别:redis-cli 每次都会打印一条安全警告(-a/ -u 写在命令行上,同机用户可以用 ps 看到),而把 flag 放到命令字之后 (get foo -a …)会报 “wrong number of arguments”——它会被当成命令参数。除 非是敲一条就跑,否则优先用环境变量;两者在认证上完全等价。

如果你的客户端完全无法做成集群感知的,就在集群前放一个 Predixy 代理: 它自己跟随重定向,对外只暴露一个普通的 redis:// 端点。

集群总线是每成员的第二个端口:永远是 client + 10000(Redis 的固定 规则,比如 6379→16379)——不从 redis_start_port 池分配、不写入 pg.yaml;pg addon list 会在组名旁把它一并显示出来:

    Role:        cluster member of "app" (127.0.0.1:6381)
    Cluster:     app (bus 16381)

拓扑会自愈:每个节点的 --cluster-config-file nodes.conf 落在自己 --dir /data 的 bind 挂载里,所以 pg addon stop/start(或主机重启)后 整个集群原样回来,不需要再 create——与本仓库 autostart 的"只启动"模型一致。

两条命令可以直接检查集群健康,都走透传(是的,-c 那条是客户端侧 flag,所以放在 -- 之后):

# 单个成员眼里的全集群——找 cluster_state:ok / cluster_size:N
pg redis-cli --host <ip> --port <port> -- -c cluster info
# 整组全面体检(slot 覆盖、副本、链路)——redis-cli 的管理面
pg redis-cli -- --cluster check <ip>:<port>

跨机成员:--advertise-host

单机集群默认用 127.0.0.1 拨对端,不需要额外 flag。Redis 集群总线不支 持 NAT 或端口重映射,所以跨机成员必须显式宣告"对端实际能到达"的地址:

# 在 10.10.0.158 上——首成员决定全组的那一个密码,装完读出来:
pg addon install redis --name n1 --cluster app --advertise-host 10.10.0.158
PW=$(pg addon password redis --name n1)
# 在 10.10.0.159、10.10.0.160 上——把同一个密码带过去:
pg addon install redis --name n2 --cluster app --advertise-host 10.10.0.159 --password "$PW"
pg addon install redis --name n3 --cluster app --advertise-host 10.10.0.160 --password "$PW"
# 一次组建(从能连通三台的任意一台执行)——操作数是各成员的宣告地址+各自端口
# (这里每台一个成员,各主机自己的端口池都从 6379 起,所以三台都是 :6379):
pg redis-cli --name n1 -- --cluster create \
    10.10.0.158:6379 10.10.0.159:6379 10.10.0.160:6379 --cluster-replicas 0

--advertise-host 会往节点 argv 里加 --cluster-announce-ip/-port/-bus-port, 并且把 PeerAddr(就是上面拼 --cluster create 操作数用的地址)从 127.0.0.1 换成这个地址。你自己还得做对两件事:主机间防火墙放行每个成员 的总线端口(client+10000——默认 6379 时即 16379)——不只是客户端端口 ——并且组内每个成员都要带这个 flag,不能只给新加入的补。

后续每台主机上的 --password "$PW" 不是摆设:共享密码的继承只在同一份 pg.yaml 内生效——单机上,后续成员自动采用组内密码;跨机上每台主机有自己 独立的配置文件,各自生成各自的秘密。因此跨机集群必须在每台主机上显式钉同 一个密码,密码从首成员用 pg addon password 读出来。

(--cluster create 用这一个密码认证操作数里的每一个节点,不一致会以逐节点 认证错误的形式暴露出来。)

状态: 跨机集群已在三台主机上端到端验证(--advertise-host → --cluster-announce-*、跨机 --cluster create、跨机 MOVED 重定向,以及各 自主机 stop/start 后的自愈)。单机流程另有自动化 e2e (test/addon/test_redis_cluster.sh),在其中覆盖两种形态:纯三主组,以及 3 主 × 1 从组(缺失 --masterauth 的重连风暴正是这样被抓出来的)。多机场 景目前还是手工流程,尚未进测试套件。

端口

每个实例从 Redis 自己的池 redis_start_port(默认 6379)取一个端口 ——与对象存储共用的 minio_start_port 池是独立游标,所以 redis、minio、 rustfs 实例互不冲突:

pg addon install redis  --name cache      # 6379
pg addon install redis  --name sessions   # 6380

--port 可钉死某个端口;自动分配会跳过显式占用的与主机上已监听的。原生集 群成员也一样——集群总线端口永远是 Redis 自己的 client+10000 (由客户端端口推导出来),不占这个池。

配置

实例位于 pg.yaml 顶层 addons.redis 映射下:

namespace: default
redis_start_port: 6379     # Redis 自己的池,与 minio_start_port 相互独立
addons:
  redis:
    cache:
      container_name: pgcli-redis-default-cache
      name: cache
      version: "8"                       # 记录/展示用的大版本
      image_tag: docker.io/library/redis:8.10.2
      # data_dir: /srv/redis-cache       # 省略则为 <base-dir>/addon/redis/cache/data
      listen: 0.0.0.0                    # 默认(全网卡);127.0.0.1 收紧
      port: 6379
      password: <generated>              # 首次安装写入
      # maxmemory: 256mb                 # 可选上限
      # maxmemory_policy: noeviction     # 仅在设了上限时有效;默认 allkeys-lru
      # aof: true                        # 在快照之上叠加写前日志
      # appendfsync: always              # 仅在开 aof 时有效;默认 everysec
      # save: "900 1 300 10"             # RDB 计划;"no" 关闭快照
      # replica_host: 127.0.0.1          # 由 --replica-of 写入:本实例是读副本
      # replica_port: 6379               # 主节点端口,与 replica_host 搭配
      # cluster: app                     # 由 --cluster 写入:本实例是原生集群成员
      # cluster_replicas: 1              # 由 --cluster-replicas 写入:每个主的从属数,供 --cluster create 用(0=纯主分片;全组共用)
      # advertise_host: 10.10.0.158      # 由 --advertise-host 写入:对等节点可见地址(仅跨机)
      autostart: false                   # pg autostart enable --redis --name cache

改 listen、port、password、maxmemory/maxmemory_policy、 aof/appendfsync/save、replica_host/replica_port、 cluster/cluster_replicas/advertise_host、image_tag/version 或 data_dir 后, 下一次 pg addon install redis --name cache --force(或用对应 flag)生效。副本角色 与集群角色通常分别由 --replica-of* / --cluster(含 --cluster-replicas)/ --advertise-host 这组 flag 设定,而非手改——见读副本与原生集群。

列表

pg addon list
Infra add-ons (redis):
  redis (name: cache)
    Status:      running
    Version:     8
    Address:     0.0.0.0:6379
    Role:        master
    Auth:        on (requirepass)
    Maxmemory:   256mb (allkeys-lru)
    Persistence: rdb + aof (default save schedule, appendonly yes)
    Data:        ~/pg/addon/redis/cache/data
    Image:       docker.io/library/redis:8.10.2
    Container:   pgcli-redis-default-cache
    Client:      pg redis-cli --name cache ping

pg addon list 从不打印密码。要有意揭示它,加 --show-password(会给每个 Redis 实例追加 Password: 与一条可直接粘贴的 Raw DSN: 行——对象存储则追加 Root password: 行),或只把某一个实例的值原样打到 stdout 供脚本使用:

pg addon password redis --name cache
export REDISCLI_AUTH="$(pg addon password redis --name cache)"

pg addon password 与其它子命令一样接受 --name(缺省即插件自身名字),并支持 --file——把值以 0600 权限写入文件而非终端,避免落入 shell 历史与回滚缓冲。

启停

pg addon start redis --name cache
pg addon stop  redis --name cache

stop 发优雅 SIGTERM——配合 --stop-timeout 30,Redis 退出前会写完最后一 份 RDB。start 只拉起已有容器(状态不当时会按配置自愈重建)。

开机自启

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

pg autostart enable --redis --name cache

Redis 自启是只启动(先 install),并从数据目录回放 RDB 快照,数据集跨重 启保留。Redis 与 PostgreSQL 栈无关,开机 pg start --autostart 把它放在最 后启动。

移除

pg addon remove redis --name cache               # 容器 + 配置项;数据保留
pg addon remove redis --name cache --clean-data  # 同时删除数据目录

不带 --clean-data 时主机数据目录(含 dump.rdb)原地保留——重装同名实例 数据集复活。--clean-data 删除它,并在 rootless podman 的从属 uid 让 Redis 写的文件对宿主用户不可删时,经 podman unshare 兜底。清空的每实例父目录会 顺手剪掉;自定义 data_dir 的父目录绝不动。

日志

pg logs addon redis --name cache        # 最近 50 行
pg logs addon redis --name cache -f     # 跟随

故障排查

  • 裸客户端报 NOAUTH Authentication required ——预期行为:每个实例都有 requirepass。用 pg addon password redis --name cache 取,或直接用会注入密 码的 pg redis-cli。
  • maxmemory 驱逐超出预期 —— 设了 --maxmemory 上限后默认策略是 allkeys-lru(到上限就驱逐任何 key:是缓存,不是硬存储)。要把上限变成 硬天花板就传 --maxmemory-policy noeviction(超限后写入报错而非驱逐),或 干脆不设 --maxmemory(无上限)。
  • AOF 未回放最新写入 —— --appendfsync everysec(默认)在硬崩溃时可能 丢最后一秒的写入;承受不起就用 --appendfsync always。优雅 pg stop 总会 刷盘,所以只有非正常 kill 才会踩到。
  • 安装时报镜像不存在 —— 两个大版本都是按需拉取的纯 docker.io/library/redis 镜像;隔离网络的主机先 podman load tar 包 (目录与导出步骤见仓库内 docs/images.md),之后会跳过拉取。
  • 手工改过、不在表中的 version —— install 报错并提示用 --image, 不会静默改写你的值。
  • 端口已被监听 —— 自动分配会扫描主机监听并跳过;要稳定端口号就用 --port 钉死。
  • 副本拒绝写入(READONLY) —— 这是角色本身,不是故障:写请发往主节点。 要让它成为独立主节点,见读副本。
  • 刚建好的副本短暂显示 master_link_status:down —— 首轮(携带 RDB 的) 全量同步期间正常;redis 8 经 rdbchannel 传快照时还会再重连一次链路。看 info replication,最终会稳定为 up 且 role:replica。

已知限制

  • 无自动故障转移 —— 读副本是支持的(见读副本),原生集群分片 也支持(见原生集群),但 HA 控制面不支持:Sentinel 不在 范围内,集群编排也一样——pgcli 装 cluster 成员,但从不替你组建、扩容、 再平衡。副本给你的是读扩展与一份冗余数据,不是一对能自愈的节点——主节点挂 掉时由你自己提升副本(replicaof no one,见上)。没有副本的单实例是单点: 可以用 --aof 让它在磁盘上耐久,但无法故障转移到其它节点。
  • 原生集群的组建与再整形仍为手工 —— --cluster 装 cluster-enabled 节点, --cluster-replicas N(回显进建议命令)可让每台主带从属,某台主挂掉时它的从 属能自动顶上。pgcli 不替你做的事是:跑 --cluster create/add-node/ resharding/del-node;pg addon remove 删成员就是纯删除,不会搬槽——要缩 容或改拓扑请先在 Redis 层把它的槽挪空、把节点忘记,再删。另注意:纯主分片 (--cluster-replicas 0)没有可提升的从属——一台主挂掉,它负责的那段槽就随 之不可用,直到你介入。
  • 跨机集群(--advertise-host)为手工流程、未进 e2e 套件 —— flag、它产生 的 --cluster-announce-* argv、防火墙/总线端口规则都已实现、单元测过,并 且已在三台主机上手工验证(组建、跨机 MOVED 重定向、自愈均通过)。单机 流程(纯主分片与 3 主 × 1 从)有自动 e2e;跨机带从属尚无自动 e2e,但它依赖 的正是让单机从属连上的那同一份逐成员 --masterauth。见[原生集群](#原生集 群)里的状态说明。
  • 无 TLS —— Redis 7 的部分构建支持原生 TLS,但这里未接入;按可信网络对 待链路并依赖 requirepass,或干脆 --listen 127.0.0.1 仅回环。跨机集群意 味着成员间的总线流量同样是明文。
  • macOS 代码完备、未实测 —— bridge 路径(发布端口、pg redis-cli 走 host.containers.internal)与对象存储同构;尚未在 Mac 上跑过。
  • 默认持久化是 RDB 粒度 —— 崩溃会丢失上次快照之后的写入。这个缺口要紧时 启用 --aof(everysec 下最多仍丢一秒;always 可封死);缓存/会话数据 接受仅快照即可,耐久数据请放 PostgreSQL。

16.12 - Predixy

将 Predixy 作为 pgcli 插件运行——Redis 协议代理,把原生集群暴露成一个普通的 redis:// 端点,集群无感知的客户端无需处理 MOVED

Predixy 是一个高性能 Redis 协议代理。 pgcli 把它做成一个独立的顶层插件:它接住一个 Redis 原生集群 (3 个以上节点、16384 个 slot、MOVED 重定向),再把它交回应用侧——形式是 一个普通的 redis:// 端点。客户端不需要懂集群:不加 -c、不处理重定向, 连上就能读写。

平台支持: 与 HAProxy、Redis 一样,Predixy 插件需要 Linux host 网络 才能拨通后端节点;macOS 不支持。

工作原理

Predixy 在客户端端口上说 Redis 协议,并到 --backend 列出的每个集群节点保持 持久连接。请求按 slot 路由:如果落地的节点并不持有该 key 的 slot,Predixy 自己 跟随 MOVED、在正确的节点上重发命令——对客户端完全透明,客户端永远看不到重定向。

pgcli 为每个实例渲染一个配置文件 <base-dir>/addon/predixy/<name>/predixy.conf,并以文件级只读 bind mount 盖到镜像内置的 /usr/local/predixy/conf/predixy.conf 上。渲染文件里的 Include license.conf 相对该文件所在目录解析,因此镜像里那份签名过的 sibling 仍然可见——pgcli 从不分发也不管理 license(见已知限制)。

一把密码,两侧复用

代理夹在两方已认证者之间——它自己的客户端,以及它后面的集群——pgcli 用 同一把密码覆盖两侧:

  • Authority { Auth "<pw>" { Mode admin } }——客户端必须用这把密码 AUTH 才能用代理;
  • ClusterServerPool { Password "<pw>" }——Predixy 用它向集群节点认证。

之所以成立,是因为这把密码就是被代理集群自己的 requirepass:客户端本来就 认它,节点本来就接受它。直接从 redis 插件里取出来:

PW=$(pg addon password redis --name n1)

于是一把集群成员密码同时也守住代理端点——不必再同步第二个密钥。(想要一把独立 的客户端密码,那是另一种 Authority 形态,pgcli 不渲染;见 已知限制。)

安装

--backend 与 --password 都必填。--backend 是集群的完整节点列表—— pgcli 从不从本机 redis 插件推导它,因为被代理的集群常常在别的机器上;拓扑只有 你知道,所以由你来列。可重复传该 flag,也可一次逗号分隔;重复节点会被拒绝。

# 单机集群(成员在 127.0.0.1:6379/6380/6381)
PW=$(pg addon password redis --name n1)
pg addon install predixy --name proxy \
  --backend 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381 \
  --password "$PW"

# 跨机集群——每台机器一个成员,都用默认端口
pg addon install predixy --name app-proxy \
  --backend 10.10.0.158:6379,10.10.0.159:6379,10.10.0.160:6379 \
  --password "$PW" --workers 2

摘要会打印客户端应当指向的那一个端点:

✓ predixy installed: "proxy"
  Container:    pgcli-predixy-default-proxy
  Image:        ghcr.io/mars-base/pgcli/predixy:7.0.1-alpine
  Listen:       0.0.0.0:7617
  Workers:      2
  Backends:     3 (127.0.0.1:6379, 127.0.0.1:6380, 127.0.0.1:6381)
  Config:       ~/pg/addon/predixy/proxy/predixy.conf

  Password:     <那把密码>
  Raw DSN:      redis://:<password>@0.0.0.0:7617/0

  Client:       redis-cli -h 0.0.0.0 -p 7617 -a '<password>' ping
  The proxy absorbs MOVED: an ordinary redis client — no -c, no
  redirect handling — reads and writes the whole cluster key space
  through this one endpoint.

所有校验都在拉取或创建任何东西之前完成:缺 --backend、条目格式不对(无端口、 端口非数字或越界、空条目)、节点重复、缺 --password、密码里含 " 或换行 (Predixy 的引号字符串语法无法转义它们)、--workers < 1,都会快速失败并给出清晰提示。

重跑 install 与别处一样是幂等的:运行中的容器原样保留(只重写配置),停着的容器 被启动,--force 才会重建容器,从而让改动的 port/listen/password/backend 生效。 后端列表每次整体替换,所以集群扩容(再装 redis 成员、--cluster add-node …) 之后,用更新后的完整列表重跑同一条 install 即可。

Workers

--workers N 设置 Predixy 的 WorkerThreads(默认 1)。每个 worker 是一个 忙轮询事件线程;在专用于代理的多核机器上,N 约等于你愿意投入的核心数,吞吐大致线性扩展。

使用代理

任何 Redis 客户端连到代理端口、带上集群密码,表现就如同在跟一个单节点 Redis 说话—— 整片 keyspace 都通过这一个端点服务:

redis-cli -h 127.0.0.1 -p 7617 -a "$PW" set foo bar    # OK — 没有 MOVED,不用 -c
redis-cli -h 127.0.0.1 -p 7617 -a "$PW" get foo        # "bar"

(DSN 形态:redis://:<password>@<listen>:<port>/0。)

用 pg redis-cli

pg redis-cli 经 --host/--port 连到代理——没有指向 Predixy 实例的 --name 路径(--name 只解析 redis 插件):

# 在代理自己所在的主机上
pg redis-cli --host 127.0.0.1 --port 7617 set foo bar
pg redis-cli --host 127.0.0.1 --port 7617 get foo

# 从没有本地 redis 插件的主机:默认大版本镜像,密码走环境变量
REDISCLI_AUTH=$PW pg redis-cli --host 10.10.0.158 --port 7617 ping

本机若配了 redis 插件,则复用它的镜像与密码(见 redis.md 里的 pg redis-cli)。该插件恰好是集群成员时,pg redis-cli 照旧注入 -c——穿过 代理也无害,因为每一个 MOVED 都被代理自己吸收,客户端根本看不到。这正是代理的 意义所在:同样这些普通客户端命令,无论 -c 在不在,都能工作。

跨机

已端到端验证:代理装在一台机器上,前端是三主机集群 (--backend 10.10.0.15x:6379,…),而另一台机器上的普通无集群感知客户端穿过它连接—— slot 属于远端节点的 SET 被吸收并落盘,之后能从那个节点自己的端口读回。唯一的条件是可达: 代理所在主机必须能拨通每个 --backend 节点的客户端端口(集群总线端口是成员之间的,不是代理的)。

端口

每个实例从 Predixy 自己的端口池取一个端口,predixy_start_port(默认 7617, 即镜像 EXPOSE 的端口)——与 Redis 的 redis_start_port 池分开,所以代理和它前端的 集群永不冲突。--port 可固定实例端口;自动分配会跳过已被占用的。

bind 地址默认 0.0.0.0(AUTH 密码是唯一的门);--listen 127.0.0.1 收紧为仅回环。

配置

实例位于 pg.yaml 顶层的 addons.predixy 映射下:

namespace: default
predixy_start_port: 7617
addons:
  predixy:
    proxy:
      container_name: pgcli-predixy-default-proxy
      name: proxy
      image_tag: ghcr.io/mars-base/pgcli/predixy:7.0.1-alpine
      listen: 0.0.0.0
      port: 7617
      workers: 2
      backend:
        - 127.0.0.1:6379
        - 127.0.0.1:6380
        - 127.0.0.1:6381
      password: <集群的 requirepass>            # 由 install 写入,从不自动生成
      autostart: false                        # pg autostart enable --predixy --name proxy

渲染出的 predixy.conf 用 Predixy 自己的语法承载同样信息;手改它会被下次 install 抹掉—— 要调整请改 pg.yaml(或对应 flag)再重装。

列表

pg addon list
Infra add-ons (predixy):
  predixy (name: proxy)
    Status:      running
    Address:     0.0.0.0:7617
    Workers:     2
    Backends:    3 (127.0.0.1:6379, 127.0.0.1:6380, 127.0.0.1:6381)
    Client DSN:  redis://:<password>@0.0.0.0:7617/0
    Image:       ghcr.io/mars-base/pgcli/predixy:7.0.1-alpine
    Container:   pgcli-predixy-default-proxy
    Config:      ~/pg/addon/predixy/proxy/predixy.conf

pg addon list 会隐去密码;--show-password 才显示,而 pg addon password predixy --name proxy 单独裸打印这一个值以供脚本使用。

启停

pg addon start predixy --name proxy
pg addon stop  predixy --name proxy

start 只启动已存在的容器(状态不当时从磁盘上已渲染的配置重建以自愈);若该文件缺失它会 报错——重跑 install 生成。

开机自启

pg autostart enable --predixy --name proxy

开机是仅启动(先 install),且 boot service 把 Predixy 排在 Redis 之后, 于是代理拨号时被代理成员已经在起来的路上。见开机自启。

移除

pg addon remove predixy --name proxy

停止并移除容器、删除渲染出的配置目录、去掉 pg.yaml 条目。被代理的集群不受影响—— 代理只是它的一个消费者。

日志

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

启动横幅会报告实际生效的构建信息(Workers:2 等)——这是确认 --workers 是否生效的地方。

已知限制

  • 单一 admin Authority 用户——渲染器只发一条 Auth "<pw>" { Mode admin }:代理的所有客户端共用一把密码、拥有全部权限。 按用户认证、只读用户、keyspace 限制都不在渲染范围内(Predixy 本身支持更多; pgcli 的能力面到此为止)。
  • 仅集群池——ClusterServerPool 是唯一的后端类型;不配置 Sentinel 或单机池。
  • license 烤在镜像里——ghcr.io/mars-base/pgcli/predixy:7.0.1-alpine 内置的 免费版 license 把客户端数上限设为 ClientLimit 128,并于 2026-12-31 到期; pgcli 从不分发或轮转它——由镜像构建负责。到期后重新 build 镜像即可(Makefile 目标 已经会换上当前 license)。
  • 客户端侧不做 TLS(仅密码门;与 Redis 本身一样,把 listen 收回回环或放在可信网络)。
  • 仅 Linux——拨后端需要 host 网络;macOS 不支持(快速失败)。

故障排除

  • 客户端收到 ERR invalid password——客户端必须 AUTH 那把密码;用 pg addon password predixy --name proxy 核对(它应当等于集群的密码, pg addon password redis --name n1)。
  • 容器立即退出——Predixy 会当场拒绝它的配置:单独一行的 {,或含 "/换行而破坏 引号的密码。pgcli 在写文件前已校验这两项,出现这种情形说明有人手改过 predixy.conf——重装即可。
  • 命令挂起或每个 key 都报错——某个后端节点从代理所在主机不可达(地址写错、被防火墙拦)。 pg logs addon predixy 会显示拨号失败;--backend 必须列本主机能拨通的节点。
  • 集群变更后后端列表过期——install 整体替换列表;增删成员后,用当前完整节点集重跑它。

16.13 - pgAdmin 4

以 pgcli 插件方式运行 pgAdmin 4——PostgreSQL 官方 Web 管理界面,单容器直接跑上游 dpage/pgadmin4 镜像,自动生成 Web 登录账号,可选预置服务器

pgAdmin 4 是 PostgreSQL 的官方 Web 管理界面——在 浏览器里浏览 schema、执行 SQL、观察服务器状态。pgcli 将其作为独立的顶层插 件运行,CLI 表面与其他 web/infra 插件一致(install / start / stop / logs / autostart / remove),全部通过 pg.yaml 管理。

它是插件族里唯一的 UI 插件,这一点决定了它的设计:

  • 它是 Web 应用,不是数据库前置。 pgAdmin 连接哪些服务器由你自己在其 UI 里注册决定;它不像 PostgREST 或 PgBouncer 那样专职前置某一个实例。这就是它做成顶层-only 插件 (addons.pgadmin.<name>)、没有实例 sidecar 的原因。
  • 它有自己的登录。 Web UI 需要邮箱 + 密码登录。这不是 PostgreSQL 密码 ——是 pgAdmin 自己的账号,首次安装时自动生成并存入 pg.yaml。之后用 pg addon password pgadmin 取回。
  • 持久化的会话存储。 pgAdmin 把配置、已保存的服务器、浏览历史存在 bind 挂载数据目录里的 sqlite 库中。它跨 pg addon remove(不带 --clean-data) 存活,重装同名实例即复活。
  • 可选的一次性预置。 --dsn 或 --pg-name <实例名> 在 install 时向 UI 的 servers.json 预注册一个服务器,让浏览器打开时列表里已有它。这只是一个便利 ——不是运行期耦合:pgAdmin 不会因它阻塞,之后可在 UI 里增删。

建议:每个数据库一个 pgAdmin,随开随用。 pgcli 管理的 pgAdmin 是单个轻 量容器,秒级启动、内存占用极小——完全可以当一次性工具用。与其跑一个共享的 pgAdmin 把所有服务器都注册进去,不如为当前正在操作的数据库创建专用实例,用完 即删:

# 快速查看 proj01
pg addon install pgadmin --name proj01 --pg-name proj01
# ... 浏览、查询、完成
pg addon remove proj01 --clean-data

# 检查远程生产只读副本
pg addon install pgadmin --name prod-ro \
    --dsn "postgres://reader@10.0.0.7:5432/proddb"
pg addon remove prod-ro --clean-data

每个实例有独立的端口(自动分配)、独立的登录、独立的 pgpass——数据库之间无 交叉污染,共享会话存储里不会残留陈旧连接。下次需要时,秒级重装。

不在范围内: 这里 pgAdmin 只提供明文 HTTP——没有接入 TLS 终止(需要 HTTPS 就在前面架反向代理)。servers.json 本身不能携带目标服务器的密码 (pgAdmin 拒绝导入),但当预置 DSN 带了密码时,pgcli 通过 pgAdmin 的 pgpass 支持预配置它,预置服务器连接时无需再输入——见预置服务器。

参数

单容器直接跑上游 docker.io/dpage/pgadmin4 镜像(pull-only——pgcli 从不构建 它),全部经由 entrypoint 读取的 -e 环境变量配置。下表列出每个旋钮及其默认 值:

参数 / pg.yaml 键 默认值 环境变量 含义
--image / image_tag docker.io/dpage/pgadmin4:9.18 — 直接指定镜像标签
--port / host_port 自动,取自 pgadmin_start_port(基址 5050) PGADMIN_LISTEN_PORT Web 宿主端口
--listen / listen 127.0.0.1 PGADMIN_LISTEN_ADDRESS 绑定地址(0.0.0.0 对外开放,见监听与暴露)
--email / email admin@pgcli.lan PGADMIN_DEFAULT_EMAIL Web 登录账号(会经过镜像校验,见排障)
--password / password 自动生成,20 字符 PGADMIN_DEFAULT_PASSWORD Web 登录密码——不是 PG 密码
--dsn / dsn 不设 —(渲染 servers.json + pgpass) 以 URI 预注册一个服务器——带密码时同时预配置(host 是从容器内拨号的,见预置的 host 与 listen 地址)
--pg-name 不设 —(渲染 servers.json + pgpass) 改为从本机已管理实例解析该服务器
--data-dir / data_dir <base-dir>/addon/pgadmin/<name>/data (挂载到 /var/lib/pgadmin) 会话存储位置
— autostart: false — 开机自启(pg autostart enable --pgadmin)

旋钮间的配合关系:

  • email + password 只在空数据目录上生效。 pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是 权威,环境变量被忽略。因此复活既有目录的重装会打印一个对它无效的密码(install 会明说)——要真正重置登录,先 --clean-data 移除;复活时也可以把 --password 钉成原值。
  • --dsn 与 --pg-name 互斥——二者都表示"预置这个服务器",一个给 URI, 一个从本实例解析端点。--name 是插件自己的键,与 --pg-name 无关。预置里的 host 是 pgAdmin 的连接出发地,不是你浏览器的位置——见预置的 host 与 listen 地址。
  • listen 默认 loopback——Web UI 有登录门,所以除非传 --listen 0.0.0.0 从别的主机访问(summary 会警告登录成了唯一防线),它保持 127.0.0.1。完整 情形见监听与暴露。
  • email 必须过得了镜像的校验器——entrypoint 用 email_validator 检查它, 直接拒绝保留 TLD(.local、.invalid、.test),命中就崩溃重启。默认值 admin@pgcli.lan 正是为通过校验而选;自己起名时同理(.lan、.internal 这类由你掌控的名字都行——不要求是真实可投递的地址,投递检查是关掉的)。

安装

# 默认:实例名 "pgadmin",端口自 5050 自动分配,loopback,admin@pgcli.lan
pg addon install pgadmin

# 命名 console,用你自己的 Web 登录邮箱
pg addon install pgadmin --name console --email me@example.com

# 用本机已管理的一个实例预置 UI
pg addon install pgadmin --name dev --pg-name proj01

# 用任意远程 DSN 预置
pg addon install pgadmin --name remote \
    --dsn "postgres://readonly@10.0.0.7:5432/appdb"

install summary 打印 URL 与(自动生成的)Web 登录:

✓ pgAdmin installed: "console"
  Container:  pgcli-pgadmin-default-console
  Image:      docker.io/dpage/pgadmin4:9.18
  Data:       ~/pg/addon/pgadmin/console/data
  URL:        http://127.0.0.1:5050/
  Seeded:     proj01
  Seed auth:  password pre-configured (pgpass) — no prompt on first connect

  Login email:    me@example.com
  Login password: <generated>
  (Web login only — not a PostgreSQL password. Retrieve it later with `pg addon password pgadmin`.)

  Add servers to browse in the web UI itself, or reinstall with --dsn/--pg-name to pre-seed one.

Seeded: / Seed auth: 行只在传了 --dsn/--pg-name 时出现。当 DSN 携带密 码时 Seed auth: 显示 password pre-configured (pgpass);不带密码时显示 no password in the DSN — first connect will prompt。重复 install 是幂等的: 运行中的容器原样保留,已停的被启动。要让改动的端口/listen/email/预置生效,加 --force(容器按配置重建;数据目录不动)。

用 --force 重建

不带 --force 的 pg addon install pgadmin 对运行中的容器是空操作——它不动 现有配置,只确认容器在跑。当你正要修改某些东西(端口、listen、email、镜像、 预置 DSN、--pg-name 目标、data_dir)时,这恰好是错误的行为:运行中的容器 钉在创建它时的配置上,编辑看似被接受,但直到下次重建前什么都不会改变。

--force 就是应用这些改动的旋钮。它停止运行中的容器、移除它,并按当前配置创建 一个新的;数据目录不动,已存服务器和浏览历史在新容器里回来:

# 把绑定地址改成暴露到网络
pg addon install pgadmin --listen 0.0.0.0 --force

# 把预置服务器换成另一个实例
pg addon install pgadmin --pg-name proj02 --force

# 彻底去掉预置(不带 --dsn / --pg-name + --force)
pg addon install pgadmin --force

# 切到另一个上游镜像标签
pg addon install pgadmin --image docker.io/dpage/pgadmin4:9.19 --force

两个值得说清的坑:

  • email / password 仍然只对空数据目录重新生效。 pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是 权威,环境变量被忽略。单 --force 不会重置 Web 登录——要真正重置账号,得配合 --clean-data(先把存储清掉)。
  • 运行中的容器会被重建,不是原样保留。 如果浏览器正开着 UI,--force 会杀 掉会话;下一次页面加载会重新认证新容器。

使用 Web UI

把浏览器指向 summary 里的 URL(在跑 podman 的那台主机上),用登录邮箱 + 密码 签名进入,pgAdmin 自己的服务器树就出现了。在 UI 里添加连接,或在安装时用 –dsn / –pg-name 预注册一个。

如果主机不在你的浏览器旁边:设 --listen 0.0.0.0(登录成了唯一防线):

监听与暴露

--listen(配置键 listen)是 pgAdmin HTTP 服务绑定的地址,默认 loopback。 三种姿态如下——都要配 --force,因为容器需重建才会采纳改动:

# 仅 loopback(默认)——只有跑 podman 的那台主机能访问
pg addon install pgadmin --name console --listen 127.0.0.1 --force

# 全部网卡——其他主机可访问;登录成了唯一防线
pg addon install pgadmin --name console --listen 0.0.0.0 --force

# 暴露过之后再收回 loopback
pg addon install pgadmin --name console --listen 127.0.0.1 --force
  • Linux 上容器共享宿主网络,所以 --listen 0.0.0.0 会在 <宿主IP>:<端口> 应答,--listen 127.0.0.1 只在 127.0.0.1:<端口>。放宽到 0.0.0.0 意味着 pgAdmin 之外只剩邮箱/密码登录这一道门——install 会警告这一 点——因此除非网络可信,保持 loopback。
  • macOS 上情况相反:bridge 网络在 podman machine 内部总是绑 0.0.0.0, pgcli 发布端口,所以 listen 在那里实际上是恒被放宽的,你从 Mac 经 gvproxy 用 127.0.0.1:<端口> 访问 UI。见平台支持。

取回 Web 登录

pg addon password pgadmin --name console          # 裸打印,供脚本用
pg addon password pgadmin --name console --file ./pw.txt   # 写成 0600 文件

pg addon list 显示 URL 和登录邮箱,但不加 --show-password 绝不打印密码。

预置服务器

--dsn 与 --pg-name 都渲染一份一次性的 servers.json,pgAdmin 在首次启动 时导入,浏览器打开时那个服务器已在树里。--pg-name 从本机已管理实例解析端点 (其 host/port/database/user 及密码,读自 pg.yaml);--dsn 以 URI 给出:

pg addon install pgadmin --name dev --pg-name proj01
# 或
pg addon install pgadmin --name dev --dsn "postgres://app:secret@127.0.0.1:5432/appdb"

预置携带什么、不携带什么:

  • 目标服务器的密码在 DSN 携带时被预配置。 servers.json 本身不能持有密码 (pgAdmin 拒绝导入),所以 pgcli 在旁边写一个 libpq pgpass 文件,并通过 ConnectionParameters.passfile 把预置条目指向它——这是 pgAdmin 9.18 导入/导 出服务器文档中正式记录的绝对路径机制。浏览器里的连接完全跳过密码输入。不带 密码的 DSN(如 postgres://app@host/db)只渲染 servers.json,pgAdmin 在首 次连接时照常提示输入。
  • pgpass 文件以明文在磁盘上持有密码。 它位于 <base-dir>/addon/pgadmin/<name>/pgpass,mode 0600,只有宿主用户和容器内的 uid 5050 可读——但它确实在磁盘上,不像 servers.json 从不持有机密。权衡与 所有使用 .pgpass 的工具相同:免密连接以磁盘上的一个文件为代价。移除 pgAdmin 实例(无论是否带 --clean-data)会把 pgpass 与 servers.json 一并删除——明文 机密不会比实例活得更久。
  • 它是一次性便利,不是耦合。 安装后服务器活在 pgAdmin 自己的 pgadmin4.db 里;在 UI 里增、删、改名都行。无论预置目标是否可达,插件照常运行。
  • PGADMIN_REPLACE_SERVERS_ON_STARTUP=True 在有预置时被设置,所以改了 --dsn/--pg-name 的 --force 重装会声明式地重新应用预置,而不是留下过期 条目。

要去掉预置并停止声明式重载,用 --force 重装且不带 --dsn/--pg-name。

预置的 host 与 listen 地址

这两个旋钮指向不同的机器,却常被混为一谈。--listen 决定谁能访问 pgAdmin 的 Web UI;预置里的 host 决定pgAdmin 从哪里拨数据库。拨号是服务端动作—— 由 pgAdmin 容器发起,不是你的浏览器——所以用 --pg-name 预置本机管理的实例 时,记录 127.0.0.1 是正确的:pgAdmin 与那个 PostgreSQL 共享 podman 宿主 的回环。把 UI 暴露到网络并不要求预置的 host 对浏览器可路由:

# pgAdmin 在 vm01 上前置 vm01 自己的实例,但 UI 供其他主机访问
pg addon install pgadmin --listen 0.0.0.0 --pg-name demo --force
# servers.json -> Host: 127.0.0.1, Port: <demo 的 pg 端口>(容器内拨号)

# 改为预置另一台主机上的服务器——该 host 必须从 pgAdmin 容器可达,
# 因为发起连接的是容器
pg addon install pgadmin --listen 0.0.0.0 \
    --dsn "postgres://app@10.0.0.12:35432/appdb" --force

反过来的坑:用 --dsn 预置一个远程 host,再从别处用浏览器访问那个 pgAdmin——连接依然源自 pgAdmin 容器内部,所以那个 host 必须从容器的网络 可达,而不是从你的笔记本可达。

端口

每个实例从 pgAdmin 自己的端口池 pgadmin_start_port(默认 5050)取一个 端口——与 Redis、Predixy、对象存储的池各自独立游标,所以一个 pgadmin、一个 redis、一个 minio 实例永不撞车:

pg addon install pgadmin --name console   # 5050
pg addon install pgadmin --name audit     # 5051

--port 可把实例钉在特定端口;自动分配会跳过任何已显式指定或宿主上已占用的 端口。

配置

实例位于 pg.yaml 的顶层 addons.pgadmin map:

namespace: default
pgadmin_start_port: 5050     # pgAdmin 自己的端口池
addons:
  pgadmin:
    console:
      container_name: pgcli-pgadmin-default-console
      name: console
      image_tag: docker.io/dpage/pgadmin4:9.18
      # data_dir: /srv/pgadmin         # 省略则为 <base-dir>/addon/pgadmin/console/data
      listen: 127.0.0.1                # 默认;0.0.0.0 对网络开放
      host_port: 5050
      email: me@example.com            # Web 登录账号(PGADMIN_DEFAULT_EMAIL)
      password: <generated>            # Web 登录密码;首次 install 时写入
      # dsn: postgres://app@127.0.0.1:5432/appdb   # 由 --dsn/--pg-name 设置:预置
      # server_name: app               # 预置服务器的显示名
      autostart: false                 # pg autostart enable --pgadmin --name console

对 listen、host_port、email、password、image_tag、dsn/ server_name、data_dir 的修改,在下一次 pg addon install pgadmin --name console --force(或对应 flags)后生效。记住: email/password 只对空数据目录重新生效,所以对既有存储改它们需要先 --clean-data(或接受生效账号仍是原账号)。

列出

pg addon list
Web add-ons (pgadmin):
  pgadmin (name: console)
    Status:      running
    URL:         http://127.0.0.1:5050/
    Login email: me@example.com
    Seeded:      proj01
    Data:        ~/pg/addon/pgadmin/console/data
    Image:       docker.io/dpage/pgadmin4:9.18
    Container:   pgcli-pgadmin-default-console

pg addon list 从不打印密码。要刻意揭示,加 --show-password(每实例追加一行 Login password:),或用 pg addon password pgadmin --name console 裸打印—— 并且优先用 --file,让机密留在文件里而不是 shell 历史与回滚缓冲。

启停

pg addon start pgadmin --name console
pg addon stop  pgadmin --name console

start 只启动已存在的容器(并以按配置重建来自愈不当状态)。数据目录跨 stop/start 保留,pgAdmin 的已存服务器与浏览历史随之回来。

开机自启

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

pg autostart enable --pgadmin --name console

pgAdmin 自启是只启动(先 install)。开机时 entrypoint 重新 chown /var/lib/pgadmin,并从数据目录重放 pgadmin4.db,已存服务器随之复活。pgAdmin 独立于 PostgreSQL 栈——它是自己拨号的浏览器 UI——所以没有排序约束,开机 pg start --autostart 把它与其他非数据库插件一起启动。

移除

pg addon remove pgadmin --name console               # 容器 + 配置条目;数据保留
pg addon remove pgadmin --name console --clean-data  # 同时删除数据目录

不带 --clean-data 时宿主数据目录(连同 pgadmin4.db)原地保留——重装同名实 例即复活已存服务器与会话存储(复活后可用的登录是原密码,不是新生成的)。 --clean-data 删除它,当 rootless podman 的映射 uid 使 pgAdmin 的文件对宿主用 户不可删时,经 podman unshare rm 兜底。空的实例父目录被修剪;自定义 data_dir 的父目录从不动。servers.json(若预置过)宿主所有,两种移除都随实 例目录一并删掉。

日志

pg logs addon pgadmin --name console        # 最近 50 行
pg logs addon pgadmin --name console -f     # 跟踪

排障

  • 用打印的密码登录被拒 —— 你在保留的数据目录上重装了。能用的登录是 pgAdmin 在首次初始化时写进 pgadmin4.db 的那个;新生成的密码无效(install 会警告 “reviving an existing pgAdmin data dir”)。用 --clean-data 从零开 始,或用 pg addon password pgadmin 确认存的是什么,或重装时把 --password 钉成原值。
  • “pgAdmin could not start” / 容器秒退(或崩溃循环) —— 空目录上 entrypoint 需要 PGADMIN_DEFAULT_EMAIL 和 PGADMIN_DEFAULT_PASSWORD 都在;手改过 pg.yaml、密码为空且数据目录被清过,就两个都没有。让 install 生成,或传 --password。它还会校验邮箱并拒绝保留 TLD(.local、.invalid、 .test),报 “does not appear to be a valid email address”——容器随即退出, 在重启策略下反复自旋。这正是默认值取 admin@pgcli.lan 而非 .local 的原因。
  • 从别的机器打不开 UI —— listen 默认 127.0.0.1。--listen 0.0.0.0 --force 重装并接受 Web 登录成为唯一防线。
  • 预置的服务器显示但连不上 —— 若预置 DSN 不带密码,pgAdmin 在首次连接时 提示输入(预置服务器)。若 DSN 带了密码却仍然提示,说明 pgpass 文件失效了——最常见原因是容器启动时 entrypoint 的 chown 没跑(例如 直接传了 --user 5050 而不是 --user 0,导致文件属主是宿主用户,libpq 静默 丢弃它)。确认 host/port 从 pgAdmin 所在处可达。
  • install 时找不到镜像 —— docker.io/dpage/pgadmin4 按需拉取;离线宿主先 podman load 对应 tar(目录与导出步骤在仓库 docs/images.md),pull 会被跳 过。
  • 首次启动响应慢 —— pgAdmin 在 gunicorn 绑定之前先跑 sqlite 迁移;端口要 等它完成才回 302(跳 /login)。负载高的宿主上这可能要几十秒。

已知限制

  • 无 TLS —— 这里 pgAdmin 提供明文 HTTP。要 HTTPS 就在前面架反向代理(配合 PGADMIN_URL_SCHEME/PGADMIN_DISABLE_STATIC_FILE_SERVER 调参);pgcli 不接 这条线。loopback 默认让常见场景留在 127.0.0.1。
  • 不适合大数据量导入导出 —— pgAdmin 的数据流经 HTTP(导出:查询 → 容器 → 浏览器;导入:上传 → 容器 → 数据库),大表容易超时或内存溢出。应改用 pg export / pg import(pgcli 实例),或 pg exec --dsn "..." -- pg_dump(任意数据库)。详见 数据导入导出。
  • 预置密码以明文存于磁盘 —— servers.json 旁边的 pgpass 文件是 mode 0600 但没有加密;移除实例时一并清理。没有办法预配置密码而不落盘文件(pgAdmin 没有 KMS/keyring 集成)。
  • 登录只对新存储生效 —— pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是权威。没有原地改密码的路, 只有 --clean-data + 重装。
  • macOS 代码完备、未实测 —— bridge 路径(发布端口、bridge 下 proxyBindHost 把 loopback 放宽到 0.0.0.0)与 redis 同构;尚未在 Mac 上演 练过。

16.14 - Nginx

以 pgcli 插件方式运行 nginx——Web 服务前的 HTTP 反向代理,支持基于路径的路由和可选 TLS 终止

nginx 是 HTTP 反向代理,用于前置多个 Web 服务——仪表盘、API、 管理面板——通过基于路径的路由和可选 TLS 终止。pgcli 将其 作为独立的顶层插件运行,CLI 表面与其他 web/infra 插件一致(install / start / stop / logs / autostart / remove),全部通过 pg.yaml 管理。

它是插件族里的 HTTP 网关,这一点决定了它的设计:

  • 反向代理,不是负载均衡器。 与 HAProxy——前置 Patroni 的 TCP 负载均衡器——不同,nginx 是 HTTP 反向代理,按 URL 路径路由。每个 location 块 转发到一个 upstream(Web 应用、API 服务等),客户端通过单一端口访问多个服务。
  • 文件驱动配置。 pgcli 根据你声明的 backends 渲染 nginx.conf(或通过 --conf-file 接受自定义文件),以只读方式挂载到官方 nginx:1.27-alpine 镜像中。TLS 可选:未提供 BYO 证书时,pgcli 通过 tlsca 生成自签名叶子证书。
  • 独立日志挂载。 每个 backend 有自己的 access 和 error 日志文件 (access_<name>.log、error_<name>.log),位于挂载的 /var/log/nginx/ 目录下,因此无需解析共享日志即可观察每个服务的流量。
  • nginx 专属子命令。 除了通用插件生命周期(install/start/stop/remove), nginx 还暴露 test(通过临时容器运行 nginx -t 验证配置语法)、reload (通过 nginx -s reload 热加载)和 exec(在容器内运行任意命令)。

平台支持: nginx 在 Linux(主机网络)和 macOS(pgcli-net 桥接网络

  • 端口发布)上均可工作。上游镜像为多架构(amd64 + arm64),任何主机架构均可。

工作原理

路由基于路径:每个 location 块转发到一个 upstream。配置根据你声明的 backends 渲染(或从自定义文件复制),以只读方式挂载,nginx 在前台运行 nginx -g 'daemon off;'。

两种配置模式

通过 --conf-file(自定义)或 --upstream(模板)选择:

  • 模板模式(默认)——pgcli 根据 --upstream 规格渲染 nginx.conf。每个 upstream 成为 upstream 块和 location 块;TLS 在 HTTP 服务器块旁添加 HTTPS 服务器块。模板 覆盖常见场景:基于路径的路由到若干 Web 服务。
  • 自定义模式(--conf-file)——你提供完整 nginx.conf 路径;pgcli 复制到 插件目录,仅管理容器生命周期。当生产配置超出模板能力时使用(limit_req_zone、 resolver + 变量 proxy_pass、WebSocket、SNI、每个 backend 多个 location、 upstream keepalive 等)。--upstream 和 --conf-file 互斥。

按 backend 独立日志

每个 backend 在挂载的 /var/log/nginx/ 目录下有自己的日志文件:

  • access_<name>.log —— 该 backend 的 location 块的 access 日志
  • error_<name>.log —— 该 backend 的 location 块的 error 日志

全局 access.log 和 error.log 捕获服务器级事件。日志目录在主机上创建于 <base-dir>/addon/nginx/<name>/log/,跨容器重启持久化。

TLS 终止

TLS 可选。未提供 --tls-cert/--tls-key 时,pgcli 通过 tlsca.Generate 在 <base-dir>/addon/nginx/<name>/tls/ 下生成自签名叶子证书。HTTPS 监听器与 HTTP 监听器并行运行,现有明文客户端继续工作。提供 BYO 证书时,改为挂载指定文件。

BYO 证书常常就是 pg cert 签发的叶子证书——该命令签一张自签名证书,SAN 可以是 任意域名 + IP 组合,正是"客户端可控、能把 CA 文件交出去"的 Git/DB 端点所需形 态:

pg cert --host git.example.com,10.0.0.9 \
        --cert-file /srv/gitea.crt --key-file /srv/gitea.key
pg addon install nginx --name proxy --listen 0.0.0.0 \
  --upstream name=gitea,path=/,backend=127.0.0.1:3080 \
  --tls --tls-cert /srv/gitea.crt --tls-key /srv/gitea.key \
  --http-port 80 --https-port 443

代理 Git(及任何大 POST):必须抬高 client_max_body_size。 模板模式不设 请求体上限,于是 nginx 用其内建默认 1 MiB——超过它的 git push(或任何上 传)会返回 HTTP 413。模板没有对应 flag,所以给 Git 服务前置时要用自定义模 式(--conf-file);完整可用样例见下文「常见自定义配置模式」里的「Git HTTP 推送与 client_max_body_size」。

安装

backends 通过两种方式之一提供(互斥):

  • --upstream name=<n>,path=<p>,backend=<h:p>(可重复)——显式 upstream 目标;
  • --conf-file <path> —— 自定义 nginx.conf 文件(完全跳过模板渲染)。

每个 --upstream 字段的含义——以 --upstream name=web,path=/app,backend=127.0.0.1:8000 为例:

字段 含义
name nginx.conf 中的 upstream 名称(省略时默认为清理后的 path)
path location 路径前缀(例如 /app → Web 应用、/api → API 服务;默认为 /)
backend 要代理到的 host:port(例如 127.0.0.1:8000);可重复指定多个 server 做负载均衡
backends 多个 server 的简写形式:backends=h1:p1,h2:p2,...(与 backend 互斥,二选一)
# 模板模式:单个 upstream
pg addon install nginx --name proxy \
  --upstream name=web,path=/app,backend=127.0.0.1:8000

# 模板模式:多个 upstream
pg addon install nginx --name proxy \
  --upstream name=web,path=/app,backend=127.0.0.1:8000 \
  --upstream name=api,path=/api,backend=127.0.0.1:3000

# 单个 upstream 多个 server(负载均衡,nginx round-robin)
pg addon install nginx --name proxy \
  --upstream name=api,path=/,backends=127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002

# 模板模式 + TLS(自签名证书)
pg addon install nginx --name proxy \
  --upstream name=web,path=/app,backend=127.0.0.1:8000 --tls

# 自定义模式:你提供完整配置
pg addon install nginx --name custom \
  --conf-file /path/to/nginx.conf --http-port 8080

安装摘要报告分配的端口和配置路径:

✓ nginx installed: "proxy"
  Container:  pgcli-nginx-default-proxy
  Image:      docker.io/library/nginx:1.27-alpine
  HTTP:       127.0.0.1:8080
  HTTPS:      (disabled)
  Backends:   2
    - web   /app → 127.0.0.1:8000
    - api   /api → 127.0.0.1:3000
  Config:     ~/pg/addon/nginx/proxy/nginx.conf
  Logs:       ~/pg/addon/nginx/proxy/log/

使用 --conf-file 时,摘要显示 Config: <path> (from <confFile>) 并跳过 Backends: 部分(pgcli 不解析用户的配置)。

用 --force 重建

不带 --force 的 pg addon install nginx 在容器运行中时现在会自动 reload 配置改动:写入新 nginx.conf,用 nginx -t 验证,验证通过后发 nginx -s reload。 验证失败时回滚旧配置,跳过 reload。这意味着大部分改动(backends、--conf-file、 worker_connections、TLS)无需重建容器即可生效。

--force 仍用于需要重建容器的场景(镜像标签、listen 地址、端口、网络模式): 它停止运行中的容器、移除它,并按当前配置创建一个新的:

# 把绑定地址改成暴露到网络
pg addon install nginx --listen 0.0.0.0 --force

# 切到另一个上游镜像标签
pg addon install nginx --image docker.io/library/nginx:1.28-alpine --force

nginx 子命令

除了通用插件生命周期(install/start/stop/remove),nginx 还暴露三个日常运维子 命令:

test —— 验证配置语法

pg addon nginx test --name proxy
pg addon nginx test --conf-file /path/to/nginx.conf

在临时容器(podman run --rm)内运行 nginx -t,挂载与真实容器相同的卷。 不需要 nginx 容器在运行中——它启动一次性容器、验证配置、退出。这是在 reload 前测试配置改动的安全方式:

# 编辑配置文件(模板或自定义)
vim ~/pg/addon/nginx/proxy/nginx.conf

# 验证语法,不影响运行中的代理
pg addon nginx test --name proxy
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# 然后才 reload
pg addon nginx reload --name proxy

--conf-file 在安装之前验证独立的配置文件——适合 CI 流水线或部署前检查:

# 验证尚未安装的配置
pg addon nginx test --conf-file /tmp/new-nginx.conf --image docker.io/library/nginx:1.27-alpine

临时容器挂载相同的 nginx.conf(以及启用 TLS 时的证书目录),因此验证与真实 容器启动时看到的完全一致。

reload —— 热加载配置

pg addon nginx reload --name proxy

向运行中的容器发送 nginx -s reload——优雅重启,采纳配置改动而不丢弃进行中的 连接。需要容器在运行中(若已停止,先用 pg addon start nginx --name proxy)。

exec —— 在容器内运行命令

pg addon nginx exec --name proxy -- cat /etc/nginx/nginx.conf
pg addon nginx exec --name proxy -- nginx -V
pg addon nginx exec --name proxy -- ls -la /var/log/nginx/

在 nginx 容器内运行任意命令,stdio 透传。需要容器在运行中。-- 分隔符是必需的 ——之后的所有内容原样传给容器。

参数

单容器直接跑上游 docker.io/library/nginx 镜像(pull-only——pgcli 从不构建它), 全部通过渲染的(或自定义的)nginx.conf 和 podman run 标志配置。下表列出每个 旋钮及其默认值:

参数 / pg.yaml 键 默认值 含义
--image / image_tag docker.io/library/nginx:1.27-alpine 直接指定镜像标签
--http-port / http_port 自动,取自 nginx_start_port(基址 8080) HTTP 监听器宿主端口
--https-port / https_port 自动(HTTP 之后下一个空闲端口) HTTPS 监听器宿主端口(仅 TLS)
--listen / listen 127.0.0.1 绑定地址(0.0.0.0 对外开放)
--worker-connections / worker_connections 1024 events 块中的 worker_connections(每个 worker 的最大并发连接数)
--tls / tls false 在 HTTP 监听器旁启用 HTTPS 监听器
--tls-cert / tls_cert 不设 BYO 证书路径(PEM 叶子 + 链);不提供时 pgcli 生成自签名证书
--tls-key / tls_key 不设 BYO 私钥路径(PEM)
--upstream 不设(可重复) name=<n>,path=<p>,backend=<h:p>[,backend=<h:p>...] —— 一个 upstream 目标,可包含多个 backend
--conf-file / conf_file 不设 自定义 nginx.conf 路径(与 --upstream 互斥)
— autostart: false 开机自启(pg autostart enable --nginx)

旋钮间的配合关系:

  • --conf-file 与 --upstream 互斥。 二者都表示"这是配置"——一个给路径, 一个声明 backends。安装拒绝组合。
  • --tls 不提供 --tls-cert/--tls-key 时生成自签名证书。 证书位于 <base-dir>/addon/nginx/<name>/tls/,挂载到 /etc/nginx/certs。HTTPS 监听器在下一个空闲端口上与 HTTP 并行运行。
  • --tls-cert/--tls-key 不提供 --tls 时被忽略。 这些标志仅在启用 TLS 时生效。
  • --worker-connections 在 --conf-file 模式下会注入。 两者同时设置时, pgcli 会修补用户的 events 块(或新增一个)来设置指定值。用户原有的 worker_connections 指令(如有)会被替换,而非合并。
  • --listen 默认 loopback —— 代理保持 127.0.0.1,除非传 --listen 0.0.0.0 从别的主机访问。Linux 上容器共享主机网络,所以 0.0.0.0 在 <host-ip>:<port> 应答;macOS 上桥接发布端口, proxyBindHost 把 loopback 放宽到 0.0.0.0。

端口

每个实例从 nginx 自己的端口池 nginx_start_port(默认 8080)取一到两个 端口——与 Redis、Predixy、对象存储的池各自独立游标,所以一个 nginx、 一个 redis、一个 minio 实例永不撞车:

pg addon install nginx --name proxy                          # 8080(仅 HTTP)
pg addon install nginx --name proxy-tls --tls                # 8081(HTTP)+ 8082(HTTPS)
pg addon install nginx --name proxy2                         # 8083

--http-port 和 --https-port 可把实例钉在特定端口;自动分配会跳过任何已显式 指定或宿主上已占用的端口。TLS 实例消耗两个端口(HTTP + HTTPS);普通实例消耗 一个。

配置

实例位于 pg.yaml 的顶层 addons.nginx map:

namespace: default
nginx_start_port: 8080     # nginx 自己的端口池
addons:
  nginx:
    proxy:
      container_name: pgcli-nginx-default-proxy
      name: proxy
      image_tag: docker.io/library/nginx:1.27-alpine
      listen: 127.0.0.1
      http_port: 8080
      https_port: 0        # 0 = 自动分配(仅 TLS)
      worker_connections: 1024
      tls: false
      # tls_cert: /path/to/cert.pem   # BYO 证书(需 tls: true)
      # tls_key:  /path/to/key.pem
      # conf_file: /etc/nginx/custom.conf  # 自定义模式(跳过 backends)
      autostart: false     # pg autostart enable --nginx --name proxy
      backends:
        - { name: web,  path: /app, backends: ["127.0.0.1:8000"] }
        - { name: api,  path: /api, backends: ["127.0.0.1:3000"] }

对 listen、http_port、https_port、worker_connections、tls、 tls_cert/tls_key、conf_file、backends 的修改,在下一次 pg addon install nginx --name proxy 时生效——纯配置改动自动 reload(无需 --force)。需要重建容器的改动(镜像标签、listen 地址、端口)才需要 --force。

常见自定义配置模式

模板模式无法满足时,使用 --conf-file 提供完整 nginx.conf。以下模式 提取自实际 70+ server 的生产配置,覆盖最常见的场景。

完整 http 块骨架 —— 下面的模式展示的是单独的 server 块;用如下 http 块包裹它们:

worker_processes auto;
error_log /var/log/nginx/error.log;
pid /tmp/nginx.pid;

events {
    worker_connections 60000;
    multi_accept on;
}

http {
    ##
    # 基础设置
    ##

    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # 日志
    ##

    log_format postdata '$remote_addr - $request_id [$time_local] "$request" '
                        '$status $body_bytes_sent $server_name "$request_body" '
                        '$request_length "$http_referer" "$http_user_agent" '
                        '"$http_x_forwarded_for" $request_time $upstream_response_time';

    access_log /var/log/nginx/access.log postdata;

    ##
    # SSL 设置
    ##

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    ##
    # Gzip 设置
    ##

    gzip on;

    ##
    # 限速(在 http 层声明 zone,在 server/location 里应用)
    ##

    # limit_req_zone $uri zone=api_fast:10m rate=5000r/s;
    # limit_req_zone $uri zone=api_slow:10m rate=100r/s;

    ##
    # 虚拟主机
    ##

    # server { ... }  # 见下方模式
}

动态 DNS 解析:resolver + 变量 proxy_pass —— 避免 nginx 在启动时解析 主机名并永久缓存。变量强制每次请求重新查询:

http {
    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_dns.log;
        error_log  /var/log/nginx/error_dns.log;

        resolver 8.8.8.8 8.8.4.4 ipv6=off;
        set $backend "https://api.example.com";

        location / {
            proxy_pass $backend;
        }
    }
}

按 URL 限速 —— 在 http 块声明 limit_req_zone,按 location 应用。 不同速率使用不同 zone:

http {
    limit_req_zone $uri zone=api_fast:10m rate=5000r/s;
    limit_req_zone $uri zone=api_slow:10m rate=100r/s;

    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_ratelimit.log;
        error_log  /var/log/nginx/error_ratelimit.log;

        location /fast-api/ {
            limit_req zone=api_fast burst=10000 nodelay;
            limit_req_status 429;
            proxy_pass https://backend.example.com;
        }
        location /slow-api/ {
            limit_req zone=api_slow burst=200 nodelay;
            limit_req_status 429;
            proxy_pass https://backend.example.com;
        }
    }
}

WebSocket 代理 —— 需要 Upgrade、Connection 头以及 HTTP/1.1:

http {
    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_ws.log;
        error_log  /var/log/nginx/error_ws.log;

        location / {
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass https://ws-backend.example.com;
            proxy_http_version 1.1;
        }
    }
}

带 keepalive 的 upstream —— 减少高吞吐 backend 的连接开销:

http {
    upstream backend-pool {
        server 10.0.0.1:8080;
        server 10.0.0.2:8080;
        keepalive 10;
    }

    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_pool.log;
        error_log  /var/log/nginx/error_pool.log;

        location / {
            proxy_pass http://backend-pool;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

基于 Host 的路由:proxy_set_header Host —— 按 IP 代理但设置正确的 虚拟主机:

http {
    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_host.log;
        error_log  /var/log/nginx/error_host.log;

        resolver 8.8.8.8 8.8.4.4 ipv6=off;

        location / {
            proxy_set_header Host api.internal.example.com;
            proxy_pass http://10.0.0.100;
            proxy_http_version 1.1;
        }
    }
}

SNI 透传:proxy_ssl_server_name —— 当 upstream 在 HTTPS 后面且依赖 SNI 选择证书时必须开启。不开启的话,nginx 在 TLS 握手时不发送 Host 头,upstream 可能拒绝连接:

http {
    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_sni.log;
        error_log  /var/log/nginx/error_sni.log;

        resolver 8.8.8.8 8.8.4.4 ipv6=off;

        location / {
            proxy_set_header Host api.example.com;
            proxy_pass https://api.example.com;
            proxy_ssl_server_name on;
        }
    }
}

同一 server 多 location 各自限速 —— 不同路径需要不同速率限制时,声明 独立 zone 并在每个 location 应用。server 级别的 limit_req 做兜底, 特定路径可覆盖为更紧或更松的限制:

http {
    limit_req_zone $uri zone=general:10m rate=5000r/s;
    limit_req_zone $uri zone=sensitive:10m rate=100r/s;

    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_backend.log;
        error_log  /var/log/nginx/error_backend.log;

        location / {
            limit_req zone=general burst=5000 nodelay;
            limit_req_status 429;
            proxy_pass https://backend.example.com;
        }
        location /admin/ {
            limit_req zone=sensitive burst=200 nodelay;
            limit_req_status 429;
            proxy_pass https://backend.example.com;
        }
    }
}

自定义日志格式 —— 替换默认 combined 格式,捕获 request body、响应耗时 和 upstream 延迟。log_format 在 http 块声明一次,每个 server 各自设置 access_log 路径。日志目录 /var/log/nginx/ 由 pgcli 挂载,自定义日志路径 统一用这个目录:

http {
    log_format postdata '$remote_addr - $request_id [$time_local] "$request" '
                        '$status $body_bytes_sent $server_name "$request_body" '
                        '$request_length "$http_referer" "$http_user_agent" '
                        '"$http_x_forwarded_for" $request_time $upstream_response_time';

    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_api.log postdata;
        error_log  /var/log/nginx/error_api.log;

        location / {
            proxy_pass https://api.example.com;
        }
    }

    server {
        listen 127.0.0.1:9002;
        access_log /var/log/nginx/access_web.log postdata;
        error_log  /var/log/nginx/error_web.log;

        location / {
            proxy_pass https://web.example.com;
        }
    }
}

Git HTTP 推送与 client_max_body_size —— 用一份完整的自定义 nginx.conf 前置 Git 服务(Gitea、GitLab、裸 git http-backend 等),抬掉 1 MiB 请求体上限, 并让长 git-receive-pack 请求不断流:

worker_processes auto;
pid /tmp/nginx.pid;

events {
    worker_connections 1024;
}

http {
    # nginx 内建默认 1 MiB —— 更大的 git push 会以 HTTP 413 死掉。
    # 10m 覆盖常见 push;要提交更大的二进制就继续调高(或写 0 完全不限制)。
    client_max_body_size 10m;

    # push/receive-pack 的流式请求可能超过 60s 默认值。
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    upstream gitea {
        server 127.0.0.1:3080;
    }

    server {
        listen 0.0.0.0:80;
        access_log /var/log/nginx/gitea.access.log;
        error_log  /var/log/nginx/gitea.error.log;

        location / {
            proxy_pass http://gitea;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    server {
        listen 0.0.0.0:443 ssl;
        # pgcli 把 --tls-cert / --tls-key 文件挂到这两个路径。
        ssl_certificate     /etc/nginx/certs/public.crt;
        ssl_certificate_key /etc/nginx/certs/private.key;
        ssl_protocols TLSv1.2 TLSv1.3;

        access_log /var/log/nginx/gitea.access.log;
        error_log  /var/log/nginx/gitea.error.log;

        location / {
            proxy_pass http://gitea;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
pg cert --host 10.0.0.9,git.internal \
        --cert-file /srv/gitea.crt --key-file /srv/gitea.key
pg addon install nginx --name proxy --conf-file /srv/gitea-nginx.conf \
  --tls --tls-cert /srv/gitea.crt --tls-key /srv/gitea.key \
  --http-port 80 --https-port 443

--conf-file 与 --upstream 互斥,但 --tls / --tls-cert / --tls-key 在 自定义模式下依然生效:pgcli 仍把证书对挂在 /etc/nginx/certs/public.crt 与 /etc/nginx/certs/private.key——上面这份配置正是引用它们;自定义文件只是接管了 listen/ssl_certificate 这些行的书写。

静态文件服务 + 缓存 —— 从宿主挂载目录提供文件,带浏览器缓存头、 sendfile 优化和访问控制:

http {
    server {
        listen 127.0.0.1:9001;
        access_log /var/log/nginx/access_static.log;
        error_log  /var/log/nginx/error_static.log;
        root /srv/static/;

        location / {
            sendfile on;
            sendfile_max_chunk 2m;
            autoindex on;
            try_files $uri $uri/ =404;
        }
        location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1d;
            add_header Cache-Control "public";
            access_log off;
        }
        location ~ /\. {
            deny all;
        }
    }
}

列出

pg addon list
Web add-ons (nginx):
  nginx (name: proxy)
    Status:      running
    HTTP:        127.0.0.1:8080
    HTTPS:       (disabled)
    Backends:    2
      - web   /app → 127.0.0.1:8000
      - api   /api → 127.0.0.1:3000
    Config:      ~/pg/addon/nginx/proxy/nginx.conf
    Logs:        ~/pg/addon/nginx/proxy/log/
    Image:       docker.io/library/nginx:1.27-alpine
    Container:   pgcli-nginx-default-proxy

启停

pg addon start nginx --name proxy
pg addon stop  nginx --name proxy

start 只启动已存在的容器(并以磁盘上已有的 nginx.conf 重建来自愈不当状态)。 配置和日志目录跨 stop/start 保留。

开机自启

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

pg autostart enable --nginx --name proxy

nginx 自启是只启动(先 install)。开机时读取磁盘上已有的 nginx.conf,所以 代理以最后已知配置启动。nginx 独立于 PostgreSQL 栈——它是自己拨号的反向代理 ——所以没有排序约束,开机 pg start --autostart 把它与其他非数据库插件一起启动。

移除

pg addon remove nginx --name proxy

停止并删除容器,同时删除其配置目录(包括 TLS 证书和日志)。空的实例父目录被 修剪。

日志

pg logs addon nginx --name proxy        # 最近 50 行
pg logs addon nginx --name proxy -f     # 跟踪

容器日志走 stdout,所以 access 和 error 事件出现在这里。按 backend 查看日志, 看挂载的日志目录:

ls ~/pg/addon/nginx/proxy/log/
# access.log  access_web.log  access_api.log
# error.log   error_web.log   error_api.log

排障

  • 启动时配置语法错误。 容器退出,重启策略反复自旋。用 pg addon nginx test --name proxy 检查(容器停了也能用)——它在临时容器里跑 nginx -t,报告确切行号。
  • --conf-file 与 --upstream 一起用。 安装拒绝组合——选一种模式。
  • 从别的机器打不开代理。 listen 默认 127.0.0.1。--listen 0.0.0.0 --force 重装并接受代理成为 backends 前的唯一防线。
  • install 时找不到镜像。 docker.io/library/nginx 按需拉取;离线宿主先 podman load 对应 tar(目录与导出步骤在仓库 docs/images.md),pull 会被跳过。
  • Reload 失败"container not running"。 pg addon nginx reload 需要容器在 运行中。先用 pg addon start nginx --name proxy 启动。

已知限制

  • 无负载均衡或健康检查。 这里的 nginx 是反向代理(基于路径的路由,每个 location 单个 upstream),不是 TCP 负载均衡器。要在 Patroni 前做负载均衡,用 HAProxy。
  • 模板刻意保持简单。 渲染出的 nginx.conf 覆盖基于路径的路由到若干 backends,可选 TLS。生产配置需要 limit_req_zone、resolver + 变量 proxy_pass、WebSocket、SNI、每个 backend 多个 location 或 upstream keepalive 时,应改用 --conf-file。
  • macOS 代码完备、轻度测试。 桥接路径(发布端口、proxyBindHost 把 loopback 放宽到 0.0.0.0)与其他桥接插件同构;已演练但未在负载下测试。

16.15 - Grafana

以 pgcli 插件方式运行 Grafana——开源可观测性面板,单容器直接跑上游官方镜像,自动生成 Web 登录

pg addon install grafana 以受管容器方式运行 Grafana——开源可观测性面板——直接跑官方上游镜像。与 pgAdmin 一样,它是顶层插件:不依附于任何单个 PostgreSQL 实例,也不会自动前置它们——数据源和仪表盘都在 Web UI 里自己添加。v1 是纯 Web UI:配置只经环境变量(GF_*)交付,不挂载任何 datasource/dashboard 的 provisioning。

Web 登录密码在首次安装时自动生成、打印一次、存入 pg.yaml,之后用 pg addon password grafana 取回。与 pgAdmin 不同,Listen 默认 0.0.0.0:监控面板天然要给别的机器看,而容器跑在宿主网络上。暴露面由自动生成的强密码与关闭自助注册兜底——只想本机访问就传 --listen 127.0.0.1。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name grafana 实例名(map 键)
--image docker.io/grafana/grafana:13.2.4 覆盖镜像
--port 自动(基址 3000) Web 宿主端口;自 grafana_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环
--admin-user admin Web 登录账号(GF_SECURITY_ADMIN_USER)
--password 自动生成 Web 登录密码(GF_SECURITY_ADMIN_PASSWORD;缺省时生成,只在首次启动时生效,见下)
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于应用改动的端口、凭据或监听地址

数据目录存放 Grafana 自己的 sqlite 元数据库(grafana.db):仪表盘、数据源、用户与设置都在里面。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活一切。镜像的 entrypoint 不做属主修复(它以固化的 uid 472、root 组运行,目录不可写时只会告警),所以 pgcli 在创建容器前先在宿主上把数据目录 chown 过去:rootless podman 下经 podman unshare chown 472:0,rootful 宿主且 pgcli 以 root 运行时直接递归 chown。

安装

pg addon install grafana

命令会按需拉取镜像、创建容器,并打印 URL 与登录密码:

✓ Grafana installed: "grafana"
  Container:  pgcli-grafana-default-grafana
  URL:        http://0.0.0.0:3000/
  Login user:     admin
  Login password: <auto-generated>

用浏览器打开 URL,以打印的凭据登录。在 UI 里添加 PostgreSQL 数据源(Connect data → PostgreSQL;每个受管实例的宿主端口在其 pg.yaml 条目里,或由 pg status <instance> 显示),再基于它搭建仪表盘。

GF_SECURITY_ADMIN_USER / GF_SECURITY_ADMIN_PASSWORD 只在 grafana.db 不存在时生效——它们初始化登录,不覆盖登录。在保留的数据目录上重装,Web 登录仍是原来的;此次打印的密码无效(install 会明说)。要修改既有存储的登录,进容器跑 grafana-cli admin reset-admin-password <new>(pg exec 风格:podman exec -it <container> ...),或 --clean-data 移除后重装。

重复 install 会复用运行中的容器、启动已停的容器——配置改动只有 remove + 重装或 --force 之后才生效。

监听与暴露

Grafana 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以面板在宿主每个网卡的分配端口上可达。这是有意为之——监控面板就是给别的机器看的——并由自动生成的强 admin 密码加 GF_USERS_ALLOW_SIGN_UP=false(关闭自助注册)保护。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建);pg addon list 会显示 http://127.0.0.1:<port>/。--listen <addr> 也接受任意指定网卡的地址。

取回 Web 登录

这个登录是 Grafana 的 Web 签名,与任何 PostgreSQL 角色无关。随时可重读密码:

pg addon password grafana              # 裸打印到 stdout
pg addon password grafana --file ./pw  # 或写入 0600 文件

它也存于 pg.yaml(addons.grafana.<name>.password)。

端口

每个 Grafana 实例占有一个端口:Web UI。它取自以 grafana_start_port(默认 3000——Grafana 惯例端口;从 3000 起步是因为书签和文档都默认它)为基址的端口池,每多一个实例递增,并跳过任何已被受管容器或 used_ports 列表占用的端口。分配结果写入 pg.yaml,所以重装绝不会把别的插件在用的端口再发一遍:

pg addon install grafana --name dash2          # 第二个实例:下一个空闲端口(3000 被占则 3001)
pg addon install grafana --name dash3 --port 3100  # 显式端口优先

Grafana 的端口池基址与其他任何插件都不重号;显式 --port 仍然优先。

配置

addons:
  grafana:
    ui:
      name: ui
      image_tag: docker.io/grafana/grafana:13.2.4
      host_port: 3000
      listen: 0.0.0.0
      admin_user: admin
      password: <generated>
      autostart: false
      # data_dir: /srv/grafana-data    # 可选,覆盖默认位置

配置只经环境变量(GF_*)交付:镜像内的 grafana.ini 从宿主上不可编辑。pgcli 设置的变量有:登录对、监听器(端口 + 绑定地址)、关闭上报(GF_ANALYTICS_REPORTING_ENABLED / GF_ANALYTICS_CHECK_FOR_UPDATES)、GF_USERS_ALLOW_SIGN_UP=false。更多配置不在 v1 范围内;需要不同构建就用 --image 覆盖镜像。

列出

pg addon list
Web add-ons (grafana):
  grafana (name: ui)
    Status:      running
    URL:         http://0.0.0.0:3000/
    Login user:  admin
    Data:        /home/you/.pgcli/addon/grafana/ui/data
    Image:       docker.io/grafana/grafana:13.2.4
    Container:   pgcli-grafana-ui

默认不显示密码以保持列表可安全粘贴;pg addon list --show-password 揭示明文,pg addon password grafana 则为脚本裸打印单个密码。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop grafana
pg addon start grafana

开机自启

要让 Grafana 实例在宿主开机时自动启动,开启它:

pg autostart enable --grafana --name ui

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——数据目录在创建时已 chown 过,除 podman 本身外不需要任何宿主特权。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove grafana              # 保留数据目录——仪表盘与登录跨重装存活
pg addon remove grafana --clean-data # 连数据目录一起删(Grafana 的数据就没了)

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——Grafana 以容器内 uid(472)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。

日志

pg logs addon grafana            # 最近的控制台输出
pg logs addon grafana -f         # 跟踪
pg logs addon grafana -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • install 失败于 “chowning Grafana data dir … to 472:0” —— 镜像以 uid 472 运行且从不自己修复属主,所以 pgcli 在创建容器前先把宿主数据目录 chown 过去:rootless podman 下经 podman unshare chown(映射后的 subordinate uid 才是正确目标),rootful 宿主且 pgcli 自身以 root 运行时(sudo podman,或以 root 跑 pg)直接递归 chown。pgcli 是直接调用 podman CLI,所以只在这两种情形之外才会失败——例如非 root 的 pg 被显式指向别人的 rootful daemon。遇到时以 root 跑 install,或改用 rootless podman;单纯 sudo chown -R 472:0 <data-dir> 不能消除该错误,因为每次 install 都会重新校验属主。
  • 登录密码不对 —— GF_SECURITY_ADMIN_PASSWORD 只在 grafana.db 不存在时初始化登录。对既有存储用容器内 grafana-cli admin reset-admin-password <new>,或 pg addon remove grafana --clean-data + 重装(会清掉仪表盘)。
  • 从别的机器打不开面板 —— Listen 默认 0.0.0.0,端口在宿主所有网卡上是开的;打不开就是宿主防火墙的问题,不是绑定地址。用 pg addon list 确认端口,然后放行。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。
  • 数据加载失败 / 容器以权限告警退出 —— 数据目录必须保持 uid 472 所有;在宿主上手动 chown 过、或用错误属主恢复过备份都会破坏它。重跑 install(--force)重新应用属主修复。

已知限制

  • v1 是裸 Web UI:没有 datasource/dashboard provisioning,不支持反代子路径(没有 --root-url flag——GF_SERVER_ROOT_URL 不设置,所以要在根路径上用 nginx 前置),且是明文 HTTP(TLS 终止是 nginx 插件的职责)。
  • 宿主侧的属主 chown 在 rootless podman(podman unshare chown)下自动完成,pgcli 以 root 跑在 rootful 宿主上也自动完成(直接 chown)。非 root 的 pgcli 连接一个显式配置的远程 rootful daemon 时没有自动路径。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.16 - Prometheus

以 pgcli 插件方式运行 Prometheus——Grafana 读取的指标采集后端,单容器直接跑上游官方镜像

pg addon install prometheus 以受管容器方式运行 Prometheus——指标采集与告警后端——直接跑官方 docker.io/prom/prometheus 镜像。与 Grafana 一样,它是顶层插件:不前置某个具体实例(它抓很多端点),也不依附于任何一个。二者是天然搭档——Prometheus 负责采集与存储、Grafana 负责可视化——且生命周期形状完全一致(单容器、Linux 上宿主网络、各自端口池里的一个 web 端口、持久数据目录、开机自启、日志)。

Prometheus 与 Grafana 的分歧在配置方式。Grafana 只经环境变量(GF_*);Prometheus 读一份配置文件(prometheus.yml),且把运行时调优作为镜像名之后的 CLI flag 传入(--config.file / --web.listen-address / --storage.tsdb.path / --storage.tsdb.retention.time)。所以 pgcli 渲染 prometheus.yml、只读 bind mount 进容器,并让 --config.file 指向容器内该路径。

没有 Web 登录/密码。 与 Grafana(其 0.0.0.0 默认由自动生成的强密码兜底) 不同,Prometheus 完全不带鉴权。它的 HTTP API 可执行任意 PromQL,昂贵查询也 能把自身打满,所以远程暴露的实例只靠网络边界保护——防火墙,或 --listen 127.0.0.1 收紧为仅回环。请把它当作一个无鉴权的读/写控制台来对待,而 非一个有密码保护的面板。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name prometheus 实例名(map 键)
--image docker.io/prom/prometheus:v3.15.0 覆盖镜像
--port 自动(基址 9090) Web 宿主端口;自 prometheus_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环(见上方暴露警告)
--scrape —(自带自监控 job) 抓取声明 JOB=HOST:PORT[,HOST:PORT...] [KEY=VALUE...](可重复)——job 名必填,逗号分隔的多个 target 全部并入这一个 job,在渲染的 prometheus.yml 里成一个 static_configs job;target 列表之后的裸 KEY=VALUE token 成为逐 target 标签,作用于该 spec 的所有 target(带标签的 spec 含空格,整个值要用引号包成一个 shell 参数)。同一 job 名跨 spec 折叠为一个 job;同一 node 被两个 spec 提到时标签合并、只抓一次,而标签集不同的 node 拆成同 job 下多个 static_configs 块(即 nodename=api1 这类按节点打标签的原生形状)。声明的是完整 job 集合(整体替换已存列表)。目标会被宿主侧 GET /metrics 探测,探测不通仅告警
--scrape-append — 同样的声明格式(标签同样支持),但把追加的 target 连同其标签合并进已存的 --scrape 列表(同名 job 并入、新名字排到末尾)而非替换。每个被追加的目标都会探测,任一拉不到数据即中止 install——什么都不写。与 --scrape、--conf-file 互斥
--scrape-interval 15s 渲染配置里的全局 scrape_interval;与 --conf-file 互斥
--retention 15d TSDB 保留期(--storage.tsdb.retention.time);时长如 15d、6mo、2y。是 CLI flag,故与 --conf-file 可并存
--conf-file — 你自己的 prometheus.yml 路径——完全跳过渲染(与 --scrape/--scrape-append/--scrape-interval/--alertmanager-url/--remote-write-url/--remote-write-queue 互斥);见下
--alertmanager-url — 本 Prometheus 转发已触发告警到的 Alertmanager 实例(可重复):裸 HOST[:PORT](不写端口则补 9093)或 http(s)://HOST[:PORT][/path]。渲染的配置会多出一段 alerting: 指向它,并且多出一个对应的 alertmanager 抓取 job(顺带盯住 Alertmanager 自己的 /metrics——不是另一个开关,二者一体)。全部条目先校验后落盘,且必须同一种 scheme(http/https 混用即中止);不探测连通性。与 --conf-file 互斥;见下
--remote-write-url — 把每个抓取到的样本推送一份到的远端存储 URL(可重复;完整 http(s)://host:port/path URL)。渲染的配置多出一段顶级 remote_write:。本地 TSDB 仍按自己的 --retention 保留并可查——远端是一份 WAL 兜底的副本,远端挂掉只是队列积压,恢复后自动重放。先校验(必须是带 host 的 http(s) URL);不探测连通性。与 --conf-file 互斥;见"把样本推给 VictoriaMetrics"一节
--remote-write-queue — 在每个 --remote-write-url 条目下渲染一段 queue_config:(可重复 KEY=VALUE):正整数 capacity、max_shards、min_shards、max_samples_per_send,以及时长 batch_send_deadline、min_backoff、max_backoff(如 5s、1m30s、2y)。VictoriaMetrics 建议高压下加大批量(max_samples_per_send=10000 capacity=20000 max_shards=30)。必须搭配 --remote-write-url;键与值先校验后落盘(未知键、非正整数、无单位时长都会中止)。与 --remote-write-url 同为渲染模式;差异化到单个远端的调优仍需 --conf-file
--rule-file — rule 文件路径(*.rules.yml,顶层是 groups:;可重复)——pgcli 把每个文件拷进托管 rules 目录(只取 basename;两个文件撞名即中止),把该目录只读挂到 /etc/prometheus/rules,渲染的 prometheus.yml 随即用 rule_files 加载这些拷贝。与 --conf-file 可并存:拷贝和挂载无论如何都会发生,你自己的文件决定要不要引用它们。文件不存在或为空即中止——什么都不写;见下
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于不热加载改动的配置,或应用改动的端口/监听/保留期(--rule-file 从无到有一样会触发重建,见下)

数据目录是 Prometheus 的 TSDB。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活已采集的指标。镜像以固化的 nobody(uid/gid 65534)运行且不自建可写目录,所以 pgcli 在创建容器前先在宿主上把数据目录 chown 过去:rootless podman 下经 podman unshare chown 65534:65534,rootful 宿主且 pgcli 以 root 运行时直接递归 chown(与 Grafana 完全同构,只是 id 不同)。

安装

pg addon install prometheus

命令会按需拉取镜像、渲染一份 prometheus.yml、创建容器,并打印 URL、配置路径与保留期:

✓ Prometheus installed: "prometheus"
  Container:  pgcli-prometheus-default-prometheus
  Image:      docker.io/prom/prometheus:v3.15.0
  Data:       /home/you/.pgcli/addon/prometheus/prometheus/data
  Config:     /home/you/.pgcli/addon/prometheus/prometheus/prometheus.yml (rendered: 1 self job + 0 scrape job(s))
  Retention:  15d
  URL:        http://0.0.0.0:9090/

渲染的配置始终含一个抓取本服务自身 /metrics 的自监控 job,所以绝不会空转;--scrape 添加更多 job。在 Grafana 里把 http://<host>:9090/ 加成 Prometheus 数据源,即可立刻查询。

# 每个 --scrape 声明一个 job;= 前的 job 名必填,
# 逗号分隔的多个 target 共用这一个 job(同构 exporter 舰队的自然形状)
pg addon install prometheus --name p1 \
  --scrape postgres-exporter=127.0.0.1:9187 \
  --scrape node=10.0.0.5:9100,10.0.0.6:9100

给 target 打标签(Ansible 渲染配置里那种 nodename: api1)就是在 target 列表后面 追加裸 KEY=VALUE token——每个带标签的 node 各一条 --scrape:

# 一个 windows job,三个 target,各带自己的节点名
# (带标签的 spec 含空格,要整体加引号作为**一个** shell 参数)
pg addon install prometheus --name p1 \
  --scrape "windows=10.0.0.1:9182 nodename=api1" \
  --scrape "windows=10.0.0.2:9182 nodename=api2" \
  --scrape "windows=10.0.0.3:9182 nodename=api3"
# 渲染出的正是原生的按节点标签形状:一个 job_name,
# 每组不同的标签集一个带 labels 的 static_configs 块
  - job_name: windows
    static_configs:
      - targets: ["10.0.0.1:9182"]
        labels:
          nodename: "api1"
      - targets: ["10.0.0.2:9182"]
        labels:
          nodename: "api2"
      - targets: ["10.0.0.3:9182"]
        labels:
          nodename: "api3"

分组以 target 为中心,所以同一个 node 被提两次绝不会抓两遍:两条 spec 都提到 10.0.0.5:9100 时,它合并两边的标签集、只成为一个 target (node=a:1 nodename=api1 + node=a:1 env=prod → a:1 同时带两个标签)。 唯一被拒绝的是矛盾——同一 node 的同一个标签键给了两个不同值,直接中止而不是 悄悄丢掉一个。标签值不能含空白(device="eth0, eth1" 请改用 --conf-file), 标签键必须满足 [A-Za-z_][A-Za-z0-9_]*;值以引号渲染,所以 yes、2 不会在 YAML 里改变类型。

带 --scrape 重复 install 会整体替换已存的 job 集合。想增量追加就用 --scrape-append——格式与 --scrape 完全一致(标签同理),它把每个被追加 target(连同其标签)并入同名已存 job、新 job 名排到末尾,然后热加载:

pg addon install prometheus --name p1 --scrape-append node=10.0.0.7:9100
# 已存的 node job 多出第三个 target,标签不动
pg addon install prometheus --name p1 --scrape-append "node=10.0.0.5:9100 env=prod"
# ……或者给一个已存 target 就地补一个标签(合并,仍然只抓一次)

两个 flag 的探测策略刻意不同:被追加的目标必须以 200 应答 GET /metrics,否则命令在写入任何东西之前就中止(pg.yaml 与运行中的容器分毫未动);而 --scrape 声明完整集合时,探测不通只告警、install 照常——Prometheus 自己会把它标成 DOWN,且全量声明时常包含尚未就绪的端点。探测从宿主拨号——Linux(宿主网络)上与容器抓取处于同一命名空间,判定即容器所见;macOS bridge 下两者不同,故直接跳过探测。

抓取目标是从 Prometheus 容器内拨号的。Linux(宿主网络)上 127.0.0.1:<port> 直达宿主回环;macOS bridge 下它到的是容器自身命名空间,所以远程或 VM 内的目标要给一个可路由地址。

对运行中的实例重复 install:当渲染/拷贝的配置确实变了时热加载(SIGHUP),没变则原样不动;--force 则重建容器——用于应用改动的 --port / --listen / --retention(它们是容器参数,而非配置文件内容)。

把告警转发给 Alertmanager(--alertmanager-url)

让一个 Prometheus 指向一个或多个 pgcli 托管的 Alertmanager,把已触发的告警推给它们:

pg addon install alertmanager --name am1
pg addon install prometheus --name p1 \
  --scrape postgres-exporter=127.0.0.1:9187 \
  --alertmanager-url 127.0.0.1:9093

渲染出的 prometheus.yml 会因这一个 flag 多出两处——一段推送已触发告警的 alerting:,以及一个与之对应的 alertmanager 抓取 job(盯住每个 Alertmanager 自己的 /metrics):

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["127.0.0.1:9093"]
...
  - job_name: alertmanager
    static_configs:
      - targets: ["127.0.0.1:9093"]

接受的形式是裸 host[:port](不写端口则取 Alertmanager 惯用的 9093)或完整的 http(s)://host[:port][/path] URL;抓取目标那侧会剥掉 scheme 与 path,https 则在两段都带成 scheme: https(CA 文件等 TLS 旋钮超出渲染范围——那些要用 --conf-file)。所有条目先于任何写入完成校验,且必须共用一种 scheme(http 与 https 混用即中止),但不做连通性探测:Alertmanager 死没死由 Prometheus 自己跟踪,pg addon prometheus reload 也修不了网络问题。重跑 install 会整体替换已存的 URL 列表(它不像 --scrape 有追加形式)。

实机验证接线:

curl -s localhost:9090/api/v1/alertmanagers           # activeAlertmanagers 列出目标
curl -s 'localhost:9090/api/v1/targets?state=any' | grep -c alertmanager

把样本推给 VictoriaMetrics(--remote-write-url)

Prometheus 内置的 TSDB 是编译进二进制的,换不掉——把 VictoriaMetrics(或任何远端存储)垫在它后面的方式是 remote_write:每个抓取周期把每个样本的一份副本推给远端,而本地 TSDB 照常保留自己的 --retention 窗口、照常提供查询。pgcli 两端都已经能跑,所以这对组合就是两次 install:

# 长期存储——回环上的单机版 VictoriaMetrics,或 vmcluster 插件的 vminsert
# (http://<host>:8480/insert/0/prometheus/api/v1/write)
pg addon install victoriametrics --name vm --listen 127.0.0.1

# 采集后端把每个样本都推进它
pg addon install prometheus --name p1 --listen 127.0.0.1 \
  --remote-write-url http://127.0.0.1:8428/api/v1/write

# 同上,再带上 VictoriaMetrics 建议的高压批量调优
pg addon install prometheus --name p1 --listen 127.0.0.1 \
  --remote-write-url http://127.0.0.1:8428/api/v1/write \
  --remote-write-queue max_samples_per_send=10000 \
  --remote-write-queue capacity=20000 \
  --remote-write-queue max_shards=30

渲染出的 prometheus.yml 多出一段顶级 remote_write:——给了 --remote-write-queue 时,每个 URL 下再多一段 queue_config::

remote_write:
  - url: "http://127.0.0.1:8428/api/v1/write"
    queue_config:
      max_samples_per_send: 10000
      capacity: 20000
      max_shards: 30

重跑 install 会整体替换已存的 URL 列表(与 --alertmanager-url 一样没有追加形式),URL 变化靠热加载(SIGHUP)生效——不重建容器。条目必须是完整的 http(s)://host[:port]/path URL(与 Alertmanager 目标不同,没有补端口/补 scheme 的约定——path 本身就是写入端点);先校验但不探测连通性。--remote-write-queue 走同一条路:重复传入即整体替换已存列表、先校验(只收白名单键——整数容量/分片与时长超时),且同一组 KEY=VALUE 会渲染进每个远端条目下,要按远端差异化调优仍需 --conf-file。没有 --remote-write-url 却单独传它会被中止——无处可调。

验证链路活着且在投递:

# remote-write 队列在排空(pending ≈ 0,samples_total 持续上涨)
curl -s 'localhost:9090/api/v1/query?query=prometheus_remote_storage_samples_pending'
# 样本确实落进了 VictoriaMetrics
curl -s 'localhost:8428/api/v1/query?query=prometheus_build_info'

把 Grafana 从 Prometheus 改指到 VictoriaMetrics(http://<host>:8428/,集群版则是 http://<host>:8481/select/0/prometheus/)之后,Prometheus 实例就退化成薄薄一层采集+转发,VictoriaMetrics 成为可查询的存储。已在 Prometheus TSDB 里的历史数据不会自动回填——一次性迁移用 vmctl prometheus 工具。

加载告警规则(--rule-file)

Prometheus 从 rule_files: 评估告警/记录规则。把 rule 文件交给 pgcli,它就变成托管拷贝:

pg addon install prometheus --name p1 --rule-file /srv/rules/alert.rules.yml

pgcli 把每个文件拷进 <base>/addon/prometheus/<name>/rules/,把该目录只读挂到 /etc/prometheus/rules,渲染的配置按文件名引用拷贝:

rule_files:
  - "/etc/prometheus/rules/alert.rules.yml"

目录是扁平的,所以起作用的是 basename:两个源文件撞同一个 basename 会中止 install(拷贝会悄悄覆盖掉一个)。

改你的原 rule 文件,一次普通的 reload 即可生效:pg addon prometheus reload 在发信号之前会把每个 --rule-file 重拷进托管目录,所以 Prometheus 原地重读新规则——不用 reinstall、不重启容器:

# 编辑 /srv/rules/alert.rules.yml ...
pg addon prometheus reload --name p1
curl -s localhost:9090/api/v1/rules    # 组内容已是改动后的;或看 pg logs addon prometheus

坏 rule 文件的遭遇与坏配置一样:Prometheus 沿用上一份好规则集并把解析错误打进日志,所以手改规则后 reload 完记得查日志(或 rules API)。这些拷贝是托管产物,与渲染出的 prometheus.yml 一样,pg addon remove 无论有没有 --clean-data 都会删掉它们。你的原文件绝不被碰。

--rule-file 与其他渲染旋钮的不同点在于:它与 --conf-file 可并存——pgcli 无论如何都会拷规则、加挂载,是否引用 /etc/prometheus/rules 由你自己的文件决定(在里面写 rule_files: ["/etc/prometheus/rules/*.rules.yml"] 就能匹配到你传入的一切)。唯一一个新增后无法热加载生效的 flag 也是它:从无到有一样会改变挂载集合,而 /etc/prometheus/rules 这个挂载只在有 rule 文件时存在,SIGHUP 加不了 volume,所以这一跃变(及其反向)会重建容器。

自定义配置文件(--conf-file)

把你自己的 prometheus.yml 交给 pgcli,它就完全跳过渲染:

pg addon install prometheus --name p1 --conf-file /etc/prometheus/my.yml

pgcli 把你的文件拷贝进托管路径(<base>/addon/prometheus/<name>/prometheus.yml),并只读挂载这份拷贝——绝不挂你的原文件。这是刻意为之:单文件 bind mount 锁死源 inode,原地改你的原文件不会传播进运行中的容器。正因它是拷贝,改完你的原文件后跑 pg addon prometheus reload 会重拷并通知 Prometheus 重读。pgcli 不校验、不改写文件内容(与 nginx/haproxy 的 --conf-file 同一约定)。--conf-file 与渲染旋钮 --scrape/--scrape-append/--scrape-interval/--alertmanager-url/--remote-write-url/--remote-write-queue 互斥(operator 文件自己掌管其 alerting: 与 remote_write: 段);--retention、--listen 是 CLI flag,--rule-file 是文件拷贝而非渲染旋钮,三者与它并存仍有效。重装时省略 --conf-file 即回到渲染模式。

完整样例

一份接好 pgcli Alertmanager、并加载 pgcli 托管 rule 文件的真实运维配置:

# /etc/prometheus/my.yml
# pg addon install prometheus --name p1 --conf-file /etc/prometheus/my.yml
global:
  scrape_interval: 15s
  scrape_timeout: 15s
  evaluation_interval: 1m

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - localhost:9093 # 指向默认基础端口 9093 上的 pgcli alertmanager

rule_files:
  - /etc/prometheus/rules/*.rules.yml # pgcli 的托管 rules 目录(--rule-file)

remote_write: []

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets:
          - localhost:9090 # 这个 Prometheus 自身
  - job_name: nodes
    static_configs:
      - targets:
          - 10.246.36.2:9100
        labels:
          nodename: lhcloud1
      - targets:
          - 10.246.36.3:9100
        labels:
          nodename: lhcloud2
      - targets:
          - 10.246.36.4:9100
        labels:
          nodename: lhcloud3

上面这种带标签的 nodes 形状,正是如今渲染模式会产出的——三条 --scrape node=…:9100 nodename=… 折叠成一个 nodes job、各带一个有标签的 static_configs 块;渲染器没有暴露的按 job 附加项(抓取超时、relabel 等)仍归手写文件管。

localhost:9090、localhost:9093 能通,是因为 Linux 上容器跑在宿主网络里——Prometheus 容器内的 localhost 就是宿主,两个 web 端口都在上面监听。这份配置里能看到 pgcli 只参与了一处:--rule-file 会把你的 rule 文件拷进 <base>/addon/prometheus/<name>/rules/ 并把该目录挂到 /etc/prometheus/rules,所以上面的 rule_files: glob 才解析得到;而 alerting: 段完全由你自己写——--conf-file 模式下 pgcli 不往文件里加任何东西,也不派生 alertmanager 抓取 job,且 --alertmanager-url 作为渲染旋钮会被拒绝。手写文件别等 reload 后才从日志里发现错误,先自查一遍:

c=$(podman ps --filter name=pgcli-prometheus --format '{{.Names}}' | head -1)
podman exec "$c" promtool check config /etc/prometheus/prometheus.yml

检查的是 pgcli 挂载进容器的那份拷贝,所以在 install(或 reload)之后跑,而不是对着你的原文件路径跑。

热加载(配置热更新)

pg addon prometheus reload --name p1

它重写托管的 prometheus.yml(渲染模式重渲、--conf-file 模式重拷你的文件,两种模式下都会重拷 --rule-file 的拷贝),并向运行中的容器发 SIGHUP,让 Prometheus 原地重读——不重建容器、不中断抓取。它刻意用信号而非 HTTP POST /-/reload 端点,于是那条无鉴权重载路由永远不需要开启(不设 --web.enable-lifecycle)。若新配置有错,Prometheus 会沿用上一份好配置并打日志,所以对手写文件 reload 后请查 pg logs addon prometheus。实例须已在运行(pg addon start prometheus)。有一类变化是信号救不了的:--rule-file 从无到有一样会增删 /etc/prometheus/rules 挂载,所以这一跃变要靠普通 install 重建容器来应用——非空集合内部的改动,reload 就够了。

监听与暴露

Prometheus 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 UI 与 API 在宿主每个网卡的分配端口上可达。这与 Grafana 的默认一致(别的机器上的 Grafana 要能连它)——但背后没有密码。任何能连到端口的人都能执行任意 PromQL、读你采集的指标。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建);pg addon list 会显示 http://127.0.0.1:<port>/。否则请在防火墙层面限制访问。

端口

每个 Prometheus 实例占有一个端口:Web UI/API。它取自以 prometheus_start_port(默认 9090——Prometheus 惯例端口)为基址的端口池,每多一个实例递增,并跳过任何已被受管容器或 used_ports 列表占用的端口。分配结果写入 pg.yaml,所以重装绝不会把别的插件在用的端口再发一遍。9090 与 Grafana(3000)、pgAdmin(5050)、nginx(8080)都不重号;显式 --port 仍优先。

pg addon install prometheus --name p2          # 第二个实例:下一个空闲端口(9090 被占则 9091)
pg addon install prometheus --name p3 --port 9100  # 显式端口仍优先

配置

addons:
  prometheus:
    p1:
      name: p1
      image_tag: docker.io/prom/prometheus:v3.15.0
      host_port: 9090
      listen: 0.0.0.0
      retention: 15d
      scrape:
        - postgres-exporter=127.0.0.1:9187
        - node=10.0.0.5:9100,10.0.0.6:9100
        - node=10.0.0.5:9100 env=prod   # 同一 node 再次出现 → 一次抓取,标签取并集
        - windows=10.0.0.1:9182 nodename=api1   # 裸 KEY=VALUE token = 逐 target 标签
      scrape_interval: 15s
      autostart: false
      # alertmanager: [127.0.0.1:9093]            # 仅渲染模式:转发已触发告警 + 派生的抓取 job
      # remote_write: [http://127.0.0.1:8428/api/v1/write]  # 仅渲染模式:把样本推给长期存储(VictoriaMetrics)
      # remote_write_queue: [max_samples_per_send=10000, capacity=20000]  # 在上面每个远端条目下渲染 queue_config
      # rule_files: [/srv/rules/alert.rules.yml]  # 拷进 <name>/rules,挂到 /etc/prometheus/rules
      # conf_file: /etc/prometheus/my.yml   # 用户文件模式,跳过渲染
      # data_dir: /srv/prometheus-data      # 可选,覆盖默认位置

渲染的 prometheus.yml 承载全局 scrape_interval、始终存在的自监控 job、以及每个 scrape JOB 名一个抓取 job——该 job 各条 spec 列出的所有 target 折叠进去,再按合并后的标签集分桶,于是每一组不同的 KEY=VALUE 组合成为自己单独的 - targets:/labels: 块;--alertmanager-url 会加上 alerting: 段与随之而来的 alertmanager 抓取 job,--remote-write-url 会加上一段顶级 remote_write:(--remote-write-queue 在每个条目下填充 queue_config: 块),--rule-file 会加上 rule_files: 段。retention 与监听绑定不在那个文件里——它们作为 CLI flag 传给 prometheus 二进制(--storage.tsdb.retention.time、--web.listen-address),这正是它们能与 --conf-file 并存的原因。

列出

pg addon list
Web add-ons (prometheus):
  prometheus (name: p1)
    Status:      running
    URL:         http://0.0.0.0:9090/
    Config:      /home/you/.pgcli/addon/prometheus/p1/prometheus.yml [rendered (1 scrape job(s))]
    Retention:   15d
    Data:        /home/you/.pgcli/addon/prometheus/p1/data
    Image:       docker.io/prom/prometheus:v3.15.0
    Container:   pgcli-prometheus-p1

没有凭据可揭示——Prometheus 本就没有——所以 --show-password 对它不会多显示任何东西。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop prometheus
pg addon start prometheus

开机自启

要让 Prometheus 实例在宿主开机时自动启动,开启它:

pg autostart enable --prometheus --name p1

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——读磁盘上已存在的托管 prometheus.yml,且数据目录在创建时已 chown 过,除 podman 本身外不需要任何宿主特权。启动顺序上 Prometheus 排在 Grafana 之后、nginx 之前。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove prometheus              # 保留 TSDB——已采集指标跨重装存活
pg addon remove prometheus --clean-data # 连 TSDB 一起删

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——Prometheus 以容器内 uid(65534)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。--conf-file 模式只删 pgcli 的拷贝;你的原文件在何处都不被触碰。--rule-file 的原文件同样幸存——被删的只有托管的 rules/ 拷贝,且它们在每次 remove 时都消失(无论有无 --clean-data,与渲染出的 prometheus.yml 一样,没有值得复活的状态)。

日志

pg logs addon prometheus            # 最近的控制台输出
pg logs addon prometheus -f         # 跟踪
pg logs addon prometheus -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • install 失败于 “chowning Prometheus data dir … to 65534:65534” —— 镜像以 nobody(uid/gid 65534)运行且从不自己修复属主,所以 pgcli 在创建容器前先把宿主数据目录 chown 过去:rootless podman 下经 podman unshare chown(映射后的 subordinate uid 才是正确目标),rootful 宿主且 pgcli 自身以 root 运行时直接递归 chown。pgcli 是直接调用 podman CLI,所以只在这两种情形之外才会失败——例如非 root 的 pg 被显式指向别人的 rootful daemon。遇到时以 root 跑 install,或改用 rootless podman;单纯 sudo chown -R 65534:65534 <data-dir> 不能消除该错误,因为每次 install 都会重新校验属主。
  • 某个抓取目标显示 DOWN —— 确认 Prometheus 能连到它(它从容器内拨号;Linux 上 127.0.0.1:<port> 是宿主回环,但跨主机/VM 目标需要可路由地址,macOS bridge 上尤甚)。确认 exporter 在跑、路径是 /metrics。pg logs addon prometheus 会显示抓取失败。
  • --scrape-append 中止于 “returned no data” —— 这是设计行为:被追加的目标必须以 200 应答宿主的 GET /metrics,否则任何东西都不写入(已存实例与 pg.yaml 分毫未动)。要么先把 exporter 拉起来,要么改用 --scrape(仅告警)声明完整 job 集合。macOS 上跳过该探测。
  • 配置 reload 没生效 —— pg addon prometheus reload 需要容器在运行;若新的 prometheus.yml 有格式错误,Prometheus 会沿用上一份好配置并打日志。查 pg logs addon prometheus,改好文件,再 reload。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。
  • 远程 API 打不开 / 意外可访问 —— 默认 0.0.0.0 下打不开是宿主防火墙问题,不是绑定地址;反之若你不想被访问,用 --listen 127.0.0.1 --force。

已知限制

  • v1 是裸采集后端:不自带 PostgreSQL exporter(自己装好,再 --scrape postgres-exporter=127.0.0.1:9187 指向它)。但长期存储与告警都是接好的:--remote-write-url 把抓取到的样本推给 VictoriaMetrics 插件(单机版或 vmcluster),--alertmanager-url 把已触发告警转发给 pgcli 的 Alertmanager,--rule-file 加载告警规则(三者都是渲染模式;其余场景用 --conf-file 手写)。它是你让 Grafana 连的采集/存储/查询层。
  • 无鉴权。 Prometheus 的 API 执行任意 PromQL;0.0.0.0 绑定的唯一保护是网络边界。这比 Grafana 有密码兜底的 0.0.0.0 是实质更弱的态势——不要把它暴露到不可信网络。
  • pgcli 渲染或拷贝 prometheus.yml,但不校验它(install 阶段没有 promtool check config 闸门)。渲染出的配置不可能有错,但有错的 --conf-file 只会在 reload 时从容器日志里暴露。
  • 宿主侧的属主 chown 在 rootless podman(podman unshare chown)下自动完成,pgcli 以 root 跑在 rootful 宿主上也自动完成(直接 chown)。非 root 的 pgcli 连接一个显式配置的远程 rootful daemon 时没有自动路径。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.17 - Alertmanager

以 pgcli 插件方式运行 Alertmanager——Prometheus 的告警去重/分组/路由端,单容器直接跑上游官方镜像

pg addon install alertmanager 以受管容器方式运行 Alertmanager——位于 Prometheus 下游的告警去重、分组、路由与通知层——直接跑官方 docker.io/prom/alertmanager 镜像。与 Prometheus、Grafana 一样,它是顶层插件:不前置某个具体实例,也不依附于任何一个。Prometheus 与 Alertmanager 是天然一对——Prometheus 触发告警,Alertmanager 决定通知谁、怎么通知——而本插件在形状上是 Prometheus 的近乎完全同构体(单容器、Linux 上宿主网络、各自端口池里的一个 web 端口、持久数据目录、开机自启、日志、渲染配置文件 + 镜像名之后的 CLI flag、SIGHUP 热加载、无 Web 登录)。

衔接的工作在 Prometheus 那一侧。 pg addon install alertmanager 不碰任何 prometheus.yml——也不需要碰:由 Prometheus 自己指向本实例,命令是 pg addon install prometheus --alertmanager-url <本实例的 host:port>,它会注入 alerting: 段(外加一个抓取本实例 /metrics 的 job)。若 Prometheus 用的是自己的 配置文件(--conf-file),就自己在里面写 alerting: 段。两种方式只要指过来了,告警 就会流进来。

没有 Web 登录/密码——而且比 Prometheus 的赌注更大。 Alertmanager 完全不 带鉴权,且它的无鉴权 API 的可写性比 Prometheus 那个以读为主的 API 更危险:任何能 连到端口的人都能创建 silence——即悄悄压掉 oncall 期待的每一条通知,且不会在 Prometheus 自己的指标里留下任何痕迹。它的 0.0.0.0 默认因此只靠网络边界保护, 而这条边界在这里比插件家族里任何其他地方都更重要。请把它当作把一套告警体系的静音 开关交出去来对待,而非一个有密码保护的面板。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证 过。

参数

参数 默认值 说明
--name alertmanager 实例名(map 键)
--image docker.io/prom/alertmanager:v0.34.1 覆盖镜像
--port 自动(基址 9093) Web 宿主端口;自 alertmanager_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环(见上方暴露警告)
--webhook-url — 要通知的 webhook URL(可重复)。每个 URL 都被渲染成同一个 web.hook receiver 下的一条 webhook_configs——多个 URL 会扇出通知到每一个。写入任何东西之前先用 net/url 校验(可解析、http/https、Host 非空);格式错误的 URL 会让 install 直接中止,不留任何痕迹。不探测连通性(见下)。与 --conf-file 互斥
--conf-file — 你自己的 alertmanager.yml 路径——完全跳过渲染(与 --webhook-url 互斥);见下
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于不热加载改动的配置,或应用改动的端口/监听

数据目录承载 Alertmanager 的状态:silence 与通知状态。它跨 pg addon remove 存活 (用 --clean-data 才删除),重装同名实例即复活它们。镜像以固化的 nobody(uid/gid 65534)运行且不自建可写目录,所以 pgcli 在创建容器前先在宿主上把数据目录 chown 过 去:rootless podman 下经 podman unshare chown 65534:65534,rootful 宿主且 pgcli 以 root 运行时直接递归 chown(与 Prometheus、Grafana 完全同构,只是 id 不同)。

--webhook-url 不做连通性探测,这是刻意为之:不像 Prometheus 的抓取(GET /metrics,install 时即可达并可应答),webhook 只在告警真正路由过去时才发的 POST ——install 时拨它一下也告诉不了你什么可靠信息。唯一的提前闸门是 URL 的形状;端点 是否存在、是否可达、能否接受这个 payload,只能在告警实际触发时才知道(通知没到就查 pg logs addon alertmanager)。

安装

pg addon install alertmanager

命令会按需拉取镜像、渲染一份 alertmanager.yml、创建容器,并打印 URL、配置路径以及 路由渲染成什么:

✓ Alertmanager installed: "alertmanager"
  Container:  pgcli-alertmanager-default-alertmanager
  Image:      docker.io/prom/alertmanager:v0.34.1
  Data:       /home/you/.pgcli/addon/alertmanager/alertmanager/data
  Config:     /home/you/.pgcli/addon/alertmanager/alertmanager/alertmanager.yml (rendered: no-op receiver)
  URL:        http://0.0.0.0:9093/

没有 --webhook-url 时,渲染的配置是一条指向仅有名字、no-op receiver 的单路由:一 份合法、功能完整的配置——Web UI 与 API 在线、silence 可用、HA 集群端口已禁用——只是 什么都不发。这是"先把端点拉起来,路由之后再定"的默认形态。要真正发出通知,加一个 webhook:

# 一个 URL → 路由的 receiver 变成带一条 webhook_configs 的 web.hook
pg addon install alertmanager --name am1 --webhook-url http://127.0.0.1:5001/

# 多个 URL → 全部落在同一个 web.hook receiver 下(扇出到每一个)
pg addon install alertmanager --name am1 \
  --webhook-url http://127.0.0.1:5001/ \
  --webhook-url https://hooks.example.com/alert?token=abc

对运行中的实例重复 install:当渲染/拷贝的配置确实变了时热加载(SIGHUP),没变 则原样不动;--force 则重建容器——用于应用改动的 --port / --listen(它们是容器参 数,而非配置文件内容)。

带不同的 --webhook-url 集合重复 install 会整体替换已存列表(一个都不传则清空回 no-op);正是这个 diff 决定要不要 reload。

自定义配置文件(--conf-file)

把你自己的 alertmanager.yml 交给 pgcli,它就完全跳过渲染:

pg addon install alertmanager --name am1 --conf-file /etc/alertmanager/my.yml

pgcli 把你的文件拷贝进托管路径(<base>/addon/alertmanager/<name>/alertmanager.yml), 并只读挂载这份拷贝——绝不挂你的原文件。这是刻意为之:单文件 bind mount 锁死源 inode,原地改你的原文件不会传播进运行中的容器。正因它是拷贝,改完你的原文件后跑 pg addon alertmanager reload 会重拷并通知 Alertmanager 重读。pgcli 不校验、不改写文 件内容(与 nginx/haproxy/prometheus 的 --conf-file 同一约定)。--conf-file 与 --webhook-url(塑造渲染文件的那个 flag)互斥——真正的路由树(email/Slack/PagerDuty、 子路由、inhibition)活在你自己的文件里。重装时省略 --conf-file 即回到渲染模式。

这份文件就是普通的 Alertmanager YAML——pgcli 不往里加东西,除了"能解析"之外对它也没 任何要求。下面是一份完整示例,含子路由树、inhibition,以及三种不同类型的 receiver:

# 一份完整的 Alertmanager 配置,配合 `pg addon install alertmanager
# --name am1 --conf-file /path/to/this.yml` 使用——pgcli 本来会渲染的那
# 份被下面这些完全替代;只有容器 flag
# (--web.listen-address、--storage.path、--cluster.listen-address=)
# 仍归 pgcli 管。
global:
  resolve_timeout: 5m
  smtp_smarthost: "smtp.example.com:587"
  smtp_from: "alertmanager@example.com"
  smtp_auth_username: "alertmanager@example.com"
  smtp_auth_password: "CHANGE-ME"
  slack_api_url: "https://hooks.slack.com/services/T00000/B00000/XXXXXXXX"

inhibit_rules:
  # 同一实例上同一告警的 critical,会抑制它的 warning/info 噪音
  - source_matchers: ['severity = critical']
    target_matchers: ['severity =~ warning|info']
    equal: ['alertname', 'instance']

route:
  receiver: default
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    # critical 发到 oncall 邮箱,且仍会继续向下匹配到 default
    - matchers: ['severity = critical']
      receiver: oncall-email
      continue: true
    # storage 团队的告警走自己的 Slack channel
    - matchers: ['team = storage']
      receiver: storage-slack
      group_by: ['alertname']
      group_wait: 10s

receivers:
  - name: default
    webhook_configs:
      - url: "http://127.0.0.1:5001/"

  - name: oncall-email
    email_configs:
      - to: "oncall@example.com"
        send_resolved: true

  - name: storage-slack
    slack_configs:
      - channel: "#storage-alerts"
        send_resolved: true

pgcli 本身不校验这份文件(install 阶段没有 amtool check-config 闸门——见已知限 制),但镜像自带 amtool,所以你可以在信任一次 reload 之前,进正在运行的容器里检查 托管的那份拷贝(容器名以 pg addon list 打印的为准,带命名空间前缀——上面那段实录里 是 pgcli-alertmanager-default-am1):

pg addon install alertmanager --name am1 --conf-file /etc/alertmanager/my.yml
# 编辑 /etc/alertmanager/my.yml ...
podman exec pgcli-alertmanager-default-am1 amtool check-config /etc/alertmanager/alertmanager.yml
pg addon alertmanager reload --name am1   # 文件没问题了再重拷 + SIGHUP

热加载(配置热更新)

pg addon alertmanager reload --name am1

它重写托管的 alertmanager.yml(渲染模式重渲、--conf-file 模式重拷你的文件),并向 运行中的容器发 SIGHUP,让 Alertmanager 原地重读——不重建容器、不中断通知。

与 Prometheus 不同,Alertmanager 的 POST /-/reload 端点是存在的,且无法关闭(没 有类似 --web.enable-lifecycle 的开关)。pgcli 仍然用 SIGHUP 重载——不是因为那条路由 被关掉了,而是为了永远不需要依赖一条按镜像设计就一直敞开的无鉴权 HTTP 端点。任何能打 到端口的人本就能直接打这条路由;pgcli 只是不往这个面上再加东西。若你 reload 一份被拒 绝的手写 --conf-file,Alertmanager 会沿用上一份好配置并打日志,所以之后请查 pg logs addon alertmanager。实例须已在运行(pg addon start alertmanager)。

监听与暴露

Alertmanager 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 UI 与 API 在 宿主每个网卡的分配端口上可达。背后没有密码,而且——与 Prometheus 不同——这个 API 允许调用方创建 SILENCE,即压掉 oncall 期待的每一条通知,还能重载自身配置。仅回环 部署才是安全的默认:传 --listen 127.0.0.1(用 --force 重建);pg addon list 会 显示 http://127.0.0.1:<port>/。否则请在防火墙层面限制访问,并把敞开的 0.0.0.0 当 作一个可被触达的静音开关来对待。

端口

每个 Alertmanager 实例占有一个端口:Web UI/API。它取自以 alertmanager_start_port (默认 9093——Alertmanager 惯例端口)为基址的端口池,每多一个实例递增,并跳过任何 已被受管容器或 used_ports 列表占用的端口。分配结果写入 pg.yaml,所以重装绝不会把 别的插件在用的端口再发一遍。9093 与 Grafana(3000)、pgAdmin(5050)、 Prometheus(9090)、nginx(8080)都不重号;显式 --port 仍优先。

9094——HA gossip 端口——永不被开启,因为它被一个 flag 关掉了。 Alertmanager 的高可用集群默认在 TCP+UDP 9094 上监听,且对一个单实例而言会不停尝 试引导永远不会到来的 peer。pgcli 传 --cluster.listen-address=(空串)彻底关掉它 ——这是单实例约定,每次创建都强制执行,于是集群端口永远不开、也没有 UDP 监听器残 留。(这只关乎 9094 的集群用途;这个数字并未被预留,所以 web 端口池照样可以 把 9094 作为第二个实例的 web 端口分配出去——见下面的示例。)把多个 pgcli 托管的 Alertmanager 集群成一个 gossip 组并未接通(见已知限制)。

pg addon install alertmanager --name am2          # 第二个实例:9093 之后的下一个空闲端口(比如 9094——端口池是纯数字分配,被禁用的集群端口号并没有被预留)
pg addon install alertmanager --name am3 --port 9200  # 显式端口仍优先

配置

addons:
  alertmanager:
    am1:
      name: am1
      image_tag: docker.io/prom/alertmanager:v0.34.1
      host_port: 9093
      listen: 0.0.0.0
      webhook:
        - http://127.0.0.1:5001/
        - https://hooks.example.com/alert?token=abc
      autostart: false
      # conf_file: /etc/alertmanager/my.yml   # 用户文件模式,跳过渲染
      # data_dir: /srv/alertmanager-data      # 可选,覆盖默认位置

渲染的 alertmanager.yml 承载一个 global.resolve_timeout、一条根路由 (group_by: ['alertname'],默认 group_wait/interval/repeat_interval)指向单个 receiver,以及这个 receiver 的 webhook_configs——web.hook 下一个 webhook URL 一 条,列表为空时则是仅有名字的 no-op receiver。监听绑定、存储路径、被禁用的集群地址 不在那个文件里——它们作为 CLI flag 传给 alertmanager 二进制 (--web.listen-address、--storage.path、--cluster.listen-address=),这正是 --listen 能与 --conf-file 并存的原因。

列出

pg addon list
Web add-ons (alertmanager):
  alertmanager (name: am1)
    Status:      running
    URL:         http://0.0.0.0:9093/
    Config:      /home/you/.pgcli/addon/alertmanager/am1/alertmanager.yml [rendered (2 webhook)]
    Data:        /home/you/.pgcli/addon/alertmanager/am1/data
    Image:       docker.io/prom/alertmanager:v0.34.1
    Container:   pgcli-alertmanager-am1

没有凭据可揭示——Alertmanager 本就没有——所以 --show-password 对它不会多显示任何东 西,pg addon password alertmanager 也解析不出任何东西。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。 start/stop 显式管理它们:

pg addon stop alertmanager
pg addon start alertmanager

开机自启

要让 Alertmanager 实例在宿主开机时自动启动,开启它:

pg autostart enable --alertmanager --name am1

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起 来——读磁盘上已存在的托管 alertmanager.yml,且数据目录在创建时已 chown 过,除 podman 本身外不需要任何宿主特权。启动顺序上 Alertmanager 排在 Prometheus 之后、nginx 之前—— 是上面那个采集器天然的搭档,衔接也恰好在采集器那一侧完成(pg addon install prometheus --alertmanager-url …)。完整机制、状态与关闭方式见 开机 自启。

移除

pg addon remove alertmanager              # 保留 silence 与通知状态——它们跨重装存活
pg addon remove alertmanager --clean-data # 连它们一起删

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——Alertmanager 以容器内 uid(65534)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。 --conf-file 模式只删 pgcli 的拷贝;你的原文件在何处都不被触碰。

日志

pg logs addon alertmanager            # 最近的控制台输出
pg logs addon alertmanager -f         # 跟踪
pg logs addon alertmanager -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • install 失败于 “chowning Alertmanager data dir … to 65534:65534” —— 镜像以 nobody(uid/gid 65534)运行且从不自己修复属主,所以 pgcli 在创建容器前先把宿主数 据目录 chown 过去:rootless podman 下经 podman unshare chown(映射后的 subordinate uid 才是正确目标),rootful 宿主且 pgcli 自身以 root 运行时直接递归 chown。pgcli 是直接调用 podman CLI,所以只在这两种情形之外才会失败——例如非 root 的 pg 被显式指向别人的 rootful daemon。遇到时以 root 跑 install,或改用 rootless podman;单纯 sudo chown -R 65534:65534 <data-dir> 不能消除该错误,因为每 次 install 都会重新校验属主。
  • 收不到任何通知 —— 最常见的原因是 no-op receiver:一次不带 --webhook-url 的 install 渲染的配置永远什么都不发。查 pg addon list 里的 Config: 行(它写的是 no-op receiver 还是 rendered (N webhook))。若确实配了真实 webhook,它在 install 时不做连通性探测——可达性只有告警真正触发时才知道,所以要查 pg logs addon alertmanager 里的 POST 失败,并确认端点能接受 Alertmanager 的 webhook payload。
  • --webhook-url 中止于解析错误 —— 这是设计行为:每个 URL 都在 pg.yaml 或任何文 件被写入之前完成校验(net/url 解析、http/https scheme、Host 非空),所以格式错 误的 URL 不会动到运行中的实例与配置。改对 URL 再重跑。
  • 配置 reload 没生效 —— pg addon alertmanager reload 需要容器在运行;若新的 alertmanager.yml 被拒绝(路由引用了未定义的 receiver、YAML 格式错误), Alertmanager 会沿用上一份好配置并打日志。查 pg logs addon alertmanager,改好文件, 再 reload。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。
  • 远程 API 打不开 / 意外可访问 —— 默认 0.0.0.0 下打不开是宿主防火墙问题,不是绑 定地址;反之若你不想被访问(记住:可触达即意味着任何人能创建 silence),用 --listen 127.0.0.1 --force。

已知限制

  • 单向衔接。 本插件既不往 prometheus.yml 注入 alerting: 段,也不从 Prometheus 读取任何信息——它只接收被推进其 API 的东西。接通的动作在 Prometheus 那一侧完成(pg addon install prometheus --alertmanager-url …,渲染模式; --conf-file 模式就自己写 alerting: 段)。反过来,本插件这边也永远不会去检视任何 prometheus.yml。
  • 无鉴权——且其后果比 Prometheus 更尖锐。API 能创建 silence(静音 oncall)、能重载 配置,且 POST /-/reload 在镜像里无法关闭(pgcli 只是不去用它)。0.0.0.0 绑定的 唯一保护是网络边界;不要把它暴露到不可信网络。
  • 单实例 / 集群已禁用。 --cluster.listen-address= 始终以空串传入,所以 HA gossip (TCP+UDP 9094)永不打开。把多个 pgcli 托管的 Alertmanager 集群起来在 v1 里不支持。
  • Webhook 在 install 时不做连通性探测(只校验 URL 形状)。可达性与 payload 是否 被接受,在告警触发时才浮现于日志。
  • pgcli 渲染或拷贝 alertmanager.yml,但不校验它(install 阶段没有 amtool check-config 闸门)。渲染出的配置不可能有错,但有错的 --conf-file 只会在 reload 时 从容器日志里暴露。
  • 宿主侧的属主 chown 在 rootless podman(podman unshare chown)下自动完成,pgcli 以 root 跑在 rootful 宿主上也自动完成(直接 chown)。非 root 的 pgcli 连接一个显式配 置的远程 rootful daemon 时没有自动路径。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.18 - Gitea

以 pgcli 插件方式运行 Gitea——自托管 Git 服务,免向导安装、自动生成 Web 登录、可选外置 PostgreSQL 元数据库

pg addon install gitea 以受管容器方式运行 Gitea——轻量级自托管 Git 服务——直接跑官方上游镜像(docker.io/gitea/gitea,rootful 构建)。与 Grafana 一样,它是顶层插件:不依附于任何单个 PostgreSQL 实例,也不自动前置它们——但可以把某个 PG 实例用作自己的元数据库(见下文 --dsn)。单个容器同时提供 Web UI、Git HTTP 流量,以及 git-over-ssh(跑在镜像自带的 OpenSSH sshd 上——见下文「Git SSH」小节;无论 --ssh-port 与否这个 sshd 都在跑,该 flag 只决定它是否是活的克隆端点)。

安装全程非交互:INSTALL_LOCK=true 跳过 Gitea 的 Web 初始化向导,admin 账号随后由容器内 gitea admin user create 命令行创建。Web 登录密码在首次安装时自动生成、打印一次、存入 pg.yaml,之后用 pg addon password gitea 取回。Listen 默认 0.0.0.0——Git 服务天然要给别的机器克隆——暴露面由自动生成的强密码与关闭自助注册兜底;只想本机访问就传 --listen 127.0.0.1。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上跑在 podman machine 里,发布 Web 端口、以及启用时的 SSH 端口),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name gitea 实例名(map 键)
--image docker.io/gitea/gitea:28.1.0 覆盖镜像(仅限 rootful 构建,见已知限制)
--port 自动(基址 3080) Web 宿主端口;自 gitea_start_port 自动分配
--ssh-port 未指定 让那个一直在跑的 sshd 变成活的 git-over-ssh 克隆端点(见下文「Git SSH」小节)。pgcli 给每一个实例都从 gitea_ssh_start_port(基址 2222)分配一个端口用来 park sshd 离 22,SSH 开或关都一样;--ssh-port 0 关掉克隆语义、端口占用不释放
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环。同时也是 Gitea 渲染进 clone URL 的主机名
--admin-user gitea Web 登录账号(故意不用 git——那是容器内的 OS 用户,ssh:// 克隆认证的就是它)
--password 自动生成 Web 登录密码(仅在 admin 账号尚不存在时生效,见下)
--dsn 空 用 postgres://user:pass@host:port/db 把元数据放到外置 PostgreSQL,替代内嵌 SQLite;目标库必须已存在
--allow-registration false 打开自助注册(Web UI 出现注册入口)。默认关闭——见「监听与暴露」一节
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于应用改动的端口、凭据、dsn、监听地址或注册开关

安装

pg addon install gitea

命令会按需拉取镜像、创建并启动容器,等 Gitea 首启迁移跑完,创建 admin 账号,然后打印:

✓ Gitea installed: "gitea"
  Container:  pgcli-gitea-gitea
  Image:      docker.io/gitea/gitea:28.1.0
  Data:       /home/you/.pgcli/addon/gitea/gitea/data
  Web URL:    http://localhost:3080/
  Git SSH:    disabled (HTTP clone/push only; enable with --ssh-port N --force)

  Login user:     gitea
  Login password: <auto-generated>

用浏览器打开 URL,以打印的凭据登录,在 Web UI 里建第一个 org/repo。HTTP 克隆用同一个登录:

git clone http://127.0.0.1:3080/<owner>/<repo>.git

数据目录与 /data

Gitea 的所有资产都在这一个 bind 挂载目录里:仓库、渲染出的 /data/gitea/conf/app.ini、SQLite 元数据库(/data/gitea/gitea.db——用 --dsn 时不存在)、SSH host key、日志。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活一切。

镜像的 rootful 构建以 git 用户运行——uid/gid 1000,经 USER_UID/USER_GID 钉死,宿主的预 chown 与容器进程从此不可能漂移——而其 entrypoint 只 chown 它自己创建的文件,所以 pgcli 在创建容器前先在宿主上把数据目录 chown 过去:rootless podman 下经 podman unshare chown 1000:1000,rootful 宿主且 pgcli 以 root 运行时直接递归 chown。

pgcli 绝不注入 SECRET_KEY 或 INTERNAL_TOKEN。 Gitea 每次启动都会把所有 GITEA__<section>__<KEY> 环境变量折叠进持久化的 app.ini;若 pgcli 每次安装重新生成并注入加密 secret,那么在保留数据目录的重装上,新的 SECRET_KEY 会覆盖持久 app.ini,而幸存的数据库里 2FA 种子与 OAuth client secret 仍是按旧密钥加密的——一次无报错、无日志的静默锁死。所以这些密钥归 volume 所有:Gitea 首启自生成,pgcli 只拥有 admin 登录密码。

admin 密码只在数据库里不存在该 admin 账号时生效——账号是 gitea admin user create 建出来的,pgcli 从不重置。在保留的数据目录(或保留的 --dsn 数据库)上重装,Web 登录仍是原来的;此次打印的密码无效(install 会明说)。要修改既有存储的登录,进容器跑 gitea admin user change-password(pg exec 风格:podman exec -it <container> gitea admin user change-password ...),或 --clean-data 移除后重装(SQLite 模式——dsn 模式的外置库还得自己 drop)。

重复 install 会复用运行中的容器、启动已停的容器——配置改动只有 remove + 重装或 --force 之后才生效。

数据库:SQLite 或 --dsn

默认 Gitea 用 /data 里的内嵌 SQLite——零依赖、单文件、随数据目录一起复活。要给共享或长命的部署,把元数据指到 PostgreSQL:

pg addon install gitea --name git1 --dsn "postgres://gitea:secret@127.0.0.1:35432/gitea"

pgcli 在写入任何东西之前校验 DSN,并把它渲染成 GITEA__database__DB_TYPE=postgres 加 HOST/NAME/USER/PASSWD。安装摘要与 pg addon list 回显时凭据是隐藏的(只留 host:port/db)。与 PostgREST 的 --dsn 同一约定:目标库必须已存在(在受管实例上 pg exec <instance> -c 'CREATE DATABASE gitea ...',或任意其他方式建好);Gitea 首启自建自己的 schema。pg addon remove gitea——即使带 --clean-data——永远不碰外置数据库;里面只有 Gitea 的表。

Git SSH(可选)

每个 Gitea 实例都会跑一个 OpenSSH sshd——官方镜像的 s6 openssh 服务是无条件启动的(它的 run 脚本就是 exec sshd,没有任何 GITEA__server__* env 能关掉)。--ssh-port 切换的其实不是"有没有 sshd", 而是"这个 sshd 是不是活的 git-over-ssh 克隆端点"。pgcli 给每一个实例都从 gitea_ssh_start_port(基址 2222)分配一个端口——不管 SSH 开还是关——就是为了 把这个永远在跑的 sshd 挪开 22:在宿主网络上,留在 22 就会跟宿主自己的 sshd 死磕,日志会永远刷 Bind to port 22 … Address in use。pgcli 用的是镜像的 裸 env SSH_LISTEN_PORT / SSH_PORT——sshd_config 里那条 Port 行只 看这两个裸变量(只设 GITEA__server__SSH_LISTEN_PORT 仅改 Gitea 自己认为的 端口,sshd 绑的还是 22)。

  • SSH 关(默认,不传 --ssh-port):DISABLE_SSH=true,Gitea 不写 git 用户的 authorized_keys、也不显示 ssh:// clone URL。s6 的 sshd 照样在跑、parked 在池端口上,只是没有任何客户端会去连它。池里的端口占用 不释放——它本来就还要 park sshd。
  • SSH 开(--ssh-port N,N>0):git-over-ssh 就走这个 s6 sshd—— 即镜像的原生路线。DISABLE_SSH=false 让 Gitea 给 git 用户写 authorized_keys(sshd 通过 AuthorizedKeysFile 指向 /data/gitea/ssh/ 读取);Gitea 自己的内建 Go SSH 监听器用 START_SSH_LISTENER=false 显式关掉,避免同一个 netns 里两个进程抢同一个端口;SSH_DOMAIN / SSH_PORT 渲染 clone URL 为 ssh://git@<host>:N/<owner>/<repo>.git。
pg addon install gitea --name git1 --ssh-port 2222   # 显式;不传走池分配

认证走 Web UI 里给 git 账号添加的 SSH key(OS 用户是 git,Web admin 是 gitea)。host key 落在 <data>/ssh/,跨重装稳定——若用 --clean-data 移除过,客户端下次会看到新 host key 告警。

macOS bridge 下端口会额外发布,但只有 SSH 开启时才发布——parked 的 sshd 是有意不可达的,所以不给 -p。后续 install 不传 --ssh-port 会保留 已存的开关状态;--ssh-port 0 关掉克隆端点语义(端口占用不释放—— 只有 DISABLE_SSH 翻转和 clone URL 变化会随 --force 重建生效)。

监听与暴露

Gitea 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 Web UI 与 Git HTTP 在宿主每个网卡的分配端口上可达。这是有意为之——Git 服务就是给别的机器克隆的——并由自动生成的强 admin 密码加 DISABLE_REGISTRATION=true(关闭自助注册;建号请用 pg addon gitea user create)保护。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建)。

--allow-registration 翻开的就是后一道闸:pg addon install gitea --name <n> --allow-registration --force 会渲染 DISABLE_REGISTRATION=false,Web UI 上随之出现注册入口;重新关闭要用 --allow-registration=false --force——开关是存进 pg.yaml 的配置值、只在显式传该 flag 时改写,所以普通重装(或不带 flag 的重装)会原样保留存储状态,改动也只有 --force 才推入运行中的容器。放开带来的是建号垃圾而非夺权:实测注册表单建出的账号 IsAdmin=false——Gitea 没有任何办法让注册产生管理员;提升为站点管理员只有 pg addon gitea user create --admin 或 admin 面板两条路,与账号如何产生无关。若确要在公开监听下开放注册,Gitea 同属 service 段的配套收紧项(REGISTER_EMAIL_CONFIRM、验证码、REQUIRE_SIGNIN_VIEW)不在 pgcli 渲染的 env 集合里,仍属 Gitea 侧配置。

--listen 同时也是展示用 clone URL 的主机名:通配绑定不是可连地址,所以 0.0.0.0 会显示成 localhost;显式主机/IP 则原样进入 ROOT_URL 与 ssh:// URL。当 Gitea 躲在转发了 X-Forwarded-Proto 与 Host 的反代后面时——pgcli 的 nginx 模板默认就转这两个头——Gitea 会按进来的请求重写页面显示的 clone URL,所以仓库页的克隆按钮读作 https://<代理主机>/<owner>/<repo>.git,完全正确(scheme 取自 X-Forwarded-Proto、host 取自 Host、标准端口省略)。pgcli 渲染的 ROOT_URL 仍是 http://<host>:<web端口>/(没有配置项能宣告 https/代理端口),但那只管 Gitea 服务端按 ROOT_URL 直接生成的 URL;交互式 clone URL 跟着请求走。要上 TLS,见下文「经 nginx 提供 HTTPS」一节。

经 nginx 提供 HTTPS

Gitea 自己只走明文 HTTP;要用 https:// 访问,就用 nginx 插件前置它——由 nginx 终止 TLS 再反代到 Gitea 的 Web 端口。两点关键:自带证书(或让 pgcli 签一张自签证书),以及用 nginx 的自定义 --conf-file 模式——模板默认的 client_max_body_size(1 MiB)会把稍大的 git push 用 HTTP 413 拒掉。

# 1. 签一张自签叶子证书,SAN 覆盖你克隆用的主机名(IP 和/或 DNS)
pg cert --host 10.0.0.9,git.internal \
        --cert-file /srv/gitea.crt --key-file /srv/gitea.key

把代理配置写成 /srv/gitea-nginx.conf——对 Git 真正要紧的是 client_max_body_size 10m(nginx 内建默认只有 1 MiB,不抬高的话稍大的 push 还没 到 Gitea 就以 HTTP 413 死掉)和放长的 proxy_*_timeout(流式 git push 会超过 60s 默认值)。同一份文件作为通用模板样例也在 nginx 文档里(「Git HTTP 推送与 client_max_body_size」):

worker_processes auto;
pid /tmp/nginx.pid;

events {
    worker_connections 1024;
}

http {
    # 抬高 nginx 请求体上限,避免 git push 被拒。内建默认 1 MiB——更大的请求还没
    # 到 Gitea 就以 HTTP 413 死掉。10m 能覆盖常见 push;要提交更大的二进制就继续
    # 调高(或写 0 完全不限制)。
    client_max_body_size 10m;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    upstream gitea {
        server 127.0.0.1:3080;    # pgcli 给 Gitea 分配的 Web 端口
    }

    server {
        listen 0.0.0.0:80;
        location / {
            return 308 https://$host$request_uri;   # 强制 HTTPS(308 保留方法与 body,
        }                                           # git push 能挺过重定向)
    }

    server {
        listen 0.0.0.0:443 ssl;
        # pgcli 把 --tls-cert / --tls-key 文件挂到这两个路径:
        ssl_certificate     /etc/nginx/certs/public.crt;
        ssl_certificate_key /etc/nginx/certs/private.key;
        ssl_protocols TLSv1.2 TLSv1.3;

        location / {
            proxy_pass http://gitea;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;   # 页面 clone URL 读成 https:// 就靠它
        }
    }
}
# 2. 用这份文件装代理(改动后用 --force 重建)
pg addon install nginx --name proxy --conf-file /srv/gitea-nginx.conf \
  --tls --tls-cert /srv/gitea.crt --tls-key /srv/gitea.key \
  --http-port 80 --https-port 443 --listen 0.0.0.0

nginx 代理现在在 https://10.0.0.9/ 上服务,并转发到 Gitea 的 127.0.0.1:3080。用代理地址克隆与 push(把 .crt 作为 CA 交给客户端,自签证书才能通过校验):

git clone https://10.0.0.9/<owner>/<repo>.git        # 经 TLS 代理
git -c http.sslCAInfo=/srv/gitea.crt push ...        # 信任这张自签叶子

注意:

  • Gitea 的 Web 端口(3080)仍然直接开着,代理并不会把它藏起来。若只想让 nginx 能访问源站,给 Gitea 用 --listen 127.0.0.1 重装让它只绑回环,并把代理 backend 指向 127.0.0.1:3080(同一台宿主)——因为两者都跑在宿主网络上,这样是通的。
  • pg addon nginx reload 不会吃到对外部挂载 nginx.conf 的手工改动(bind mount 钉住了原始 inode,sed -i/编辑器整体重写后容器看不到,直到重启)。reload 只应用 pgcli 自己写入的改动。手改配置后,用 pg addon stop nginx --name proxy && pg addon start nginx --name proxy 重启,或 --force 重装。
  • 真实 CA 证书(Let’s Encrypt / ZeroSSL / 企业 CA)才是线上环境的正解——见下文「线上域名 + 真实 CA」;它彻底免去 --cacert 交接,浏览器与 git 默认可信。
  • git-over-ssh(--ssh-port)与此无关;这里的 TLS 只前置 Web 与 Git-over-HTTP。

线上域名 + 真实 CA(Let’s Encrypt / 企业 CA)

面向公网的部署不要发 pg cert 的自签叶子——给主机指一个真域名(如 git.example.com → 203.0.113.7),为它拿一张 CA 签发的证书。如何取得这张证书 不在 pgcli 的范畴内——任意 CA、任意工具都行;只要最后手里有一张 PEM 证书(含中 间链)和它的私钥即可。

然后照上文那样创建 nginx 代理,只把证书路径换成你的域名证书——同一份 --conf-file、端口、--tls 都不变:

pg addon install nginx --name proxy --conf-file /srv/gitea-nginx.conf \
  --tls \
  --tls-cert /etc/ssl/git.example.com/fullchain.pem \
  --tls-key  /etc/ssl/git.example.com/privkey.pem \
  --http-port 80 --https-port 443 --listen 0.0.0.0

Gitea 安装也带上 --listen git.example.com(配 --force),让 ROOT_URL 与任 何 ssh:// 提示都带上真主机名。起来后用户直接 https://git.example.com/<owner>/<repo>.git 克隆,无需任何 CA 交接——浏览器和 git 默认可信公共 CA。

记得在证书过期前更换。pgcli 在装的时候会把自带的证书对拷进插件的 TLS 目录 (<base>/addon/nginx/<name>/tls/ 下的 public.crt / private.key),而这个目 录是 bind-mount 进容器的——所以续期就是:覆盖这两个文件,然后

pg addon nginx reload --name proxy

(已实测:reload 后所服务的证书立即更换。这行得通是因为 TLS 是目录挂载;不 像 nginx.conf 那个单文件挂载,新内容运行中的容器直接可见。)

取回 Web 登录

这个登录是 Gitea 的 Web/Git-HTTP 凭据,与任何 PostgreSQL 角色无关(即使带 --dsn)。随时可重读密码:

pg addon password gitea              # 裸打印到 stdout
pg addon password gitea --file ./pw  # 或写入 0600 文件

它也存于 pg.yaml(addons.gitea.<name>.password)。

账号管理(成员与管理员)

自助注册是关闭的,所以除安装 admin 外的每个账号都由 admin 创建。 pg addon gitea user 封装了容器内的 gitea admin user … CLI(自动以 git uid 运行), 让你在宿主上直接管账号,不必手写 podman exec。实例必须在运行。

# 成员,密码自动生成(一次性打印,且不写入 pg.yaml)
pg addon gitea user create --username alice --email alice@corp.example
# 成员,指定密码
pg addon gitea user create --username bob   --password 'B0b-pin-pass'
# 再建一个站点管理员
pg addon gitea user create --username carol --admin
# 列出全部账号
pg addon gitea user list
# 删除账号(须不拥有任何仓库,否则用 --purge)……
pg addon gitea user delete --username bob
# ……或连账号拥有的一切一起清掉
pg addon gitea user delete --username bob --purge
flag(create) 默认 说明
--username (必填) 账号名
--email <username>@localhost 需要收通知/重置密码时传真实地址
--password 自动生成 指定登录密码,或让 pgcli 生成强密码并一次性打印
--admin false 建站点管理员而非普通成员
--must-change-password false Gitea 自身对"除第一个外的账号"默认 true:会强制首次登录改密,而改密前 HTTP clone/push 与 API basic-auth 一律 403。pgcli 显式发 =false,让成员建号即可克隆——只有要发一次性密码时才设 true

这些是 Gitea 自己的账号、非 pgcli 托管,所以自动生成的密码只打印一次、 不写入 pg.yaml——建号时当场记下。(只有安装 admin 的登录会被存,那个用 pg addon password gitea 取回。)要改既有账号密码,进容器跑 gitea admin user change-password。

delete 拒删安装 admin(那是 pgcli 存的登录)。普通删除只对"不拥有任何东西"的 账号成功;若该账号还有仓库,Gitea 会以 user still has ownership of repositories 拒绝——加 --purge 才能连账号及其拥有的一切(仓库、组织、评论)一起删掉。

端口

每个 Gitea 实例占一个 Web 端口,取自以 gitea_start_port(默认 3080——Gitea 惯例的 3000 在 pgcli 里是 Grafana 池的基址,所以 Gitea 往后挪了一个池;仍然一眼认得出"Gitea 的端口")为基址的端口池,每多一个实例递增。每一个实例——SSH 开或关都一样——还从第二个池(gitea_ssh_start_port,默认 2222——Gitea 惯例的 SSH 映射)再拿一个端口用来 park 那个永远在跑的 s6 sshd 离 22;SSH 是否启用只决定那个端口是活的克隆端点还是一个没人连的空闲监听。两个池都跳过任何已被受管容器或 used_ports 列表占用的端口,分配结果写入 pg.yaml:

pg addon install gitea --name git2                 # 第二个实例:下一个空闲 Web 端口
pg addon install gitea --name git3 --port 3100     # 显式端口优先
pg addon install gitea --name git4 --ssh-port 0    # SSH 关掉,parked sshd 仍占池端口

两个池的基址(3080 / 2222)与其他任何插件的池都不重号。

配置

addons:
  gitea:
    git1:
      name: git1
      image_tag: docker.io/gitea/gitea:28.1.0
      host_port: 3080
      listen: 0.0.0.0
      admin_user: gitea
      password: <generated>
      ssh: true
      ssh_port: 2222
      dsn: postgres://gitea:secret@127.0.0.1:35432/gitea
      autostart: false
      # data_dir: /srv/gitea-data    # 可选,覆盖默认位置

配置只经环境变量(GITEA__<section>__<KEY>,外加两个裸 SSH_LISTEN_PORT / SSH_PORT 变量——sshd 只读裸的这两个,GITEA__server__* 那份只驱动 Gitea 自己的 clone-URL 渲染)交付:镜像每次启动把它们折叠进 app.ini,/data 里那份就是被这些变量重写出来的,所以手改 app.ini 不是受支持的配置路径—— pgcli 没设的 Gitea 项虽在 app.ini 里能存活,但请自担风险。pgcli 设置的变量 有:跳过向导(INSTALL_LOCK)、监听器(HTTP_PORT/HTTP_ADDR)、展示 URL (ROOT_URL)、注册闸门(DISABLE_REGISTRATION,两态都显式渲染——默认 true,--allow-registration 时 false;因为 Gitea 自己的默认是开, 少发一行就覆盖不掉持久 app.ini 里的旧值)、总是发出的裸 SSH_LISTEN_PORT/SSH_PORT 对(把 s6 sshd park 离 22)、DISABLE_SSH=true (SSH 关:Gitea 不写 authorized_keys、不显示 ssh:// URL)或——SSH 开 时——DISABLE_SSH=false 加 GITEA__server__ 下渲染 URL 用的 SSH_LISTEN_PORT/SSH_PORT/SSH_DOMAIN 三件套,并显式 START_SSH_LISTENER=false 让 Gitea 自己的 Go 监听器不与 s6 sshd 在同 netns 里双绑同一端口、可选的 database 块,以及进程 uid 钉死(USER_UID/USER_GID)。

列出

pg addon list
Web add-ons (gitea):
  gitea (name: git1)
    Status:      running
    Web URL:     http://localhost:3080/
    Git SSH:     ssh://git@localhost:2222/
    Database:    external PostgreSQL (127.0.0.1:35432/gitea)
    Login user:  gitea
    Data:        /home/you/.pgcli/addon/gitea/git1/data
    Image:       docker.io/gitea/gitea:28.1.0
    Container:   pgcli-gitea-git1

默认不显示密码以保持列表可安全粘贴;pg addon list --show-password 揭示明文,pg addon password gitea 则为脚本裸打印单个密码。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop gitea
pg addon start gitea

开机自启

要让 Gitea 实例在宿主开机时自动启动,开启它:

pg autostart enable --gitea --name git1

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——数据目录在创建时已 chown 给 uid 1000,除 podman 本身外不需要任何宿主特权。带 --dsn 时外置 PostgreSQL 也必须在跑:有自启标记的受管实例在同一趟里排得更靠前;其他端点只需届时可达即可。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove gitea              # 保留数据目录——仓库与登录跨重装存活
pg addon remove gitea --clean-data # 连数据目录一起删(仓库就没了)

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——Gitea 以容器内 git uid(1000)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。带 --dsn 时,两种方式都绝不碰外置数据库——真要删数据得自己去 drop。

日志

pg logs addon gitea            # 最近的控制台输出
pg logs addon gitea -f         # 跟踪
pg logs addon gitea -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • install 失败并报 chown 诊断 —— 镜像以 uid 1000 运行且从不修复既有挂载的属主,所以 pgcli 在创建容器前先把宿主数据目录 chown 过去:rootless podman 下经 podman unshare chown,rootful 宿主且 pgcli 自身以 root 运行时(sudo podman,或以 root 跑 pg)直接递归 chown。pgcli 是直接调用 podman CLI,所以只在这两种情形之外才会失败——例如非 root 的 pg 被显式指向别人的 rootful daemon。遇到时以 root 跑 install,或改用 rootless podman;单纯 sudo chown -R 1000:1000 <data-dir> 不能消除该错误,因为每次 install 都会重新校验属主。
  • 重装后登录密码不对 —— admin 账号只被创建,从不被重置。对既有存储(保留的 /data 或 --dsn 数据库)用容器内 gitea admin user change-password,或推倒重来(--clean-data;dsn 模式的外置库还要手动 drop)。
  • 明明部署在服务器上,clone URL 却显示 localhost —— 通配的 --listen 0.0.0.0 在展示 URL 里坍缩成 localhost,因为 0.0.0.0 不可连接。传 --listen <host-or-ip>(配 --force)让 clone URL 变真实。
  • ssh:// 克隆报 connection refused / 端口不对 —— 没带 --ssh-port 装的话克隆端点就是关的(s6 sshd 一直在跑,但只是 parked、不是 clone 端点)。而且 pgcli 让 SSH_PORT(URL 里显示的)、GITEA__server__SSH_LISTEN_PORT(Gitea 自己认为的端口)和裸 SSH_LISTEN_PORT(s6 sshd 实际绑的)三者保持一致——手工配出不同的值正是坏的形态。
  • 日志刷 Bind to port 22 … Address in use —— sshd 的 park 端口被改回 22 了(例如手改的 app.ini 或剥掉了裸 env)。一次干净的 install 不会走到这一步:pgcli 给每个实例都发出裸 SSH_LISTEN_PORT=<池端口>,SSH 开或关都一样。
  • HTTP git push 要密码且拒收 Web 密码 —— Gitea 要的是账号凭据;若该账号开了 2FA,HTTP push 必须改用 personal access token 或 SSH key,而不是密码。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port / --ssh-port 另选。

已知限制

  • 镜像必须钉在版本 tag 上,升级=换 --image(Gitea 在新镜像首启时向前迁移数据库——没有回退路径)。rootful 与 rootless 构建的数据布局互不兼容:pgcli 钉的是 rootful 构建,换成 -rootless tag 读不了现有 /data(也读不了别的 rootless 部署写的数据)。
  • 明文 HTTP:TLS 终止与真域名是 nginx 插件的职责——用它的自定义 --conf-file 模式前置 Gitea(模板默认的 1 MiB client_max_body_size 会打断稍大的 git push)。Gitea 依 X-Forwarded-Proto + Host 重写页面 clone URL,所以在 nginx 默认转发这些头时,克隆按钮读作 https://<代理>/… 是对的。pgcli 把 ROOT_URL 渲染成 http://<host>:<web端口>/,v1 没有配置项能改——目前可见的影响只是:凡 Gitea 直接依 ROOT_URL(而非依请求)生成的 URL,仍用内部 http 源。
  • v1 不配置 OAuth、SMTP、webhook 调优或仓库存储(MinFS)选项——上表之外的一切都是 Gitea 侧配置,pgcli 刻意不碰。自助注册是关的;建号请用 pg addon gitea user create。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.19 - VictoriaMetrics

以 pgcli 插件方式运行 VictoriaMetrics——高性能、兼容 Prometheus 的时序数据库,单容器直接跑上游官方镜像

pg addon install victoriametrics 以受管容器方式运行 VictoriaMetrics——高性能、兼容 Prometheus 的时序数据库——直接跑官方 docker.io/victoriametrics/victoria-metrics 镜像。与 Prometheus 和 Grafana 一样,它是顶层插件:不前置某个具体实例(它从多个来源摄入指标),也不依附于任何一个。三者是天然搭档——Prometheus 负责采集、VictoriaMetrics 以优异的压缩率做长期存储、Grafana 负责可视化——且生命周期形状完全一致(单容器、Linux 上宿主网络、各自端口池里的一个 web 端口、持久数据目录、开机自启、日志)。

VictoriaMetrics 与 Prometheus 的分歧在配置方式。Prometheus 读一份配置文件(prometheus.yml)加 CLI flags;VictoriaMetrics 完全不需要配置文件——一切都作为镜像名之后的 CLI flag 传入(-storageDataPath / -httpListenAddr / -retentionPeriod / 自定义 flags 经 --extra-arg)。所以 pgcli 在创建容器时直接传这些 flags;无需渲染配置、无需 SIGHUP reload、无需 chown(镜像以 root 运行)。

没有 Web 登录/密码。 与 Grafana(其 0.0.0.0 默认由自动生成的强密码兜底) 不同,VictoriaMetrics 完全不带鉴权。它的 HTTP API 可执行任意 MetricsQL (PromQL 的超集),昂贵查询也能把自身打满,所以远程暴露的实例只靠网络边界 保护——防火墙,或 --listen 127.0.0.1 收紧为仅回环。请把它当作一个无鉴权的读/写 控制台来对待,而非一个有密码保护的面板。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name vm 实例名(map 键)
--image docker.io/victoriametrics/victoria-metrics:v1.154.1 覆盖镜像
--port 自动(基址 8428) Web 宿主端口;自 victoriametrics_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环(见上方暴露警告)
--retention-months 1 数据保留月数(-retentionPeriod);如 3 = 3 个月、12 = 1 年
--extra-arg — 传给二进制的额外 CLI flag(可重复);最后追加,故 last-wins 覆盖早期 flags。如 --extra-arg -search.maxUniqueTimeseries=1000000
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于应用改动的端口/监听/保留期/额外参数(config 变化时 install 也会自行重建)

数据目录是 VictoriaMetrics 的 TSDB。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活已存储的时序数据。镜像以 root 运行(与 Prometheus 的 uid 65534 不同),故无需预先 chown 数据目录。

安装

pg addon install victoriametrics

命令会按需拉取镜像、创建容器,并打印 URL、保留期与任何额外参数:

✓ VictoriaMetrics installed: "vm"
  Container:  pgcli-victoriametrics-default-vm
  Image:      docker.io/victoriametrics/victoria-metrics:v1.154.1
  Data:       /home/you/.pgcli/addon/victoriametrics/vm/data
  Retention:  1 month(s)
  URL:        http://0.0.0.0:8428/

VictoriaMetrics 立即可摄入和查询。在 Grafana 里把 http://<host>:8428/ 加成 Prometheus 数据源(VictoriaMetrics 兼容 Prometheus),即可用 PromQL 或 MetricsQL 查询。

# 自定义保留期和搜索限制
pg addon install victoriametrics --name vm1 \
  --retention-months 12 \
  --extra-arg -search.maxUniqueTimeseries=1000000

对运行中的实例重复 install:当配置确实变了(端口、监听、保留期、额外参数)时重建,没变则原样不动;--force 即使配置未变也重建容器。

VictoriaMetrics 不支持热配置重载(SIGHUP),所以所有变更都需要重建容器。这比 Prometheus 简单(无配置文件要渲染、无 reload 信号),但意味着改配置时会有短暂中断。

自定义参数(--extra-arg)

VictoriaMetrics 接受许多 CLI flags 来调优缓存大小、查询限制、摄入速率等。经 --extra-arg 传入(可重复):

pg addon install victoriametrics --name vm1 \
  --extra-arg -search.maxUniqueTimeseries=1000000 \
  --extra-arg -selfScrapeInterval=10s

额外参数最后追加到容器的命令行,故它们经 last-wins 语义覆盖早期 flags。这让你在 pgcli 的一等旋钮之外调优 VictoriaMetrics,无需编辑任何配置文件。

验证 flags 已生效:

podman logs pgcli-victoriametrics-default-vm1 | grep -E '(maxUniqueTimeseries|selfScrapeInterval)'
# 2026-10-22T...  -search.maxUniqueTimeseries="1000000"
# 2026-10-22T...  -selfScrapeInterval="10s"

监听与暴露

VictoriaMetrics 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 UI 与 API 在宿主每个网卡的分配端口上可达。这与 Prometheus 的默认一致(别的机器上的 Grafana 要能连它)——但背后没有密码。任何能连到端口的人都能执行任意 MetricsQL、读你存储的时序数据。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建);pg addon list 会显示 http://127.0.0.1:<port>/。否则请在防火墙层面限制访问。

端口

每个 VictoriaMetrics 实例占有一个端口:Web UI/API。它取自以 victoriametrics_start_port(默认 8428——VictoriaMetrics 惯例 HTTP 端口)为基址的端口池,每多一个实例递增,并跳过任何已被受管容器或 used_ports 列表占用的端口。分配结果写入 pg.yaml,所以重装绝不会把别的插件在用的端口再发一遍。8428 与 Prometheus(9090)、Grafana(3000)、pgAdmin(5050)、nginx(8080)都不重号;显式 --port 仍优先。

pg addon install victoriametrics --name vm2          # 第二个实例:下一个空闲端口(8428 被占则 8429)
pg addon install victoriametrics --name vm3 --port 8500  # 显式端口仍优先

配置

addons:
  victoriametrics:
    vm1:
      name: vm1
      image_tag: docker.io/victoriametrics/victoria-metrics:v1.154.1
      host_port: 8428
      listen: 0.0.0.0
      retention: "1"
      extra_args:
        - -search.maxUniqueTimeseries=1000000
        - -selfScrapeInterval=10s
      autostart: false
      # data_dir: /srv/victoriametrics-data      # 可选,覆盖默认位置

没有配置文件——一切都作为 CLI flags 传给 victoria-metrics 二进制(-storageDataPath、-httpListenAddr、-retentionPeriod,加上任何 extra_args)。retention 存为字符串(月数),extra_args 是最后追加的 CLI flags 列表。

列出

pg addon list
Web add-ons (victoriametrics):
  victoriametrics (name: vm1)
    Status:      running
    URL:         http://0.0.0.0:8428/
    Retention:   1 month(s)
    Extra args:  -search.maxUniqueTimeseries=1000000 -selfScrapeInterval=10s
    Data:        /home/you/.pgcli/addon/victoriametrics/vm1/data
    Image:       docker.io/victoriametrics/victoria-metrics:v1.154.1
    Container:   pgcli-victoriametrics-default-vm1

没有凭据可揭示——VictoriaMetrics 本就没有——所以 --show-password 对它不会多显示任何东西。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop victoriametrics
pg addon start victoriametrics

开机自启

要让 VictoriaMetrics 实例在宿主开机时自动启动,开启它:

pg autostart enable --victoriametrics --name vm1

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——数据目录在 install 时已创建,除 podman 本身外不需要任何宿主特权。启动顺序上 VictoriaMetrics 排在 Prometheus 之后。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove victoriametrics              # 保留 TSDB——已存储时序数据跨重装存活
pg addon remove victoriametrics --clean-data # 连 TSDB 一起删

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——VictoriaMetrics 以 root(容器内 uid 0)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。

日志

pg logs addon victoriametrics            # 最近的控制台输出
pg logs addon victoriametrics -f         # 跟踪
pg logs addon victoriametrics -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • 查询返回空数据 —— 确认 VictoriaMetrics 在运行(pg addon list 显示 running)、健康端点有响应(curl -s http://<host>:<port>/-/healthy 输出 VictoriaMetrics is Healthy.)、且你查的是正确的时间范围(用 query_range 配显式 start/end/step)。查 pg logs addon victoriametrics 看摄入错误。
  • Install 失败于 “container already exists” —— 容器名由 --name 和命名空间派生;用 --force 重建,或先 pg addon remove 清理一次不完整的 install。
  • 额外参数没生效 —— --extra-arg 在容器创建时应用,而非运行时。用 --force reinstall 以新 flags 重建容器。查 podman logs <container> 确认 flags 出现在启动日志里。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。
  • 远程 API 打不开 / 意外可访问 —— 默认 0.0.0.0 下打不开是宿主防火墙问题,不是绑定地址;反之若你不想被访问,用 --listen 127.0.0.1 --force。

已知限制

  • v1 是裸存储后端:不自带抓取能力(用 pg addon install prometheus --remote-write-url http://<host>:8428/api/v1/write 把 pgcli 的 Prometheus 实例指向它,或单独跑 vmagent)、不做告警。它是你让 Prometheus 和 Grafana 连的长期存储层。
  • 无鉴权。 VictoriaMetrics 的 API 执行任意 MetricsQL;0.0.0.0 绑定的唯一保护是网络边界。这与 Prometheus 同一态势——不要把它暴露到不可信网络。
  • pgcli 不校验 --extra-arg 值(install 阶段没有 pre-flight 检查)。有错的 flag 只会在启动时从容器日志里暴露。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.20 - BeeGFS

以 pgcli 插件方式运行 BeeGFS 并行文件系统。

BeeGFS 是一套走「易用」路线的并行文件系统:2004 年生于德国弗劳恩霍夫工业数学研究所(ITWM)——当年一个课题组要用 Lustre,嫌它 部署运维太重,干脆自己写了一个,目标从一开始就定死:不求规模最大,但求部署和 维护最省人力。2014 年核心成员从研究所出来成立商业公司 ThinkParQ,产品改名 BeeGFS,一路从研究所内部工具长成欧洲高校与区域超算里常见的共享存储。

它解决的是「多台机器共享一份高吞吐数据」的问题:本地文件系统只活在一台机器 上,NFS 这类共享存储在多客户端并发读写大文件时很快见顶。BeeGFS 和 Lustre 同 属并行文件系统——元数据与数据分离、文件切块摊到多台存储服务器、客户端拿到位置 信息后直连数据节点读写,聚合带宽随服务器数线性增长——但在每一个可以简化的地方 都选了简单:服务角色可以混布在同一台机器上、守护进程启动即自动注册组网(不需要 手工分发配置)、扩容就是加机器、普通以太网就能跑而不强制 InfiniBand。代价是不 追求极限规模与最细的调优自由度,换来的是没有专职存储工程师的课题组、部门级集 群也能一两天用起来。

典型场景:几台到两三百台机器的 HPC 集群、AI 训练的数据供给层(7.3 起支持 GPUDirect Storage,存储与 GPU 显存直通,大规模训练/推理直接读共享数据集)、 影视行业后期编辑的素材数据(多人并发剪辑大码流素材不抢带宽)、海量小文件 或大文件顺序读写的部门级共享盘。一句话选型:国家级超算、上千节点、有专职存储 团队——Lustre;「当天用上、不想养存储团队」——BeeGFS。

平台:仅 Linux。BeeGFS 守护进程跑 host 网络(BeeGFS 说的是自家 TCP/UDP 协议而非 HTTP,不存在可分配的容器端口映射)。macOS 的 podman 没有 host 网络,pg addon install beegfs 在 Mac 上会带明确报错直接拒绝。

客户端刻意不在管辖范围内。 挂载 BeeGFS 需要 beegfs-client 内核模块 (DKMS)。内核模块无法容器化,所以 pgcli 既不安装也不挂载它——只管服务端 服务(收敛宿主上是三个容器,跨主机成员上是本机承担的角色子集)。 pg addon beegfs mount-hint 会打印装了 beegfs-client + beegfs-utils 包的主机所需的逐字配置。见「挂载客户端」一节。

参数

参数 默认值 说明
--name beegfs 文件系统名(map 键);容器名为 <前缀>-<角色>,按本机承担的角色各建一个
--image 8.4.0 版本后缀——manager 据此拼出 ghcr.io/thinkparq/beegfs-<角色>:<version>(各角色共用同一 tag)
--data-dir <base>/addon/beegfs/<name> 一个目录管整个文件系统;三个服务用 <dir>/{mgmtd,meta,storage}
--listen 0.0.0.0 客户端自引用 mgmtd 的地址(sysMgmtdHost);host 网络下守护进程实际绑所有网卡
--password 自动生成(仅含 mgmtd 的宿主) 连接认证密钥(ConnAuth),全部守护进程与所有客户端共享;首装生成仅限本机跑 mgmtd——join 宿主必须显式传集群既有值
--roles 全部三角色 v2:本机承担哪些服务——mgmtd,meta,storage 的逗号列表;不传即收敛单机文件系统
--mgmtd-host — v2:本机 meta/storage 注册指向的 mgmtd 地址(--roles 不含 mgmtd 时必填)
--node-id 1 v2:本机 meta/storage 的 BeeGFS 节点号(1..655);storage target ID 由它派生(2 → 201、202…)。跨主机须按服务类型唯一
--storage-target 单目录 v2:一个宿主目录一个 storage target——多块盘就重复传(每盘一个 target)
--license-file — ThinkParQ license .pem 的路径——搬运进 mgmtd 容器(内容存 pg.yaml,注入为 BEEGFS_LICENSE_FILE_DATA)。仅限 mgmtd 宿主——pgcli 从不生成 license,之后用 pg addon beegfs license 查看/热换
--beegfs-mgmtd-port 8008 移动 mgmtd 的 BeeMsg 口(meta/storage 注册指向的唯一可移动口);0 = 用默认
--beegfs-mgmtd-grpc-port 8010 移动 mgmtd 的 gRPC 口(beegfs CLI 拨的口);0 = 用默认
--force — 即使本机容器已在跑也重建

meta/storage 刻意没有 --port:见「端口」一节。

安装

pg addon install beegfs                    # 默认名 "beegfs" 的文件系统
pg addon install beegfs --name fs1
pg addon install beegfs --listen 10.0.0.5  # 对客户端宣告可达的 mgmtd 地址

全程非交互。先一次性拉齐本机角色所需的镜像(避免网络故障把集群卡在"半注册" 状态),然后按依赖序拉起:先 mgmtd,再 meta,再 storage——join 宿主(如 --roles storage)只跑自己那一步。meta/storage 会阻塞等它们打出 Registration and management info download complete——这一行同时证明 “mgmtd 可达"与"该服务已入集群”。每个容器首启执行一步 beegfs-setup-<service>(由镜像 entrypoint 消费的 beegfs_setup_N 环境变量 驱动)初始化自己的 target 目录;幂等 marker 保证对幸存数据目录重建容器时直接 复用既有 target。

重复执行 install 是安全的,且以集群为粒度(按本机角色计):

  • 本机角色全在跑 → 跳过(要重建用 --force,要看健康用 pg addon beegfs check);
  • 本机角色全停且存在 → 直接启动存好的集群(不 pull、不重建);
  • 其余情况(partial、或全新安装)→ EnsureCluster 按序重建整套。

安装摘要打印本机角色与对应容器名、镜像、mgmtd 地址与两个口(join 宿主标 remote)、节点号与数据目录、conn-auth 状态,以及指向 pg addon beegfs mount-hint 的一行;跑 mgmtd 的宿主还会提示 pg addon beegfs join-hint。--listen 是通配 0.0.0.0 时附 暴露 NOTE(关掉 TLS 的并行文件系统,对任何握着 conn-auth 密钥的机器都是一扇 门)。

跨主机集群(v2)

BeeGFS 的扩展轴是加机器,不是手工同步配置:每个服务守护进程启动时只需 sysMgmtdHost=<mgmtd地址> 加共享 conn-auth 密钥,就会自行注册进集群—— mgmtd 本身就是注册中心。pgcli 完全照搬这个模型:没有中心化部署命令,pgcli 从不 SSH。每台宿主各自跑一条 pg addon install beegfs --roles <子集>,用自己 本地的 pg.yaml 描述本机承担的角色,剩下的组网全是 BeeGFS 自己完成——与 Patroni 跨主机成员同一套声明式逐机模型(那边聚合拓扑靠 etcd,这边靠 mgmtd 的 SQLite 注册表)。

三台机器一个文件系统:

# vm01 —— 本宿主跑管理 + 元数据;conn-auth 密钥在这台生成:
pg addon install beegfs --name fs1 --roles mgmtd,meta --listen 192.0.2.10

# 在 vm01 上渲染逐字 join 命令(含密钥):
pg addon beegfs join-hint --name fs1

# vm02 —— 两块数据盘,一块盘一个 storage target:
pg addon install beegfs --name fs1 --roles storage --mgmtd-host 192.0.2.10 \
    --node-id 2 --password <conn-auth> \
    --storage-target /mnt/d1 --storage-target /mnt/d2

# vm03 —— 再来一个 storage 节点(或 --roles meta 把元数据挪离 vm01):
pg addon install beegfs --name fs1 --roles storage --mgmtd-host 192.0.2.10 \
    --node-id 3 --password <conn-auth> --storage-target /mnt/d3

客户端照旧从 vm01 的地址挂载——跨主机对内核模块客户端完全透明。

规则与边界,如实说明:

  • --roles 不含 mgmtd 时 --mgmtd-host 必填(没有 localhost 可回落), 且 join 必须带集群既有的 conn-auth——pgcli 绝不替既有集群的成员生成密 钥。密码传错几秒内报错并指路 pg addon password beegfs,而不是等注册超时。
  • --node-id 在跨主机间必须按服务类型唯一(node ID 是按服务类型分的命名 空间:meta_1 与 storage_1 共存)。它派生 storage target ID:节点 2 的 target 是 201、202…(node_id*100 + 序号,上限 node_id 655)。
  • 每台宿主的 pg.yaml 条目只存自己承担的角色;check、 start/stop、list、autostart 全是本宿视角。集群完整成员表在 mgmtd 里 (beegfs ... node list)。
  • mgmtd 仍是单点:它挂掉后已挂载客户端的 IO 照常(客户端直连 meta/storage), 但注册与管理操作停摆。管理节点双活是企业 license 功能,不归 pgcli 接线。 license 同样只装在 mgmtd 宿主——join 宿主既不存也不需要 PEM (join-hint 会说明这一点),证书由 mgmtd 为整个文件系统校验。
  • 仍然一宿主一文件系统——扩展轴是跨主机,不是同宿主第二个文件系统 (固定口 8005/8003 必撞)。

数据目录与持久化

收敛宿主拥有三个宿主目录,各自 bind 挂载进自己容器的 /data/<role>:

<base>/addon/beegfs/<name>/mgmtd     mgmtd.sqlite——节点/target 注册表
<base>/addon/beegfs/<name>/meta      元数据 target
<base>/addon/beegfs/<name>/storage   数据 target(真正的文件内容)

--data-dir <dir> 一个 flag 排出全部三个目录(<dir>/{mgmtd,meta,storage}) ——使用者从不按角色分别寻址。跨主机成员只用自己角色的那个目录。多 storage target(重复 --storage-target)时,每个宿主目录挂进同一个 storage 容器的 /data/storage/<targetID>;单 target 保持朴素的 /data/storage 路径,v1 的 存量 store 因此原样复活(target 目录内部记录自身路径,复活必须挂同一路径)。 三个镜像以 root 运行并自属数据目录,所以与 Gitea(uid 1000)、Grafana(uid 472)不同,没有预 chown 环节——宿主目录 直接可写。

数据全部跨容器删除存活:pg addon remove beegfs 不带 --clean-data 保留目录, 同名重装即复活文件系统(检测到既有 mgmtd.sqlite 时安装会打印 NOTE)。复活沿用的是存储里原有的注册表与连接认证——新传的 --password 只作用 于全新初始化的守护进程,改不动幸存的 SQLite 注册表。

故障处理:跨节点条带做性能,ZFS 做可靠性

BeeGFS 的跨节点条带是性能机制,不是冗余机制:默认 RAID0 布局下,丢任意一 个 storage target 就丢它承载的 chunk——条带化的文件即随之损坏。节点级数据冗 余是 buddy mirroring,而那是企业 license 功能。所以社区版把两件事干净地分层, 而存储侧需要一个本地故障的底层方案:

  • 跨节点:条带做吞吐。 每个文件的 chunk 摊到多台机器的多个 target 上,聚 合带宽随服务器数线性增长——--num-targets/--chunk-size 调的就是这一层。 它买的是速度,不是安全。

  • 每台 storage 服务器内部:target 底下垫一个有弹性的卷。 推荐底座是 ZFS——因为 BeeGFS 的 storage target 就是"一个目录",ZFS 免费把这个目录 变成自愈存储:checksum 防静默损坏、copy-on-write、快照、压缩。守护进程对此 毫无感知,所以这一层完全不需要 pgcli 或 BeeGFS 参与:建池、每个 target 一个 dataset、把 --storage-target 指过去即可。

    # 两块盘 → mirror vdev(ZFS 的 RAID1:容量=单盘大小,任坏一块数据仍在)。
    # raidz1/2 需要 3/4 块盘起。
    zpool create bgtank mirror /dev/disk1 /dev/disk2
    zfs create -o recordsize=1M -o compression=lz4 bgtank/t1
    # ……然后:  pg addon install beegfs --roles storage ... \
    #              --storage-target /bgtank/t1

    (mdadm RAID1/5/6 也能用,但 ZFS 的 checksum 能抓住静默位翻转——否则会被 原样喂给客户端。)一个 target 一个 dataset,可让磁盘故障根本不进 BeeGFS 的 target 路径。

  • 整机故障仍然救不回来(社区版):一台服务器倒下,它的 storage target 随 之消失——本地 ZFS/RAID 扛得住坏盘,扛不住烧掉的机器。无 license 的缓解 手段:把 meta/mgmtd 服务在多台上做重复(免费——这是服务冗余,文件 系统仍能挂载、管理操作仍可运转)、对元数据做快照、并在负载允许时把 storage 数据当作"可重灌"处理(训练集可以重新生成——这是社区版诚实的取舍)。想要真 正的跨节点镜像,只能 temporary 全功能试用证(免费、限期、一系统一张)或付 费 enterprise license。

  • 元数据同样值得照顾:meta target 丢失则整棵树读不出来(与数据是否冗余无 关)——它的目录也应放本地镜像(体量很小)。pgcli 不做 BeeGFS 备份:保护 mgmtd 注册表与 meta target 靠的是宿主级快照——对承载角色目录的卷做 ZFS 快 照,或在守护进程停住时做一次静默文件拷贝。

端口

BeeGFS 的四个监听口是协议原生固定口,不是端口池:

mgmtd   8008  BeeMsg——meta/storage 在此注册   (可移动:--beemsg-port)
mgmtd   8010  gRPC  ——`beegfs` CLI 拨此口     (可移动:--grpc-port)
meta    8005  元数据服务                       (固定)
storage 8003  数据服务                         (固定)

实机核验:BeeGFS 8.4 的 meta 与 storage 守护进程不接受任何监听口覆盖 (connPortsList/connTcpPort 被拒为非法参数,connMetaPort 收下但注册就 断)。只有 mgmtd 的两个口能动;移动 mgmtd 的 BeeMsg 口后,meta/storage 的注册 会自动重定向(manager 给两者注入 connMgmtdPort=<port>)。直接推论:每台宿 主只能有一个 BeeGFS 文件系统——第二个无论如何绑不了 8005/8003,与 pgcli 分 配什么无关,所以 EnsureCluster 显式挡掉第二个,CLI 也会带真实理由拒绝 pg.yaml 里手改的非默认 meta_port/storage_port。跨主机则各绑各的、互不冲 突——这正是 v2 用 --roles 扩角色的原因(见「跨主机集群(v2)」)。因此没有 beegfs_start_port 端口池——与其他插件不同,BeeGFS 的端口是固定的,不是分 配的。

连接认证与 TLS

文件系统里的每个守护进程与所有客户端共享一个连接认证密钥(ConnAuth,经 CONN_AUTH_FILE_DATA 环境变量交给镜像 init 写成 /etc/beegfs/conn.auth)。 首装自动生成(20 位随机)仅限跑 mgmtd 的宿主、打印一次、存入 pg.yaml, 之后用 pg addon password beegfs 取回;跨主机 join 宿主必须把这个既有值经 --password 带过去(见「跨主机集群(v2)」),pgcli 绝不替既有集群的成员生成 密钥。与其他 pgcli secret 一样,ApplyDefaults 绝不把它默认成字面量。

mgmtd 的 TLS 是关闭的(--tls-disable true,v1 的明确决策)。这意味着只 有同样带 --tls-disable 的客户端能入集群,且这是真实的暴露考量:通配 --listen 0.0.0.0 + TLS 关闭 = 网络上任何握着 conn-auth 密钥的机器都能触达 8008/8010/8005/8003。非私有网络请绑回环(--listen 127.0.0.1)或对这四个口 做防火墙。

License:自 BeeGFS 8 起每个部署都应有 license。pgcli 从不生成 license(与 Gitea 加密 secret 同一原则),但可搬运你申领的那张:PEM 内容存入 pg.yaml(随配置备份一起走),注入 mgmtd 容器为 BEEGFS_LICENSE_FILE_DATA——官方镜像的 init 把它写成 /etc/beegfs/license.pem,mgmtd 的 start.sh 再转成 --license-cert-file。 只有 mgmtd 宿主能带 license:它的镜像是唯一自带 license 库的,证书由它的 守护进程为整个文件系统校验(join 宿主传 --license-file 会被安装直接拒绝)。

license 三档(全部走 ThinkParQ 自助门户,社区版一分钟可领):

档位 价格 作用
community 免费 解除无 license 的限制(8.4 的五个客户端挂载封顶、quota 告警);有容量上限,不解锁企业功能
temporary 免费试用 限期全功能;8.4 起一个系统限持一张
enterprise 付费 storage pools、quota、mirroring(伙伴镜像)、管理节点 HA 双活

重点:mirroring(伙伴镜像)是 ENTERPRISE 功能——必须付费 license。 无 license 时 mgmtd 拒绝一切镜像请求(beegfs mirror create → “no license loaded”,实机核验)。免费的 community license 不解锁它, 只有付费 enterprise(或限期全功能的 temporary 试用证)才行。社区版的 数据冗余止步于多跑几个 meta/mgmtd daemon——那是服务冗余,不是数据镜像。

申领在体系外完成:beegfs license --get 打印系统绑定的申领 URL → 浏览器 打开 → 下载 .pem。然后交给 pgcli——安装时 --license-file license.pem, 或事后 pg addon beegfs license --set(见「License 状态与热换」一节)。

无 license 跑 8.4 的现实是:mgmtd 打 quota 告警、每条 beegfs 命令都 带 warning、客户端挂载上限五个(第六个被拒)、企业功能直接拒绝(如 beegfs mirror create 报 “no license loaded”)。装上任意有效 license——包 括免费的社区版——即解除告警与挂载上限。

挂载客户端

客户端是宿主内核模块——pgcli 不装。要在 Debian/Ubuntu 主机上挂载文件系统, 需自行添加 ThinkParQ apt 仓库并安装客户端包。8.4 + Debian 12(bookworm)的完整 命令(已在测试机实测):

# 1. BeeGFS 签名 key(指纹 0683 AF2B 9AC5 BC7D 59B2 D623 40CE 4E24 3CD5 0804)
wget https://www.beegfs.io/release/beegfs_8.4/gpg/GPG-KEY-beegfs -O /etc/apt/trusted.gpg.d/beegfs.asc
# 2. repo 文件(bookworm = Debian 12;Debian 13 用 beegfs-trixie.list,
#    Ubuntu 用 beegfs-jammy/noble.list,……)
wget https://www.beegfs.io/release/beegfs_8.4/dists/beegfs-bookworm.list -O /etc/apt/sources.list.d/beegfs.list
apt update
# 3. 客户端 + 工具;内核模块由 DKMS 编译
apt install beegfs-client beegfs-utils beegfs-tools linux-headers-$(uname -r)

RHEL 系主机上同一仓库以 .repo 文件发布于 https://www.beegfs.io/release/beegfs_8.4/dists/ 下(rpm --import …/GPG-KEY-beegfs + wget …/beegfs-rhel9.repo -O /etc/yum.repos.d/beegfs.repo)。URL 一律以 ThinkParQ 下载 页 上你的 BeeGFS 版本 + 发行版对应的为准—— 服务端 pin 在 8.4,客户端就应挂对应 8.4 的仓库。

beegfs-client 依赖 build-essential、推荐内核头文件——DKMS 在安装时对着 linux-headers-$(uname -r) 编译 beegfs 模块,所以运行中内核的头文件必须就位, 且每次内核升级都会重新编译。

然后照抄 pg addon beegfs mount-hint 的输出——它逐字渲染三个文件:

pg addon beegfs mount-hint --name fs1
1. /etc/beegfs/conn.auth(0600)——与服务端完全相同的密钥
2. /etc/beegfs/beegfs-client.conf——main 段设置:
     sysMgmtdHost=<mgmtd-host>      # 仅当 mgmtd BeeMsg 口被移动过时才有 connMgmtdPort=<port>
3. /etc/beegfs/beegfs-mounts.conf(一行一个挂载,字段:
   <挂载点> <client-conf> <fstype>):
     /mnt/beegfs /etc/beegfs/beegfs-client.conf beegfs

conn.auth 的坑: 客户端的连接认证文件必须以换行符结尾。 echo <secret> > /etc/beegfs/conn.auth 可以;printf <secret> > …(无行尾换 行)会被拒为 tried to authenticate stream with wrong secret——尾部那个字节 是两侧比对内容的一部分。

挂载与验证:

systemctl restart beegfs-client            # 读 beegfs-mounts.conf 并挂载
df -h /mnt/beegfs
echo hi > /mnt/beegfs/t && cat /mnt/beegfs/t   # 端到端写读

由于集群关着 TLS,每个 beegfs CLI 调用都要带关 TLS 的形态、指向 gRPC 口; node list 会列出全部四类节点(management、meta、storage,以及挂载成功后的本客 户端):

beegfs --tls-disable --mgmtd-addr=<mgmtd-host>:8010 node list

常用客户端操作(全部经 8.4 实测——beegfs CLI 是 8.x 的重写版,7.x 那套 beegfs-*-ctl 用法不适用):

B="beegfs --tls-disable --mgmtd-addr=<mgmtd-host>:8010"

$B node list                          # 所有节点:mg / meta / storage / client
$B target list                        # storage target、所属节点与状态
$B entry info /mnt/beegfs/big.bin     # 单文件的条带模式,以及各 chunk 落在哪些 target
$B entry set /mnt/beegfs/data --num-targets 3 --chunk-size 4Mi
                                      # data/ 下新建文件条带到 3 个 target、4 MiB 块
                                      #(目录设的模式作用于其后创建的内容)
$B entry set /mnt/beegfs/data --num-targets 0   # 恢复集群默认
$B health check                       # 全集群健康报告
$B stats server                       # 各服务器实时 IO 计数(8.x 的 stats)

mount-hint 是纯本地渲染——从不碰 podman,客户端本就不归 pgcli 管。mgmtd 地 址按配置条目渲染:跨主机成员上是存好的 --mgmtd-host;本机 --listen 为通配 时打印占位符 <mgmtd-host-ip>(0.0.0.0 不可连),而不是客户端连不上的值。

健康检查

pg addon beegfs check             # 默认名
pg addon beegfs check --name fs1

报告本机角色的聚合状态(running / stopped / partial (mgmtd,meta)), 并按服务给出容器是否在跑、是否打出就绪日志行——mgmtd 是 Waiting for shutdown signal(在服务),meta/storage 是 Registration and management info download complete(已入集群)。镜像都不 含 beegfs-ctl/check-servers,所以守护进程自己的日志就是权威的容器内信 号;集群完整成员表在 mgmtd 里——看全局用上面的 beegfs gRPC CLI (node list)。check 对停着的集群同样可用(它报状态,不要求运行中)。

取回连接认证密钥

pg addon password beegfs             # "beegfs" 文件系统
pg addon password beegfs --name fs1

pg addon list 只显示 presence 标记(Auth: none / set (connection-auth)), 只有加 --show-password 才打印密钥本身——随手 list 绝不泄密。

License 状态与热换

pg addon beegfs license                     # mgmtd 怎么看这张证书?
pg addon beegfs license --name fs1 --set /path/license.pem   # 装载/更换,重启 mgmtd
pg addon beegfs license --name fs1 --clear                    # 摘掉回到无 license

在 mgmtd 宿主上跑(license 只活在那台——见「连接认证与 TLS」)。不带参数 时报告两侧:pg.yaml 里有没有 PEM,以及 mgmtd 加载它时自己打了什么——有效 证书是 Successfully loaded license certificate,坏/过期证书则是 mgmtd 的 校验错误行。pgcli 只做 PEM 形状检查(BEGIN/END 块),真正的校验永远是 mgmtd 的事。

--set <pem> 读文件、把内容写进 pg.yaml,然后重建 mgmtd 容器让新 env 落地(podman 的 env 只在创建时存在)。meta/storage 和全部客户端会自行重注册 ——换证只需几秒且不碰数据。--clear 抹掉 PEM 同样重启,回到无 license 态。 --set 与 --clear 互斥;安装时带 --license-file 是同一件事的首次装载 (容器本来就在那时创建,无需重建)。

List

pg addon list

BeeGFS 区块(“Infra add-ons (beegfs)")每个文件系统一条:按本机角色计的聚合 状态、角色列表、mgmtd 地址与两个口(join 宿主标注 remote)、固定的 meta/storage 口、conn-auth presence、镜像、本机角色对应的数据目录(每个 storage target 一 行)、容器名。podman 不可用时仍然列出配置条目(状态显示 n/a)。

启停

容器带 podman 的 unless-stopped 重启策略,宿主重启后随 podman 服务自动回 来。start/stop 按依赖顺序显式管理本机承担的角色:

pg addon stop beegfs     # storage → meta → mgmtd(反序,target 保持 GOOD)
pg addon start beegfs    # mgmtd → meta → storage

跨主机成员只动自己那一个子集——--roles storage 的宿主 start/stop 只有 storage。

开机自启

pg autostart enable --beegfs --name fs1

BeeGFS 是唯一的集群粒度 autostart:pg start --autostart 拉起本宿主所跑的 文件系统部分(按 mgmtd → meta → storage 顺序,或配置的角色子集),而不是单个 容器。因为镜像以 root 运行并 自属目录,autostart 除 podman 外不需要任何宿主权限(没有 Gitea 那样的 chown 环节)。每台挂载主机上的客户端经由自己的 beegfs-mounts.conf 自行重挂—— pgcli 从不管理那一步。完整机制、状态与关闭方式见 Autostart。

移除

pg addon remove beegfs                # 保留数据目录——同名重装复活文件系统
pg addon remove beegfs --clean-data   # 连目录一起删(数据没了)

移除按反序拆本机角色(storage → meta → mgmtd)。--clean-data 拒删仍是 挂载点的目录。 指向本宿主的客户端挂载点无论如何都得先卸载——pgcli 够不着客户端(那不归 它管),请先去客户端主机上卸载。

日志

一个文件系统最多横跨三个容器,所以 pg logs addon beegfs 用 --service 选一 个(默认 mgmtd):

pg logs addon beegfs                       # mgmtd(默认)
pg logs addon beegfs --service meta -f     # 跟踪元数据守护进程
pg logs addon beegfs --service storage -n 200

--service 只对 beegfs 有效(配其他插件类型时报错)。

排障

  • install beegfs --name fs2 报 “a BeeGFS filesystem already exists on this host”——这是一宿主一文件系统规则,不是 bug:8.4 里 meta(8005)与 storage(8003)的口动不了,同宿主第二个文件系统永远绑不上。扩容请在另一台 宿主上加角色(pg addon beegfs join-hint 渲染逐字命令),或先 pg addon remove 掉第一个。
  • check 显示 meta/storage 一直 not registered——mgmtd 必须先在服务; mgmtd 容器若退出,两者会阻塞到 beegfsSetupTimeout(60 秒)让安装失败而非 挂死。看 pg logs addon beegfs --service mgmtd 找 mgmtd 退出原因(宿主上 端口被占、或数据目录不可写)。
  • install 在 meta_port/storage_port 配置校验处拒绝——pg.yaml 被手 改出了非默认值。守护进程根本绑不了那些口,删掉钉死的值回落 8005/8003。
  • 客户端挂不上 / beegfs CLI 报 TLS 错误——集群跑的是 mgmtd TLS 关闭;所 有客户端与 beegfs CLI 调用都必须同样带 --tls-disable,且 /etc/beegfs/conn.auth 必须与 pg addon password beegfs 的值逐字相同。
  • join 宿主秒报 “wrong secret”——它的 install 传入的 --password 不是集群 既有的连接认证值(BeeGFS 日志打 tried to authenticate stream with wrong secret)。安装几秒内中止而不是等注册超时;到 mgmtd 宿主用 pg addon password beegfs 取回正确值后重装。
  • pg addon beegfs license 打出 mgmtd 的校验错误——存进去的 PEM 对 这台 系统无效(证书是系统绑定的——申领要在 mgmtd 宿主跑 beegfs license --get)或已过期。容器照常跑、社区功能不受影响,mgmtd 只是表现为无 license 态;--clear(或用对的 PEM --set)修复配置侧。
  • 别的机器对着 localhost 挂载失败——通配 --listen 0.0.0.0 时 mount-hint 把 sysMgmtdHost 渲染成占位符;用 --listen <可达IP>(配 --force)重装,客户端才能拿到真实 mgmtd 地址。

已知限制

  • 仅 Linux。 host 网络是硬性要求,podman 只在 Linux 提供;macOS 上安装被 拒。不存在 bridge 兜底——BeeGFS 不是 HTTP。
  • 一宿主一文件系统、端口固定。 pgcli 钉死原生口 (8008/8010/8005/8003);8.4 里 meta/storage 的监听口不可移动,单宿主只容得 下一个文件系统。向外扩展是跨主机(--roles),不是同宿主第二个文件系统。
  • pgcli 从不安装、从不挂载客户端。 客户端是内核模块(DKMS),无法容器 化;pgcli 只管服务端容器,挂载靠 mount-hint 输出文档。
  • TLS 关闭;license 可搬运、不代发。 mgmtd 以 --tls-disable true 运行, beegfs gRPC CLI 与客户端必须同样带 --tls-disable。pgcli 从不生成 license、也不管续费——它只把你线下申领(beegfs license --get 打印系统 绑定 URL)的 ThinkParQ PEM 搬进 mgmtd 容器,pg addon beegfs license 查看与热换。mirroring、pools、quota 仍是需要购买的 enterprise license 功能;无 license 的 8.4 客户端挂载上限为五个。
  • 跨主机部署是逐机手动的(v2 设计如此)。 每台宿主各自跑自己的 pg addon install beegfs --roles ...;pgcli 从不 SSH,也没有"从一台机器部 署整个集群"的命令。扩容 = 在新宿主上跑一条命令(conn-auth 密钥靠运维手工 带过去——见 join-hint),每台宿主的 pg.yaml 只描述自己那一片。
  • mgmtd 是单点。 管理守护进程就是集群的注册表;它停摆后,新注册与管理操作 停摆(已挂载的客户端照常对 meta/storage 做 IO)。管理节点双活是 BeeGFS 企业 license 功能,不在 pgcli 的 v1/v2 接线范围内。

16.21 - VMCluster

以 pgcli 插件方式运行 VictoriaMetrics 集群版——3 个组件(vminsert + vmselect + vmstorage)各跑一个容器,支持跨主机部署和水平扩展

pg addon install vmcluster 以受管容器方式运行 VictoriaMetrics 集群版——3 个组件各跑一个容器,使用官方 docker.io/victoriametrics/{vminsert,vmselect,vmstorage}:v1.154.1-cluster 镜像。与单机版 victoriametrics addon(1 容器 1 端口)不同,它管理最多 3 个容器、3 个镜像、5 个端口,支持跨主机部署和水平扩展。

架构:

组件 职责 HTTP 端口 内部 TCP
vminsert 摄入端,按 metric hash 分片写入所有 vmstorage 8480 拨 vmstorage:InternalInsertPort
vmselect 查询端,从所有 vmstorage 合并结果 8481 拨 vmstorage:InternalSelectPort
vmstorage 数据存储(可多节点分片+副本) 8482 监听 InternalInsertPort + InternalSelectPort

没有 Web 登录/密码。 与单机版 VictoriaMetrics 一样,集群版完全不带鉴权。 它的 HTTP API 可执行任意 MetricsQL(PromQL 的超集),昂贵查询也能把自身打满, 所以远程暴露的实例只靠网络边界保护——防火墙,或 --listen 127.0.0.1 收紧 为仅回环。请把它当作一个无鉴权的读/写控制台来对待。

平台:仅 Linux。VMCluster 需要 host networking 来承载组件间的内部 TCP 端口。 macOS 上安装会被拒绝并给出明确错误提示。

部署形态

形态 1 — 单机收敛(默认)

3 个组件全在一台主机上,类似 BeeGFS v1:

pg addon install vmcluster --name vmc
# 1 host, 3 containers (vminsert + vmselect + vmstorage)
# --storage-node 自动推导为 127.0.0.1:<internalPorts>

形态 2 — 跨主机分布

# Host A — 存储节点 1
pg addon install vmcluster --name vmc --roles storage --listen 10.0.0.1

# Host B — 存储节点 2
pg addon install vmcluster --name vmc --roles storage --listen 10.0.0.2

# Host C — 接入层(vminsert + vmselect)
pg addon install vmcluster --name vmc --roles insert,select \
  --storage-node 10.0.0.1:8483:8484 \
  --storage-node 10.0.0.2:8483:8484

形态 3 — 水平扩展 + 副本

# replicationFactor=2 → 每条数据写 2 个 vmstorage
pg addon install vmcluster --name vmc --roles insert,select \
  --replication-factor 2 \
  --storage-node 10.0.0.1:8483:8484 \
  --storage-node 10.0.0.2:8483:8484

参数

参数 默认值 说明
--name vmc 实例名(map 键)
--roles insert,select,storage 本主机运行的组件子集(逗号分隔,跨主机时选子集)
--storage-node 自动推导 vmstorage 地址(可重复;格式 host:insertPort:selectPort),列出集群中所有 vmstorage 节点。单机收敛时省略即可
--insert-port 自动(基址 8480) vminsert HTTP 端口
--select-port 自动(基址 8481) vmselect HTTP 端口
--storage-port 自动(基址 8482) vmstorage HTTP 端口
--internal-insert-port 自动(基址 8483) vmstorage TCP 端口(vminsert 协议)
--internal-select-port 自动(基址 8484) vmstorage TCP 端口(vmselect 协议)
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环
--retention-months 1 数据保留月数(仅 vmstorage 角色)
--replication-factor 1 副本因子(每条数据写几个 vmstorage 节点)
--insert-image docker.io/victoriametrics/vminsert:v1.154.1-cluster vminsert 镜像覆盖
--select-image docker.io/victoriametrics/vmselect:v1.154.1-cluster vmselect 镜像覆盖
--storage-image docker.io/victoriametrics/vmstorage:v1.154.1-cluster vmstorage 镜像覆盖
--insert-extra-arg — vminsert 的额外 CLI flag(可重复)
--select-extra-arg — vmselect 的额外 CLI flag(可重复)
--storage-extra-arg — vmstorage 的额外 CLI flag(可重复)
--data-dir 自动 vmstorage 数据目录覆盖
--force false 即使正在运行也重建

安装

pg addon install vmcluster --name vmc

命令会按需拉取 3 个镜像、按依赖序创建容器(storage → insert → select),并打印摘要:

✓ VMCluster installed: "vmc"
  Roles:       storage, insert, select (converged on this host)
  Containers:  pgcli-vmcluster-default-vmc-storage, pgcli-vmcluster-default-vmc-insert, pgcli-vmcluster-default-vmc-select
  Images:      docker.io/victoriametrics/vmstorage:v1.154.1-cluster, docker.io/victoriametrics/vminsert:v1.154.1-cluster, docker.io/victoriametrics/vmselect:v1.154.1-cluster
  Data:        /home/you/.pgcli/addon/vmcluster/vmc/data
  Retention:   1 month(s)
  Insert URL:  http://0.0.0.0:8480/ (vminsert — ingestion)
  Select URL:  http://0.0.0.0:8481/ (vmselect — queries)
  Storage URL: http://0.0.0.0:8482/ (vmstorage — admin/metrics)
  Internal:    TCP 8483 (vminsert), 8484 (vmselect)

VMCluster 立即可摄入和查询。

集群版 URL 格式

重要:集群版的 URL 路径与单机版 VictoriaMetrics 不同。所有 API 端点都需要在路径中包含 <accountID>(使用 0 表示默认账户):

通过 vminsert 摄入数据:

# Prometheus remote_write
curl -X POST -d 'test_metric{job="pgcli"} 42' \
  http://127.0.0.1:8480/insert/0/prometheus/api/v1/import/prometheus

# InfluxDB line protocol
curl -X POST -d 'cpu,host=vm01 value=42' \
  http://127.0.0.1:8480/insert/0/influx/write

# Graphite plaintext
echo "test.metric 42 $(date +%s)" | nc 127.0.0.1 8480

通过 vmselect 查询数据:

# Prometheus query API
curl 'http://127.0.0.1:8481/select/0/prometheus/api/v1/query?query=test_metric'

# Range query
curl 'http://127.0.0.1:8481/select/0/prometheus/api/v1/query_range?query=test_metric&start=2024-01-01T00:00:00Z&end=2024-01-02T00:00:00Z&step=1h'

# Export
curl 'http://127.0.0.1:8481/select/0/prometheus/api/v1/export?match[]={__name__!=""}'

Grafana 数据源:指向 vmselect,URL 为 http://<host>:8481/select/0/prometheus/

Prometheus remote_write:配置 remote_write 到 http://<host>:8480/insert/0/prometheus/api/v1/write

端口

每个 VMCluster 实例占有 5 个连续端口,取自 vmcluster_start_port 端口池(默认 8480):

  • +0 InsertPort(vminsert HTTP — 摄入端点)
  • +1 SelectPort(vmselect HTTP — 查询端点)
  • +2 StoragePort(vmstorage HTTP — 管理/指标端点)
  • +3 InternalInsertPort(vmstorage TCP — vminsert 拨此端口)
  • +4 InternalSelectPort(vmstorage TCP — vmselect 拨此端口)

连续分配是因为 --storage-node 显式引用内部 TCP 端口,整块必须保持连续。8480 与 VictoriaMetrics(8428)、Prometheus(9090)、Grafana(3000)等都不重号。

配置

addons:
  vmcluster:
    vmc:
      name: vmc
      container_prefix: pgcli-vmcluster-default-vmc
      insert_image_tag: docker.io/victoriametrics/vminsert:v1.154.1-cluster
      select_image_tag: docker.io/victoriametrics/vmselect:v1.154.1-cluster
      storage_image_tag: docker.io/victoriametrics/vmstorage:v1.154.1-cluster
      listen: 0.0.0.0
      insert_port: 8480
      select_port: 8481
      storage_port: 8482
      internal_insert_port: 8483
      internal_select_port: 8484
      roles:
        - storage
        - insert
        - select
      storage_nodes:
        - address: "10.0.0.1:8483"
          select_port: 8484
        - address: "10.0.0.2:8483"
          select_port: 8484
      replication_factor: 1
      retention: "1"
      autostart: false

没有配置文件——一切都作为 CLI flags 传给 3 个二进制。storage_nodes 是跨主机拓扑;空即自动推导单节点。

动态添加存储节点

VictoriaMetrics 集群版没有动态发现机制——-storageNode 列表在 vminsert/vmselect 容器启动时写入。添加 vmstorage 节点需要一个短暂的维护窗口来重建接入层容器(不是热加载)。现有 vmstorage 里的历史数据不受影响。

维护影响:重建 vminsert/vmselect 会导致几秒写入中断。如果写入连续性很重要,请在低峰期操作。vmstorage 里的历史数据在整个过程中始终可用——只有摄入会短暂中断。

步骤

  1. 在新主机上创建 vmstorage:
# Host C — 新存储节点
pg addon install vmcluster --name vmc --roles storage --listen 10.0.0.3
  1. 在接入层主机上重建 vminsert + vmselect,追加新的 --storage-node:
pg addon install vmcluster --name vmc --roles insert,select --force \
  --storage-node 10.0.0.1:8483:8484 \
  --storage-node 10.0.0.2:8483:8484 \
  --storage-node 10.0.0.3:8483:8484

--force 会删掉并重建 vminsert/vmselect 容器,中间有几秒写入中断。已有的 vmstorage 数据完全不受影响(只有接入层容器被替换)。

添加节点后的数据分布

  • 旧数据保留在原来的 vmstorage 节点上(不会自动 rebalance)。
  • 新写入按 metric hash 在所有节点(旧 + 新)之间重新分片。
  • vmselect 从所有节点合并结果,查询端无缝看到新旧数据。

VictoriaMetrics 集群版不会把已有数据迁移到新加入的节点。对大多数监控场景来说这不是问题——时间序列是持续写入的,几小时后新节点上就有足够的数据让查询均衡。如果需要完全均匀分布,只能导出再重新导入(/api/v1/export → /api/v1/import)。

移除存储节点

反向操作:先重建 vminsert/vmselect 时去掉要下线节点的 --storage-node,再在目标主机上执行 pg addon remove vmcluster。被移除的 vmstorage 上的数据将不再对查询可见——如果需要保留,请提前导出。

启停

pg addon stop vmcluster
pg addon start vmcluster

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。

开机自启

pg autostart enable --vmcluster --name vmc

开机启动以集群为单位:每台主机只拉自己的角色容器,按依赖序(storage → insert → select),已有容器直接启动、仅缺失/异常的重建,不会重建正在运行的容器。跨主机部署时要在每台主机上分别执行。完整机制见 开机自启。

移除

pg addon remove vmcluster              # 保留 vmstorage 数据——重装即复活时序数据
pg addon remove vmcluster --clean-data # 连 vmstorage 数据一起删

只有 vmstorage 有持久数据;vminsert 和 vmselect 是 stateless,删除后不留任何东西。

日志

pg logs addon vmcluster                    # vmstorage 日志(默认 service)
pg logs addon vmcluster --service insert   # vminsert 日志
pg logs addon vmcluster --service select   # vmselect 日志

已知限制

  • 无鉴权。 集群版的 API 执行任意 MetricsQL;0.0.0.0 绑定的唯一保护是网络边界。
  • 仅 Linux。 组件间的内部 TCP 端口需要 host networking。
  • 无自动负载均衡。 多 vminsert/vmselect 实例需在前面手动放 HAProxy 或 nginx。
  • 无 TLS。 需要 TLS 终止请用 nginx addon 前置。
  • pgcli 不校验 --extra-arg 值。有错的 flag 只会在启动时从容器日志里暴露。

16.22 - JuiceFS

以 pgcli 插件方式把 S3 后端 POSIX 文件系统挂载为本地目录(宿主二进制)。

JuiceFS 把真正的 POSIX 文件系统铺在对象存储之上:文件 数据切块后存进 S3 桶,另由一个元数据引擎(SQLite、Redis、PostgreSQL……) 保存命名空间、文件属性与块映射。你拿到的是一个可以 ls、cat、grep 的普通 目录——每次写入最终都化作桶里不可变的对象。「想要文件系统、但字节应该住在 S3」就是它解决的问题:快照近乎免费(对象不可变)、容量就是桶的容量、任何知道 元数据 URL 的主机都能把同一个桶再挂起来。

作为 pgcli 插件,JuiceFS 是第一个宿主二进制形态的 addon:没有容器。 pg addon install juicefs 从 GitHub releases 下载官方二进制(sha256 校验; PATH 上已有 juicefs 则直接复用),面向一个外部 S3 服务 format 卷,再在 宿主上完成挂载。

平台:仅 Linux(与 BeeGFS 一样明确拒绝 macOS)。挂载点是一个普通宿主 目录——只对宿主进程可见,不会传播进 PG 实例容器(v1 不做 rshared 挂载传播; addon 管理的是本机的挂载,不是集群的)。

一切全显式

卷的两端都在命令行上点名——pgcli 不做同主机 addon 的自动识别或凭据继承:

  • 对象存储:一个外部 S3 服务(云,或另一台机器上的 MinIO),用 --s3-endpoint http(s)://host[:port] 加 --access-key/--secret-key 给出。 pgcli 在 format 前先探一次端点,不可达的主机会快速失败并提示 “not answering”,而不是卡死在 juicefs format 里。端点会在 format 时被烤进卷 元数据——之后挂载只需要元数据 URL。
  • 元数据引擎:一个显式 URL——--dsn(别名 --meta-dsn),接受 postgres://、redis:// 或 sqlite3://。pgcli 从不创建或删除该数据库; PostgreSQL 时它必须已经存在。
  • 桶:从不由 pgcli 创建。用 pg mc 这个包装命令自己供给一次——安装时会 原样打印出那两条命令:
pg mc alias set externals3 http://minio.example.com:9000 AK '***'
pg mc mb externals3/juicefs-data
pg addon install juicefs --name data \
    --s3-endpoint http://minio.example.com:9000 \
    --access-key AK --secret-key SK \
    --bucket juicefs-data \
    --dsn "sqlite3:///var/lib/juicefs/data.db"

换引擎也只是换 URL:

# PostgreSQL:库必须已存在(pgcli 从不创建或删除),juicefs 会在其中自建
# jfs_* 表。
--dsn "postgres://jfs:pass@meta-host:5432/jfs?sslmode=disable"

# Redis(+TLS):/N 后缀选择逻辑库。
--dsn "redis://:pass@redis-host:6379/0"
--dsn "rediss://user:pass@redis-host:6380/1"

挂载点完全由你指定——默认落在 pgcli 的 addon 目录下,用 --mount-point 指到 任意位置。目录要你亲手建好(mkdir -p),且运行 pg 的用户对其有读写权限—— pgcli 从不创建挂载点:

mkdir -p /mnt/cloudfs
pg addon install juicefs --name data \
    --s3-endpoint https://s3.amazonaws.com \
    --access-key AK --secret-key SK \
    --dsn "postgres://jfs:pass@meta-host:5432/jfs?sslmode=disable" \
    --bucket juicefs-data --mount-point /mnt/cloudfs

三种引擎都可以就用 pgcli 自己的服务——先用 pgcli 建好,再作为普通的"外部 URL"显式接线(安装代码从不自动查找它们;换成托管数据库或云桶也一模一样):

# PostgreSQL 元数据:先建实例、再建库(pgcli 的安装路径从不建/删库)。
# `pg status` 会打印 admin 连接 URL——把其中的 /postgres 换成新库名。
pg create meta-pg
pg exec meta-pg -c "CREATE DATABASE jfs"
pg status meta-pg

pg addon install juicefs --name data \
    --s3-endpoint http://minio-host:9000 --access-key AK --secret-key SK \
    --bucket juicefs-data \
    --dsn "postgres://admin:PASS@127.0.0.1:35432/jfs?sslmode=disable"
# Redis 元数据(pgcli redis addon;`pg addon password redis` 裸输出随机口令,
# 手工拼进 URL,/N 选逻辑库):
pg addon install redis --name mred
pg addon password redis --name mred
pg addon install juicefs --name r1 \
    --s3-endpoint http://minio-host:9000 --access-key AK --secret-key SK \
    --bucket juicefs-r1 --dsn "redis://:PASS@127.0.0.1:6379/1"
# MinIO 做对象存储(pgcli minio addon——silo/rustfs/garage/seaweedfs 同理;
# 桶仍要手动建,pgcli 从不建;root 用户默认 admin,除非安装时 --root-user
# 另有指定;`pg addon password minio` 裸输出 secret key):
pg addon install minio --name ms3
pg mc mb ms3/juicefs-data
pg addon install juicefs --name m1 \
    --s3-endpoint http://127.0.0.1:9000 --bucket juicefs-data \
    --access-key admin --secret-key "$(pg addon password minio --name ms3)" \
    --dsn "sqlite3:///var/lib/juicefs/m1.db"

以上示例中 pg 与存储同机,loopback(默认 127.0.0.1)即可。JuiceFS 客户 端在别的机器上时,把存储发布到网络、--s3-endpoint 指向它的地址——端点在 format 时被烤进卷元数据,所以从一开始就发布好:

pg addon install minio --name ms3 --listen 0.0.0.0   # 已装过则加 --force
# 在另一台机器上:
pg addon install juicefs --name m1 --s3-endpoint http://10.0.0.5:9000 …

(redis 示例同理:redis 默认就绑 0.0.0.0——口令即门禁。)

凭据和端点已经住在元数据里,所以同一个卷之后在任何有元数据 URL 的主机都能挂。

参数

参数 默认 说明
--name juicefs 卷名(配置 map 的键;format 时的 JuiceFS 卷名也是它)
--dsn / --meta-dsn — 必填。元数据引擎 URL:postgres://、redis:// 或 sqlite3://
--s3-endpoint — 必填。外部 S3 服务,形如 http(s)://host[:port](不带桶路径、不内嵌凭据)
--access-key / --secret-key — 必填且必须成对。外部 S3 的凭据
--bucket juicefs-<name> 桶名——pgcli 不创建(见上)
--mount-point <base>/addon/juicefs/<name>/mnt 挂载的宿主目录——pgcli 从不创建:先 mkdir -p 建好,且运行 pg 的用户可写
--cache-dir <base>/addon/juicefs/<name>/cache 本地数据缓存目录
--install-dir /usr/local/bin 宿主二进制落点(不可写时退到 ~/.local/bin——注意保持其在 PATH 上)
--version 内置 pin(1.4.1) 要下载的版本(其 sha256 已 pin 在 pgcli 源码里)
--no-mount 关 一切就绪但不挂载,之后手动 pg addon juicefs mount
--force 关 按命令行参数重建配置条目(元数据引擎里卷已存在时 format 依然跳过)

FUSE 本身(/dev/fuse + fusermount3)只探测、不安装——缺 fuse3 包时安装 会失败并给出该执行的 apt 命令。

日常管理

pg addon juicefs status --name data      # 端点、桶、元数据 URL、二进制、挂载状态
pg addon juicefs umount --name data
pg addon juicefs mount --name data       # juicefs mount -d 自带守护
pg addon juicefs mount --name data --mount-point /mnt/cloudfs
                       # 挂到自定义路径(先 mkdir -p);未挂载时这样挂会顺带
                       # 永久迁移该卷的挂载点
pg addon start juicefs --name data       # 顶层别名 = mount/umount
pg addon stop  juicefs --name data
pg logs addon juicefs --name data -f     # 追挂载日志
pg addon password juicefs --name data    # 取回存的 S3 secret key
pg addon remove juicefs --name data      # 卸载 + 忘掉的只是配置;桶、元数据、二进制都在
pg addon remove juicefs --name data --clean-data   # 连宿主目录一起删

remove 从不删 S3 数据、也从不碰元数据存储——弄丢 --dsn 指向的目标就等于 弄丢整个文件系统。挂载属主就是运行 pg 的用户。

换挂载路径

挂载点记在配置条目上;mount 接受 --mount-point 覆盖,把卷迁到新路径—— 新路径会持久化进 pg.yaml,之后的普通 mount(含 autostart)都用它。旧的 挂载必须先卸掉,新目录同样要预先建好且可写(与安装契约一致):

pg addon juicefs umount --name data
mkdir -p /srv/cloudfs
pg addon juicefs mount --name data --mount-point /srv/cloudfs
#   [i] mount point moved to /srv/cloudfs (saved)
# ✓ juicefs "data" mounted at /srv/cloudfs

仍在挂载时尝试迁移会被拒绝并提示先 umount;指到不存在的目录则报出 mkdir -p 补救命令。挂载点与 pgcli 自己的簿记(pid/日志/缓存都在 <base>/addon/juicefs/<name>/)完全解耦,迁移从不影响卷本身——换的只是同一 个桶加元数据在另一处的挂法。

开机自启

pg autostart enable --juicefs --name data

JuiceFS 是宿主二进制 addon,所以 pg start --autostart 在开机时做的是 重新挂载而不是起容器。后端是外部服务——只有 S3 端点重新可达时开机才能成 功。绝不重新 format。挂载以 boot 服务所运行的用户身份完成。

16.23 - VictoriaLogs

以 pgcli 插件方式运行 VictoriaLogs——VictoriaMetrics 家族的日志数据库,单容器直接跑上游官方镜像

pg addon install victorialogs 以受管容器方式运行 VictoriaLogs——VictoriaMetrics 家族的高性能、低成本日志数据库——直接跑官方 docker.io/victoriametrics/victoria-logs 镜像。与 VictoriaMetrics 和 Prometheus 一样,它是顶层插件:不前置某个具体实例(它从多个来源摄入日志),也不依附于任何一个。VictoriaMetrics 存指标、VictoriaLogs 存日志、Grafana 同时可视化两者——三者生命周期形状完全一致(单容器、Linux 上宿主网络、各自端口池里的一个 web 端口、持久数据目录、重启策略)。

VictoriaLogs 在配置方式上是 VictoriaMetrics 插件的精确镜像:完全不需要配置文件——一切都作为镜像名之后的 CLI flag 传入(-storageDataPath / -httpListenAddr / -retentionPeriod / 自定义 flags 经 --extra-arg)。所以 pgcli 在创建容器时直接传这些 flags;无需渲染配置、无需 SIGHUP reload、无需 chown(镜像以 root 运行)。单容器在单个 HTTP 端口上提供一切:摄入(JSON Lines、Elasticsearch 兼容 bulk、syslog、OTLP)、LogsQL 查询与内置 UI。

没有 Web 登录/密码。 VictoriaLogs 完全不带鉴权。它的 HTTP API 可摄入 并查询任意日志,昂贵查询也能把自身打满,所以远程暴露的实例只靠网络边界保护 ——防火墙,或 --listen 127.0.0.1 收紧为仅回环。请把它当作一个无鉴权的读/写 控制台来对待,而非一个有密码保护的面板。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name vl 实例名(map 键)
--image docker.io/victoriametrics/victoria-logs:v1.53.0 覆盖镜像
--port 自动(基址 9428) Web 宿主端口;自 victorialogs_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环(见上方暴露警告)
--retention 二进制默认 数据保留时长(30d、3months、1y 等),原样作为 -retentionPeriod 传入。省略则保持 victoria-logs 自身默认
--extra-arg — 传给二进制的额外 CLI flag(可重复);最后追加,故 last-wins 覆盖早期 flags。如 --extra-arg -search.maxQueryDuration=60s
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于应用改动的端口/监听/保留期/额外参数

数据目录是 VictoriaLogs 的日志存储。它跨 pg addon remove 存活(用 --clean-data 才删除),重装同名实例即复活已存储的日志。镜像以 root 运行(与 Prometheus 的 uid 65534 不同),故无需预先 chown 数据目录。

安装

pg addon install victorialogs

命令会按需拉取镜像、创建容器,并打印 URL 与保留期:

✓ VictoriaLogs installed: "vl"
  Container:  pgcli-victorialogs-default-vl
  Image:      docker.io/victoriametrics/victoria-logs:v1.53.0
  Data:       /home/you/.pgcli/addon/victorialogs/vl/data
  Retention:  binary default (--retention to change)
  URL:        http://0.0.0.0:9428/

VictoriaLogs 立即可摄入和查询。打开 http://<host>:9428/select/vmui/ 进入内置 Web UI(根路径 / 只是可用端点索引页),或用 HTTP API 查询:

# 摄入一条 JSON Lines 日志(Content-Type 必须为 application/json——
# 缺失时 VictoriaLogs 返回 200 但会静默丢弃数据)
echo '{"_msg":"hello from pgcli","app":"demo"}' | \
  curl -s -X POST -H 'Content-Type: application/json' \
    http://127.0.0.1:9428/insert/jsonline --data-binary @-

# 用 LogsQL 查询
curl -s http://127.0.0.1:9428/select/logsql/query \
  --data-urlencode 'query=app:demo' --data-urlencode 'limit=10'

对运行中的实例重复 install:当配置确实变了(端口、监听、保留期、额外参数)时重建,没变则原样不动;--force 即使配置未变也重建容器。

VictoriaLogs 不支持热配置重载,所以所有变更都需要重建容器——与 VictoriaMetrics 一致。这比 Prometheus 简单(无配置文件要渲染、无 reload 信号),但意味着改配置时会有短暂中断。

Web UI

每个 VictoriaLogs 实例都自带 Web UI(VMUI),无需额外安装任何组件。打开:

http://<host>:<port>/select/vmui/

根路径(/)只是可用 HTTP 端点的索引页;UI 位于 /select/vmui/。

UI 是一个面向日志的 LogsQL 控制台:

  • 查询 —— 输入 LogsQL 查询(如 app:demo、level:error、error app:demo-api)并选择时间范围,执行后即时返回结果。
  • 视图 —— 可在 Group(按 stream 字段分组)、Table(点击行展开全部字段)、JSON(/select/logsql/query 原始响应)与 Live(实时 tail 新到日志)之间切换。
  • 探索 —— 字段与 stream 选择器会列出已存储的字段名/值(底层调用 /select/logsql/field_names、field_values、facets 等),无需预先了解 schema 即可构造查询。

例如,摄入一条日志后在 UI 里搜索它(查询 app:demo):

curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"_msg":"hello from pgcli","app":"demo"}' \
  http://127.0.0.1:9428/insert/jsonline

UI 与 HTTP API 共享同一端口,也共享同样的无鉴权态势:任何能连到端口的人都能查询、也能写入日志。按上文所述用 --listen 127.0.0.1 或防火墙限制访问。

常用查询

查询使用 LogsQL。下面的示例走 HTTP 查询 API;同样的查询串可直接用在 /select/vmui/ 的 VMUI 里。

curl -s http://127.0.0.1:9428/select/logsql/query \
  --data-urlencode 'query=_time:15m level:error' \
  --data-urlencode 'limit=20'

响应是逐行 JSON,每条日志一个对象。

查生产数据时务必带 _time: 过滤条件。不带时 VictoriaLogs 会扫描保留期内的全部日志。

目标 查询
最近 5 分钟全部日志 _time:5m
全文检索 _time:15m error
按字段过滤 _time:1h app:demo level:error
精确值(区分大小写) _time:1h level:=error
正则匹配(RE2) _time:1h path:~"/api/v[0-9]+/"
短语 / 前缀匹配 _time:1h _msg:"payment timeout" / _time:1h _msg:timeout*
数值比较 _time:1h request_duration:>1s
AND / OR / NOT _time:5m level:error -app:buggy / _time:5m (level:error OR level:warn)
stream 过滤 _time:1h {app="demo"}
搜索所有字段 _time:5m | *:timeout
按时间倒序取前 10 条 _time:5m error | sort by (_time) desc | limit 10
只保留部分字段 _time:5m error | fields _time, level, _msg
按 level 统计 _time:1h | stats by (level) count()
总数统计 _time:1h error | stats count() errors
去重值及命中次数 _time:1h | uniq by (user) with hits
Top N 值 _time:1d level:error | top 10 (path)

说明:

  • field:word 匹配字段中的词;field:=value 是精确匹配,field:~"regex" 是正则匹配。取反形式为 -filter、!=、!~。
  • 时间过滤支持相对时长(5m、1h、30d)、绝对时间(2026-10-23Z、2026-10-23T10:00:00Z)和区间(_time:[2026-10-01Z, 2026-10-23Z])。
  • 管道从左到右执行,过滤条件必须写在 sort、limit、stats、top 等处理阶段之前。

日志写入:vlagent、HTTP API、采集器

pgcli 只负责运行 VictoriaLogs 后端——日志怎么进来由你决定。共有三种方式。

vlagent(官方 agent)

vlagent 是 VictoriaLogs 的官方日志 agent——vmagent 的日志对应物(vmagent 只负责指标,不要用它写日志)。它在自己的端口(默认 9429)上接受与 VictoriaLogs 相同的 HTTP 摄入协议,可以从磁盘跟踪日志文件,并通过 -remoteWrite.url 转发到 VictoriaLogs,该 URL 必须以 /insert/native 结尾。pgcli 会替你运行并管理它——完整参数见 vlagent 插件页面:

# 仅转发:在 :9429 接受 HTTP 推送并转发给 VictoriaLogs
pg addon install vlagent \
  --remote-write-url http://127.0.0.1:9428/insert/native

# 跟踪宿主日志文件:/var/log/nginx 以只读方式挂载到容器内相同路径;
# --include/--exclude 使用 vlagent 原生 glob 语法(不是正则)
pg addon install vlagent \
  --log-path /var/log/nginx \
  --include '/var/log/nginx/**/*.log' \
  --exclude '/var/log/nginx/*.gz' \
  --remote-write-url http://127.0.0.1:9428/insert/native

第一种形式不跟踪任何日志文件:没有 --log-path/--include(也没有经 --extra-arg 传入的 -kubernetesCollector)时,vlagent 只接收推到端口 9429 的 HTTP 日志并转发。--log-path 接受任意宿主文件或目录;不给 --include 时,目录默认展开为 <dir>/**/*.log,单个文件默认就是它自身。glob 是在容器内匹配的,所以 pgcli 把每个宿主路径挂载到完全相同的绝对路径。

适合使用 vlagent 的场景:

  • 复制 / 高可用 —— 为每个 VictoriaLogs 实例重复传 --remote-write-url。vlagent 会把每条日志复制到所有目标,并为每个 URL 维护独立磁盘缓冲(-remoteWrite.tmpDataPath,可用 -remoteWrite.maxDiskUsagePerURL 限制大小);后端宕机期间采集的日志会在其恢复后重放。
  • Kubernetes Pod 日志 —— 用 --extra-arg -kubernetesCollector,并通过 --log-path 把宿主 /var/log(使用 Docker runtime 时再加 /var/lib 以解析 runtime 符号链接)挂入。它跟踪 /var/log/pods 与 /var/log/containers,自动附加容器元数据,并把读取位点持久化在数据目录(-tmpDataPath)下。
  • 磁盘文件 —— --log-path /var/log/nginx --include '/var/log/nginx/*.log'(glob 必须指向文件而非目录)。默认 hostname 与 file 成为 stream 字段,读取位点与缓冲位于插件的数据目录。

如果只有单个、网络稳定的 VictoriaLogs 实例,直接发送(见下)更简单——这是推荐的默认方式。

HTTP API

VictoriaLogs 在 UI/查询 API 的同一端口上提供多个摄入端点:

端点 格式 说明
/insert/jsonline JSON Lines(ndjson) 必须带 Content-Type: application/json;缺失时返回 200 但数据被静默丢弃
/insert/elasticsearch/_bulk Elasticsearch / OpenSearch bulk Filebeat / Logstash / Fluent Bit 的 Elasticsearch output 指向这里
/insert/loki/api/v1/push Loki JSON push Promtail / Grafana Alloy / Loki 客户端指向这里
/insert/opentelemetry/v1/logs OTLP/HTTP logs OpenTelemetry Collector 的 exporter 指向这里
syslog TCP / UDP / Unix RFC3164 / RFC5424 经 --extra-arg -syslog.listenAddr.tcp=:514(或 .udp / .unix)启用并重建

所有 HTTP 摄入端点都接受同一组调优参数(query string 或 VL-* 请求头):_msg_field(日志消息所在字段,如 message)、_time_field(如 @timestamp)、_stream_fields(用逗号分隔的 stream 字段)、extra_fields(env=prod)、ignore_fields、decolorize_fields,以及 debug=1(只记录 payload 而不入库)。当采集器输出的 schema 无法被自动识别时用它们做映射。

# JSON Lines,显式字段映射
curl -s -X POST -H 'Content-Type: application/json' \
  --data-binary @app.jsonl \
  'http://127.0.0.1:9428/insert/jsonline?_stream_fields=host,app&_time_field=@timestamp&_msg_field=message'

日志采集器

任何能使用上述格式之一的采集器都可以;pgcli 不安装也不配置它们。常用采集器及官方配置指南:

采集器 发往
Filebeat Elasticsearch output → /insert/elasticsearch/_bulk
Logstash Elasticsearch 或 HTTP output
Fluent Bit / Fluentd Elasticsearch 或 HTTP output
Vector http sink → /insert/jsonline,或 Elasticsearch sink
Promtail / Grafana Alloy Loki push → /insert/loki/api/v1/push
OpenTelemetry Collector OTLP/HTTP exporter → /insert/opentelemetry/v1/logs
Telegraf http output → jsonline
journald Vector / Fluent Bit 的 journald source → jsonline,或用 vlagent
rsyslog / syslog-ng syslog listener(-syslog.listenAddr.*)
Datadog Agent Datadog intake API

采集器也可以不直接连 VictoriaLogs,而是指向 vlagent(端口 9429)——需要缓冲、复制或统一出口时更合适。

自定义参数(--extra-arg)

VictoriaLogs 接受许多 CLI flags 来调优缓存大小、查询限制、摄入速率等。经 --extra-arg 传入(可重复):

pg addon install victorialogs --name vl1 \
  --extra-arg -search.maxQueryDuration=60s \
  --extra-arg -memory.allowedPercent=80

额外参数最后追加到容器的命令行,故它们经 last-wins 语义覆盖早期 flags。这让你在 pgcli 的一等旋钮之外调优 VictoriaLogs,无需编辑任何配置文件。验证 flags 已生效(二进制不会把 flags 打进日志):

podman inspect pgcli-victorialogs-default-vl1 --format '{{json .Config.Cmd}}'

监听与暴露

VictoriaLogs 的 Listen 默认 0.0.0.0:容器共享宿主网络命名空间,所以 UI 与 API 在宿主每个网卡的分配端口上可达。这与 VictoriaMetrics 的默认一致(别的机器上的日志采集器和 Grafana 要能连它)——但背后没有密码。任何能连到端口的人都能执行任意 LogsQL、读你存储的日志。想要仅回环的部署,传 --listen 127.0.0.1(用 --force 重建);pg addon list 会显示 http://127.0.0.1:<port>/。否则请在防火墙层面限制访问。

端口

每个 VictoriaLogs 实例占有一个端口:Web UI/API。它取自以 victorialogs_start_port(默认 9428——VictoriaLogs 惯例 HTTP 端口)为基址的端口池,每多一个实例递增,并跳过任何已被受管容器或 used_ports 列表占用的端口。分配结果写入 pg.yaml,所以重装绝不会把别的插件在用的端口再发一遍。9428 与 VictoriaMetrics(8428)、vmcluster(8480-8484)、Prometheus(9090)、Grafana(3000)、pgAdmin(5050)、nginx(8080)都不重号;显式 --port 仍优先。

pg addon install victorialogs --name vl2          # 第二个实例:下一个空闲端口(9428 被占则 9429)
pg addon install victorialogs --name vl3 --port 9500  # 显式端口仍优先

配置

addons:
  victorialogs:
    vl1:
      name: vl1
      image_tag: docker.io/victoriametrics/victoria-logs:v1.53.0
      host_port: 9428
      listen: 0.0.0.0
      retention: "30d"
      extra_args:
        - -search.maxQueryDuration=60s
        - -memory.allowedPercent=80
      # data_dir: /srv/victorialogs-data      # 可选,覆盖默认位置

没有配置文件——一切都作为 CLI flags 传给 victoria-logs 二进制(-storageDataPath、-httpListenAddr、-retentionPeriod,加上任何 extra_args)。retention 存为时长字符串(省略 = 二进制默认),extra_args 是最后追加的 CLI flags 列表。

列出

pg addon list
Web add-ons (victorialogs):
  victorialogs (name: vl1)
    Status:      running
    URL:         http://0.0.0.0:9428/
    Retention:   30d
    Extra args:  -search.maxQueryDuration=60s -memory.allowedPercent=80
    Data:        /home/you/.pgcli/addon/victorialogs/vl1/data
    Image:       docker.io/victoriametrics/victoria-logs:v1.53.0
    Container:   pgcli-victorialogs-default-vl1

没有凭据可揭示——VictoriaLogs 本就没有——所以 --show-password 对它不会多显示任何东西。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop victorialogs
pg addon start victorialogs

开机自启

pg autostart enable --victorialogs --name vl

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——日志数据目录在 install 时已创建,重启后已有日志仍在。关闭用 pg autostart disable --victorialogs --name vl;完整机制见 开机自启。

移除

pg addon remove victorialogs              # 保留日志存储——已存储日志跨重装存活
pg addon remove victorialogs --clean-data # 连日志存储一起删

rootless podman 下,--clean-data 经 podman unshare rm 删除数据目录——VictoriaLogs 以 root(容器内 uid 0)写文件,它映射到宿主的 subordinate uid,宿主用户无法直接 rm。

排障

  • 摄入返回 200 但查询为空 —— JSON Lines 端点要求 Content-Type: application/json;缺失时 VictoriaLogs 接受请求但会静默丢弃数据(/metrics 上的 vl_rows_ingested_total{type="jsonline"} 保持 0)。加上该 header,或改用会自动设置的采集客户端。
  • 查询返回空数据 —— 确认 VictoriaLogs 在运行(pg addon list 显示 running)、健康端点有响应(curl -s http://<host>:<port>/-/healthy)、且你查的是正确的时间范围与 stream 过滤(_stream: 前缀、_time: 边界)。查 podman logs <container> 看摄入错误。
  • Install 失败于 “container already exists” —— 容器名由 --name 和命名空间派生;用 --force 重建,或先 pg addon remove 清理一次不完整的 install。
  • 额外参数没生效 —— --extra-arg 在容器创建时应用,而非运行时。用 --force reinstall 以新 flags 重建容器。查 podman inspect <container> --format '{{json .Config.Cmd}}' 确认 flags 在命令行上(二进制不会打印它们)。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。
  • 远程 API 打不开 / 意外可访问 —— 默认 0.0.0.0 下打不开是宿主防火墙问题,不是绑定地址;反之若你不想被访问,用 --listen 127.0.0.1 --force。

已知限制

  • VictoriaLogs 本身是裸日志后端:pgcli 另外提供专门的 vlagent 插件做文件/HTTP 采集(也可以用上文直连 HTTP API 或第三方采集器),但不安装任何第三方采集器,也不做告警(自行跑 vlalert)。它是你让 Grafana(VictoriaLogs 数据源)和采集器连的长期日志存储。
  • 无鉴权。 VictoriaLogs 的 API 摄入并查询任意日志;0.0.0.0 绑定的唯一保护是网络边界。这与 VictoriaMetrics 同一态势——不要把它暴露到不可信网络。
  • pgcli 不校验 --extra-arg 值(install 阶段没有 pre-flight 检查)。有错的 flag 只会在启动时从容器日志里暴露。
  • macOS 上容器跑在 podman machine 里;目前只在 Linux 上验证过。

16.24 - vlagent

以 pgcli 插件方式运行 vlagent——VictoriaLogs 官方日志 agent,跟踪任意宿主日志路径并转发给 VictoriaLogs

pg addon install vlagent 以受管容器方式运行 vlagent——VictoriaLogs 家族的官方日志 agent、vmagent 的日志对应物——直接跑官方 docker.io/victoriametrics/vlagent 镜像。它是顶层插件:不前置某个具体 PostgreSQL 实例,是 VictoriaLogs 插件的天然搭档——VictoriaLogs 存日志,vlagent 从文件或 HTTP 采集日志并转发给一个或多个 VictoriaLogs 实例。

与 VictoriaMetrics、VictoriaLogs 一样,完全不需要配置文件——一切都作为镜像名之后的 CLI flag 传入。pgcli 在其上增加文件采集接线:每个 --log-path 都以只读方式挂载到容器内完全相同的绝对路径,--include/--exclude 使用 vlagent 的原生 glob 语法(不是正则表达式)选择文件。

没有鉴权。 vlagent 的 HTTP 监听端口接受任何能连上的人推送的日志,而它转发的 VictoriaLogs(默认)也没有鉴权。远程暴露的实例只靠网络边界保护——用 --listen 127.0.0.1 收紧为仅回环,或在防火墙层面限制访问。

平台:Linux 是已实测的平台。设计上也支持 macOS(镜像多架构;容器在 pgcli-net bridge 上以发布端口的方式跑在 podman machine 里),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name va 实例名(map 键)
--image docker.io/victoriametrics/vlagent:v1.53.0 覆盖镜像
--port 自动(基址 9429) HTTP 摄入宿主端口;自 vlagent_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡;127.0.0.1 收紧为仅回环
--remote-write-url 必填 转发的 VictoriaLogs 端点(可重复)。每个 URL 必须以 /insert/native 结尾
--log-path — 要跟踪的宿主文件或目录(可重复)。以只读方式挂载到容器内相同绝对路径
--include 自动推导 选择文件的 glob(可重复;绝对容器路径)。默认目录为 <dir>/**/*.log,文件为自身
--exclude — 排除文件的 glob(可重复;与 --include 按位置配对)
--extra-arg — 传给二进制的额外 CLI flag(可重复);最后追加,故 last-wins 覆盖早期 flags
--data-dir 自动 覆盖数据目录(默认在 base 目录下)
--force false 即使正在运行也重建——用于应用改动的端口/监听/URL/路径/glob

数据目录存放 vlagent 的磁盘转发缓冲与文件采集检查点(读取位点)。它跨 pg addon remove 存活(用 --clean-data 才删除),所以重装同名实例会从采集停止的位置继续,而不是重发或跳过文件内容。镜像以 root 运行,无需预先 chown。

没有 --password:vlagent 没有本地账号概念,连接两端也都不鉴权。

安装

仅转发——在 :9429 接受 HTTP 推送并转发给本机 VictoriaLogs:

pg addon install vlagent \
  --remote-write-url http://127.0.0.1:9428/insert/native

跟踪一个宿主日志目录(最常见):

pg addon install vlagent \
  --log-path /var/log/nginx \
  --remote-write-url http://127.0.0.1:9428/insert/native

不给 --include 时,该目录默认展开为 /var/log/nginx/**/*.log——即目录下任意深度的所有 .log 文件。上面的引号只是为了 shell 安全;pgcli 按字面存储 glob。

✓ vlagent installed: "va"
  Container:  pgcli-vlagent-default-va
  Image:      docker.io/victoriametrics/vlagent:v1.53.0
  Data:       /home/you/.pgcli/addon/vlagent/va/data
  Listen:     http://0.0.0.0:9429/ (HTTP ingest: jsonline, ES bulk, Loki, OTLP)
  Remote:     http://127.0.0.1:9428/insert/native
  Mount:      /var/log/nginx (dir, read-only, same path in the container)
  Include:    /var/log/nginx/**/*.log
  Checkpoint: file-checkpoints.json under /var/lib/vlagent

跟踪单个文件,或多个路径并显式指定 glob 与排除:

# 单个文件(默认 include 就是它自身)
pg addon install vlagent --name va-app \
  --log-path /var/log/myapp/app.log \
  --remote-write-url http://127.0.0.1:9428/insert/native

# 多个路径,每个路径各自 glob,并排除压缩归档
pg addon install vlagent --name va-all \
  --log-path /var/log/nginx --include '/var/log/nginx/*.log' --exclude '/var/log/nginx/*.gz' \
  --log-path /var/log/redis --include '/var/log/redis/**/*.log' \
  --remote-write-url http://127.0.0.1:9428/insert/native

每个 --log-path 变成一个只读 bind mount(host:host:ro),所以 pgcli 传给 vlagent 的 glob 就是宿主路径本身。路径必须是绝对路径且已存在。

对运行中的实例重复 install:配置确实变了(端口、监听、URL、日志路径、include/exclude、额外参数)时重建,没变则原样不动;--force 即使配置未变也重建。列表型 flag 会替换整个存储列表:要新增路径,请重传完整集合。

文件采集的工作方式

vlagent 的文件采集由 -fileCollector.glob 驱动,pgcli 的 flag 一一对应:

pgcli 参数 vlagent flag 说明
--log-path (bind mount) -v <path>:<path>:ro——容器必须能看见该文件
--include -fileCollector.glob 可重复;必须匹配文件而非目录
--exclude -fileCollector.excludeGlob 与第 N 个 include 按位置配对
--data-dir -tmpDataPath 包含缓冲与 file-checkpoints.json

每条采集到的日志额外带两个字段:file(来源路径)与 hostname。二者默认都是 stream 字段,所以每个文件在 VictoriaLogs 里是独立的 log stream——正是 LogsQL 过滤想要的效果。可用 --extra-arg -fileCollector.streamFields=...、-fileCollector.fileField=... 或 -fileCollector.hostnameField=... 调整。

采集器默认每 10 秒轮询新文件(-fileCollector.refreshInterval)。它支持 create 与 nocreate 两种日志轮转策略:轮转时先读完旧文件再切换;重启后会从保存位点继续读取轮转后的文件。检查点写在数据目录下。

Glob 语法

这不是正则表达式——是 vlagent 自己的 glob 方言:

模式 匹配
* 任意字符序列,不跨路径分隔符
/**/ 零个或多个目录(递归)
? 任意单个字符,不跨路径分隔符
[abc]、[a-z] 字符类;前缀 ^ 或 ! 表示取反
{a,b,c} 备选——匹配逗号分隔项中的任意一个
\c 转义字符字面量

示例:

--include '/var/log/nginx/*.log'              # 下一层
--include '/var/log/nginx/**/*.log'           # 任意深度
--include '/var/log/nginx/{access,error}.log' # 备选
--include '/var/log/nginx/access.log.[0-9]'   # 轮转文件

* 不匹配隐藏文件(以 . 开头);需要时请显式写出开头的点(.*.log)。

Exclude 按位置配对

vlagent 把第 N 个 --exclude 与第 N 个 --include 配对,与它自己的 flag 语义一致。空字符串 '' 是占位符,表示“这个位置不排除”:

--include '/var/log/*/*.log' --exclude '/var/log/nginx/*' \
--include '/var/log/nginx/*.log' --exclude '' \

第一对采集除 nginx 外的所有日志,第二对再单独采集 nginx。没有位置配对的话,nginx 的排除会把第二对也一起排除掉。

pgcli 会输出到最后一个非空 exclude 为止;末尾的空占位符直接丢弃。如果只改 --log-path 而不传 --include,会按新路径重新计算默认 glob,并裁剪超出新配对数量的已存 exclude(想覆盖就在同一次运行里显式传 --exclude)。

复制与高可用

为每个 VictoriaLogs 实例重复传 --remote-write-url:

pg addon install vlagent --name va-ha \
  --log-path /var/log/myapp \
  --remote-write-url http://10.0.0.1:9428/insert/native \
  --remote-write-url http://10.0.0.2:9428/insert/native

vlagent 把每条日志复制到所有目标,为每个 URL 维护独立磁盘缓冲,并在后端恢复后重放缓冲日志。可用 --extra-arg -remoteWrite.maxDiskUsagePerURL=... 限制每个缓冲的大小。

其他 HTTP 摄入

即使没有 --log-path,vlagent 也是一个有用的 HTTP 前门:它在监听端口上接受与 VictoriaLogs 相同的协议并转发,因此采集器可以推给 vlagent 而不是直连 VictoriaLogs:

端点 格式
/insert/jsonline JSON Lines(要求 Content-Type: application/json)
/insert/elasticsearch/_bulk Elasticsearch / OpenSearch bulk
/insert/loki/api/v1/push Loki JSON push
/insert/opentelemetry/v1/logs OTLP/HTTP logs

Kubernetes 节点日志:用 --extra-arg -kubernetesCollector,并通过 --log-path 挂入节点 /var/log(Docker runtime 再加 /var/lib)。完整协议与采集器列表见 VictoriaLogs 日志写入一节。

端口

每个 vlagent 实例占一个端口:HTTP 摄入监听。它取自以 vlagent_start_port(默认 9429——vlagent 惯例端口)为基址的端口池,每多一个实例递增,并跳过任何已被受管容器或 used_ports 列表占用的端口。该池与 VictoriaLogs(9428)分开,所以新增 VictoriaLogs 实例绝不会抢走 vlagent 的端口。显式 --port 仍优先。

pg addon install vlagent --name va2 \
  --remote-write-url http://127.0.0.1:9428/insert/native   # 下一个空闲端口(9429 被占则 9430)

配置

addons:
  vlagent:
    va:
      name: va
      image_tag: docker.io/victoriametrics/vlagent:v1.53.0
      host_port: 9429
      listen: 0.0.0.0
      remote_write_urls:
        - http://127.0.0.1:9428/insert/native
      log_paths:
        - /var/log/nginx
      includes:
        - /var/log/nginx/**/*.log
      excludes:
        - /var/log/nginx/*.gz
      extra_args:
        - -fileCollector.refreshInterval=5s
      # data_dir: /srv/vlagent-data        # 可选,覆盖默认位置

一切都作为 CLI flags 传给 vlagent 二进制:remote_write_urls 变成重复的 -remoteWrite.url,includes 变成 -fileCollector.glob,excludes 变成 -fileCollector.excludeGlob,log_paths 变成只读 bind mount,data_dir 变成 -tmpDataPath。

列出

pg addon list
Log agents (vlagent):
  vlagent (name: va)
    Status:      running
    Listen:      http://0.0.0.0:9429/
    Remote:      http://127.0.0.1:9428/insert/native
    Log paths:   /var/log/nginx
    Includes:    /var/log/nginx/**/*.log
    Excludes:    /var/log/nginx/*.gz
    Data:        /home/you/.pgcli/addon/vlagent/va/data
    Image:       docker.io/victoriametrics/vlagent:v1.53.0
    Container:   pgcli-vlagent-default-va

启停

pg addon stop vlagent
pg addon start vlagent

容器以 podman 的 unless-stopped 重启策略运行。文件读取位点会检查点写入数据目录,所以 stop/start(或宿主重启)会从原位置继续,不重复、不丢文件内容。

开机自启

pg autostart enable --vlagent --name va

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——带文件检查点与转发缓冲的数据目录在 install 时已创建,agent 会从采集停止的位置继续。启动顺序上 vlagent 排在 VictoriaLogs 之后,所以同机的 remote-write 目标已经先监听。关闭用 pg autostart disable --vlagent --name va;完整机制见 开机自启。

移除

pg addon remove vlagent              # 保留缓冲与文件检查点
pg addon remove vlagent --clean-data # 连它们一起删

移除 agent 不会删除任何被跟踪的日志文件——它们以只读方式挂载,从未被修改。

排障

  • VictoriaLogs 里没有文件日志 —— 确认容器在运行(pg addon list)、glob 能匹配真实文件(--include 必须匹配文件而非目录)、且路径在容器内可见。podman inspect pgcli-vlagent-<ns>-<name> --format '{{json .Config.Cmd}}' 可查看确切 flags;podman logs <container> 查看采集器错误。
  • 部分文件被跳过 —— * 不匹配隐藏文件,轮转名如 app.log.1 也不匹配 *.log。请加显式模式(如 --include '/var/log/myapp/app.log.[0-9]')。exclude 按位置配对:第一对里过宽的排除会连带影响后面的配对。
  • 写入的 stream 不对 —— file 与 hostname 默认是 stream 字段;用 --extra-arg -fileCollector.streamFields=... 覆盖。
  • 额外参数没生效 —— --extra-arg 在容器创建时应用。重装(配置变化会自动重建)或用 --force;用 podman inspect <container> --format '{{json .Config.Cmd}}' 确认。
  • 容器读不到日志(SELinux 主机) —— pgcli 对日志路径只用 :ro、刻意不用 :z,因为给 /var/log 打标签会改动宿主。在 enforcing 的 SELinux 系统上请自行按工作负载打标签(如共享的 container_log_t 标签)或使用匹配的 security profile。
  • 远程写入失败 —— URL 必须以 /insert/native 结尾;检查 VictoriaLogs 健康(curl -s http://<vl>:9428/-/healthy)以及主机间防火墙。可达后缓冲日志会自动重放。

已知限制

  • 不支持正则表达式。 vlagent 只支持 glob(*、/**/、?、[a-z]、{a,b})。复杂文件名规则需要放宽 include,再在查询侧用 LogsQL 过滤。
  • 摄入监听与远程写入两侧都没有鉴权;用 --listen 127.0.0.1 或防火墙保护。
  • pgcli 不校验 --extra-arg 的值;错误的 flag 只会在启动时从容器日志里暴露。
  • macOS 代码完备但未实测;Linux 是已验证平台。

16.25 - VLCluster

以 pgcli 插件方式运行 VictoriaLogs 集群版——vlinsert + vlselect + vlstorage 共用同一镜像,支持跨主机分片部署

pg addon install vlcluster 以受管容器方式运行 VictoriaLogs 集群版——可水平扩展的日志数据库。与单机版 victorialogs addon(1 容器 1 端口)不同,它管理最多 3 个容器,但三个角色共用同一个镜像(docker.io/victoriametrics/victoria-logs:v1.53.0),角色完全由启动参数区分;每台主机占用 3 个 HTTP 端口。

架构:

组件 职责 HTTP 端口 通信对象
vlinsert 摄入端,按 stream hash 分片写入所有 vlstorage 9430 拨每个 vlstorage:StoragePort
vlselect 查询端,并行查询所有 vlstorage 并合并结果 9431 拨每个 vlstorage:StoragePort
vlstorage 数据存储(分片,不复制) 9432 在该端口上提供 /internal/insert + /internal/select/*

vlinsert 和 vlselect 都是无状态的,且拨向每个 vlstorage 的同一个端口——不像 VMCluster 需要 host:insertPort:selectPort 两个内部 TCP 端口。因此存储节点地址就是 host:storagePort。

没有 Web 登录/密码。 与单机版 VictoriaLogs 一样,集群版完全不带鉴权。 它的 HTTP API 可以摄入、查询和删除任意日志,所以远程暴露的实例只靠网络边界 保护——防火墙,或 --listen 127.0.0.1 收紧为仅回环。请把它当作一个无鉴权的 读/写控制台来对待。

平台:仅 Linux。VLCluster 需要 host networking 来承载 vlinsert/vlselect 拨向各 vlstorage 的端口。macOS 上安装会被拒绝并给出明确错误提示。

没有副本。 vlinsert 只做分片、不做复制:每条 stream 只存在于一个 vlstorage 节点上。任一 vlstorage 下线,查询会返回 HTTP 502,直到它恢复(摄入仍在继续, 可由 vlagent 或客户端缓冲)。这是上游的设计属性,不是 pgcli 的限制——容量规划 和备份请按此考虑。

部署形态

形态 1 — 单机收敛(默认)

3 个组件全在一台主机上:

pg addon install vlcluster --name vlc --listen 127.0.0.1
# 1 host, 3 containers (vlstorage + vlinsert + vlselect)
# --storage-node 自动推导为 127.0.0.1:<StoragePort>

形态 2 — 跨主机分片

与 VMCluster 相同的 --roles + --storage-node 模式:每台主机各跑自己的 pg addon install vlcluster,pgcli 从不 SSH。

# Host A — 存储节点 1
pg addon install vlcluster --name vlc --roles storage --listen 10.0.0.1

# Host B — 存储节点 2
pg addon install vlcluster --name vlc --roles storage --listen 10.0.0.2

# Host C — 摄入 + 查询层
pg addon install vlcluster --name vlc --roles insert,select --listen 10.0.0.3 \
  --storage-node 10.0.0.1:9432 \
  --storage-node 10.0.0.2:9432

仅运行 access 角色(--roles insert 和/或 select)的主机必须传 --storage-node,否则安装直接报错——自动推导的 loopback 地址会造出一个谁都不认识的孤立节点。

所有主机必须为同名实例分配相同的端口块,地址才能对上。默认基址是 9430(见 端口);如果某台主机修改了 vlcluster_start_port,其他主机也要改成一样的值。

参数

参数 默认值 说明
--name vlc 实例名(map key)
--roles insert,select,storage 本机运行的角色,逗号分隔(跨主机时取子集)
--storage-node 自动推导 vlstorage 地址(可重复;格式 host:storagePort,如 10.0.0.1:9432)。列出集群中所有 vlstorage 节点以分片。单机收敛时省略
--insert-port 自动(基址 9430) vlinsert HTTP 端口
--select-port 自动(基址 9431) vlselect HTTP 端口
--storage-port 自动(基址 9432) vlstorage HTTP 端口
--listen 0.0.0.0 绑定地址——127.0.0.1 表示仅回环
--retention 二进制默认 vlstorage 保留期,如 30d(仅在本机运行 storage 角色时生效)
--image docker.io/victoriametrics/victoria-logs:v1.53.0 镜像覆盖(三个角色共用一个镜像)
--insert-extra-arg — vlinsert 额外 CLI 参数(可重复,追加在最后)
--select-extra-arg — vlselect 额外 CLI 参数(可重复,追加在最后)
--storage-extra-arg — vlstorage 额外 CLI 参数(可重复,追加在最后)
--data-dir 自动 vlstorage 数据目录覆盖
--force false 即使运行中且配置未变也重建

--retention 刻意不设默认值:留空表示使用 victoria-logs 二进制自己的默认保留期。与 vmcluster 的 --retention-months 不同,VictoriaLogs 解析的是 30d、3months、1y 这类时长。

安装

pg addon install vlcluster --name vlc --listen 127.0.0.1

命令会按需拉取镜像,按依赖顺序(storage → insert → select)创建容器,并打印摘要:

✓ VLCluster installed: "vlc"
  Roles:       storage, insert, select (converged on this host)
  Containers:  pgcli-vlcluster-default-vlc-storage, ...
  Image:       docker.io/victoriametrics/victoria-logs:v1.53.0
  Data:        /home/you/.pgcli/addon/vlcluster/vlc/data
  Retention:   binary default (--retention to change)
  Insert URL:  http://127.0.0.1:9430/ (vlinsert — ingestion)
  Select URL:  http://127.0.0.1:9431/ (vlselect — LogsQL queries)
  Storage URL: http://127.0.0.1:9432/ (vlstorage — internal APIs + admin)

用相同参数重装是幂等的(显示 unchanged);参数变化时只重建需要的部分;从 --roles 中移除的角色,其残留容器会被删除。

URL 格式

集群版与单机版 VictoriaLogs 使用完全相同的 API 路径——没有 account ID,也没有额外前缀:

经 vlinsert 摄入:

curl -X POST http://127.0.0.1:9430/insert/jsonline \
  -H 'Content-Type: application/json' \
  -d '{"_msg":"hello from vlcluster","app":"demo"}'

经 vlselect 查询:

curl 'http://127.0.0.1:9431/select/logsql/query?query=app:demo&limit=10'

vlselect 还在 http://<host>:9431/select/vmui/ 提供内置 UI。vlagent 的转发目标为 http://<vlinsert>:9430/insert/native。

vlstorage 端口(9432)的 /internal/insert 与 /internal/select/* 供另外两个角色调用;它自己的 LogsQL 端点只查询本节点本地数据,适合单节点排障,但不是集群级查询。

端口

每个 VLCluster 实例从 vlcluster_start_port 池(默认 9430)占用 3 个连续端口:

  • +0 InsertPort(vlinsert HTTP——摄入端点)
  • +1 SelectPort(vlselect HTTP——查询端点)
  • +2 StoragePort(vlstorage HTTP——内部摄入/查询 API)

即使某台主机只运行部分角色,三个端口也都会分配,这样存储节点地址在各主机间可预测。基址紧挨单机版 VictoriaLogs(9428)和 vlagent(9429);三个端口池互相感知,不会发放同一个端口。

配置

addons:
  vlcluster:
    vlc:
      name: vlc
      container_prefix: pgcli-vlcluster-default-vlc
      image_tag: docker.io/victoriametrics/victoria-logs:v1.53.0
      listen: 0.0.0.0
      insert_port: 9430
      select_port: 9431
      storage_port: 9432
      roles:
        - storage
        - insert
        - select
      storage_nodes:
        - "10.0.0.1:9432"
        - "10.0.0.2:9432"
      retention: 30d
      autostart: false

容器内没有配置文件——一切都以 CLI 参数传入。storage_nodes 是跨主机拓扑;留空表示自动推导的单机模式。

增加存储节点

vlinsert/vlselect 在启动时固化 -storageNode 列表,因此增加节点需要重建 access 层容器(摄入会短暂中断,已有数据不受影响):

# 新存储主机
pg addon install vlcluster --name vlc --roles storage --listen 10.0.0.3

# access 层主机:带完整节点列表重装
pg addon install vlcluster --name vlc --roles insert,select --listen 10.0.0.3 \
  --storage-node 10.0.0.1:9432 \
  --storage-node 10.0.0.2:9432 \
  --storage-node 10.0.0.3:9432

没有自动 rebalance:旧数据留在原节点,只有新写入会分布到扩容后的节点集合;vlselect 会合并所有节点,所以查询能看到新旧全部数据。如需重新均衡旧数据,可等 retention 自然过期,或手工搬运按天分区(上游没有 rebalance 工具)。

启停

pg addon stop vlcluster
pg addon start vlcluster

容器以 podman 的 unless-stopped 策略运行,主机重启后会自动恢复。停止顺序与依赖相反(select → insert → storage)。

开机自启

pg autostart enable --vlcluster --name vlc

启动是集群粒度的:每台主机只拉起自己的角色,按依赖顺序(storage → insert → select),已存在的容器直接 start,缺失的才重建。跨主机部署需在每台主机上执行。完整机制见 Autostart。

移除

pg addon remove vlcluster              # 保留 vlstorage 数据——同名重装可复活已有日志
pg addon remove vlcluster --clean-data # 连同数据目录一起删除

移除会删除本机全部三个角色容器,包括角色变更后遗留的孤儿容器。在 access-only 主机上执行不会影响远端存储节点。

日志

pg logs addon vlcluster                # vlstorage 日志(默认 service)
pg logs addon vlcluster --service insert   # vlinsert 日志
pg logs addon vlcluster --service select   # vlselect 日志

本机未运行的角色会被明确拒绝。

已知限制

  • 没有副本。 每条 stream 只有一份;任一 vlstorage 宕机,查询返回 502。摄入会继续,可由上游(如 vlagent 磁盘队列)缓冲。跨集群高可用需要多个 vlagent --remote-write-url 目标加 vmauth 之类路由层——不在本 addon 范围内。
  • 没有自动 rebalance。 增加存储节点只影响新写入的数据。
  • 没有鉴权。 HTTP API 可摄入和查询任意日志;0.0.0.0 绑定只能靠网络边界保护。
  • 仅 Linux。 需要 host networking。
  • 没有 TLS。 在前面用 nginx 终止 TLS,或在私有网络内使用。
  • 状态是本机视角。 pg addon list 只报告当前主机配置的角色,不会探测远端存储节点。
  • pgcli 不校验 --*-extra-arg 的取值,错误参数只会在容器启动日志中暴露。

16.26 - ZeroFS

以 pgcli 插件方式挂载加密、自维护的 S3 文件系统(宿主二进制)。

ZeroFS 是把一切关在桶里的 S3 文件系 统:不像 JuiceFS 依赖外部元数据引擎,它自己在对象存储上跑一套 LSM 树,且每个 对象都用卷数据密钥加密——该密钥由你的加密口令包裹(wrap)。快照与一致性 由后台 compaction 负责——没有要备份的数据库、没有要养活的 Redis。它经本地 9P 一跳再以 FUSE 挂载,日常使用与普通文件系统无异;而桶本身就是这个卷完整的、 加密的副本。

和 JuiceFS 一样,ZeroFS 是宿主二进制 addon——没有容器。 pg addon install zerofs 下载官方 release 二进制(sha256 校验;PATH 上已有 zerofs 则复用),自己写出卷的 config.toml,并运行两个宿主进程:

  • server(zerofs run -c config.toml)——掌管 S3 与加密侧,在 loopback 端口上讲 9P(默认 5564)。9P 没有认证,所以 pgcli 硬拒非 loopback 的 --listen;
  • client(zerofs mount 127.0.0.1:<port> <mount-point>)——你实际使用的 FUSE 挂载,server 重启时自动重连。

平台:仅 Linux(宿主 FUSE;与 BeeGFS 同样拒绝 macOS)。挂载点是普通宿主 目录,不会传播进 PG 实例容器。

一切全显式

对象存储是一个外部 S3 服务(云,或另一台机器上的 MinIO),在命令行上点名 ——pgcli 不做同主机 addon 的自动识别或凭据继承:

pg addon install zerofs --name z1 \
    --s3-endpoint http://minio.example.com:9000 \
    --access-key AK --secret-key SK \
    --bucket zerofs-data
  • --s3-endpoint 是裸的 http(s)://host[:port](不带桶路径、不内嵌凭据); pgcli 在起 server 前先探一次,不可达的主机快速失败,而不是陷进 server 的 重试循环。
  • --access-key/--secret-key 必填,且必须成对。
  • 桶从不由 pgcli 创建——用 pg mc 包装命令自己供给;安装时会原样打印那两条 命令。
  • ZeroFS 会对请求签名,因此 --s3-region 有效(可选——zerofs 自身默认 us-east-1,多数 MinIO 兼容存储接受默认值)。

挂载点完全由你指定——默认落在 pgcli 的 addon 目录下,用 --mount-point 指到 任意位置。目录要你亲手建好(mkdir -p),且运行 pg 的用户对其有读写权限—— pgcli 从不创建挂载点:

mkdir -p /mnt/cloudfs
pg addon install zerofs --name z1 \
    --s3-endpoint http://minio.example.com:9000 \
    --access-key AK --secret-key SK \
    --bucket zerofs-data --mount-point /mnt/cloudfs

目录不存在或不可写时,安装(以及之后的每次 mount)都会在下载和启动任何东 西之前失败,并给出逐字的 mkdir -p 补救命令。

两个后端都可以就用 pgcli 自己的服务——先用 pgcli 建好,再作为普通的"外部 URL"显式接线(安装代码从不自动查找它们;换成云桶或远端 Redis 也一模一样):

# MinIO 做对象存储(pgcli minio addon——silo/rustfs/garage/seaweedfs 同理;
# 桶仍要手动建,pgcli 从不建;root 用户默认 admin,除非安装时 --root-user
# 另有指定;`pg addon password minio` 裸输出 secret key,可直接内联):
pg addon install minio --name ms3
pg mc mb ms3/zerofs-data

pg addon install zerofs --name z1 \
    --s3-endpoint http://127.0.0.1:9000 --bucket zerofs-data \
    --access-key admin --secret-key "$(pg addon password minio --name ms3)"
# Redis 做 fence 后端(--conditional-put redis——给缺 conditional put 的存储
# 用;pgcli 的 MinIO 并不需要,这里只演示接法)。`pg addon password redis`
# 裸输出随机口令,手工拼进 URL,/N 选逻辑库:
pg addon install redis --name fence
pg addon password redis --name fence

pg addon install zerofs --name z2 \
    --s3-endpoint http://silo.example.com:9000 \
    --access-key AK --secret-key SK --bucket zerofs-fenced \
    --conditional-put redis --redis-dsn "redis://:PASS@127.0.0.1:6379/0"

以上示例中 pg 与存储同机,loopback(默认 127.0.0.1)即可。zerofs 客户端 在别的机器上时,把存储发布到网络、--s3-endpoint 指向它的地址——端点写在 config.toml 里,之后要改就 --force 重渲染:

pg addon install minio --name ms3 --listen 0.0.0.0   # 已装过则加 --force
# 在另一台机器上:
pg addon install zerofs --name z1 --s3-endpoint http://10.0.0.5:9000 …

(redis 默认就绑 0.0.0.0——口令即门禁。注意这里说的只是 S3/redis 后端: zerofs 自己的 9P --listen 必须保持 loopback——9P 没有认证。)

口令即数据

⚠️ 🔑 pg addon remove 会销毁口令的唯一副本——动手前先存好。 remove 保留桶,但删除配置条目,而口令只活在那个条目和它的 config.toml 里。没有 原口令,被保留的桶就永远无法解读——不存在任何恢复途径。任何将来可能反悔的 remove 之前:pg addon password zerofs --name z1 > /某处安全的地方(放在 pgcli 目录之外)。

加密口令包裹着桶的数据加密密钥(DEK)。首次安装自动生成——随时取回:

pg addon password zerofs --name z1

丢了它,桶里的内容就无法恢复。这对 pg addon remove 尤其关键:它刻意保留 桶,但配置条目(连同口令的副本)没了——于是朴素的 reinstall 会生成新口 令,无法 unwrap 旧 DEK,server 起不来并报 Invalid password or corrupted key。要重新接上保留的桶,把原口令传回去:

pg addon install zerofs --name z1 --encryption-password '<the stored one>'

(真要推倒重来:清空桶,或换一个 --bucket。)

Fencing:conditional put

ZeroFS 用 S3 conditional put(If-None-Match)对并发写者做 fencing。 --conditional-put 三选一:

模式 含义
auto(默认) 用存储原生 If-None-Match——需要 MinIO RELEASE.2022-08+(pgcli 默认镜像满足)或真 S3
redis 改经 Redis fencing——给不支持 conditional-put 的存储(如 silo);必须显式给 --redis-dsn redis://…(无同主机自动解析)
none 不 fencing——只在绝无他人碰这个桶时才安全

若 server 因存储缺少 conditional-put 支持而起不来,安装会识别日志特征并提示你 改用 --conditional-put redis --redis-dsn … 重试。

--redis-dsn 就是一个普通的 redis:// URL——DSN 会原样写进 config.toml, 任何 Redis 都行,包括 pgcli 自己的 redis addon(接法见上文示例;pg addon password redis 裸输出随机口令,/N 选逻辑库)。TLS 端点与 pgcli 其他处一 致:rediss://user:pass@redis-host:6380/1。

9P 端口每个实例独占:bind 撞车的 zerofs server 会静默启动、什么都不监听,所 以 install 和 start 对已被占用的 --port 直接拒绝——多卷并存请各给一个端口。

参数

参数 默认 说明
--name zerofs 卷名(配置 map 的键)
--s3-endpoint — 必填。外部 S3 服务,形如 http(s)://host[:port]
--s3-region zerofs 自身的 us-east-1 端点的 SigV4 region
--bucket zerofs-<name> 桶——pgcli 不创建
--access-key / --secret-key — 必填且成对。S3 凭据
--encryption-password 生成 wrap 口令;重接保留的桶时传回原口令
--mount-point <base>/addon/zerofs/<name>/mnt 挂载的宿主目录——pgcli 从不创建:先 mkdir -p 建好,且运行 pg 的用户可写
--cache-dir <base>/addon/zerofs/<name>/cache 服务端磁盘缓存目录
--server-cache-gb 100 服务端缓存大小(GiB)
--listen 127.0.0.1 9P 绑定地址——必须 loopback(9P 无认证)
--port 5564 9P 端口(已有应答者时直接拒绝——每个 zerofs 实例一个)
--conditional-put auto auto / redis / none(见 Fencing)
--redis-dsn — Redis fence 后端,--conditional-put redis 时必填
--install-dir /usr/local/bin 宿主二进制落点(不可写退到 ~/.local/bin)
--version 内置 pin(2.3.5) 要下载的版本(摘要已 pin 在 pgcli 源码里)
--no-mount 关 写好配置、起好 server,但不挂载
--force 关 重渲染 config.toml 并重启——保留存的加密口令(新口令无法 unwrap 既有数据)

FUSE(/dev/fuse + fusermount3)只探测、不安装。

日常管理

pg addon zerofs status --name z1     # server 进程、挂载状态、文件位置、fence 模式
pg addon zerofs umount --name z1     # 拆掉 FUSE client
pg addon zerofs mount  --name z1     # 重新挂上(server 需在跑)
pg addon zerofs mount  --name z1 --mount-point /mnt/cloudfs
                                     # 自定义路径(先 mkdir -p);未挂载时这
                                     # 样挂会顺带永久迁移该卷的挂载点
pg addon zerofs stop   --name z1     # 卸载 + 杀 server
pg addon zerofs start  --name z1
pg addon start/stop zerofs --name z1 # 顶层别名(start=server,stop=全停)
pg logs addon zerofs --name z1 -f                      # server 日志
pg logs addon zerofs --name z1 --service mount -f      # FUSE client 日志
pg addon remove zerofs --name z1     # 停 + 忘掉条目;桶和加密数据原地不动
                                     # 🔑 先存口令:pg addon password zerofs --name z1
pg addon remove zerofs --name z1 --clean-data   # 连宿主目录一起删

config.toml、server/client 日志与 pid 文件都在 <base>/addon/zerofs/<name>/ 下;口令与 S3 密钥放在 0600 的 config.toml 里——同时,和 pgcli 所有 addon 凭据一样,也明文存在于你的 pg.yaml。

换挂载路径的做法与 JuiceFS 完全一致:先 umount,用 mkdir -p 把新目录建好(可写——与安装契约相同),再带覆盖参数挂载;新路径 会持久化进 pg.yaml,之后的普通 mount(含 autostart)都用它:

pg addon zerofs umount --name z1
mkdir -p /srv/cloudfs
pg addon zerofs mount --name z1 --mount-point /srv/cloudfs
#   [i] mount point moved to /srv/cloudfs (saved)
# ✓ zerofs "z1" mounted at /srv/cloudfs

仍在挂载时尝试迁移会被拒绝并提示先 umount;FUSE 挂载点与 pgcli 的簿记完全 解耦(9P server 照常跑在自己的端口上,config.toml/日志/pid 始终在 <base>/addon/zerofs/<name>/),迁移从不触碰卷本身。

开机自启

pg autostart enable --zerofs --name z1

开机时 pg start --autostart 先起 9P server(等端口监听)再起 FUSE client。后端是外部服务——只有 S3 端点重新可达时开机才能成功。不重渲染、不重 生成:既有的 config.toml 与存的口令原样复用。

16.27 - DuckDB

以 pgcli 插件方式安装嵌入式 OLAP 分析数据库(宿主二进制)。

DuckDB 是嵌入式分析数据库——常被称作"SQLite for analytics"。 C++ 编写的列式引擎,向量化执行,可以直接查询 Parquet、CSV、JSON 和 Arrow 文件, 无需服务器进程。把它当成一个单二进制的 OLAP 引擎,指向你的数据即可使用: 加载 httpfs 扩展后,SELECT * FROM read_parquet('s3://bucket/data.parquet') 开箱即用。

作为 pgcli 插件,DuckDB 是宿主二进制形态:没有容器,没有守护进程。 pg addon install duckdb 从 GitHub releases 下载官方 CLI zip(sha256 校验; PATH 上已有 duckdb 则直接复用),安装 httpfs 扩展(parquet 和 json 已内置于二进制中),并在 pg.yaml 中记录条目。

平台:仅 Linux,仅 amd64(macOS 及其他架构明确拒绝并给出错误提示)。 DuckDB 是一个独立的 CLI 工具——不作为后台服务运行、不监听端口、不会传播进 PG 实例容器。

为什么在 pgcli 旁边装 DuckDB

DuckDB 填补了 PostgreSQL 行存引擎不擅长的分析场景:对大量 Parquet/CSV 数据集的 即席查询、数据湖探索、加载进 PostgreSQL 之前的 ETL 预处理,或者对已经躺在 S3 里 的文件做快速聚合。pgcli 已经管理了多种 S3 存储(MinIO、silo、rustfs、garage、 seaweedfs)——DuckDB 通过 httpfs 直接查询其中的内容。

用法

# 默认:v1.5.6 LTS,安装 httpfs 扩展(parquet/json 已内置)
pg addon install duckdb

# 指定版本
pg addon install duckdb --version 2.0.0

# 自定义名称和数据目录
pg addon install duckdb --name analytics --data-dir /srv/duckdb

# 跳过扩展安装
pg addon install duckdb --no-extensions

# 重新安装并更换参数(条目已存在时)
pg addon install duckdb --version 2.0.0 --force

安装完成后,启动交互式 shell 或执行一次性查询:

# 交互式 shell(数据目录作为工作目录)
pg addon duckdb shell

# 对本地 Parquet 文件的一次性查询
pg addon duckdb shell -- -c "SELECT * FROM read_parquet('/tmp/data.parquet')"

# 直接查询 S3(需要 httpfs 扩展,默认已安装)
duckdb -c "SELECT count(*) FROM read_parquet('s3://bucket/data/*.parquet')"

# 打开持久化数据库文件
duckdb ~/.pgcli/addon/duckdb/duckdb/analytics.duckdb

参数

参数 默认 说明
--name duckdb 实例名(配置 map 的键)
--version 1.5.6 要下载的版本(sha256 已 pin 在 pgcli 源码里;支持 1.5.6 LTS 和 2.0.0)
--data-dir <base>/addon/duckdb/<name> .duckdb 数据库文件的默认目录
--install-dir /usr/local/bin 宿主二进制落点(不可写时退到 ~/.local/bin——注意保持其在 PATH 上)
--no-extensions 关 跳过安装 httpfs 扩展(parquet 和 json 已内置于二进制中)
--force 关 按命令行参数重建配置条目

日常管理

pg addon duckdb status --name duckdb        # 二进制版本、路径、数据目录
pg addon duckdb shell --name duckdb         # 交互式 DuckDB 会话
pg addon duckdb shell -- -c "SELECT 1"      # 一次性查询(-- 后面的参数传给 duckdb)
pg addon list                               # duckdb 显示在"Analytics add-ons"下
pg addon start duckdb                       # 无操作(CLI 工具,非守护进程)
pg addon stop  duckdb                       # 无操作(CLI 工具,非守护进程)
pg addon remove duckdb --name duckdb        # 忘掉配置;二进制和数据目录保留
pg addon remove duckdb --name duckdb --clean-data   # 同时删除数据目录

remove 从不删除二进制——它可能被多个命名实例共享。加 --clean-data 删除 数据目录(数据库文件);不加则保留目录并打印提示。

Server/Client 模式(v2.0.0+)

DuckDB v2.0.0 引入了 Quack 协议,支持原生 server/client 模式。 DuckDB 可以通过 HTTP 对外提供数据库服务,多个客户端可以同时查询同一个实例。

启动服务端

# 交互式 server(stdin 必须保持打开)
duckdb /path/to/db.duckdb -serve

# 后台 server(重定向 stdin 以保持存活)
nohup duckdb /path/to/db.duckdb -serve < /dev/zero > /tmp/duckdb-serve.log 2>&1 &

# 或在 SQL 中使用 quack_serve()
duckdb -c "INSTALL quack; LOAD quack; CALL quack_serve(token='mytoken')"

服务端默认监听 localhost:9494,启动时会打印自动生成的 auth token。 默认只接受本地连接——如需远程访问,需在前面放一个 TLS 终止反向代理 (参见 DuckDB 文档)。

跨主机访问(绑定 0.0.0.0)

要接受来自任意网络接口的连接,使用 quack_serve() 并指定参数(而非 -serve):

-- 服务端:绑定到所有接口的 9494 端口
CALL quack_serve(
  listen_uri='quack:0.0.0.0:9494',
  allow_other_hostname=true,  -- 必须,允许非 localhost 绑定
  disable_ssl=true,            -- 可选,默认是 HTTPS
  token='mytoken'
);

需要三个参数:

  • listen_uri='quack:0.0.0.0:9494' — 绑定到所有接口(不只是 localhost)
  • allow_other_hostname=true — 绕过仅限 localhost 的限制
  • disable_ssl=true — 使用纯 HTTP(默认是 HTTPS 并自动生成证书)

客户端使用服务端 IP 地址连接:

# 客户端连接远程服务端
duckdb -c "ATTACH 'quack:192.0.2.10:9494' AS qk (TOKEN 'mytoken'); CONNECT qk; SELECT * FROM t"

安全警告:绑定 0.0.0.0 并使用 disable_ssl=true 会将数据库以明文形式暴露到网络。 仅在可信网络或防火墙后面使用。生产环境请使用带 TLS 的反向代理。

客户端连接

# 使用 -connect 参数(v2.0.0+)
duckdb -connect -c "SELECT * FROM t"

# 或通过 SQL(简写形式,默认 localhost)
duckdb -c "ATTACH 'quack:localhost' AS qk (TOKEN 'BAED...'); CONNECT qk; SELECT * FROM t"

# 显式 ip:port 形式(远程连接或非默认端口时使用)
duckdb -c "ATTACH 'quack:127.0.0.1:9494' AS qk (TOKEN 'BAED...'); CONNECT qk; SELECT * FROM t"

-connect 参数会自动发现服务端输出的 token。手动连接时,从服务端启动 信息中复制 token 即可。

注意事项

  • stdin 必须保持打开:服务端在 stdin 关闭后会退出(后台运行用 < /dev/zero 或 tail -f /dev/null |)
  • 仅限本地:远程访问需要带 TLS 终止的反向代理
  • Auth token:自动生成,启动时打印,客户端连接时必须提供
  • pgcli 不管理 Quack server:pg addon start/stop duckdb 仍然是无操作——Quack server 是一个独立进程,需要自行管理

扩展

DuckDB 的扩展系统在首次使用时安装共享库。pgcli 在安装时预装 httpfs—— 它启用通过 HTTP/S3 读取文件的能力。parquet 和 json 是静态链接在 DuckDB CLI 二进制里的,始终可用,无需网络下载。

  • httpfs —— 通过 HTTP/S3 读取文件(read_parquet('s3://...'))
  • parquet —— Parquet 文件读写器(内置)
  • json —— JSON 文件读取器(read_json_auto(...))(内置)

后续可以从 DuckDB shell 里安装更多扩展:

INSTALL 'spatial';       -- 地理空间函数
INSTALL 'sqlite';        -- 读写 SQLite 数据库
INSTALL 'mysql';         -- 直接查询 MySQL
LOAD 'httpfs';           -- 在当前会话中加载已安装的扩展

如果希望手动管理扩展,或安装时主机无法上网,可在安装时加 --no-extensions。

访问 S3、Parquet、JSON 和 CSV 数据

DuckDB 天生为文件分析查询而生——无需服务器、无需 COPY、无需导入步骤。 直接指向你的数据即可。

Parquet 文件

-- 单个文件
SELECT * FROM read_parquet('/tmp/events.parquet');

-- 通配符:目录下所有 parquet 文件
SELECT * FROM read_parquet('/data/logs/*.parquet');

-- schema 推断 + 计数
SELECT count(*) FROM read_parquet('s3://bucket/year=2026/*.parquet');

-- 指定列 + 过滤
SELECT user_id, event, ts
FROM read_parquet('/data/events.parquet')
WHERE ts > '2026-01-01'
LIMIT 100;

JSON 文件

-- 自动推断 schema(支持 JSON lines 和嵌套 JSON)
SELECT * FROM read_json_auto('/tmp/users.json');

-- 显式 schema
SELECT * FROM read_json('/tmp/users.json',
    columns = {'id': 'INTEGER', 'name': 'VARCHAR', 'email': 'VARCHAR'},
    format = 'newline_delimited');

-- 嵌套对象:用 ->> 提取字段
SELECT
    data->>'user_id' AS user_id,
    data->>'event'   AS event
FROM read_json_auto('/tmp/events.jsonl');

CSV 文件

-- 自动推断表头和类型
SELECT * FROM read_csv_auto('/tmp/sales.csv');

-- 显式列类型(自动推断不准时)
SELECT * FROM read_csv('/tmp/sales.csv',
    columns = {'id': 'INTEGER', 'amount': 'DOUBLE', 'date': 'DATE'},
    header = true);

-- TSV 或自定义分隔符
SELECT * FROM read_csv_auto('/tmp/data.tsv', delim = '\t');

S3 访问(httpfs 扩展)

httpfs 扩展启用 S3 访问。凭据和端点通过 CREATE SECRET(v2.0.0+)或传统 SET 命令(v1.5.x)配置。

DuckDB v2.0.0+ — CREATE SECRET

-- 配置 S3 凭据(v2.0.0+ 语法)
CREATE SECRET (
  TYPE S3,
  ENDPOINT 'minio.example.com:9000',
  KEY_ID 'admin',
  SECRET 'your-secret-key',
  USE_SSL false,
  URL_STYLE 'path'   -- MinIO 及兼容存储必须设置
);

-- 从 S3 直接查询 Parquet
SELECT count(*) FROM read_parquet('s3://my-bucket/events/2026/*.parquet');

-- 从 S3 查询 CSV
SELECT * FROM read_csv_auto('s3://my-bucket/users.csv') LIMIT 10;

-- 从 S3 查询 JSON
SELECT * FROM read_json_auto('s3://my-bucket/config.json');

-- 写回 Parquet 到 S3(需要桶的写权限)
COPY (SELECT * FROM read_parquet('/tmp/*.parquet') WHERE year = 2026)
  TO 's3://output-bucket/filtered/2026.parquet' (FORMAT parquet);

DuckDB v1.5.x — 传统 SET 命令

-- 在 DuckDB 会话中配置 S3 凭据(仅 v1.5.x)
SET s3_endpoint = 'minio.example.com:9000';
SET s3_access_key_id = 'admin';
SET s3_secret_access_key = 'your-secret-key';
SET s3_use_ssl = false;   -- https 端点用 true
SET s3_region = 'us-east-1';   -- MinIO 写 'us-east-1' 即可(region 被忽略)
SET s3_url_style = 'path';   -- MinIO 及兼容存储必须设置

连接 pgcli 管理的 S3 存储

pgcli 管理了多种 S3 兼容存储(MinIO、silo、rustfs、garage、seaweedfs)。 DuckDB 直接查询它们——s3_endpoint 就是存储的 HTTP 地址,凭据来自 pg addon password。

先创建 S3 存储实例和桶。MinIO、silo(Pigsty 的 MinIO 分支)和 rustfs (Rust 重新实现)的设置方式完全相同——它们共用 pg mc 包装命令,DuckDB 端也需要相同的 s3_url_style = 'path':

# MinIO
pg addon install minio --name ms3
S3_SECRET=$(pg addon password minio --name ms3)

# 或 silo(功能面一致,与 minio 共用端口池)
pg addon install silo --name ms3
S3_SECRET=$(pg addon password silo --name ms3)

# 或 rustfs(Rust S3 兼容存储,仅 Linux)
pg addon install rustfs --name ms3
S3_SECRET=$(pg addon password rustfs --name ms3)

# 通过 pg mc 包装命令创建桶
pg mc alias set ms3 http://127.0.0.1:9000 admin "${S3_SECRET}"
pg mc mb ms3/my-bucket

# 上传测试数据(可选)
pg mc cp /tmp/data.parquet ms3/my-bucket/data.parquet

然后从 DuckDB 查询——MinIO、silo、rustfs 及兼容存储需要 URL_STYLE 'path'(不支持 virtual-host style):

# DuckDB v2.0.0+
duckdb -c "
  CREATE SECRET (
    TYPE S3,
    ENDPOINT '127.0.0.1:9000',
    KEY_ID 'admin',
    SECRET '${S3_SECRET}',
    USE_SSL false,
    URL_STYLE 'path'
  );
  SELECT count(*) FROM read_parquet('s3://my-bucket/data/*.parquet');
"

# DuckDB v1.5.x(传统 SET 语法)
duckdb -c "
  SET s3_endpoint = '127.0.0.1:9000';
  SET s3_access_key_id = 'admin';
  SET s3_secret_access_key = '${S3_SECRET}';
  SET s3_use_ssl = false;
  SET s3_url_style = 'path';
  SELECT count(*) FROM read_parquet('s3://my-bucket/data/*.parquet');
"

如需更持久的配置,创建 DuckDB 启动文件(~/.duckdbrc),其中的命令 会在每次会话启动时执行:

-- ~/.duckdbrc (DuckDB v2.0.0+)
CREATE SECRET (
  TYPE S3,
  ENDPOINT 'minio.example.com:9000',
  KEY_ID 'admin',
  SECRET 'your-secret-key',
  USE_SSL false,
  URL_STYLE 'path'
);

S3 环境变量

DuckDB 从 DUCKDB_S3_* 环境变量读取端点和 SSL 设置,但凭据必须使用标准 AWS 变量(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY)——DuckDB 的 enable_global_s3_configuration 会自动检测这些变量。

v2.0.0 限制:URL_STYLE 无法通过环境变量设置。它默认为 virtual-host, 会导致 MinIO/silo/rustfs(需要 path-style)无法访问。对于这些存储, 应使用 CREATE SECRET 并指定 URL_STYLE 'path'。

# 端点和 SSL 通过 DUCKDB_S3_*
export DUCKDB_S3_ENDPOINT='127.0.0.1:9000'
export DUCKDB_S3_USE_SSL='false'

# 凭据通过 AWS_*(标准变量,DuckDB 自动检测)
export AWS_ACCESS_KEY_ID='admin'
export AWS_SECRET_ACCESS_KEY='your-secret-key'

# URL 样式仍需在会话或启动文件中设置(v1.5.x)
duckdb -c "SET s3_url_style = 'path'; SELECT * FROM read_parquet('s3://my-bucket/data.parquet');"

# v2.0.0 中 URL_STYLE 必须通过 CREATE SECRET 设置——环境变量无法覆盖
# MinIO/silo/rustfs 不支持 virtual-host style,必须显式指定 path
duckdb -c "
  CREATE SECRET (TYPE S3, ENDPOINT '127.0.0.1:9000', KEY_ID 'admin',
    SECRET 'your-secret-key', USE_SSL false, URL_STYLE 'path');
  SELECT * FROM read_parquet('s3://my-bucket/data.parquet');
"

对于 MinIO/silo/rustfs,仍需设置 s3_url_style = 'path',因为这些存储 不支持虚拟主机样式 URL。

16.28 - Node_exporter

以 pgcli 插件方式运行 node_exporter——Prometheus 官方宿主机指标导出器,单容器直接跑上游官方镜像

pg addon install nodeexporter 以受管容器方式运行 node_exporter——Prometheus 官方的宿主机指标导出器,在 /metrics 上发布整台机器的 CPU、内存、磁盘、网络与文件系统指标——直接跑官方 docker.io/prom/node-exporter 镜像。与 Prometheus、VictoriaMetrics、Grafana 一样,它是顶层插件:它描述的是机器,不是某个 PostgreSQL 实例,故不依附于任何一个。它是 pgcli Prometheus 插件的天然搭档——node_exporter 负责暴露、Prometheus 负责抓取。

node_exporter 与 VictoriaMetrics 形状相同——单容器、无配置文件(一切都作为镜像名之后的 CLI flag 传入)、无热重载、无鉴权——但它有一条别的 web 插件都没有的硬性要求,见下。

不可协商的一条:宿主挂载。 一个容器若读自己的 /proc,报的是容器的指标, 不是宿主的。为导出整台机器,pgcli 写死三条只读 bind 挂载——宿主 /proc → /host/proc、/sys → /host/sys、/ → /rootfs——并用 --path.procfs / --path.sysfs / --path.rootfs 把二进制指向它们。这三个挂载固定在 pgcli 里、 不由用户配置;少了它们,addon 会静默导出错误的数字。它们只以 :ro 挂载(绝不 用 :z),所以 pgcli 绝不会给宿主根文件系统重 labeling。

没有登录/密码,也没有 TLS。 node_exporter 只用纯文本提供 /metrics。因为抓取 它的 Prometheus 通常在另一台主机上,Listen 默认 0.0.0.0——任何能连到端口的 人都能读到细粒度的宿主信息(硬件、挂载表、网络)。请把它限制在你的监控网段内,或 对同机 Prometheus 传 --listen 127.0.0.1。

平台:Linux 是已实测的平台(容器用宿主网络,故导出器直接绑宿主端口)。设计上也 支持 macOS(bridge + 发布端口),但尚未在 Mac 上验证过。

参数

参数 默认值 说明
--name nodeexporter 实例名(map 键)
--image docker.io/prom/node-exporter:v1.12.1 覆盖镜像
--port 自动(基址 9100) 宿主端口;自 nodeexporter_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡(跨主机 Prometheus);127.0.0.1 收紧为仅回环
--extra-arg — 传给二进制的额外 CLI flag(可重复);最后追加,故 last-wins 覆盖早期 flags。如 --extra-arg --no-collector.cpufreq
--force false 即使正在运行也重建——用于应用改动的端口/监听/额外参数

node_exporter 无状态——它不落盘,故没有数据目录。pg addon remove 只删容器;重装总是从头开始。

安装

pg addon install nodeexporter

命令会按需拉取镜像、带着三条宿主挂载创建容器,并打印抓取 URL:

✓ node_exporter installed: "nodeexporter"
  Container:  pgcli-nodeexporter-default-nodeexporter
  Image:      docker.io/prom/node-exporter:v1.12.1
  Metrics:    http://0.0.0.0:9100/metrics
  Host mounts: /proc, /sys, / (read-only) → /host/proc, /host/sys, /rootfs

因为默认绑定是 0.0.0.0,summary 还会打印一条暴露警告和对应的 Prometheus 命令。验证导出器给的是真实宿主数字:

curl -s http://127.0.0.1:9100/metrics | grep -E '^node_(memory_MemAvailable_bytes|load1|cpu_seconds_total)'

这些值应与宿主一致(free -b、cat /proc/loadavg)——若它们跟的是容器而非宿主,说明缺了挂载(经 pgcli 不可能缺,但手跑的 node_exporter 经常犯这个错)。

接入 Prometheus

导出器只有被抓取才有意义。pgcli 的 Prometheus 插件靠一行 --scrape job 就能抓它(无需服务发现、无需改 Prometheus 配置):

pg addon install prometheus --name p1 --scrape node-exporter=127.0.0.1:9100
# 远程 Prometheus → 用宿主的局域网地址而非 127.0.0.1:
pg addon install prometheus --name p1 --scrape node-exporter=<host-ip>:9100

要把这个导出器加进一个已经在跑、已经在抓别的 job 的 pgcli Prometheus,用 --scrape-append——它把 node-exporter job 折叠进已存的抓取列表(同名 job 合并、 新名字追加)并 SIGHUP 热重载,绝不会清掉现有 target:

pg addon install prometheus --name p1 --scrape-append node-exporter=127.0.0.1:9100

(普通的 --scrape 会整体替换 job 集,所以用重跑 install 会丢掉 p1 原本在抓的 其他 job。)之后在 Prometheus 里,up{job="node-exporter"} 为 1、 count(node_cpu_seconds_total) 很大。--scrape / --scrape-append 的完整机制见 Prometheus 插件。

对运行中的实例重复 install:当配置确实变了(端口、监听、额外参数)时重建,没变则原样不动;--force 即使配置未变也重建。node_exporter 没有 SIGHUP reload,所以每次改动都需要重建容器。

自定义参数(--extra-arg)

node_exporter 有几十个 collector flag。经 --extra-arg(可重复)逐个开关 collector,或加 --collector.*/--no-collector.*:

pg addon install nodeexporter --extra-arg --no-collector.cpufreq \
                              --extra-arg --collector.netdev.device-exclude='^veth.*'

额外参数最后追加到命令行,故经 last-wins 覆盖 pgcli 注入的默认值(包括 pgcli 自己的 --collector.filesystem.mount-points-exclude——它过滤掉 pgcli 刚挂上的那些伪文件系统,让 node_filesystem_* 不全是噪音)。没有配置文件要编辑。

监听与暴露

Listen 默认 0.0.0.0:在 Linux 上容器共享宿主网络命名空间,所以 /metrics 在宿主每个网卡的分配端口上可达。这是刻意的——抓取 node_exporter 的 Prometheus 通常在另一台机器。但没有鉴权,且 node_exporter 暴露细粒度宿主细节(挂载表、网络接口、硬件)。对同机 Prometheus,传 --listen 127.0.0.1(用 --force 重建);否则请在防火墙层面限制端口。(在 macOS bridge 构建下,pgcli 会自动把绑定放宽到 0.0.0.0,好让发布的端口经 podman VM 保持可达。)

端口

每个 node_exporter 实例占有一个端口——指标端点——取自以 nodeexporter_start_port(默认 9100,node_exporter 惯例端口)为基址的端口池,每多一个实例递增,并跳过任何被受管容器或 used_ports 占用的端口。分配结果写入 pg.yaml,故重装绝不会把别的插件在用的端口再发一遍。9100 与 Prometheus(9090)、VictoriaMetrics(8428)、Grafana(3000)、Alertmanager(9093)、nginx(8080)都不重号;显式 --port 仍优先。

pg addon install nodeexporter --name ne2            # 第二个实例:下一个空闲端口(9101)
pg addon install nodeexporter --name ne3 --port 9200  # 显式端口仍优先

配置

addons:
  nodeexporter:
    nodeexporter:
      name: nodeexporter
      image_tag: docker.io/prom/node-exporter:v1.12.1
      host_port: 9100
      listen: 0.0.0.0
      extra_args:
        - --no-collector.cpufreq
      autostart: false

没有配置文件——一切都作为 CLI flags 传给 node_exporter 二进制,三条宿主挂载由 pgcli 固定。extra_args 是最后追加的 flag 列表。

列出

pg addon list
Web add-ons (nodeexporter):
  nodeexporter (name: nodeexporter)
    Status:      running
    Metrics:     http://0.0.0.0:9100/metrics
    Extra args:  --no-collector.cpufreq
    Image:       docker.io/prom/node-exporter:v1.12.1
    Container:   pgcli-nodeexporter-default-nodeexporter

没有凭据可揭示——node_exporter 本就没有。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop nodeexporter
pg addon start nodeexporter

开机自启

要让 node_exporter 实例在宿主开机时自动启动:

pg autostart enable --nodeexporter --name nodeexporter

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责把容器拉起来——node_exporter 无状态,无需准备任何东西。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove nodeexporter   # 删容器;没有数据要清理

node_exporter 不在磁盘上留存任何东西,故 --clean-data 除了给一条提示外没有任何效果。

日志

pg logs addon nodeexporter            # 最近的控制台输出
pg logs addon nodeexporter -f         # 跟踪
pg logs addon nodeexporter -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • 指标像是容器的而非宿主的 —— 经 pg addon install 不可能(挂载写死了),但若你手跑 node_exporter,必须传 /proc//sys// 的只读挂载和对应的 --path.* flags。用 curl -s …/metrics | grep node_uname_info 并把 node_memory_MemAvailable_bytes 与宿主 free -b 对比来确认。
  • Prometheus 显示 target DOWN —— 确认导出器在运行(pg addon list)、抓取目标的地址/端口对得上(--scrape node-exporter=<host>:<port>),且一个 --listen 127.0.0.1 的导出器没被别的机器抓。
  • 某个 collector flag 没生效 —— --extra-arg 在容器创建时应用。用 --force reinstall 重建,再查 pg logs addon nodeexporter 看 flag 是否出现在启动行上。
  • 端口已被分配 —— 已分配的端口记在 pg.yaml 里并为后续实例跳过;先 pg addon remove 释放,或用 --port N 另选一个。

已知限制

  • 无鉴权、无 TLS。 node_exporter 提供纯文本 /metrics;0.0.0.0 绑定的唯一保护是网络边界。若要离开可信网络,请前置 nginx 或用防火墙限制。
  • 无状态 —— 没有数据目录、没有保留期;除运行中的进程外什么都不留存。重装总是从头开始。
  • rootful 假设。 -v /:/rootfs:ro 挂载读整棵宿主目录树;rootless podman 下部分宿主路径可能读不到。所有目标 VM 都跑 rootful podman,与 pgcli 其余部分一致。
  • pgcli 不校验 --extra-arg 值;有错的 flag 只会在启动时从容器日志里暴露。
  • macOS 上容器跑在 podman machine 里、端口发布出来;目前只在 Linux 上验证过。

16.29 - InfluxDB

以受管容器方式运行 InfluxDB 1.x——内置认证、配置覆盖、备份恢复,以及面向本地/远程实例的 influx CLI 透传命令

pg addon install influxdb 以受管容器方式运行 InfluxDB 1.13.1 OSS——单节点时序数据库,TSM 存储、InfluxQL 查询——直接跑 pgcli 自建的 ghcr.io/mars-base/pgcli/pgcli-influxdb 镜像(官方 linux/amd64 发行版二进制 + Alpine)。与 VictoriaMetrics 一样,它是顶层插件,不前置任何 PostgreSQL 实例。

本插件仅支持 Linux/amd64(镜像为单架构)。InfluxDB 1.x 没有集群/HA、没有内置 Web UI,且上游已把 v1 置于维护模式;pgcli 固定最后一个 1.13.1 发行版。新部署若需要仍在维护的引擎,可考虑 VictoriaMetrics 或 VictoriaLogs;InfluxDB 1.x 适合需要与既有工具保持 InfluxQL 兼容的场景。

认证始终开启。 安装时生成 admin 密码,存入 pg.yaml,可随时用 pg addon password influxdb [--name NAME] 取回。HTTP API 不允许匿名访问。 备份/恢复 RPC 端口设计上仅绑回环——上游对该协议没有认证,所以无论 --listen 如何设置,pgcli 都不会把它暴露到网络上。

参数

参数 默认值 说明
--name influxdb 实例名(map 键;各子命令的默认名同名)
--version 1.13.1 固定的镜像发行版(决定使用哪个已发布的 pgcli-influxdb tag)
--data-dir <base_dir>/addon/influxdb/<name>/data 数据目录,绑定挂载到容器内 /var/lib/influxdb;绝对路径或相对 base_dir
--listen 0.0.0.0 HTTP 绑定地址。RPC 端点始终只绑 127.0.0.1
--http-port 自动(基址 8086) HTTP 宿主端口;自 influxdb_start_port 自动分配
--rpc-port 自动(基址 8088) 备份/恢复 RPC 端口,仅回环;每个实例分配唯一端口
--admin-user admin InfluxQL 管理员用户名(无 Web UI)
--admin-password 自动生成 管理员密码。--force 时默认保留已存密码,除非显式指定
--config-option — 额外 influxdb.conf 设置,格式 KEY=VALUE(顶层)或 SECTION.KEY=VALUE(表),值为原始 TOML(可重复)。替换已存覆盖列表并持久化
--force false 重写生成的配置并重建容器,保留数据与已存管理员密码

生成的配置只覆盖路径、监听和认证,其余全部沿用上游 1.13.1 默认值。渲染后的文件位于 <base_dir>/addon/influxdb/<name>/influxdb.conf,只读挂载进容器。覆盖项同时存入 pg.yaml 和该文件,因此重启与重新渲染都不会丢失。

安装

pg addon install influxdb --name metrics
✓ InfluxDB "metrics" installed
  Version:   1.13.1
  Image:     ghcr.io/mars-base/pgcli/pgcli-influxdb:1.13.1
  Container: pgcli-influxdb-metrics
  HTTP:      http://0.0.0.0:8086 (auth enabled)
  RPC:       127.0.0.1:8088 (backup/restore, loopback only)
  Admin:     admin
  Password:  <自动生成>
  Data dir:  /home/you/.pgcli/addon/influxdb/metrics/data
  Config:    /home/you/.pgcli/addon/influxdb/metrics/influxdb.conf

第二个及之后的实例会自动从各自端口池取下一个空闲 HTTP/RPC 端口(例如 8087/8089),并可用 --data-dir 指向任意目录:

pg addon install influxdb --name metrics2 --data-dir /srv/influx-data --listen 127.0.0.1

不带 --force 重复 install 是幂等空操作,只报告当前状态。--force 会重写配置并重建容器,但保留数据目录与已存管理员密码(否则既有 admin 用户将无法登录);由于 --config-option 会替换已存列表,需要保留覆盖项时必须重新传入。

凭据

pg addon password influxdb --name metrics   # 打印管理员密码
pg addon list                               # 显示所有实例及状态

密码保存在 pg.yaml(默认权限 0644——请保护该文件)。所有需要凭据的命令都通过环境变量把凭据传给容器,绝不放进 podman 命令行。

配置覆盖

# 安装时持久化覆盖项
pg addon install influxdb --name metrics --config-option 'data.index-version="tsi1"'

# 在线修改并立即生效
pg addon influxdb config set --name metrics 'data.cache-max-memory-size=1000000000' --restart
pg addon influxdb config list --name metrics
pg addon influxdb config unset --name metrics data.cache-max-memory-size

config set/unset 同时改写生成的配置文件与 pg.yaml;--restart 会重建容器使改动立即生效,不带 --restart 则下次启动生效。值按原始 TOML 处理:字符串要带引号('data.index-version="tsi1"'),数字直接写。

上游默认索引是 inmem。高基数序列请设置 data.index-version="tsi1"——该设置只能在全新数据目录上修改,务必在写入数据前决定。

pg influx CLI

pg influx 从同一镜像启动一个一次性容器运行 influx 客户端——宿主机不安装任何东西,命令结束后容器自动删除。pg 自身解析器不认识的 flag 放在 -- 之后;参数按原样、按顺序透传。

本地实例

pg influx --name metrics -- -execute "SHOW DATABASES"
pg influx --name metrics --database demo -- -execute "SELECT * FROM cpu LIMIT 5"
pg influx --name metrics                      # 交互式 shell

主机、端口和凭据会自动注入。

远程 InfluxDB 1.x

# host/port flags
pg influx --host db.example.com --username admin --ssl -- -execute "SHOW DATABASES"

# DSN(userinfo、数据库与 TLS 查询参数)
pg influx --dsn 'influxdb://admin:secret@db.example.com:8086/demo' -- -execute "SHOW MEASUREMENTS"
pg influx --dsn 'https://admin:secret@db.example.com:8086/demo?unsafe_ssl=true' -- -execute "SHOW DATABASES"

两种模式都支持从 INFLUX_USERNAME / INFLUX_PASSWORD 回退取凭据。优先用环境变量而非 --password,后者会出现在进程列表中。--ssl 启用 HTTPS;--unsafe-ssl 跳过证书校验,必须与 --ssl 同用。--dsn 已携带全部连接信息,不能与 --host/--port/--ssl/... 组合;--name 不能与任何远程 flag 组合。

远程模式下的 import 文件

pg influx --host db.example.com --database demo -- -import -path /tmp/demo.txt

-path 指定的文件会以原绝对路径绑定挂载进一次性容器,所以 pg addon influxdb export(或 influx_inspect export)产出的文件也能导入远程服务器。

# DML 段和 # CONTEXT-DATABASE: 标记是必需的,# DDL 段可选。目标库可能不存在时再带上 DDL——库已存在时 CREATE DATABASE 是空操作。导入目标由 context 标记决定,不是 --database;目标库不存在又没有 DDL 时会报 database not found。没有 # DML 头的文件会按 InfluxQL 解析并报语法错误。

/tmp/demo.txt 是运行 pg 的机器上的文件(它被挂载进容器),不是 db.example.com 上的文件。最简样例:

# DDL
CREATE DATABASE demo

# DML
# CONTEXT-DATABASE: demo
# CONTEXT-RETENTION-POLICY: autogen
cpu,host=server01,region=us-west value=0.64 1434055562000000000
cpu,host=server02,region=us-west value=0.55 1434055562000000001
mem,host=server01 used_percent=42.1,free=123456789i 1434055563000000000

# DDL 段按 InfluxQL 在数据之前执行(CREATE DATABASE 对已存在的库是空操作)。# DML 之后全部是 line protocol;时间戳可省略(默认纳秒,-precision 可改变解释方式)。可用 -pps N 限速;文件是 gzip 压缩时加 -compressed。远程服务器开启认证时,用 --username/--password 或环境变量 INFLUX_USERNAME/INFLUX_PASSWORD 传凭据。

跨主机操作

pg influx 的远程模式完全在客户端机器上运行——只需要 podman 和 pgcli-influxdb 镜像,不需要本机安装 InfluxDB,也不需要本地实例。

# 在任何能访问 db.example.com 的机器上
export INFLUX_USERNAME=admin
export INFLUX_PASSWORD=secret

pg influx --host db.example.com --database demo -- -execute "SHOW MEASUREMENTS"
pg influx --host db.example.com --database demo -- -execute "SELECT * FROM cpu LIMIT 10"
pg influx --host db.example.com -- -execute "CREATE DATABASE demo"
pg influx --host db.example.com -- -execute "CREATE RETENTION POLICY rp_30d ON demo DURATION 30d REPLICATION 1 DEFAULT"
pg influx --host db.example.com --database demo          # 交互式 shell

# HTTPS,以及使用自签名证书的 HTTPS
pg influx --host influx.example.com --ssl --database demo -- -execute "SHOW MEASUREMENTS"
pg influx --host influx.example.com --ssl --unsafe-ssl --database demo -- -execute "SHOW MEASUREMENTS"

addon 的生命周期和备份命令只操作本机的 podman 实例,无法直接跨主机:pg addon influxdb status|backup|restore|export|import、pg addon start|stop|remove 和 pg logs addon influxdb 都必须在 InfluxDB 所在主机上执行——登录到该主机后直接运行。在其他机器上请使用 pg influx --host/--dsn 或下文介绍的 HTTP API(/query、/write)。

跨主机迁移数据用 export + import:

# 源主机:离线导出(导出期间容器会停止)
pg addon influxdb export --name metrics --database demo
# 把命令输出的文件(默认位于 <instance dir>/exports/ 下)传到目标主机,再在目标主机上导入

# 登录到目标主机:导入到本地实例
pg influx --name metrics --database demo -- -import -path /tmp/demo-export.txt

# 或者在任何持有该文件的客户端:导入远程服务器(文件会在本地被挂载进容器)
pg influx --host target.example.com --database demo -- -import -path /tmp/demo-export.txt

恢复使用仅回环的 RPC 端点,因此备份目录必须在目标主机上,并在目标主机上执行恢复:

# 目标实例是全新的、没有 demo 库:保留原库名
pg addon influxdb restore /tmp/influx-backup --name metrics --database demo

# 目标实例已存在 demo 库时,influxd restore 拒绝覆盖,改用新库名恢复
pg addon influxdb restore /tmp/influx-backup --name metrics --database demo --newdb demo_copy

常用操作

数据库

# pgcli 封装命令
pg addon influxdb database create demo --name metrics
pg addon influxdb database create demo --retention 30d --shard-duration 1d --rp rp_30d --name metrics
pg addon influxdb database list --name metrics
pg addon influxdb database drop demo --name metrics --yes   # --yes 跳过确认

# 等价的 InfluxQL
pg influx --name metrics -- -execute "CREATE DATABASE demo"
pg influx --name metrics -- -execute "SHOW DATABASES"
pg influx --name metrics -- -execute "DROP DATABASE demo"

保留策略(retention policy)

每个数据库至少有一个保留策略;默认的 autogen 永不过期。保留策略决定数据保留多久、按什么粒度分片,过期分片会被自动删除。

# pgcli 封装命令
pg addon influxdb retention list   --name metrics --database demo
pg addon influxdb retention create --name metrics --database demo --rp rp_7d --duration 7d --default
pg addon influxdb retention alter  --name metrics --database demo --rp rp_7d --duration 14d --default
pg addon influxdb retention drop   --name metrics --database demo --rp rp_7d --yes

等价的 InfluxQL:

# 创建保留 30 天的策略(OSS 版 REPLICATION 必须为 1)
pg influx --name metrics -- \
  -execute "CREATE RETENTION POLICY rp_30d ON demo DURATION 30d REPLICATION 1"

# 同上,并设为默认策略:写入/查询未指定 RP 时使用它
pg influx --name metrics -- \
  -execute "CREATE RETENTION POLICY rp_30d ON demo DURATION 30d REPLICATION 1 DEFAULT"

# 建库时直接指定有界默认 RP
pg influx --name metrics -- \
  -execute "CREATE DATABASE demo WITH DURATION 30d REPLICATION 1 SHARD DURATION 1d NAME rp_30d"

# 之后修改保留时长 / 默认策略
pg influx --name metrics -- \
  -execute "ALTER RETENTION POLICY rp_30d ON demo DURATION 90d DEFAULT"

# 查看与删除
pg influx --name metrics -- -execute "SHOW RETENTION POLICIES ON demo"
pg influx --name metrics -- -execute "DROP RETENTION POLICY rp_30d ON demo"

# 显式写入/查询非默认策略
pg influx --name metrics -- -execute "INSERT INTO demo.rp_30d cpu,host=server01 value=0.64"
pg influx --name metrics -- -execute "SELECT * FROM demo.rp_30d.cpu LIMIT 10"
  • DURATION 支持 h、d、w 和 INF,最小值为 1h;SHARD DURATION 可选,默认由 DURATION 推导。
  • 只能有一个 DEFAULT 策略;把另一个设为默认会自动降级原来的默认策略。不带 RP 前缀的查询使用默认策略。
  • DROP RETENTION POLICY 会删除该策略的数据。过期分片由后台保留检查清理(默认每 30 分钟一次),因此磁盘占用会在下一次检查后下降。

交互式 shell

pg influx --name metrics --database demo

直接输入 InfluxQL(SHOW MEASUREMENTS、SELECT ...、use otherdb 等);exit 或 Ctrl-D 退出。--database 指定初始数据库上下文。

查看 schema

# 所有 measurement
pg influx --name metrics --database demo -- -execute "SHOW MEASUREMENTS"
# 用正则匹配 measurement
pg influx --name metrics --database demo -- -execute "SHOW MEASUREMENTS WITH MEASUREMENT =~ /cpu.*/"
# field key 及其类型
pg influx --name metrics --database demo -- -execute "SHOW FIELD KEYS FROM cpu"
# tag key
pg influx --name metrics --database demo -- -execute "SHOW TAG KEYS FROM cpu"
# 某个 tag 的所有取值
pg influx --name metrics --database demo -- -execute 'SHOW TAG VALUES FROM cpu WITH KEY = "host"'
# 所有 series(measurement + tag 组合)
pg influx --name metrics --database demo -- -execute "SHOW SERIES FROM cpu"

InfluxQL 引号规则:双引号用于标识符("host"、"cpu"),单引号用于字符串值('server01')。查询本身含双引号时,像 SHOW TAG VALUES 示例那样用单引号包住整个 -execute 参数。

查询数据

# 最新 10 条,时间戳可读
pg influx --name metrics --database demo -- -precision rfc3339 \
  -execute "SELECT * FROM cpu ORDER BY time DESC LIMIT 10"

# 最近一小时
pg influx --name metrics --database demo -- \
  -execute "SELECT * FROM cpu WHERE time > now() - 1h"

# 按 host 做 5 分钟平均
pg influx --name metrics --database demo -- \
  -execute "SELECT mean(value) FROM cpu WHERE time > now() - 1h GROUP BY time(5m), host"

# 行数
pg influx --name metrics --database demo -- -execute "SELECT COUNT(value) FROM cpu"

# JSON 输出,便于脚本处理
pg influx --name metrics --database demo -- -format json -pretty \
  -execute "SELECT * FROM cpu LIMIT 5"

写入与删除

# 写入一个点(line protocol;时间戳可省略,默认取服务器时间)
pg influx --name metrics --database demo -- \
  -execute "INSERT cpu,host=server01,region=us-west value=0.64"

# 删除 30 天前的数据(支持 now())
pg influx --name metrics --database demo -- \
  -execute "DELETE FROM cpu WHERE time < now() - 30d"

# 不带 RP 前缀的 DELETE 作用于所有保留策略;加前缀可限定范围
pg influx --name metrics --database demo -- \
  -execute "DELETE FROM autogen.cpu WHERE time < now() - 30d"

# 删除整个 measurement / 指定 series
pg influx --name metrics --database demo -- -execute "DROP MEASUREMENT cpu"
pg influx --name metrics --database demo -- \
  -execute "DROP SERIES FROM cpu WHERE host = 'server01'"

批量写入用 pg influx ... -- -import -path FILE,见导出与导入。

用户与权限

pg influx --name metrics -- -execute "SHOW USERS"
pg influx --name metrics -- -execute 'SHOW GRANTS FOR "admin"'
pg influx --name metrics -- -execute 'CREATE USER "readonly" WITH PASSWORD '\''change-me'\'''
pg influx --name metrics -- -execute 'GRANT READ ON demo TO "readonly"'
pg influx --name metrics -- -execute 'REVOKE ALL PRIVILEGES ON demo FROM "readonly"'
pg influx --name metrics -- -execute 'DROP USER "readonly"'

不要把管理员密码放进 argv(ps 可见);用交互式 shell、INFLUX_USERNAME/INFLUX_PASSWORD,或 --password '' 交互输入。

日志与状态

pg addon influxdb status --name metrics        # 运行状态、端点、版本、路径
pg logs addon influxdb --name metrics          # 最近 50 行容器日志
pg logs addon influxdb --name metrics -n 200   # 最近 200 行
pg logs addon influxdb --name metrics -f       # 持续跟踪

HTTP API(URL 操作)

插件在 <listen>:<http-port> 上提供标准 InfluxDB 1.x HTTP API,认证始终开启(HTTP Basic)。同样的调用既适用于本机(http://127.0.0.1:8086),也适用于远程(http://db.example.com:8086,或由 nginx 终止 TLS 后的 https://influx.example.com)。

URL=http://db.example.com:8086
AUTH='admin:secret'     # 建议用 --netrc-file:写在 -u 里的密码会出现在 ps 中

# 健康检查 —— 204,无需认证
curl -i "$URL/ping"

# 执行 InfluxQL(GET /query)
curl -s -G "$URL/query" \
  --data-urlencode 'db=demo' \
  --data-urlencode 'q=SHOW MEASUREMENTS' \
  -u "$AUTH"

# 创建数据库和保留策略
curl -s -XPOST "$URL/query" -u "$AUTH" \
  --data-urlencode 'q=CREATE DATABASE demo'
curl -s -XPOST "$URL/query" -u "$AUTH" \
  --data-urlencode 'q=CREATE RETENTION POLICY rp_30d ON demo DURATION 30d REPLICATION 1 DEFAULT'

# 写入一个点(POST /write)—— 成功返回 204
curl -s -o /dev/null -w '%{http_code}\n' -XPOST "$URL/write?db=demo&precision=s" -u "$AUTH" \
  --data-binary 'cpu,host=web01,region=us-east value=0.42 1793069000'

# 从 line protocol 文件批量写入(每行一个点)
curl -s -XPOST "$URL/write?db=demo" -u "$AUTH" --data-binary @points.txt

# 查询并美化 JSON(不带 epoch 时时间戳为 RFC3339)
curl -s -G "$URL/query" -u "$AUTH" \
  --data-urlencode 'db=demo' \
  --data-urlencode 'q=SELECT * FROM cpu ORDER BY time DESC LIMIT 10' \
  --data-urlencode 'pretty=true'

# 用 Unix 秒时间戳
curl -s -G "$URL/query" -u "$AUTH" \
  --data-urlencode 'db=demo' --data-urlencode 'epoch=s' \
  --data-urlencode 'q=SELECT * FROM cpu LIMIT 10'

# 删除数据库
curl -s -XPOST "$URL/query" -u "$AUTH" --data-urlencode 'q=DROP DATABASE demo'
参数 端点 含义
db /query、/write 目标数据库(多数语句必需)
rp /query、/write 目标保留策略(默认用数据库的默认策略)
q /query InfluxQL 语句;需要 URL 编码(--data-urlencode 会处理)
pretty=true /query 缩进 JSON 响应
epoch /query ns、u、ms、s、m、h;省略则返回 RFC3339
precision /write 写入行协议里的时间戳精度(默认 ns)

说明:

  • 未认证请求返回 401。写入成功返回 204;非法数据点返回 400 和 partial write 信息,指出被丢弃的点。
  • curl -u user:pass 会让密码出现在进程列表中。建议使用权限 0600 的 netrc 文件: machine db.example.com login admin password secret,然后 curl --netrc-file ~/.netrc-influx ...。
  • 本机日常操作优先用 pg influx(自动通过环境变量传凭据);HTTP API 适合从其他机器、脚本和应用集成。

备份与恢复

portable 备份是在线的——实例保持运行:

pg addon influxdb backup --name metrics                        # 整个实例
pg addon influxdb backup --name metrics --database demo
pg addon influxdb backup --name metrics --since 2026-10-01T00:00:00Z
pg addon influxdb backup --name metrics --start 2026-10-01T00:00:00Z --end 2026-10-02T00:00:00Z

默认输出目录为 <实例目录>/backups/<UTC 时间戳>;--out DIR 可覆盖。备份目录包含 .manifest、.meta 和按 shard 的 .tar.gz 文件。

⚠️ 备份文件要放到远程 S3。 备份默认写在 InfluxDB 主机本地,主机损坏时备份会一起丢失。请用任意 S3 客户端把每个备份目录上传,例如 pg mc 插件:

pg mc alias set store https://s3.example.com ACCESS_KEY SECRET_KEY   # 只需一次
pg mc mb store/influxdb-backups
pg mc cp /path/to/backup-demo store/influxdb-backups/ -- --recursive

恢复时 restore 通过回环 RPC 读取备份目录,因此需要先把备份下载回目标主机再执行 pg addon influxdb restore。

恢复同样是在线的(辅助容器经回环 RPC 与运行中的服务通信;实例已停止时会先启动)。单个数据库完整的"备份 → 删除 → 恢复"流程:

# 1. 替换前先备份
pg addon influxdb backup --name metrics --database demo --out /path/to/backup-demo

# 2. 删除老库
pg addon influxdb database drop demo --name metrics --yes

# 3. 以原库名恢复
pg addon influxdb restore /path/to/backup-demo --name metrics --database demo

influxd restore 拒绝覆盖已存在的数据库。如果想在保留老库的同时另存一份,用新库名恢复——--newdb 必须与 --database 同用:

pg addon influxdb restore /path/to/backup-demo --name metrics --database demo --newdb demo_copy

恢复完成后实例保持运行。

⚠️ 恢复不是完整的身份/元数据副本。 用户、权限和连续查询不会被恢复,需要手工重建;保留策略及其 duration 可以正确恢复。

导出与导入

export 是离线的:pgcli 先停止容器,让 influx_inspect 读取一致的 TSM 文件,结束后自动重启(--no-start 可保持停止)。

pg addon influxdb export --name metrics --database demo
pg addon influxdb export --name metrics --out /tmp/all.txt --compress --no-start
pg addon influxdb export --name metrics --start 2026-10-01T00:00:00Z --end 2026-10-02T00:00:00Z

默认文件为 <实例目录>/exports/<database|all>-<UTC 时间戳>.txt(--compress 时加 .gz)。

⚠️ 导入后必须手工重建:用户、权限、连续查询,以及非默认保留策略的真实 DURATION/SHARD DURATION。 导出 DDL 会把 RP 写成 DURATION 0s 且不带 shard duration,不修正的话 30d 保留期会变成永久。

导出文件是逻辑数据副本,不是完整实例快照:它包含全部数据点(TSM + WAL),按库/保留策略写入 context 标记,并附带 CREATE DATABASE / CREATE RETENTION POLICY DDL。不加 --database 时会导出所有库(含 _internal)。灾备请用 backup/restore,export/import 适合跨实例搬运数据点。

import 需要实例运行中,并接受 export 格式文件:

pg addon influxdb database create demo_copy --name metrics
pg addon influxdb import /tmp/demo.txt --name metrics --database demo_copy
pg addon influxdb import /tmp/demo.txt --name metrics --precision ms

带 --database 时,pgcli 会把文件复制到实例目录下并改写 # CONTEXT-DATABASE: 行到目标数据库,原导出文件不受影响。导入远程服务器用 pg influx --host ... -- -import -path FILE(见上文)。

与 restore 不同,import 没有"必须换新库名"的限制:它把 line protocol 数据点写入目标库并与已有数据合并(相同 series + 时间戳的点会覆盖旧值)。上面的 demo_copy 只是导入到另一个库的示例——导入回原来的 demo 完全可以。目标库必须已存在(文件里的 # DDL 可以建库),除非 --database 指定的库是你事先建好的。

生命周期

pg addon start influxdb --name metrics     # 启动容器
pg addon stop influxdb --name metrics
pg addon influxdb status --name metrics    # 状态、ping、端点、版本
pg logs addon influxdb --name metrics -f   # 容器日志
pg addon remove influxdb --name metrics    # 删除容器与 pg.yaml 条目,保留数据
pg addon remove influxdb --name metrics --clean-data   # 连同配置/备份与数据目录一起删除

不带 --clean-data 时,数据与生成的配置在 remove 后仍保留,同名重装即可复活已存序列。--clean-data 也会删除自定义的 --data-dir。

开机自启:

pg autostart enable --influxdb --name metrics
pg autostart status
pg autostart disable --influxdb --name metrics

开机时 pgcli 只启动既有容器,不会重新执行认证 bootstrap。

生产注意事项

  • 没有 HTTPS 监听。 InfluxDB 1.x 只提供明文 HTTP。需要 TLS 时用 nginx 插件 前置,或 --listen 127.0.0.1 保持本地访问。
  • RPC 仅回环。 备份/恢复 RPC 协议没有认证;pgcli 把它固定在 127.0.0.1,每个实例分配唯一端口。不要对外暴露。
  • 没有 HA。 InfluxDB 1.x OSS 是单节点。备份(backup / export)是唯一的灾备手段;pgcli 不做定时备份,需要时把 pg addon influxdb backup 接入你自己的定时任务。
  • v1 处于维护模式。 版本固定 1.13.1;升级意味着发布新镜像 tag 并重新测试,且不存在从 v1 到 v2/v3 的原地升级路径。
  • 高基数: 在写入数据前切换到 data.index-version="tsi1"(见上文)。
  • 平台: 仅 Linux/amd64——镜像打包的是官方 amd64 压缩包。

完整 influxdb.conf 参数参考

pgcli 生成的配置是最小化的:只写入下表中的托管项,其余全部沿用 InfluxDB 1.13.1 的默认值。下表默认值来自 1.13.1 的 influxd config 实际输出。

pgcli 托管键 渲染值 说明
reporting-disabled true 关闭上游使用统计上报
bind-address(顶层) "127.0.0.1:<rpc-port>" 备份/恢复 RPC;上游对此协议无认证,因此永不对外监听
[meta] dir /var/lib/influxdb/meta 位于 data-dir 挂载内
[data] dir /var/lib/influxdb/data 位于 data-dir 挂载内
[data] wal-dir /var/lib/influxdb/wal 位于 data-dir 挂载内
[http] bind-address <listen>:<http-port> 容器使用 host 网络,这就是宿主机实际监听地址
[http] auth-enabled true 认证始终开启

这七个键会被 --config-option / config set 拒绝——请通过 pg addon install / config 的对应参数修改。其余键均可覆盖。

覆盖方式

# 表内键用 SECTION.KEY=VALUE,顶层键用 KEY=VALUE
pg addon influxdb config set --name metrics 'data.index-version="tsi1"' --restart
pg addon influxdb config set --name metrics 'http.max-row-limit=50000' --restart
pg addon influxdb config unset --name metrics data.index-version

值按原始 TOML 写入——字符串、时长、大小要加引号('cache-max-memory-size="2g"'),数字和布尔值不加引号。限制:

  • 只支持单表 [section]。重复表 [[graphite]]、[[collectd]]、[[opentsdb]]、[[udp]] 无法表达;嵌套键(如 https-client-ca.paths、http.headers)同样无法表达。
  • 这类设置需要直接编辑 <base_dir>/addon/influxdb/<name>/influxdb.conf 并重启容器(pg addon stop|start influxdb)。普通启停不会改动文件;install --force 以及任何 config set/unset 都会重新渲染配置,手工修改会丢失。
  • --config-option 会整体替换已保存的覆盖列表;改其中一项时要重新传全部条目。

顶层

键 默认值 说明
reporting-disabled false pgcli 渲染为 true(托管)
bind-address "127.0.0.1:8088" 备份/恢复 RPC 监听地址(托管)
hardening-enabled false 限制 Flux HTTP 请求只能访问公网地址(SSRF 加固)

[meta]

键 默认值 说明
dir ~/.influxdb/meta(容器内 /var/lib/influxdb/meta) 元数据/Raft 数据库目录(托管)
retention-autocreate true CREATE DATABASE 时自动创建 autogen 保留策略
logging-enabled true 输出 meta 服务日志

[data]

键 默认值 说明
dir ~/.influxdb/data TSM 存储目录(托管)
wal-dir ~/.influxdb/wal WAL 目录(托管)
wal-fsync-delay "0s" fsync 前的批量窗口;0s 每次写入都 fsync,0–100ms 对非 SSD 磁盘有帮助
index-version "inmem" 分片索引:inmem(默认)或 tsi1(高基数;只能在全新数据目录上设置)
validate-keys false 拒绝非法 UTF-8/unicode 的 key(少量写入开销)
skip-field-size-validation false 跳过写入时的字段大小上限检查
strict-error-handling false 返回错误而不是静默丢弃非法点(如 SELECT INTO 中的 ±Inf)
query-log-enabled true 执行前记录查询日志(可能包含敏感数据)
cache-max-memory-size "1g" 单分片 cache 上限,超出后拒绝写入
cache-snapshot-memory-size "25m" 达到该大小即把 cache 快照为 TSM
cache-snapshot-write-cold-duration "10m" 多久无写入/删除后快照 cache
compact-full-write-cold-duration "4h" 多久无写入/删除后做 full compaction
compact-throughput "48m" compaction 写入限速(字节/秒)
compact-throughput-burst "48m" 允许的短时突发上限
aggressive-points-per-block 10000 激进 compaction 的每块点数
ingress-metric-by-measurement-enabled false 按 measurement 统计写入指标
ingress-metric-by-login-enabled false 按登录用户统计写入指标
max-series-per-database 1000000 每库 series 上限,超出后丢弃写入(0 = 不限)
max-values-per-tag 100000 每 tag 的 value 上限(0 = 不限)
max-concurrent-compactions 0 并发 full/level compaction 数;0 = GOMAXPROCS 的 50%
max-concurrent-optimized-compactions 0 并发 optimized compaction 数(0 = 默认)
max-index-log-file-size "1m" TSI 索引 WAL 压缩进索引文件的阈值
max-concurrent-deletes 1 每分片同时执行的 DELETE 数
series-id-set-cache-size 100 TSI series-id-set 缓存条目数(0 关闭)
series-id-set-cache-max-size 0 自适应扩容上限(0 = 关闭自适应)
series-id-set-cache-target-hit-rate 0.0 自适应缩容的目标命中率(0.0 = 关闭,1.0 非法)
series-id-set-cache-shrink-conservatism 2.5 缩容策略的标准差余量
series-file-max-concurrent-snapshot-compactions 0 并发 series-file 快照压缩数(0 = 默认)
trace-logging-enabled false TSM 引擎详细 trace 日志
tsm-use-madv-willneed false 对 TSM 文件使用内核 MADV_WILLNEED 建议
tar-stream-buffer-size "1m" 备份/改名/复制时 tar 流缓冲区大小

[coordinator]

键 默认值 说明
write-timeout "10s" 写入等待超时
max-concurrent-queries 0 并发查询上限(0 = 不限)
query-timeout "0s" 查询超时后被终止(0s = 不限)
log-queries-after "0s" 慢查询日志阈值(0s = 关闭)
log-timedout-queries false 记录被 query-timeout 杀掉的查询
max-select-point 0 单 SELECT 最大处理点数(0 = 不限)
max-select-series 0 单 SELECT 最大 series 数(0 = 不限)
max-select-buckets 0 GROUP BY time() 的最大桶数(0 = 不限)
termination-query-log false 收到 SIGTERM 时打印正在执行的查询

[flux-controller]

键 默认值 说明
query-concurrency 0 并发 Flux 查询数(0 = 默认)
query-initial-memory-bytes "0" 每个 Flux 查询的初始内存预算
query-max-memory-bytes "0" 每个 Flux 查询的内存上限
total-max-memory-bytes "0" 所有 Flux 查询的总内存上限
query-queue-size 0 Flux 查询队列长度

[retention]

键 默认值 说明
enabled true 启用保留策略执行
check-interval "30m" 保留策略检查间隔

[shard-precreation]

键 默认值 说明
enabled true 数据到达前预创建分片
check-interval "10m" 预创建检查间隔
advance-period "30m" 在分片组结束前多久创建下一组

[monitor]

键 默认值 说明
store-enabled true 记录内部统计
store-database "_internal" 目标数据库
store-interval "10s" 记录间隔

[http]

键 默认值 说明
enabled true 启用 HTTP API
bind-address ":8086" HTTP 监听地址(托管:<listen>:<http-port>)
auth-enabled false 要求认证(托管:true)
realm "InfluxDB" Basic-auth 质询 realm
log-enabled true HTTP 请求日志
suppress-write-log false 从 HTTP 日志中隐藏写请求
access-log-path "" 访问日志文件;空则写 stderr
access-log-status-filters [] 状态码过滤(NNN、NNX、NXX);空则全部记录
write-tracing false 详细写日志
flux-enabled false 启用 Flux 查询端点
flux-log-enabled false 记录 Flux 查询
flux-testing false 暴露 Flux 测试端点
pprof-enabled true 启用 /debug/pprof
pprof-auth-enabled false pprof 要求管理员认证
debug-pprof-enabled false 启动时额外在 localhost:6060 绑定 pprof
ping-auth-enabled false /ping、/metrics、/status 要求认证
prom-read-auth-enabled false Prometheus remote-read API 要求认证
https-enabled false 提供 HTTPS
https-certificate "/etc/ssl/influxdb.pem" TLS 证书(PEM)
https-private-key "" TLS 私钥;空表示与证书合并存放
https-insecure-certificate false 允许不安全的证书文件权限
https-ignore-sanity-checks false 加载未通过服务端认证检查的证书
https-client-auth-type 未设置 双向 TLS 客户端认证模式(NoClientCert、RequestClientCert、RequireAnyClientCert、VerifyClientCertIfGiven、RequireAndVerifyClientCert)
https-client-ca 未设置 客户端 CA:{ paths = [...], include-system = false }
shared-secret "" JWT 校验密钥
max-row-limit 0 分块结果行数上限(0 = 不限)
max-connection-limit 0 并发 HTTP 连接上限(0 = 不限)
unix-socket-enabled false 通过 Unix socket 提供 HTTP
unix-socket-permissions "0777" Unix socket 权限
bind-socket "/var/run/influxdb.sock" Unix socket 路径
max-body-size 25000000 请求体最大字节数(0 = 不限)
max-concurrent-write-limit 0 并发写入上限(0 = 不限)
max-enqueued-write-limit 0 排队写入上限(0 = 不限)
enqueued-write-timeout "30s" 写入最长排队等待时间
user-query-bytes-enabled false 按用户统计查询响应字节数
user-write-bytes-enabled false 按用户统计写入请求字节数
[http.headers] 未设置 任意响应头,如 X-Header-1 = "value"

[logging]

键 默认值 说明
format "auto" 日志编码:auto、logfmt、json
level "info" 最低级别:error、warn、info、debug
suppress-logo false 不打印启动 logo

[subscriber]

键 默认值 说明
enabled true 启用订阅(把每次写入复制到指定端点)
http-timeout "30s" 订阅写入超时
insecure-skip-verify false 跳过订阅端点的 TLS 校验
ca-certs "" PEM CA 包路径;空则使用系统根证书
root-ca 未设置 额外的订阅 CA:{ paths = [...], include-system = false }
certificate "" 向订阅端点出示的客户端证书(双向 TLS)
private-key "" certificate 对应的私钥
insecure-certificate false 允许不安全的证书/私钥文件权限
ignore-cert-sanity-checks false 加载未通过服务端认证检查的证书
write-concurrency 40 写协程数
write-buffer-size 1000 channel 中缓冲的写入数
total-buffer-bytes "0" 总缓冲预算(0 = 不限)

重复监听表

这些是数组表([[graphite]]、[[collectd]]、[[opentsdb]]、[[udp]]),无法用 --config-option 设置;需要时直接编辑生成的文件。每张表默认关闭。

段 键 默认值 说明
[[graphite]] enabled false 启用 Graphite 监听
bind-address ":2003" 监听地址
database "graphite" 目标数据库
retention-policy "" 目标保留策略
protocol "tcp" tcp 或 udp
consistency-level "one" 写入一致性级别
batch-size 5000 刷盘前缓冲点数
batch-pending 10 挂起批次数
batch-timeout "1s" 最长刷盘等待
udp-read-buffer 0 UDP 读缓冲(0 = 系统默认)
separator "." 模板 measurement 各部分的连接符
tags [] 默认 tag,如 ["region=us-east"]
templates [] 模板规则(默认一条)
[[collectd]] enabled false 启用 collectd 监听
bind-address ":25826" 监听地址
database "collectd" 目标数据库
retention-policy "" 目标保留策略
typesdb "/usr/share/collectd/types.db" types DB 文件或目录
security-level "none" none、sign、encrypt
auth-file "/etc/collectd/auth_file" 签名/加密流量的认证文件
batch-size 5000 刷盘前缓冲点数
batch-pending 10 挂起批次数
batch-timeout "10s" 最长刷盘等待
read-buffer 0 UDP 读缓冲(0 = 系统默认)
parse-multivalue-plugin "split" 多值插件按 split 或 join 解析
[[opentsdb]] enabled false 启用 OpenTSDB 监听
bind-address ":4242" 监听地址
database "opentsdb" 目标数据库
retention-policy "" 目标保留策略
consistency-level "one" 写入一致性级别
tls-enabled false 提供 TLS
certificate "/etc/ssl/influxdb.pem" TLS 证书
private-key "" TLS 私钥;空表示与证书合并存放
insecure-certificate false 允许不安全的证书文件权限
ignore-cert-sanity-checks false 加载未通过检查的证书
client-auth-type 未设置 双向 TLS 客户端认证模式
client-ca 未设置 客户端 CA:{ paths = [...], include-system = false }
log-point-errors true 记录每个格式错误的点
batch-size 1000 刷盘前缓冲点数(仅 telnet)
batch-pending 5 挂起批次数
batch-timeout "1s" 最长刷盘等待
[[udp]] enabled false 启用 line protocol UDP 监听
bind-address ":8089" 监听地址
database "udp" 目标数据库
retention-policy "" 目标保留策略
precision "n" 输入精度:""、n、u、ms、s、m、h
batch-size 5000 刷盘前缓冲点数
batch-pending 10 挂起批次数
batch-timeout "1s" 最长刷盘等待
read-buffer 0 UDP 读缓冲(0 = 系统默认)
writers 1 并行写协程数

[continuous_queries]

键 默认值 说明
enabled true 执行连续查询
log-enabled true 记录 CQ 执行日志
query-stats-enabled false 把 CQ 统计写入监控库
run-interval "1s" CQ 检查间隔

[tls]

键 默认值 说明
ciphers Go 默认 允许的加密套件(Go 名称)
min-version Go 默认 最低 TLS 版本,如 "tls1.2"
max-version Go 默认 最高 TLS 版本,如 "tls1.3"

插件里的 HTTPS: pgcli 从不开启 [http] https-enabled。需要 TLS 时使用 nginx 插件 做终止,或只监听回环地址。[tls] 段只调整已经启用 TLS 的监听器的加密套件/版本协商。

16.30 - Pg_exporter

以 pgcli 插件方式运行 pg_exporter——Pigsty 的声明式 PostgreSQL 指标导出器,可监控 pgcli 纳管实例或任意远程 PostgreSQL

pg addon install pgexporter 以受管容器方式运行 pg_exporter——Pigsty 的 PostgreSQL 指标导出器——直接跑官方 docker.io/pgsty/pg_exporter 镜像。node_exporter 插件描述的是机器,而 pg_exporter 描述的是一个数据库:它连上某个 PostgreSQL 服务,在 /metrics 上发布约 460 个 pg_* 指标(活动、锁、复制、膨胀、WAL、按库统计……)。与 Prometheus、VictoriaMetrics、Grafana、node_exporter 一样,它是顶层插件:本身不依附任何实例——而是指向它监控的目标。

它与 node_exporter 并存的理由是统一采集:目标既可以是 pgcli 纳管的实例,也可以是任意可达、pgcli 并不托管的 PostgreSQL 服务。这正是它的全部意义——一种导出器形状,同时采集 pgcli 内外的数据库指标。

三种目标模式,互斥——只传其一。 --pg-name <inst> 监控一个 pgcli 纳管的实例;其 admin DSN 每次启动都从 pg.yaml 现推(故轮换过的密码或改动的端口永不会失效)。对 pgcli 不托管的服务器,目标只用一个 URL 给出——而你传哪个 URL 决定 pgcli 是否碰这个库:

  • --admin-dsn postgres://…——一个 admin 权限 URL。pgcli 用它一次性在目标上建一个低权限监控角色,随后丢弃它:只保留其不含凭据的地址(host/port/database),导出器的 host/port/database 就取自它。此后导出器只以监控角色连库。
  • --dsn postgres://…——原样拨号。导出器就用这个 URL 自带的凭据连接,pgcli 在目标上绝不执行任何 SQL。适合角色已存在、或根本不想让 pgcli 碰库的场景。

--dsn 与 --admin-dsn 从不同时出现——不需要第二个 URL,因为目标的 host/port/database 都来自你传的那一个。

本地与 --admin-dsn 两种模式下,导出器从不用 admin 身份连库:pgcli 创建一个低权限监控角色(pg_monitor 成员、CONNECTION LIMIT 5),并把该角色的 URL 交给容器。要建这个角色,pgcli 需要一次 admin 权限:--pg-name 时 admin 凭据自动派生;--admin-dsn 时你给出一个 admin URL,它只用于那一批 CREATE ROLE/GRANT SQL、绝不落进 pg.yaml。生成的监控密码只打印一次,之后用 pg addon password pgexporter 取回。在原样拨号的 --dsn 模式下 pgcli 什么都不建——角色已存在是你(或某次先前的 --admin-dsn 安装)的事。

导出器端点没有鉴权、没有 TLS,且 pg_exporter 在同一端口上还服务管理端点(/reload、/explain、/stat)——任何能连到端口的人既能抓指标、也能触发重载。因为抓取它的 Prometheus 通常在另一台主机,Listen 默认 0.0.0.0,唯一保护是网络边界。请把它限制在监控网段内,或对同机 Prometheus 传 --listen 127.0.0.1。

平台:Linux 是已实测平台(宿主网络,故导出器直接绑宿主端口、并能拨本地实例的 127.0.0.1 目标)。在 macOS 上容器跑在 pgcli-net bridge 里,够不到本地实例的 127.0.0.1 DSN——那里用远程模式(--dsn 或 --admin-dsn)指向一个可达地址(远程路径可用、本地路径不可用)。

参数

参数 默认值 说明
--name pgexporter 实例名(map 键)
--pg-name — 按名监控一个 pgcli 纳管实例(admin DSN 每次启动现推;与 --dsn、--admin-dsn 互斥)
--admin-dsn — 远程建角色模式:一个 admin 权限 URL。pgcli 用它一次性建监控角色后即丢弃——只保留其不含凭据的地址(host/port/database),绝不含 admin 密码;此后导出器以监控角色连接。与 --dsn、--pg-name 互斥
--dsn — 远程原样拨号模式:导出器就用这个 URL 自带的凭据连接,pgcli 绝不在目标上跑 SQL——适合角色已存在、或不想让 pgcli 碰库。与 --admin-dsn、--pg-name 互斥
--monitor-user pgexporter pgcli 在目标上创建的低权限角色名(pg_monitor 成员;仅限普通标识符)。多个 exporter 监控同一目标时,每个实例传不同的值
--image docker.io/pgsty/pg_exporter:1.5.0 覆盖镜像
--port 自动(基址 9630) 宿主端口;自 pgexporter_start_port 自动分配
--listen 0.0.0.0 绑定地址——默认开放全部网卡(跨主机 Prometheus);127.0.0.1 收紧为仅回环
--config-file — 自定义 pg_exporter 采集器 YAML 的路径,只读挂到 /etc/pg_exporter.yml(不设则用二进制内建的默认采集器集)
--extra-arg — 传给二进制的额外 CLI flag(可重复);最后追加,故 last-wins 覆盖早期 flags。如 --extra-arg --auto-discovery=false
--force false 即使正在运行也重建——用于应用改动的端口/监听/目标/额外参数

pg_exporter 无状态——它不落盘,故没有数据目录。pg addon remove 只删容器(目标上建的监控角色会保留,因为它可能被共享或手工管理);重装总是从头开始。

安装

监控一个 pgcli 纳管实例(监控角色自动创建):

pg addon install pgexporter --pg-name demo

命令派生本地 admin DSN、在 demo 上建监控角色、按需拉取镜像、用监控 URL 起容器,并打印抓取信息:

✓ pg_exporter installed: "pgexporter"
  Container:  pgcli-pgexporter-pgexporter
  Image:      docker.io/pgsty/pg_exporter:1.5.0
  Metrics:    http://0.0.0.0:9630/metrics
  Target:     instance demo
  Monitor as: pgexporter (pg_monitor)

  Monitor password: pgx_3f9a…            # 只打印一次
  (printed once — retrieve it later with `pg addon password pgexporter --name pgexporter`)

监控一个 pgcli 不托管的库、并让 pgcli 建监控角色——只给一个 admin URL;目标的 host/port/database 就取自它:

pg addon install pgexporter --name ext \
  --admin-dsn "postgres://admin:adminpw@db.example.com:5432/app"

--admin-dsn 跑一次性的 CREATE ROLE/GRANT pg_monitor 后即被丢弃——pg.yaml 里只留下不含凭据的地址 postgres://db.example.com:5432/app。此后导出器只以 pgexporter 连,pg addon password pgexporter --name ext 取回该角色密码。

若监控角色已存在、你宁可 pgcli 完全不碰目标,就改传一个带可用凭据的 --dsn(与 --admin-dsn 互斥——只给一个 URL,不是两个):

pg addon install pgexporter --name ext \
  --dsn "postgres://pgexporter:existingpw@db.example.com:5432/app"

这里 pgcli 不跑任何 SQL:导出器就用这个 URL 及其自带凭据连接。

(要手工建角色,最小集是 CREATE ROLE pgexporter LOGIN CONNECTION LIMIT 5 PASSWORD '…'; GRANT pg_monitor TO pgexporter;——在 PostgreSQL 10–13 上再加 GRANT SELECT (oid) ON pg_subscription。)

验证导出器给的是目标的真实数字:

curl -s http://127.0.0.1:9630/metrics | grep -E '^pg_(up|version_info)'
curl -s http://127.0.0.1:9630/up        # 200 == 目标可达

pg_up 1 表示监控角色连上了。把 pg_version_info 与 pg exec demo -c "SHOW server_version" 对照校验——并确认角色已落到目标上:pg exec demo -c "\du pgexporter"(或查 pg_roles / pg_auth_members 里的 pg_monitor 成员关系)。默认带 --auto-discovery 时,指标会按目标上的每个库带一个 datname 标签。

重复安装与共享的监控用户

建监控角色的 SQL 是幂等的,所以重复 install 绝不会损坏目标上的角色:对一个在跑、配置未变的实例做普通重装是空操作(根本不执行 SQL);--force 重装则对已存在的角色做 ALTER——其 OID 与授权都保留,pg_monitor 成员关系也绝不重复。

--force / 配置变化的重装会做一件事:轮换生成的密码(该实例自身没问题——新密码与注入是同步的——但它会覆盖你手工设过或复用过的密码;用 pg addon password pgexporter 取回当前的)。正因为每次这样的安装都会给角色写入一个新密码,两个 exporter 实例共享同一目标、又都用默认的 pgexporter 用户就会冲突:装第二个时轮换了这个共享角色的密码,第一个手里那份就此过期——它已建立的连接池还能用,但下一次重连会认证失败、掉到 pg_up 0。要对同一目标跑多个 exporter 时,给每个实例配一个自己的角色:

pg addon install pgexporter --name pe2 --pg-name demo --monitor-user pgexporter2

接入 Prometheus

导出器只有被抓取才有意义。pgcli 的 Prometheus 插件靠一行 --scrape job 就能抓它:

pg addon install prometheus --name p1 --scrape pg=127.0.0.1:9630
# 远程 Prometheus → 用宿主的局域网地址而非 127.0.0.1:
pg addon install prometheus --name p1 --scrape pg=<host-ip>:9630

要把这个导出器加进一个已经在跑、已经在抓别的 job 的 pgcli Prometheus,用 --scrape-append——它把 pg job 折叠进已存的抓取列表(同名 job 合并、新名字追加)并 SIGHUP 热重载,绝不会清掉现有 target:

pg addon install prometheus --name p1 --scrape-append pg=127.0.0.1:9630

(普通的 --scrape 会整体替换 job 集,所以用重跑 install 会丢掉 p1 原本在抓的其他 job。)之后在 Prometheus 里,up{job="pg"} 为 1、count(pg_activity_count) 为正数。--scrape / --scrape-append 的完整机制见 Prometheus 插件。

复用健康探针

pg_exporter 的 /primary 与 /replica 只在目标处于该角色时返回 200,故它们可兼作 HAProxy 式存活探针用于读写路由——把 pgcli 的 HAProxy 接上某个 PostgreSQL 做指向探见的接线见 HAProxy 插件。/up(目标是否可达)与 /read 是另外两个探针。

对运行中的实例重复 install:不带 --force 时是空操作(你传的字段会存下,但容器照旧服务)。--force 则重建容器——--pg-name 模式总会重跑幂等建角色(轮换密码),--admin-dsn 模式只有再传一个新鲜的 --admin-dsn 才重跑(否则沿用已存的监控密码,因为 pgcli 刻意没保留 admin 权限),--dsn 模式从不跑 SQL。改端口 / 监听 / 额外参数需要 --force(重建容器)——只有采集器配置可热重载(见下)。

自定义采集器(--config-file)与重载

pg_exporter 把一整套默认采集器内建在二进制里,并在运行时按服务版本/角色/扩展来规划,所以要拿到有用的指标不需要任何配置文件——大多数安装都不带 --config-file。当你确实要自定义采集器集时,把 --config-file 指向一份 pg_exporter 采集器 YAML;它被只读挂到 /etc/pg_exporter.yml 并经 PG_EXPORTER_CONFIG 选中。在宿主上编辑该文件后,用 SIGHUP 就地热重载(不重启容器):

pg addon install pgexporter --pg-name demo --config-file /srv/collectors.yml
# …编辑 /srv/collectors.yml,然后:
pg addon pgexporter reload --name pgexporter

对没带 --config-file 安装的实例做 reload 是无害的空操作——pgcli 会明说(内建默认集在磁盘上没有可重读的东西)。URL / 端口 / 监听改动不被 SIGHUP 覆盖;用 install --force 重建。

自定义参数(--extra-arg)

pg_exporter 在 env 配置的 URL 之外还有丰富的 flag 面。经 --extra-arg(可重复)开关特性:

pg addon install pgexporter --pg-name demo \
  --extra-arg --auto-discovery=false \
  --extra-arg --tag=env=prod

额外参数最后追加到命令行,故经 last-wins 覆盖 pgcli 注入的 flags。常见的有:--auto-discovery=false(不再按库加 datname)、--exclude-database(跳过某些库)、--tag(加常量标签)、--namespace(给每个指标加前缀)。--force 之后,用 curl -s http://127.0.0.1:9630/metrics 确认一个 --auto-discovery=false 的目标只报 postgres 库的标签。

监听与暴露

Listen 默认 0.0.0.0:在 Linux 上容器共享宿主网络命名空间,所以 /metrics 在宿主每个网卡的分配端口上可达。这是刻意的——抓取它的 Prometheus 通常在另一台机器。但没有鉴权,且 pg_exporter 在同一端口上还服务 /reload、/explain、/stat,故任何能连到端口的人不仅能抓指标、还能读配置和触发重载。对同机 Prometheus 传 --listen 127.0.0.1(用 --force 重建);否则请在防火墙层面限制端口。(在 macOS bridge 构建下,pgcli 会自动把绑定放宽到 0.0.0.0,好让发布的端口经 podman VM 保持可达。)

端口

每个 pg_exporter 实例占有一个端口——指标端点——取自以 pgexporter_start_port(默认 9630,pg_exporter 惯例端口)为基址的端口池,每多一个实例递增,并跳过任何被受管容器或 used_ports 占用的端口。分配结果写入 pg.yaml,故重装绝不会把别的插件在用的端口再发一遍。9630 与 node_exporter(9100)、Prometheus(9090)、VictoriaMetrics(8428)、Grafana(3000)、Alertmanager(9093)都不重号;显式 --port 仍优先。

pg addon install pgexporter --name pe2 --monitor-user pgexporter2 --pg-name demo   # 第二个实例:下一个空闲端口(9631)
pg addon install pgexporter --name pe3 --port 9700 --monitor-user pgexporter3 --pg-name demo  # 显式端口仍优先

(同一目标上每多一个实例都需要自己的 --monitor-user——见「重复安装与共享的监控用户」。)

配置

addons:
  pgexporter:
    pgexporter:
      name: pgexporter
      image_tag: docker.io/pgsty/pg_exporter:1.5.0
      host_port: 9630
      listen: 0.0.0.0
      target_instance: demo              # 本地模式
      monitor_user: pgexporter
      monitor_dsn: postgres://pgexporter:…@127.0.0.1:5432/demo?sslmode=disable   # 容器实际拨的
      monitor_password: "…"              # 一个 secret——生成的监控密码
      autostart: false
    ext:
      name: ext
      host_port: 9631
      listen: 0.0.0.0
      admin_dsn: postgres://db.example.com:5432/app   # --admin-dsn 模式:只存不含凭据的地址
      monitor_user: pgexporter
      monitor_dsn: postgres://pgexporter:…@db.example.com:5432/app
      monitor_password: "…"

三个目标字段互斥——只设其一:target_instance(pgcli 纳管实例,DSN 每次启动现推)、admin_dsn(pgcli 在其上建了监控角色的远程服务器——pgcli 只保留其不含凭据的地址,你传的 admin URL 绝不持久化)、或 dsn(原样拨号的远程服务器,连同自带凭据原样存,且从不跑建角色 SQL)。monitor_dsn / monitor_password 是 pgcli 生成并注入容器的(dsn 模式下没有,此时导出器直接拨 dsn 本身)。extra_args 是最后追加的 flag 列表;config_file 是可选的、只读挂载的宿主路径。

列出

pg addon list
Web add-ons (pgexporter):
  pgexporter (name: pgexporter)
    Status:      running
    Metrics:     http://0.0.0.0:9630/metrics
    Target:      instance demo
    Monitor as:  pgexporter (pg_monitor)
    Extra args:  --auto-discovery=false
    Image:       docker.io/pgsty/pg_exporter:1.5.0
    Container:   pgcli-pgexporter-pgexporter

远程模式下 Target 行会被遮盖(DSN 里的密码绝不出现)。监控密码不显示——用 pg addon password pgexporter 取回。

密码

pg addon password pgexporter          # 打印已存的监控角色密码

这是导出器连接所用的 pgexporter 角色密码——不是某个 PostgreSQL admin 密码。

启停

容器以 podman 的 unless-stopped 重启策略运行,宿主重启后随 podman 服务自动回来。start/stop 显式管理它们:

pg addon stop pgexporter
pg addon start pgexporter

start 会先重新派生监控 DSN(对 --pg-name 目标,故轮换过的实例密码或改动的端口在重建时生效)。

开机自启

pg autostart enable --pgexporter --name pgexporter

这会在 pg.yaml 里给实例打标记,并安装/刷新一个开机服务(Linux 上是 systemd --user unit,macOS 上是 LaunchAgent)来执行 pg start --autostart。开机启动只负责用已存的监控凭据把容器拉起来——它不重跑建角色的 SQL(那只在 install 时做)。完整机制、状态与关闭方式见 开机自启。

移除

pg addon remove pgexporter   # 删容器;没有数据要清理

pg_exporter 不在磁盘上留存任何东西,故 --clean-data 除了给一条提示外没有任何效果。目标上建的监控角色不会被删掉——它可能被共享或手工管理。要自行移除,需先撤销它的按库授权(只要它在某个库拿到过 pg_subscription 的列授权,裸 DROP ROLE 就会失败):在每个被监控的库里执行 REVOKE SELECT (oid) ON pg_catalog.pg_subscription FROM "pgexporter";,之后再 DROP ROLE "pgexporter";。

日志

pg logs addon pgexporter            # 最近的控制台输出
pg logs addon pgexporter -f         # 跟踪
pg logs addon pgexporter -n 200     # 最近 200 行(也支持 -i / --tail)

排障

  • pg_up 0 / 没有 pg_* 指标 —— 监控角色连不上。确认 /up 返回 200、目标的 pg_hba.conf 放行容器的来源地址、已存密码与角色一致(pg addon password pgexporter;若已偏离,重装来轮换它)。对 --dsn 目标,确认该 DSN 的 host/port 从导出器所在主机可达(一个 127.0.0.1 的 DSN 意味着目标在导出器本机)。
  • 刚装完同目标的第二个 exporter,第一个就掉成 pg_up 0 —— 两者共用默认的 pgexporter 角色,第二个安装轮换掉了它的密码、让第一个手里那份过期。给第一个做 install … --force 重建即可重新同步,或给每个实例配各自的 --monitor-user(见「重复安装与共享的监控用户」)。
  • 指标有但很稀疏/偏空 —— 漏了 GRANT pg_monitor 会得到一个已连上但采集器不全的导出器。重装(建角色是幂等的)并确认成员关系:pg exec <inst> -c "SELECT r.rolname, m.rolname FROM pg_auth_members a JOIN pg_roles r ON r.oid=a.member JOIN pg_roles m ON m.oid=a.roleid WHERE r.rolname='pgexporter'"。
  • Prometheus 显示 target DOWN —— 导出器在跑(pg addon list),但抓取地址/端口对不上(--scrape pg=<host>:<port>),或一个 --listen 127.0.0.1 的导出器被别的机器抓。
  • --dsn and --admin-dsn are mutually exclusive(--dsn 与 --admin-dsn 互斥) —— 你传了两个远程 URL;只给一个:--admin-dsn 让 pgcli 建监控角色(其 host/port/database 即成为目标),或 --dsn 用你自己管理的凭据原样拨号。
  • has an admin_dsn target but no stored monitor password —— 在一个 --admin-dsn 实例上做 --force 重装、而已存的监控密码丢失(pgcli 从未存 admin 凭据)。重新传 --admin-dsn postgres://… 让 pgcli 再建一次角色。
  • 某个 flag / discovery 改动没生效 —— --extra-arg 与目标/URL 改动在容器创建时应用;用 --force reinstall。只有采集器配置(--config-file)经 SIGHUP 重载。
  • macOS 本地模式什么都不出 —— bridge 容器够不到宿主的 127.0.0.1 PG;在 Mac 上用指向可达地址的 --dsn。
  • 端口已被分配 —— 先 pg addon remove 释放,或用 --port N 另选一个。

已知限制

  • 无鉴权、无 TLS,且管理端点暴露。 pg_exporter 提供纯文本 /metrics 外加 /reload、/explain、/stat;0.0.0.0 绑定的唯一保护是网络边界。若要离开可信网络,请前置 nginx 或用防火墙限制。
  • 监控 DSN(含其密码)存在 pg.yaml 里。 那是低权限 pg_monitor 角色的凭据,不是 admin 凭据;用于建它的 --admin-dsn 绝不持久化。请相应地保护 pg.yaml。
  • 官方镜像基于 scratch —— 它不带系统 CA 集,故对一个外部服务器的 TLS(sslmode=require)--dsn 可能证书校验失败;请经镜像供给信任、或在可信网络上连接。
  • 无状态 —— 没有数据目录、没有保留期;除运行中的进程外什么都不留存。重装总是从头开始,且目标侧的监控角色绝不自动删除。
  • macOS 本地实例模式不可用(bridge 容器够不到宿主回环);远程 --dsn 模式在 Mac 上可用。Linux 是已实测平台。
  • pgcli 不校验 --extra-arg 值;有错的 flag 只会在启动时从容器日志里暴露。

16.31 - influx2tsdb

以受管容器方式运行 influx2tsdb 代理——在 PostgreSQL + TimescaleDB 之上提供 InfluxDB 1.x HTTP API(Line Protocol + InfluxQL),保留与压缩由 TimescaleDB 自动调度

pg addon install influx2tsdb 以受管容器方式运行 influx2tsdb-proxy 1.3.2—— 一个 InfluxDB 1.x HTTP API 适配器(Line Protocol 写入、InfluxQL 查询),数据落到 启用了 TimescaleDB 扩展的 PostgreSQL。镜像由 pgcli 自建: ghcr.io/mars-base/pgcli/pgcli-influx2tsdb。与 InfluxDB 插件 不同, 这里没有 TSM 引擎:每个数据点都存进 TimescaleDB hypertable,代理本身无状态。

本插件仅支持 Linux/amd64(镜像为单架构,基于上游发行版二进制 + debian:12-slim)。 代理容器可以随时重建,数据因存放在 PostgreSQL 中而不会丢失。

HTTP API 没有认证。 上游代理只提供明文 HTTP、绑定所有网卡,且不校验 InfluxDB 凭据。需要 TLS 和访问控制时用 nginx 插件 前置,或直接 防火墙隔离端口。不可在不可信网络上使用。

适用场景与取舍

原生 InfluxDB 1.x 使用自己的内存索引 + TSM 文件,直接服务 InfluxQL。 influx2tsdb 则把每个请求翻译成 SQL,把数据行存进 PostgreSQL/TimescaleDB。这层 翻译加上关系型存储引擎会带来吞吐和延迟开销:纯时序写入/查询负载下,原生 InfluxDB 比本代理更快,差距在大范围聚合扫描和高基数场景下更明显。

换来的好处是时序数据变成了普通的数据库数据:

  • ACID 事务与一致性。 数据点与关系数据处于同一个事务存储中;批量写入要么 整体提交、要么失败,时序数据可以与业务表在一条 SQL 里 join。
  • SQL 生态。 TimescaleDB 把 hypertable 暴露成普通表:join、视图、CTE、窗口 函数、ORM 和 BI 工具都能用。InfluxQL 只是写入/查询入口,不是封闭的孤岛。
  • 成熟的备份、PITR、复制与 HA。 pg_dump/pgBackRest、WAL 归档、流复制、 Patroni 等 PostgreSQL 运维手段原样适用——不需要再维护第二套存储和灾备流程。
  • 统一的运维、认证与审计。 用户、角色、TLS、监控和合规控制都复用 PostgreSQL,不必额外加固一个数据库。
  • 灵活的数据生命周期。 保留与压缩是 TimescaleDB 原生策略(见下文),同时 仍可对任意行 UPDATE/DELETE、做 schema 迁移和表结构重塑。
  • 复用现有 PostgreSQL 基础设施与 DBA 技能。 如果 PostgreSQL 本来就有专业 运维,新增一个时序负载的边际成本很低。

如果最看重 InfluxQL/TSM 性能和忠实还原 InfluxDB 1.x 行为,选 InfluxDB 插件;如果首要目标是把一切留在 PostgreSQL 里,选 influx2tsdb。

前置条件

  • 目标 PostgreSQL 数据库已安装 TimescaleDB 扩展。代理启动时会尝试 CREATE EXTENSION IF NOT EXISTS timescaledb,但实例中必须有可安装的扩展包。 pgcli 实例上执行:

    pg extension install timescaledb -i tsdb --auto-restart
  • 目标必须在代理启动前处于运行状态。本地目标会预先检查 (pg start -i tsdb);PostgreSQL 不可达时代理会立即退出。

安装

本地目标——DSN 在每次启动时从 pgcli 实例重新推导,端口和密码变化会自动同步:

pg addon install influx2tsdb --pg-name tsdb
✓ influx2tsdb "influx2tsdb" installed and running
  Version:   1.3.2
  Image:     ghcr.io/mars-base/pgcli/pgcli-influx2tsdb:1.3.2
  Container: pgcli-influx2tsdb-influx2tsdb
  Target:    instance tsdb
  InfluxDB database: tsdb_db
  Endpoint:  http://<host>:8087 (InfluxDB 1.x HTTP API; binds all interfaces, no auth)
  Pool:      10 connections

远程或非受管目标——传完整 DSN:

pg addon install influx2tsdb --dsn 'postgres://user:pass@db.example.com:5432/metrics?sslmode=disable' \
  --database metrics --port 8087

DSN(含密码)会存入 pg.yaml,同时出现在容器命令行里,可通过 podman inspect 看到;请保护配置文件,并使用只授权目标数据库的最小权限角色。

第二个实例会从 influx2tsdb_start_port(默认 8087)起自动分配下一个空闲端口:

pg addon install influx2tsdb --name metrics2 --pg-name tsdb

不带 --force 重复 install 是幂等空操作。--force 会重写目标/端口设置并重建 容器;代理无数据,因此不影响任何已存数据。

参数

参数 默认值 说明
--name influx2tsdb 实例名(配置 map 键)
--pg-name — 使用本地受管 pgcli 实例作为目标;与 --dsn 互斥
--dsn — 远程/非受管 PostgreSQL/TimescaleDB 的完整 DSN
--database 目标库名 默认 InfluxDB 数据库(代理 -db);对应一个 PostgreSQL schema
--port 自动(基址 8087) InfluxDB 协议 HTTP 宿主端口;0 = 自动分配
--pool-size 10 PostgreSQL 连接池大小(-pool)
--verbose false 输出翻译后的 SQL 和查询细节(-verbose)
--version 1.3.2 固定的镜像发行版
--force false 重写设置并重建无状态容器

没有 --data-dir 和 --listen:代理不保留本地状态,使用 host 网络,且始终 绑定所有网卡。

数据模型

每个 InfluxDB 数据库对应目标库中的一个 PostgreSQL schema:

  • CREATE DATABASE "demo" → CREATE SCHEMA "demo"
  • measurement 变成该 schema 下的 TimescaleDB hypertable
  • tag 变成文本列;field 变成带类型的列;time 是 hypertable 的分区列
  • 代理元数据位于目标库的 public schema:_influx_databases、_influx_meta、 _retention_policy

数据就是普通 PostgreSQL 数据,可以直接用 pg exec 查看和 join:

pg exec -i tsdb 'SELECT * FROM "tsdb_db"."cpu" ORDER BY time DESC LIMIT 5'
pg exec -i tsdb "SELECT time_bucket('1 minute', time) AS m, avg(value) FROM \"tsdb_db\".\"cpu\" GROUP BY 1 ORDER BY 1 DESC LIMIT 5"

写入与查询

API 遵循 InfluxDB 1.x:写入成功返回 204,非法 line protocol 返回 400。

URL=http://127.0.0.1:8087
DB=tsdb_db

# 单点写入
curl -sS -o /dev/null -w '%{http_code}\n' -XPOST "$URL/write?db=$DB" \
  --data-binary 'cpu,host=web01 value=0.42 1700000000000000000'

# 批量写入
curl -sS -o /dev/null -w '%{http_code}\n' -XPOST "$URL/write?db=$DB" --data-binary '
cpu,host=a value=10.5,ok=true,state="up" 1700000000000000000
cpu,host=b value=20.5,ok=false,state="down" 1700000060000000000'

# InfluxQL
curl -sS -G "$URL/query" --data-urlencode "db=$DB" \
  --data-urlencode 'q=SELECT * FROM "cpu" ORDER BY time DESC LIMIT 5'
curl -sS -G "$URL/query" --data-urlencode "db=$DB" \
  --data-urlencode 'q=SELECT mean("value") FROM "cpu" WHERE time > now() - 1h GROUP BY time(5m), "host"'

当前版本支持的常用语句:

类别 语句
数据库 CREATE DATABASE、DROP DATABASE、SHOW DATABASES
保留策略 CREATE / ALTER / DROP / SHOW RETENTION POLICIES
写入/删除 line protocol 写入、DELETE FROM、DROP MEASUREMENT
元数据 SHOW MEASUREMENTS / TAG KEYS / TAG VALUES / FIELD KEYS(后三者需带 FROM <measurement>)
查询 带 tag/time 过滤的 SELECT、mean/sum/min/max/count、first/last、GROUP BY time(...) 与 tag、子查询

使用 InfluxDB 1.x 命令行客户端

官方 influx CLI(1.13.1)可以直接操作代理。pgcli 通过一次性容器运行它, remote 模式指向代理即可(代理无认证,无需用户名密码):

CLI="pg influx --host 127.0.0.1 --port 8087"

数据库与保留策略:

$CLI -- -execute "SHOW DATABASES"
$CLI -- -execute "CREATE DATABASE demo"
$CLI --database demo -- -execute \
  "CREATE RETENTION POLICY rp1 ON demo DURATION 1d REPLICATION 1"
$CLI --database demo -- -execute \
  "ALTER RETENTION POLICY rp1 ON demo DURATION 2d DEFAULT"
$CLI --database demo -- -execute "SHOW RETENTION POLICIES ON demo"
# 默认策略不能直接删除:先把另一个策略设为 DEFAULT 再删
$CLI --database demo -- -execute \
  "ALTER RETENTION POLICY autogen ON demo DURATION INF REPLICATION 1 DEFAULT"
$CLI --database demo -- -execute "DROP RETENTION POLICY rp1 ON demo"
$CLI -- -execute "DROP DATABASE demo"

写入(line protocol):

$CLI --database tsdb_db -- -execute \
  'INSERT cpu,host=web01 value=42,ok=true 1700000000000000000'
$CLI --database tsdb_db -- -execute \
  'INSERT cpu,host=web02,region=us value=7.5 1700000060000000000'

查询与元数据:

$CLI --database tsdb_db -- -execute \
  "SELECT * FROM cpu WHERE host = 'web01' AND time >= '2023-11-14 22:13:20'"
$CLI --database tsdb_db -- -execute \
  'SELECT mean(value) FROM cpu GROUP BY time(1m),host'
$CLI --database tsdb_db -- -execute 'SHOW MEASUREMENTS'
$CLI --database tsdb_db -- -execute 'SHOW TAG KEYS FROM cpu'
$CLI --database tsdb_db -- -execute 'SHOW TAG VALUES FROM cpu WITH KEY = host'
$CLI --database tsdb_db -- -execute 'SHOW FIELD KEYS FROM cpu'

删除与输出格式:

$CLI --database tsdb_db -- -execute "DELETE FROM cpu WHERE host = 'web01'"
$CLI --database tsdb_db -- -execute 'DROP MEASUREMENT cpu'
$CLI --database tsdb_db -- -format json -execute 'SELECT * FROM cpu'
$CLI --database tsdb_db -- -format csv  -execute 'SELECT * FROM cpu'
$CLI --database tsdb_db -- -precision ms -execute 'SELECT * FROM cpu'

批量导入(influx 1.x import 格式):

cat >/tmp/influx-import.txt <<'DATA'
# DDL
CREATE DATABASE demo

# DML
# CONTEXT-DATABASE: demo
# CONTEXT-RETENTION-POLICY: autogen
weather,location=us-midwest temperature=82 1700000000000000000
weather,location=us-midwest temperature=83 1700000060000000000
DATA

$CLI -- -import -path /tmp/influx-import.txt

实测可用:数据库与保留策略 DDL、INSERT(line protocol)、SELECT(含 GROUP BY time(...)、tag/time 过滤和 -format column/csv/json)、 SHOW MEASUREMENTS、带 FROM 的 SHOW TAG KEYS / FIELD KEYS / TAG VALUES、 DELETE、DROP MEASUREMENT 以及 -import。

与原生 InfluxDB 的差异:

  • 元数据命令必须带 FROM <measurement>:SHOW TAG KEYS、SHOW FIELD KEYS 和 SHOW TAG VALUES 不带 FROM 会返回 missing FROM clause(原生 InfluxDB 允许省略);WITH KEY =~ /regex/ 也不支持。
  • 不支持 SHOW SERIES(返回 unsupported query: SHOW SERIES)。
  • 默认保留策略不能直接 DROP(与原生 InfluxDB 一致),先把另一个策略设为 DEFAULT。
  • -precision 的 ns/u/ms/s/m/h 都生效;rfc3339 不生效,仍输出纳秒整数。

时间精度注意事项

  • 时间戳一律按纳秒解释。 在 E2E 实测中,当前版本忽略 /write 的 precision= 查询参数;请发送纳秒时间戳(InfluxDB 默认精度),避免数据时间 偏移。

  • 时间字面量两种写法都可用。 实测中,WHERE time ... 既接受 'YYYY-MM-DD HH:MM:SS',也接受 RFC3339(如 '2023-11-14T22:13:20Z',含 毫秒或 +08:00 时区偏移);Grafana 经 $timeFilter 插值出的 epoch 毫秒 形式(如 1793083539315ms)同样可用:

    curl -sS -G "$URL/query" --data-urlencode "db=$DB" \
      --data-urlencode "q=SELECT * FROM \"cpu\" WHERE time >= '2023-11-14 22:13:20' AND time <= '2023-11-14 22:15:20'"
  • now() - 1d 这类表达式可用。

已知上游问题:DROP MEASUREMENT 元数据缓存

执行 DROP MEASUREMENT 后,代理会删除 _influx_meta 中该 measurement 的行, 但保留内存中的 schema 缓存。之后用相同名字重新创建 measurement 时,元数据 不会被重新插入,SHOW MEASUREMENTS / SHOW TAG KEYS / SHOW FIELD KEYS 可能 为空,而 SELECT 仍然可用。自动化中请使用每次唯一的 measurement 名,或在删除 并重建后重启代理。仓库自带测试脚本采用前者。

Grafana 集成

Grafana 内置的 InfluxDB 数据源可以直接接入本代理(已在 Grafana 13.2.4 上 端到端验证):数据源类型选 InfluxDB,Query Language 选 InfluxQL (不是 Flux),URL 指向代理,即可查询底层 TimescaleDB 中的 hypertable。

字段 值
Type InfluxDB
Query Language InfluxQL
URL http://<proxy-host>:8087
Database tsdb_db(或你的默认库)
User / Password 留空
HTTP Method GET(默认)

数据源的 “Save & test” 健康检查可用;Grafana 查询编辑器和面板生成的请求 ($timeFilter、epoch=ms、fill(null)、GROUP BY time)已实测返回正确的 时间序列,SHOW MEASUREMENTS / TAG KEYS / TAG VALUES / FIELD KEYS 也受支持, 因此查询构建器里的 measurement/tag/field 下拉框可正常使用。

注意事项:

  • 代理无认证、无 TLS;Grafana 与代理不在可信网络时用 nginx 插件 前置。
  • 只支持 InfluxQL 子集;不支持的表达式可能返回错误或空结果,也不支持 Flux (Flux 依赖 InfluxDB v2 API,本代理不提供)。
  • 代理忽略写入的 precision= 参数;Grafana 用 epoch=ms 读取,展示不受 影响。

通过 Grafana HTTP API 集成

自动化场景不必点 Web UI:Grafana 的 HTTP API 可以直接创建数据源,并验证 “代理写入 → Grafana API 读取”的完整链路(以下命令已在 Grafana 13.2.4 上 实测)。

GRAFANA_PW=$(pg addon password grafana)   # Grafana admin 密码
G=http://127.0.0.1:3000

# 1. 创建 InfluxDB/InfluxQL 数据源
curl -sS -u "admin:$GRAFANA_PW" \
  -H 'Content-Type: application/json' \
  -XPOST "$G/api/datasources" -d '{
    "name": "influx2tsdb",
    "type": "influxdb",
    "access": "proxy",
    "url": "http://127.0.0.1:8087",
    "database": "tsdb_db",
    "user": "",
    "password": "",
    "isDefault": true,
    "jsonData": {"httpMode": "GET", "version": "InfluxQL"}
  }'

# 按名字取回 uid(也可直接读上一步响应里的 datasource.uid)
UID=$(curl -sS -u "admin:$GRAFANA_PW" \
  "$G/api/datasources/name/influx2tsdb" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["uid"])')

# 2. 健康检查
curl -sS -u "admin:$GRAFANA_PW" \
  "$G/api/datasources/uid/$UID/health"
# {"message":"datasource is working. 0 measurements found","status":"OK"}

# 3. 通过代理写入一个点(纳秒时间戳)
curl -sS -o /dev/null -w '%{http_code}\n' \
  -XPOST "http://127.0.0.1:8087/write?db=tsdb_db" \
  --data-binary "cpu,host=api value=42 $(date +%s%N)"

# 4. 经 Grafana 的 datasource proxy 读取(InfluxQL)
curl -sS -u "admin:$GRAFANA_PW" -G \
  "$G/api/datasources/proxy/uid/$UID/query" \
  --data-urlencode 'db=tsdb_db' \
  --data-urlencode 'epoch=ms' \
  --data-urlencode 'q=SELECT last("value") FROM "cpu" WHERE time >= now() - 1h'

写入返回 204;第 4 步返回该 measurement 的 last(value),time 列为 epoch 毫秒(与 epoch=ms 一致)。

要点:

  • access: "proxy" 让 Grafana 服务端转发查询,浏览器不直连代理,也不涉及 CORS。
  • jsonData 中的 version: "InfluxQL" 和 httpMode: "GET" 与面板查询使用 的模式一致,创建时显式给出。
  • 删除数据源: curl -u "admin:$GRAFANA_PW" -X DELETE "$G/api/datasources/uid/$UID" (返回 200)。

保留策略、压缩与 TimescaleDB 任务

支持 InfluxDB 语义的 CREATE / ALTER / DROP RETENTION POLICY(按数据库隔离、 只有一个默认策略、最短 15m、INF/0s 表示无限)。在存储层,代理会立即对该 数据库 schema 中的每个 hypertable 调用 TimescaleDB 自带的策略函数:

SELECT add_retention_policy('"tsdb_db"."cpu"', INTERVAL '86400 seconds',
                            schedule_interval => INTERVAL '1 hour');
SELECT add_compression_policy('"tsdb_db"."cpu"', INTERVAL '6 hours',
                              schedule_interval => INTERVAL '1 hour');
SELECT set_chunk_time_interval('"tsdb_db"."cpu"', INTERVAL '1 hour');

清理本身是自动的,不需要 pg_cron。 任务运行在 TimescaleDB 自带的后台 job scheduler 中,可在 timescaledb_information.jobs 查看:

pg exec -i tsdb "
  SELECT job_id, application_name, schedule_interval, proc_name,
         hypertable_schema, hypertable_name, config
  FROM timescaledb_information.jobs
  WHERE hypertable_schema = 'tsdb_db' ORDER BY job_id"

一个 1 天保留策略会产生类似这样的任务:

 job_id |  application_name        | schedule_interval |    proc_name     | config
--------+--------------------------+-------------------+------------------+-----------------------------------------
   1050 | Retention Policy [1050]  | 01:00:00          | policy_retention | {"drop_after": "24:00:00", ...}
   1051 | Columnstore Policy [1051]| 01:00:00          | policy_compression | {"compress_after": "06:00:00", ...}

运维要点:

  • 调度器不是代理。 真正执行任务的是 TimescaleDB;只要 PostgreSQL 在运行, 停止或删除代理容器并不会停止数据过期清理。反之,代理停机期间新建的 hypertable 会在下一次同步时补上策略。
  • 同步节奏。 CREATE / ALTER / DROP RETENTION POLICY 会立即触发一次同步; 另有后台循环每 5 分钟对所有 hypertable 重新下发策略(覆盖新表,以及因锁 超时被跳过的 DDL)。
  • 任务节奏。 代理会按推导值设置调度间隔:保留策略小于 24h 时请求每 5 分钟、 否则每小时;压缩小于 24h 时请求每分钟、否则每小时。但上游 1.3.2 存在已知 quirk:选择间隔时会遍历所有数据库的策略,且结果受 map 遍历顺序影响,多库 部署下实际间隔可能与推导值不同(实测 1h 保留策略的 retention 任务被排成 1 小时)。需要精确节奏时以 timescaledb_information.jobs.schedule_interval 为准;代理每次同步都会重新下发推导出的间隔。
  • 只有默认策略会真正生效。 schema 内所有 hypertable 都使用该数据库默认策略 的时长(若没有默认策略,则取第一个有限时长的策略)。非默认策略只是元数据; 代理不会按策略拆分 schema,也不会按 measurement 单独保留。没有有限时长策略 (或默认策略为 INF)时数据会持续增长。
  • 删除以 chunk 为粒度。 TimescaleDB 整块删除 chunk,因此过期数据最多会多留 一个 chunk 间隔,稳态数据量约为「保留时长 + 最多一个 chunk 间隔」,之后不再 无限增长。chunk 间隔来自 SHARD DURATION 或按保留时长推导(最小 1h, retention/24,上限 1 天)。
  • 把策略改成 INF 会在下次同步时移除 TimescaleDB 保留任务,数据不再过期。
  • 压缩自动启用。 tag 作为 compress_segmentby,时间倒序作为 compress_orderby;compress_after 由保留时长推导(retention/4,上限 1 天, 最小 15m)。

在 PostgreSQL 侧核查:

# 当前策略及最近一次执行
pg exec -i tsdb "SELECT application_name, schedule_interval, last_run_started_at, last_successful_finish
                 FROM timescaledb_information.job_stats s
                 JOIN timescaledb_information.jobs j USING (job_id)
                 WHERE proc_name IN ('policy_retention','policy_compression')"

安全与暴露面

  • 代理没有任何凭据;能连到端口的人就能读写目标 DSN 暴露的所有数据库。
  • 只绑可信网络,或用 nginx 插件 前置做 TLS 与 IP/Basic Auth 限制。 代理端口本身没有 TLS。
  • PostgreSQL 密码会出现在 pg.yaml 和容器 argv 中。请使用仅授权目标库的专用 角色,并保护配置文件。

生命周期与开机自启

pg addon start influx2tsdb --name influx2tsdb
pg addon stop influx2tsdb --name influx2tsdb
pg addon influx2tsdb status --name influx2tsdb
pg logs addon influx2tsdb --name influx2tsdb -f
pg addon remove influx2tsdb --name influx2tsdb          # 容器 + pg.yaml 条目

remove 从不删除数据:数据在 PostgreSQL 里。--clean-data 为与其他插件保持 一致而接受,但没有可删除的内容。

开机自启会重建无状态代理。本地目标会先启动实例,再重新推导 DSN:

pg autostart enable --influx2tsdb --name influx2tsdb
pg autostart status
pg autostart disable --influx2tsdb --name influx2tsdb

备份、恢复与 HA

代理侧没有需要备份的内容,直接使用 PostgreSQL 的常规工具:

  • pg backup <instance> / pgBackRest / pg_dump——所有 schema、hypertable 以及 public._influx_* 元数据表都会包含在内。
  • PITR / WAL 归档 / 流复制 / Patroni——原样适用。故障切换或迁移后,把代理指向 新主库(--dsn)并重建;本地实例则在实例 DSN 变化后重启代理。

代理容器是一次性的:pg addon install influx2tsdb --force 用同一目标重建即可。

测试脚本

test/addon/test_influx2tsdb.sh 覆盖常用时序操作:ping、单点/批量写入(含 tag 和多种 field 类型)、count、时间范围与 tag 过滤、mean/sum/min/max、 GROUP BY time() 与 tag、first/last、SHOW 元数据、可选的 PostgreSQL 行数/hypertable 检查、DELETE、DROP MEASUREMENT、数据库生命周期、保留策略 CRUD,以及非法写入的处理。

# 本地目标;从 pg addon influx2tsdb status 自动发现 URL 和数据库
test/addon/test_influx2tsdb.sh --instance tsdb

# 显式端点
test/addon/test_influx2tsdb.sh --url http://127.0.0.1:8087 --db tsdb_db

# 保留测试 measurement/数据库以便检查
test/addon/test_influx2tsdb.sh --instance tsdb --keep

由于上述 DROP MEASUREMENT 元数据缓存问题,脚本使用每次运行唯一的 measurement 和数据库名。

test/addon/test_influx2tsdb_retention.sh 专门验证保留策略的自动化,覆盖:

  • 自动创建 policy_retention / policy_compression 任务,drop_after / compress_after 与 RP 推导一致;
  • CALL run_job(<id>) 真正删除过期 chunk,新数据保留;
  • 停掉代理后直接向 PostgreSQL 插入过期行,任务仍能删除(证明调度不依赖代理);
  • 默认 RP 切到 INF 后保留任务消失、压缩任务保留并回退到 1 天;
  • DROP DATABASE 后该 schema 的任务全部清理;
  • 目标库未安装 pg_cron。
# 本地目标;默认从 pg addon influx2tsdb status 发现端点、数据库和实例
test/addon/test_influx2tsdb_retention.sh

# 显式指定
test/addon/test_influx2tsdb_retention.sh --instance tsdb \
  --url http://127.0.0.1:8087 --db tsdb_db

# 跳过停代理阶段(只验证任务与配置)
test/addon/test_influx2tsdb_retention.sh --skip-proxy-stop

脚本通过 pg exec 访问目标 PostgreSQL,因此需要 --instance 或 --pg-dsn; 它会在运行中停止并重启本地代理(除非 --skip-proxy-stop)。

限制

  • 仅 Linux/amd64。
  • 代理无认证、无 TLS;请用 nginx 前置或做网络隔离。
  • InfluxQL 子集。 当前版本只实现上文列出的语句,不是完整 InfluxQL;例如 SHOW SERIES 不支持,SHOW TAG KEYS / FIELD KEYS / TAG VALUES 必须带 FROM。不支持的语法会返回错误或空结果。不支持 Flux。
  • 时间戳按纳秒解释;当前版本忽略写入的 precision= 参数。查询输出精度由 epoch=ms 控制(Grafana 即使用该参数)。
  • 仅支持按数据库的保留策略。 一个默认策略作用于 schema 内所有 hypertable。
  • 时序负载性能低于原生 InfluxDB(见上文)。
  • 代理的元数据表(_influx_databases、_influx_meta、_retention_policy)位于 目标库的 public schema。

CLI 参数参考

pg addon install influx2tsdb

参数 默认值 说明
--name NAME influx2tsdb 实例名
--pg-name NAME — 本地 pgcli 实例作为目标;与 --dsn 互斥
--dsn DSN — postgres://user:pass@host:port/db 目标;与 --pg-name 互斥
--database DB 目标库名 默认 InfluxDB 数据库(-db)
--port N 自动(基址 8087) HTTP 宿主端口;0 = 自动分配
--pool-size N 10 PostgreSQL 连接池大小
--verbose false 输出翻译后的 SQL
--version V 1.3.2 固定的镜像发行版
--force false 重写设置并重建容器

pg addon start|stop|remove influx2tsdb

命令 参数 说明
pg addon start influx2tsdb --name NAME 启动(重建)容器;本地目标会先做运行检查
pg addon stop influx2tsdb --name NAME 停止容器
pg addon remove influx2tsdb --name NAME、--clean-data 删除容器与配置条目;数据留在 PostgreSQL

pg addon influx2tsdb status

参数 默认值 说明
--name NAME influx2tsdb 要查看的实例(状态、ping、目标、数据库、端口)

pg logs addon influx2tsdb

参数 默认值 说明
--name NAME influx2tsdb 要查看容器日志的实例
-n N、--tail N 50 行数
-f、--follow false 持续跟踪

pg autostart enable|disable

参数 说明
--influx2tsdb 选择该代理插件
--name NAME 要切换的实例(默认 influx2tsdb)

全局参数

参数 说明
-c, --config FILE 配置文件(默认 ~/.pgcli/pg.yaml)
-o, --output FILE 输出配置路径
-i, --instance NAME PostgreSQL 实例选择器——此处不使用;请用 --name

16.32 - ClickHouse

以受管容器方式运行单机 ClickHouse 列式 OLAP 数据库——内置认证、SQL 辅助命令、用户/授权、配置覆盖与 BACKUP/RESTORE

pg addon install clickhouse 以受管容器方式运行 ClickHouse 26.8 LTS——单机列式 OLAP 数据库——直接跑官方 docker.io/clickhouse/clickhouse-server 多架构镜像。与 VictoriaMetrics、InfluxDB 一样,它是顶层插件,不前置任何 PostgreSQL 实例。

ClickHouse 用两个端口承载两种协议:HTTP(curl、JDBC/ODBC 以及大量驱动)和原生 TCP 协议(clickhouse-client)。pgcli 以 host 网络运行它,因此两者都是直接的宿主端口。认证始终开启——安装时创建 admin 账号,密码存入 pg.yaml。

admin 密码在安装时生成,可随时用 pg addon password clickhouse [--name NAME] 取回。 HTTP 与原生监听对每个请求 都要求认证,不存在匿名访问。--listen 默认 127.0.0.1——需要其他主机连接时 显式传 0.0.0.0,并用 nginx 前置做 TLS。

参数

参数 默认值 说明
--name clickhouse 实例名(map 键;也是各子命令的默认名)
--version 26.8.36.3 上游镜像 tag,直接从 docker.io/clickhouse/clickhouse-server 拉取
--data-dir <base_dir>/addon/clickhouse/<name>/data 数据目录,绑定挂载到 /var/lib/clickhouse;必须是绝对路径
--listen 127.0.0.1 HTTP 与原生监听的绑定地址。0.0.0.0 会把两者暴露到网络
--http-port 自动(基址 8123) HTTP 宿主端口;0 = 自 clickhouse_http_start_port 自动分配
--tcp-port 自动(基址 9000) 原生协议宿主端口;0 = 自 clickhouse_tcp_start_port 自动分配(与 S3 API 池共享区间)
--admin-user admin 由镜像 entrypoint 创建的 admin 账号
--admin-password 自动生成 admin 密码。--force 时默认保留已存密码,除非显式指定
--config-option K=V — 额外服务端设置,点分 XML 路径(logger.level=debug、max_server_memory_usage=2147483648);可重复;替换已存覆盖 map
--force false 重新渲染配置与 secret 并重建容器;保留数据、已存密码与已存覆盖项

ClickHouse 不适用 --image——请用 --version。

生成的配置只固定监听、端口、日志与备份盘;其余服务端设置全部沿用上游默认值。覆盖项存在 pg.yaml 的 addons.clickhouse.<name>.settings 下并渲染进生成的 XML,因此重启与容器重建都不会丢失。

安装

pg addon install clickhouse --name ch1
✓ ClickHouse "ch1" installed
  Version:   26.8.36.3
  Image:     docker.io/clickhouse/clickhouse-server:26.8.36.3
  Container: pgcli-clickhouse-ch1
  HTTP:      http://127.0.0.1:8123
  TCP:       127.0.0.1:9000
  Admin:     admin
  Password:  <自动生成>
  Data dir:  /home/you/.pgcli/addon/clickhouse/ch1/data
  Backups:   /home/you/.pgcli/addon/clickhouse/ch1/backups
  Config:    /home/you/.pgcli/addon/clickhouse/ch1/config.d/zz-pgcli.xml

第二个实例会自动从各自端口池取下一个空闲 HTTP/TCP 端口(例如 8124/9001),并可使用自定义数据目录:

pg addon install clickhouse --name analytics --data-dir /srv/clickhouse-data --listen 0.0.0.0

不带 --force 重复 install 是幂等空操作,只报告当前状态。--force 会重新渲染配置、重建容器,并保留数据目录、已存 admin 密码与已存设置;只有想替换覆盖 map 时才需要重新传 --config-option。

镜像直接从 Docker Hub 拉取,不涉及 pgcli 自建镜像。--version 接受任意合法的上游 tag,但该 tag 必须存在于 registry,否则拉取失败。pgcli 默认固定当前 LTS 线(26.8.36.3)。

端口

端点 默认 协议 端口池
HTTP 8123 HTTP(查询、/ping、大量驱动) clickhouse_http_start_port,默认 8123
Native 9000 ClickHouse 原生 TCP(clickhouse-client) clickhouse_tcp_start_port,默认 9000,与 S3 API 池共享

两个池共享同一个“已分配”集合,因此一个池中的显式端口不会与另一个池自动分配的端口相撞。--http-port 与 --tcp-port 必须不同,且所有 ClickHouse 实例的 HTTP/TCP 端口必须全局唯一。自动分配会跳过宿主上已在监听的端口。

当 --listen 为通配地址(0.0.0.0、::)时,pgcli 自己的客户端命令会拨 127.0.0.1;打印的端点同样遵循此规则。LAN --listen 绑定在容器内仍会保留 loopback,保证容器内的 clickhouse-client 始终可用。

pgcli 创建的内容

单个实例的全部状态位于 <base_dir>/addon/clickhouse/<name>/:

宿主路径 容器路径 说明
config.d/ /etc/clickhouse-server/config.d 只读;替换镜像自带的 config.d(否则会监听所有网卡)。zz-pgcli.xml 是生成的覆盖文件
data/ /var/lib/clickhouse 数据库数据与系统表
logs/ /var/log/clickhouse-server clickhouse-server.log 与 clickhouse-server.err.log(也可经 podman logs 查看)
backups/ /var/lib/clickhouse-backups backup/restore 使用的受管 backups 盘
secrets/admin_password /run/secrets/admin_password 权限 0600,entrypoint 经 CLICKHOUSE_PASSWORD_FILE 读取

生成的 zz-pgcli.xml 设置:

  • listen_host(完全等于 --listen 的选择)与 listen_try
  • http_port / tcp_port
  • logger.level、logger.log、logger.errorlog、logger.console、日志轮转(size、count)
  • 位于 /var/lib/clickhouse-backups 的本地盘 backups,并允许其用于 BACKUP/RESTORE

容器以 --network host、--restart unless-stopped、30 秒停止超时和 nofile=262144 运行。数据在容器内的属主是 uid/gid 101(镜像 entrypoint 以 root 完成挂载目录 chown 后再降权)。

凭据

pg addon password clickhouse --name ch1   # 打印 admin 密码
pg addon list                             # 查看 Analytics add-ons 区块

镜像 entrypoint 会创建 admin(或 --admin-user 指定的名字)并赋予全部权限,同时开启访问管理(CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1),因此本 CLI 发出的 CREATE USER / GRANT 可以生效。密码存入 pg.yaml(默认权限 0644——请保护该文件),并同步写入挂载进容器的 0600 secret 文件。

所有与 ClickHouse 通信的 pgcli 命令都通过环境变量传递凭据、在容器内展开;密码绝不出现在 podman 命令行或宿主进程列表中。SQL 经 stdin 传入。

连接

用打印出的端点即可从任意宿主工具连接:

# HTTP 接口(Basic 认证)
curl -sS 'http://127.0.0.1:8123/?query=SELECT%20version()' \
  --user "admin:$(pg addon password clickhouse --name ch1)"

# 原生协议
clickhouse-client --host 127.0.0.1 --port 9000 --user admin --password

pgcli 不会在宿主上安装 clickhouse-client——请用下面的 query/shell 子命令,或让外部客户端连原生端口。/ping 无需认证并返回 Ok.,是 pgcli 启动时轮询的健康检查。

Shell 与查询

pg addon clickhouse shell --name ch1                  # 进入容器内 bash
pg addon clickhouse query --name ch1 'SELECT version()'
pg addon clickhouse query --name ch1 --database demo 'SELECT count() FROM events'
pg addon clickhouse query --name ch1 --file schema.sql
echo "SELECT 1" | pg addon clickhouse query --name ch1 -

query 以 admin 用户运行 clickhouse-client,结果写到 stdout,可直接管道或重定向。语句由位置参数拼接;--file 读取宿主文件(宿主读取后经 stdin 传入,无需挂载);- 表示从 stdin 读取。客户端调用时未加 --multiquery,因此每次调用只传一条语句。

query、database、user、grant、backup、restore 都要求实例正在运行;shell 在容器停止时仍会尝试打开,但会打印警告。

CREATE TABLE demo.events
(
    id   UInt64,
    ts   DateTime,
    name String
)
ENGINE = MergeTree
ORDER BY id;
pg addon clickhouse query --name ch1 --database demo --file events.sql
echo "INSERT INTO demo.events VALUES (1, now(), 'boot')" | \
  pg addon clickhouse query --name ch1 --database demo -
pg addon clickhouse query --name ch1 --database demo 'SELECT * FROM demo.events ORDER BY id'

数据库

pg addon clickhouse database create demo --name ch1
pg addon clickhouse database create logs --engine Atomic --comment 'application logs' --if-not-exists --name ch1
pg addon clickhouse database list --name ch1
pg addon clickhouse database drop demo --name ch1 --yes

create 接受裸 engine 名(默认 Atomic;不暴露 engine 参数)。drop 是破坏性操作,除非传 --yes,否则会要求确认。database list 展示 system.databases 中的 name、engine 与 data_path。db 是 database 的别名。

用户与权限

# 显式密码与默认数据库
pg addon clickhouse user create app --password 'change-me' --default-database demo --name ch1

# 省略密码:自动生成并打印一次
pg addon clickhouse user create reader --default-database demo --readonly --name ch1

# 限制来源地址 / 主机名(可重复;默认 HOST ANY)
pg addon clickhouse user create etl --host-ip 10.0.0.0/8 --host-ip 192.168.1.10 --name ch1
pg addon clickhouse user create svc --host-name app.internal --name ch1
pg addon clickhouse user create localonly --host-local --name ch1

pg addon clickhouse user list --name ch1
pg addon clickhouse user drop app --name ch1 --yes

用户以 IDENTIFIED WITH sha256_password 创建。未给任何 host 参数时默认 --host-any;--host-local、--host-ip(IP 或 CIDR)与 --host-name 会追加 HOST 子句。--readonly 追加 SETTINGS readonly = 1。自动生成的密码只打印一次且不会存入 pg.yaml——请自行保存。--if-not-exists 让 user create 幂等。

# 单库读写
pg addon clickhouse grant app --read --write --database demo --name ch1

# DDL 关键字 = CREATE, ALTER, DROP, TRUNCATE, OPTIMIZE
pg addon clickhouse grant app --ddl --database demo --name ch1

# 表级范围(必须显式指定 database)
pg addon clickhouse grant reader --read --database demo --table events --name ch1

# 显式列表、通配范围、grant option
pg addon clickhouse grant app --privileges 'SELECT,INSERT' --name ch1
pg addon clickhouse grant app --all --with-grant-option --name ch1

pg addon clickhouse grant list app --name ch1
pg addon clickhouse grant app --read --database demo --revoke --name ch1

--read 即 SELECT,--write 即 INSERT,--ddl 展开为 CREATE, ALTER, DROP, TRUNCATE, OPTIMIZE,--all 授予 ALL,--privileges 接受逗号分隔列表(会转大写并与 flag 选择去重)。不给 --database 时目标是 *(所有数据库);--table 需要显式 database。--revoke 发出 REVOKE ... FROM。grant list 运行 SHOW GRANTS FOR。

这些命令都以实例的 admin 账号执行;CLI 从不保存每个用户的密码或凭据。

配置

服务端设置用点分 XML 路径寻址;叶子值就是生成 XML 中该元素的文本。

# 安装时
pg addon install clickhouse --name ch1 \
  --config-option max_server_memory_usage=2147483648 \
  --config-option logger.level=warning

# 运行中的实例
pg addon clickhouse config list --name ch1
pg addon clickhouse config get max_server_memory_usage --name ch1
pg addon clickhouse config set max_server_memory_usage=2147483648 --name ch1
pg addon clickhouse config set logger.level=debug query_log.flush_interval_milliseconds=1000 --name ch1
pg addon clickhouse config unset logger.level --name ch1
pg addon clickhouse config show --name ch1        # 完整生成的 XML
  • config set KEY=VALUE ... 接受一个或多个键值对,校验后重写 XML 与 pg.yaml,然后重启运行中的容器使改动立即生效。传 --no-restart 可推迟到下次启动。
  • config unset KEY ... 删除覆盖项(一个或多个),重启规则相同。unset 不存在的键会报错。
  • config get 打印自定义覆盖或受管设置的有效值;config list 先列受管设置,再按 key 排序列出自定义覆盖。
  • config show 渲染与写入 config.d/zz-pgcli.xml 相同的 XML;不触碰文件或容器。
  • 安装时的 --config-option 会替换整个覆盖 map;不传则保留已存内容。

logger.level 特殊处理:它折叠进受管的 <logger> 块,而不是产生第二个 <level> 元素。合法级别为 trace、debug、information、warning、error、fatal 和 none。

pgcli 受管的键会被拒绝,因为覆盖它们要么产生重复 XML,要么与已存路径/端口静默冲突:listen_host、listen_try、http_port、https_port、tcp_port、interserver_http_port、interserver_http_host、mysql_port、postgresql_port、path、tmp_path、user_files_path、format_schema_path、access_control_path、backups.*、user_directories、profiles、users、storage_configuration.disks、storage_configuration.policies,以及除 logger.level 外的所有 logger.* 键。含空格的键或含控制字符的值同样会被拒绝。

常用设置:

pg addon clickhouse config set max_server_memory_usage=2147483648 --name ch1
pg addon clickhouse config set background_pool_size=8 max_concurrent_queries=100 --name ch1
pg addon clickhouse config set query_log.flush_interval_milliseconds=1000 --name ch1
pg addon clickhouse config set merge_tree.max_suspicious=0 --name ch1

本 CLI 不支持服务端设置的热加载;每次有效变更都会重启或重建容器(ClickHouse 在启动时读取该生成文件)。

pgcli 只校验 key 形式与受管键规则——值由 ClickHouse 在启动时校验。被它拒绝的值(例如低于 merge/mutation 健全性检查要求的 background_pool_size)会中止启动,而容器的 unless-stopped 策略会把它变成重启循环。按下面步骤恢复,不会丢数据:

pg addon stop clickhouse --name ch1
pg addon clickhouse config unset background_pool_size --no-restart --name ch1
pg addon start clickhouse --name ch1

备份与恢复

备份使用 ClickHouse 的 SQL BACKUP 命令写入本地 backups 盘(/var/lib/clickhouse-backups)。备份是在线的,实例必须正在运行。这里只暴露受管的本地盘——没有配置 S3 目标。

pg addon clickhouse backup --name ch1 --database demo
# ✓ backup written: <instance dir>/backups/demo-20261027-120000.zip (… bytes)

# 显式文件名或备份目录内的嵌套路径
pg addon clickhouse backup --name ch1 --database demo --out nightly.zip
pg addon clickhouse backup --name ch1 --database demo --out archive/2026-q4.zip

pg addon clickhouse backups --name ch1
pg addon clickhouse restore demo-20261027-120000.zip --name ch1 --database demo
pg addon clickhouse restore demo-20261027-120000.zip --name ch1 --database demo --as demo_restored
  • 两个命令都必须给 --database:它表示备份中记录的数据库名(v1 没有“全部数据库”形式)。
  • --out 在备份目录内时按相对路径解析,否则按绝对路径;绝对路径必须位于该目录内,且文件必须以 .zip 结尾。默认名为 <database>-YYYYMMDD-HHMMSS.zip(UTC)。
  • restore 接受备份目录内的文件,外加记录中的数据库名。恢复到已存在的数据库会失败(ClickHouse 拒绝覆盖);用 --as NEW 并存恢复。
  • restore 本身是在线的,但需要足够空间与运行中的实例;backups 按最新优先列出 .zip 文件。

备份目录就是普通宿主目录——真正的灾备请把 .zip 复制到别的机器。没有定时备份;需要时把 pg addon clickhouse backup 接进自己的定时任务。

# 异地复制
rsync -a /home/you/.pgcli/addon/clickhouse/ch1/backups/ backup-host:/backups/clickhouse-ch1/

生命周期

pg addon start clickhouse --name ch1      # 不存在则创建,然后等待 /ping
pg addon stop clickhouse --name ch1
pg addon clickhouse status --name ch1     # 状态、ping、端点、版本、路径、覆盖项数量
pg logs addon clickhouse --name ch1 -f    # 容器日志(stdout/stderr)
pg addon remove clickhouse --name ch1     # 移除容器与 pg.yaml 条目,保留数据
pg addon remove clickhouse --name ch1 --clean-data   # 同时删除实例目录与自定义数据目录

start 在启动前会从 pg.yaml 重新渲染生成的配置与密码文件,因此停止中的实例会拾取 config set --no-restart 的改动与变更后的 admin 密码。HTTP /ping 可应答时,status 报告 running (ping ok)。

不带 --clean-data 时,remove 会在磁盘上保留 <实例目录>(数据、配置、日志、备份、secret)与自定义 --data-dir;同名重装即可复活数据,并复用保留的 admin 密码(用 --admin-password 可覆盖)。--clean-data 会同时删除实例目录与自定义数据目录(uid 101 属主的文件经 podman unshare 删除),但从不删除已拉取的镜像。

重启后自动启动:

pg autostart enable --clickhouse --name ch1
pg autostart status
pg autostart disable --clickhouse --name ch1

开机启动同样会先重渲染配置与 secret,再启动现有容器。pg start 会在 nginx 之前启动所有启用 autostart 的 ClickHouse 实例,因此指向 ClickHouse HTTP 端口的 nginx 后端在代理启动时已经就绪。

生产注意事项

  • 仅单机。 本插件没有复制、分片与 HA。ReplicatedMergeTree/Keeper 拓扑不在范围内;备份是灾备路径。
  • 无 TLS 监听。 HTTP 与原生端口都是明文。需要带 TLS 在网络中暴露 ClickHouse 时,用 nginx 插件 前置 HTTP 端口(或使用外部代理);不要把原生端口直接暴露给不可信网络。
  • 默认仅回环。 --listen 默认 127.0.0.1。认证虽然始终开启,但网络绑定仍会扩大攻击面;只在可信网络中使用 0.0.0.0。
  • 密码存储。 admin 密码位于 pg.yaml(默认 0644);请保护该文件,并优先用 pg addon password clickhouse 取用,而不是四处复制。
  • 资源占用。 pgcli 不限制容器的 CPU/内存;ClickHouse 自身的内存预算与推荐设置见下文 内存限制。
  • 升级。 --force --version <新版本> 会拉取新 tag 并在同一数据目录上重建容器。ClickHouse 只支持向前升级——绝不要让旧版本服务端启动在新版本写过的数据上。升级前先备份。
  • 数据属主。 数据文件属主为 uid 101。请用 --clean-data 而不是手工删目录;pgcli 会处理权限回退。
  • 平台。 仅 Linux(host 网络);官方镜像为多架构(amd64/arm64)。

内存限制

pgcli 不会给 podman 传 --memory/--cpus,因此容器没有 cgroup 内存上限;内存由 ClickHouse 自己约束:

  • 服务端级。 max_server_memory_usage 是进程级上限(字节)。默认值 0 表示继续按 max_server_memory_usage_to_ram_ratio(默认宿主内存的 0.9)计算。这对独享宿主没问题,共享宿主则很危险。共享时应显式设置绝对值,并给 OS page cache 和其他服务留出余量;独享宿主常见起点是内存的 50%–70%。
  • 并发。 max_concurrent_queries 与 background_pool_size 也是服务端设置,可用 config set 修改。服务端上限覆盖所有并发查询,因此要保证 单查询峰值内存 × 并发数 落在预算内。
  • 查询级。 max_memory_usage、max_bytes_before_external_group_by、max_bytes_before_external_sort、max_threads 属于用户/profile 设置,不是服务端 XML,config set 不会应用它们。可在 pg addon clickhouse shell 启动的 clickhouse-client 中用 SET 只影响当前会话(一次性的 query 调用不共享会话状态),或在 SQL 创建用户时用 CREATE USER ... SETTINGS ... 持久定义。
  • 溢写而非 OOM。 max_bytes_before_external_group_by / max_bytes_before_external_sort 让重聚合和排序在超过阈值时溢写到磁盘,以延迟换更低的内存上限。JOIN 通常是内存大户——按右表大小估算,或避免大表 JOIN。

查看当前生效值:

pg addon clickhouse query --name ch1 "SELECT name, value, changed FROM system.server_settings WHERE name = 'max_server_memory_usage'"

示例——把整个服务端限制在 8 GiB,并允许 20 个并发查询:

pg addon clickhouse config set max_server_memory_usage=8589934592 max_concurrent_queries=20 --name ch1

数值单位是字节;config set 默认会重启运行中的容器(--no-restart 推迟到下次启动)。参考:2 vCPU / ~2 GiB 的测试 VM 用 max_server_memory_usage=805306368(768 MiB)可以跑小数据量 OLAP;大数据量、JOIN 或高基数聚合密集型负载需要按比例增加内存。

CLI 参数速查

pg addon install clickhouse

参数 默认值 说明
--name NAME clickhouse 实例名(map 键);[A-Za-z0-9][A-Za-z0-9_-]*,最长 48 字符
--version TAG 26.8.36.3 docker.io/clickhouse/clickhouse-server 的上游镜像 tag
--data-dir DIR <base_dir>/addon/clickhouse/<name>/data 绝对宿主数据目录,挂载到 /var/lib/clickhouse
--listen ADDR 127.0.0.1 两个监听的绑定地址
--http-port N 自动(基址 8123) HTTP 宿主端口;0 = 自动分配
--tcp-port N 自动(基址 9000) 原生宿主端口;0 = 自动分配;必须与 --http-port 不同
--admin-user USER admin admin 账号名;[A-Za-z0-9_.-]{1,64}
--admin-password PW 自动生成 admin 密码;--force 时默认保留已存值,除非显式指定
--config-option K=V — 点分 XML 路径设置;可重复;替换已存覆盖 map
--force false 重渲染配置/secret 并重建容器,保留数据、密码与设置

pg addon clickhouse status / shell / query [SQL...]

参数/flag 默认值 说明
--name NAME clickhouse 目标实例
SQL... — 仅 query:由位置参数拼接的 SQL
--database DB — 仅 query:追加 --database DB
--file FILE — 仅 query:从宿主文件读取 SQL(- = stdin)
-(位置参数) — 仅 query:从 stdin 读取 SQL

pg addon clickhouse database create|list|drop

参数/flag 默认值 说明
NAME — 数据库名(create/drop,位置参数)
--engine NAME Atomic 裸 engine 名(不含参数)
--comment TEXT — 数据库注释
--if-not-exists false 仅 create
--if-exists false 仅 drop
--yes false 仅 drop:跳过确认

db 可作为 database 的别名。

pg addon clickhouse user create|list|drop

参数/flag 默认值 说明
NAME — 用户名(create/drop,位置参数)
--password PW 自动生成 密码;省略时生成并打印一次
--default-database DB — DEFAULT DATABASE 子句
--host-any 隐含 允许任意主机(没有任何 host flag 时生效)
--host-local false 仅 LOCAL(回环)
--host-ip IP|CIDR — 可重复的 IP host 子句
--host-name NAME — 可重复的 NAME host 子句
--readonly false 追加 SETTINGS readonly = 1
--if-not-exists false 仅 create
--if-exists false 仅 drop
--yes false 仅 drop:跳过确认

pg addon clickhouse grant USER / grant list USER

参数/flag 默认值 说明
USER — 目标用户(位置参数)
--read false 授予 SELECT
--write false 授予 INSERT
--ddl false 授予 CREATE, ALTER, DROP, TRUNCATE, OPTIMIZE
--all false 授予 ALL
--privileges LIST — 额外逗号分隔权限,与上述 flag 合并
--database DB 全部(*) 限定到某个数据库
--table TABLE 全部表 限定到某张表;需要 --database
--revoke false 发出 REVOKE ... FROM 而非 GRANT ... TO
--with-grant-option false 追加 WITH GRANT OPTION(仅授予时)

pg addon clickhouse backup / restore FILE / backups

参数/flag 默认值 说明
FILE — 仅 restore:备份目录内的 .zip 文件
--database DB 必需 要备份的数据库 / 备份中记录的数据库名
--out NAME <db>-<UTC 时间戳>.zip 仅 backup:备份目录内的输出路径
--as NEW — 仅 restore:恢复到新数据库名

pg addon clickhouse config list|get|set|unset|show

参数/flag 默认值 说明
KEY=VALUE... — 仅 set:一个或多个点分 XML 设置
KEY... — get/unset:一个设置键(get 只接受一个)
--name NAME clickhouse 目标实例
--no-restart false 仅 set/unset:下次启动生效,而不是立即重启

生命周期、凭据、日志、autostart

命令 参数 说明
pg addon start clickhouse --name NAME 启动(不存在则创建),等待 /ping
pg addon stop clickhouse --name NAME 停止容器,保留数据
pg addon remove clickhouse --name NAME、--clean-data 移除容器与配置条目;--clean-data 同时删除实例目录与自定义数据目录
pg addon password clickhouse --name NAME 打印 admin 密码
pg logs addon clickhouse --name NAME、-n N(默认 50)、-f 容器日志
pg autostart enable|disable --clickhouse、--name NAME 切换单个实例的开机启动

全局参数

参数 说明
-c, --config FILE 配置文件(默认 ~/.pgcli/pg.yaml)
-o, --output FILE 输出配置路径(默认同一文件)
-i, --instance NAME PostgreSQL 实例选择器——ClickHouse 不使用;请改用 --name

16.33 - Kafka

以托管插件方式运行 Apache Kafka 4.x(KRaft 模式,无需 ZooKeeper)——支持单机或跨主机静态 controller quorum,含 topic 管理、渲染 server.properties 与 host 网络

pg addon install kafka 以托管容器方式运行 Apache Kafka 4.4.0,直接使用官方 docker.io/apache/kafka 多架构镜像。Kafka 4.x 是 KRaft-only:元数据 controller 内建在 broker 进程里(process.roles=broker,controller,combined 模式),不安装也不依赖 ZooKeeper。

和 ClickHouse 一样,Kafka 是顶层插件,不代理某个 PostgreSQL 实例。pgcli 负责渲染 server.properties、固定 KRaft cluster id、绑定挂载数据目录,并以 host 网络启动容器。 单条命令安装一个单机 combined 节点;加上 --cluster、--node-id 和 --peer 则安装跨主机静态 controller quorum 中的一个节点,每台主机各自执行一次 pg addon install kafka(pgcli 不会通过 SSH 远程操作)。见多机集群。

没有认证。 Kafka 默认只使用 PLAINTEXT,--listen 默认 127.0.0.1。TLS 是可选功能 (--tls),只加密传输,不认证客户端(ssl.client.auth=none),且 PLAINTEXT 监听 仍然保留以保持兼容。只有在可信内网或防火墙/代理之后才对外暴露。任何能连到任一监听端口 的人都可以读写所有 topic。

参数

参数 默认值 说明
--name kafka 实例名(map key,也是子命令默认名字)
--version 4.4.0 上游镜像 tag,直接从 docker.io/apache/kafka 拉取
--data-dir <base_dir>/addon/kafka/<name>/data 数据目录,挂载到 /var/lib/kafka/data;必须是绝对路径
--listen 127.0.0.1 PLAINTEXT、SSL(启用 --tls 时)与 CONTROLLER 监听地址。0.0.0.0 会暴露所有已启用的监听端口;集群模式下必须是 0.0.0.0 或本节点 advertise 地址
--advertise-host 127.0.0.1 告知客户端使用的地址,必须是具体 IP(不能是 0.0.0.0);单机模式下同时作为 controller voter 地址,集群模式下是本节点的 voter 地址,因此必须是具体非 loopback IP,且全新集群安装必须显式传入
--broker-port 自动(基准 9092) broker 宿主机端口;0 = 从 kafka_broker_start_port 自动分配
--controller-port 自动(基准 19093) KRaft controller 宿主机端口;0 = 从 kafka_controller_start_port 自动分配。集群安装必须显式传入:所有 peer 都会把它写进 voter 列表
--cluster NAME — 加入名为该值的多机集群;所有主机必须相同,KRaft cluster id 由它确定性推导
--node-id N 单机 1 本节点 1-based KRaft node id;全新集群安装必填,集群内唯一;单机模式下会被拒绝
--peer ID@HOST:PORT — 另一个 controller voter。每个其他节点各写一次(本节点自动折叠进去);每台主机都要传完整 peer 列表。host 必须是 IP,port 必须与该节点的 --controller-port 一致
--tls false 在明文 broker 之外增加 SSL 监听,使用 pgcli 自签 CA 签发的证书;PLAINTEXT 监听保留
--tls-pem — 用这个合并后的 PEM(证书链 + 私钥)提供 TLS,替代自动生成的证书。隐含 --tls;文件会先校验再复制为 config/tls/server.pem。支持 PKCS#8、PKCS#1 与 SEC1 私钥。集群模式下证书必须覆盖本节点的 --advertise-host
--tls-port 自动(基准 9093) SSL 监听宿主机端口;0 = 从 kafka_tls_start_port 自动分配;需要 --tls
--cluster-id 集群推导 / 单机生成 集群模式下 id 由 --cluster 推导,显式值必须与之相等;单机模式可显式采用 22 字符 KRaft cluster id,否则采纳 meta.properties 或生成
--heap SIZE 镜像默认(1G) JVM 堆大小,以 KAFKA_HEAP_OPTS=-XmsSIZE -XmxSIZE 传入(512m、2g、4g);集群中每节点各自设置;修改需要 --force
--config-option K=V — 额外的 server.properties 配置(num.partitions=3、log.retention.hours=168);可重复;替换已存储的覆盖表
--force false 重新渲染 server.properties 并重建容器;保留数据、已存储的 cluster id 与 node id;增删 TLS 必须带 --force。集群身份字段(cluster、node id、cluster id、peers、advertise host、controller port)即使带 --force 也不可修改

Kafka 不使用 --image,用 --version。

controller 默认端口是 19093 而不是 9093——9093 属于 Alertmanager 的默认端口池(TLS 池 虽然从 9093 开始,但会跳过其他插件已占用的端口)。拓扑相关键由 pgcli 自己渲染, --config-option 只能添加非托管配置:process.roles、node.id、listeners、 advertised.listeners、协议映射、controller 监听/voters、log.dirs、SSL keystore/ 客户端认证键、内部 topic 的 replication factor、default.replication.factor、 min.insync.replicas 等都会被拒绝。

安装

pg addon install kafka --name k1
✓ Kafka "k1" installed
  Version:     4.4.0
  Image:       docker.io/apache/kafka:4.4.0
  Container:   pgcli-kafka-k1
  Broker:      127.0.0.1:9092
  Controller:  127.0.0.1:19093
  Node ID:     1
  Cluster ID:  <22-character id>
  Data dir:    /home/you/.pgcli/addon/kafka/k1/data
  Config:      /home/you/.pgcli/addon/kafka/k1/config/server.properties

第二个实例会从各自的端口池取下一个空闲端口(例如端口未被占用时的 9093/19094):

pg addon install kafka --name k2 --data-dir /srv/kafka-data --listen 0.0.0.0 --advertise-host 192.0.2.10

不带 --force 重复 install 是幂等的,只打印当前状态。--force 会重写配置并重建容器, 保留数据目录、cluster id 与 node id。对已格式化的数据目录改 --cluster-id 会被拒绝—— 数据属于原来的 cluster id。

镜像直接从 Docker Hub 拉取,不涉及 pgcli 自建镜像。--version 接受任意合法的上游 tag, 但 tag 必须真实存在,否则 pull 失败。

多机集群

集群是静态 KRaft quorum:每个节点都携带完整的 controller.quorum.voters 列表, 成员关系不能在线变更。每台主机用一个节点安装,使用相同的 --cluster 名字、集群内唯一的 --node-id、本机的 --advertise-host、显式的 --controller-port,并为每个其他 节点传一条 --peer。命令由你在每台主机上分别执行;pgcli 不会通过 SSH 批量操作。

以三台主机 192.0.2.11、192.0.2.12、192.0.2.13 为例:

# 节点 1,在 192.0.2.11 上
pg addon install kafka --name prod --cluster prod --node-id 1 \
  --listen 0.0.0.0 --advertise-host 192.0.2.11 \
  --broker-port 9092 --controller-port 19093 \
  --peer 2@192.0.2.12:19093 --peer 3@192.0.2.13:19093

# 节点 2,在 192.0.2.12 上
pg addon install kafka --name prod --cluster prod --node-id 2 \
  --listen 0.0.0.0 --advertise-host 192.0.2.12 \
  --broker-port 9092 --controller-port 19093 \
  --peer 1@192.0.2.11:19093 --peer 3@192.0.2.13:19093

# 节点 3,在 192.0.2.13 上
pg addon install kafka --name prod --cluster prod --node-id 3 \
  --listen 0.0.0.0 --advertise-host 192.0.2.13 \
  --broker-port 9092 --controller-port 19093 \
  --peer 1@192.0.2.11:19093 --peer 2@192.0.2.12:19093

身份信息是这样拼出来的:

  • Cluster ID。 由 --cluster 确定性哈希推导,所有主机无需传 --cluster-id 也能得到 相同值。若显式传入 --cluster-id,必须等于推导值。
  • Voters。 ID@HOST:PORT 按 node id 排序;本节点的条目来自 --node-id、 --advertise-host 和 --controller-port,其余由 peer 提供。两个 voter 不允许共享 同一个 controller endpoint。
  • Controller 端口。 它出现在所有 peer 的 voter 列表里,所以全新集群安装必须显式 传 --controller-port(默认且推荐值:19093)。自动分配可能在其他主机选到不同端口。
  • 端口与地址。 不同主机可以使用相同的 broker/controller 端口。--listen 必须是 0.0.0.0(或本节点的 advertise 地址);loopback 会被拒绝。

前两台安装会打印 controller quorum 尚未就绪的警告——在最后一个 voter 装好之前这是正常的。 集群模式下就绪等待是 15 秒而不是 90 秒。所有节点起来后:

pg addon kafka cluster status --name prod    # leader + 3 个 voter
pg addon kafka status --name prod            # cluster、node x of N、voter、voters、RF/minISR
pg addon list                                # 显示 "cluster prod" 和 voter 列表

Topic 默认副本数跟随 quorum 大小:--replication-factor 0 表示 min(3, voters) (三节点即 3),min.insync.replicas = min(2, RF)。允许两 voter 集群,但零容错—— 失去任一节点就失去元数据 quorum——因此 pgcli 会打印警告。HA 请用 3 或另一个奇数。

安装后集群身份不可变,即使带 --force:--cluster、--node-id、--cluster-id、 --peer 集合、--advertise-host 和 --controller-port 变化都会被拒绝。端口、TLS、 版本、--data-dir 与 --config-option 仍可通过 --force 修改。要改变拓扑,只能在本机 remove 后重装;本插件不提供在线增删 controller。

端口

端点 默认值 协议 端口池
Broker 9092 Kafka 客户端协议(PLAINTEXT) kafka_broker_start_port,默认 9092
Controller 19093 KRaft 元数据 quorum(PLAINTEXT) kafka_controller_start_port,默认 19093
SSL(TLS) 9093(启用 --tls 时) TLS 上的 Kafka 客户端协议 kafka_tls_start_port,默认 9093

三个池共享同一个"已分配"集合,并预占其他流式插件(Prometheus、ClickHouse、InfluxDB、 influx2tsdb 和对象存储)的显式端口,避免互相抢占。Kafka 端口分配还会显式避让 Alertmanager 已占端口(Alertmanager 池也反向预占 Kafka 端口),因为两者的默认池都在 9093 附近。broker、controller 与 TLS 端口必须互不相同,且所有 Kafka 实例的端口必须 全局唯一。自动分配会跳过宿主机上已在监听的端口。

--listen 为通配地址时,pgcli 自己的 topic/健康检查命令走 127.0.0.1,打印的本地 controller 端点同理。单机模式的静态 voter 是 AdvertiseHost:ControllerPort,所以如果 把 --advertise-host 设成内网地址,必须同时把 --listen 设为 0.0.0.0(或同一个 内网地址),否则 controller 连不上自己。

集群模式下 controller 端口不只是本地监听——它会被其他所有主机写进自己的 controller.quorum.voters。所有节点请使用相同的 --controller-port(除非你刻意让每个节点 用不同端口,此时每条 --peer 必须携带对应节点的正确端口),并确保该端口在所有主机上 都空闲。端口不一致意味着 voter 列表不同,quorum 永远无法形成。broker 端口是按节点广播的, 可以不同,但全部使用 9092 能让客户端 bootstrap 列表更简单。

副本默认值

副本数跟随静态 voter 数量,单机和集群都不需要手工修改 server.properties:

Voters Topic 默认 RF min.insync.replicas 可容忍故障(acks=all)
1 1 1 不支持任何 broker 故障
2 2 2 不支持任何故障(两个副本必须都在同步中)
3+ 3 2 停 1 台
  • 同样的值会写入 default.replication.factor、min.insync.replicas 以及内部 topic(offsets、transaction.state.log、share coordinator),因此新集群在创建 任何业务 topic 之前就能提交 offset 和事务。
  • pg addon kafka topic create NAME 使用该默认值;可用 --replication-factor N 按 topic 覆盖。RF 不能超过实际承载该分区数据的 broker 数。
  • 集群再大 RF 也封顶为 3:更多副本只会增加写入成本和延迟,在 min.insync.replicas=2 下并不能多容忍一台机器。
  • min.insync.replicas=2 时,ISR 少于 2 的分区会拒绝 acks=all 写入(Kafka 3.0 起生产者的默认值)。用 pg addon kafka topic describe NAME 查看 ISR。

TLS

用 --tls 启用 SSL 监听:

pg addon install kafka --name k1 --tls                 # SSL 使用 9093 起的首个空闲端口
pg addon install kafka --name k1 --tls --tls-port 9093

install 与 status 会同时打印两个端点和 CA 路径:

  Broker:      127.0.0.1:9092
  TLS:         127.0.0.1:9093 (CA .../addon/kafka/k1/config/tls/ca.crt)
  Controller:  127.0.0.1:19093

PLAINTEXT 监听保持开启,已有客户端不受影响。TLS 只在创建或 --force 时生效; 只传 --tls-port 而不传 --tls 会报错。关闭用 --tls=false --force——生成的文件仍留在 config/tls/,重新启用时复用。

证书由 pgcli 自签 CA(与 MinIO/silo/rustfs 相同的 helper)签发,SAN 覆盖 localhost、 宿主机所有网卡地址和 --advertise-host。客户端需要信任 config/tls/ca.crt。

集群 TLS

每个节点各自签发独立的 CA 和叶子证书,因此可能连接任意 broker 的客户端需要把它们 放进同一个信任文件:

# 在各主机上取出该节点的 CA,然后在客户端上拼接。
for v in vm01 vm02 vm03; do
  ssh $v 'cat ~/.pgcli/addon/kafka/prod/config/tls/ca.crt'
done > kafka-ca-bundle.pem

SSL 监听是按节点开启的:需要统一的 SSL bootstrap 列表时请给所有节点启用 --tls。 如果只有部分节点启用了 TLS,这些节点会广播 SSL://... 端点,其余节点只广播 PLAINTEXT://...;使用 security.protocol=SSL 的客户端仍能连到启用了 TLS 的节点, 但纯明文节点保持明文。请逐台执行 --tls --force,保证三 voter 的 quorum 始终保留两个。

--tls 只增加面向客户端的 SSL 监听。CONTROLLER quorum 与 broker 间流量始终是 PLAINTEXT,所以增删 TLS 不会破坏 quorum。

使用自带证书(pg cert)

Kafka 的 PEM keystore 只读一个文件:先是证书链,随后是私钥。pg cert 会把证书和私钥 分别写出,因此安装前先手动合并:

# 1. 签发服务端证书。--host 要包含客户端实际拨号的所有地址,
#    尤其是 --advertise-host。
pg cert --host 127.0.0.1,192.0.2.11 \
  --cert-file kafka.crt --key-file kafka.key

# 2. 合并证书(链)与私钥为一个 PEM。
cat kafka.crt kafka.key > kafka-server.pem

# 3. 用合并后的文件安装 —— --tls 会被自动启用。
pg addon install kafka --name k1 --tls-pem ./kafka-server.pem

有中间证书时把叶子证书放在最前:cat leaf.crt intermediate.crt key.key > kafka-server.pem。

安装时 pgcli 会校验:文件包含证书且只有一个未加密私钥、两者匹配、证书当前有效、允许 服务端认证用途、且不是 CA 证书。还会检查 SAN 是否覆盖 --advertise-host:集群模式下 证书不覆盖本节点 advertise IP 是硬错误(其他 voter 和客户端会用该地址访问本节点); 单机模式只打印警告。支持 PKCS#8(BEGIN PRIVATE KEY,即 pg cert 的输出)、 PKCS#1(BEGIN RSA PRIVATE KEY)与 SEC1(BEGIN EC PRIVATE KEY);加密私钥会被拒绝。

pgcli 会把合并后的文件复制为 config/tls/server.pem(0600,属主 uid 1000),不会 生成 ca.crt 或 public.crt/private.key,源文件保持不变。自动续期不适用:更换源 PEM 后 需要带 --force 重新 install,才能把新证书装进新容器。

自签的 pg cert 证书需要客户端直接信任证书文件——把下文示例中的 ca.crt 换成 kafka.crt。如果证书由公共或私有 CA 签发,则让客户端信任该 CA 链,并把中间证书一并 放进 kafka-server.pem。

集群中请为每个节点单独签发覆盖其 advertise IP 的证书,并且只传给该节点的 install:

# 节点 1,其 --advertise-host 为 192.0.2.11:
pg cert --host 192.0.2.11 --cert-file kafka-node1.crt --key-file kafka-node1.key
cat kafka-node1.crt kafka-node1.key > kafka-node1.pem
pg addon install kafka --name prod --cluster prod --node-id 1 --force ... --tls-pem ./kafka-node1.pem

# 客户端需要信任所有节点:把各节点证书(或 CA)拼起来。
cat kafka-node1.crt kafka-node2.crt kafka-node3.crt > kafka-ca-bundle.pem

如果某节点的证书没有覆盖它的 --advertise-host,集群安装会在重建容器之前被拒绝。

要从自带证书切回 pgcli 生成的证书,用 --tls --tls-pem '' --force 重新安装; --tls-pem 传空值会清除已存储的路径。

Java 客户端(client.properties):

security.protocol=SSL
ssl.truststore.type=PEM
ssl.truststore.location=/path/to/addon/kafka/k1/config/tls/ca.crt

librdkafka 系客户端:

security.protocol=SSL
ssl.ca.location=/path/to/addon/kafka/k1/config/tls/ca.crt

用 OpenSSL 验证监听:

openssl s_client -connect 127.0.0.1:9093 \
  -CAfile <base_dir>/addon/kafka/k1/config/tls/ca.crt -verify_return_error </dev/null

在容器内通过 TLS 读写(TLS 目录随 config 目录只读挂载):

pg addon kafka shell --name k1 -- bash -lc \
  'printf "security.protocol=SSL\nssl.truststore.type=PEM\nssl.truststore.location=/mnt/shared/config/tls/ca.crt\n" > /tmp/client.properties
   printf "hello\n" | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9093 --producer.config /tmp/client.properties --topic demo'

pg addon kafka shell --name k1 -- bash -lc \
  'printf "security.protocol=SSL\nssl.truststore.type=PEM\nssl.truststore.location=/mnt/shared/config/tls/ca.crt\n" > /tmp/client.properties
   /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9093 --consumer.config /tmp/client.properties --topic demo --from-beginning --max-messages 1'
  • TLS 只负责加密:ssl.client.auth=none,客户端无需证书,能连到 SSL 端口的人依然可以连接。 监听地址仍是唯一的安全边界。
  • 请用 IP 连接(或证书中存在的名字)。不在宿主机网卡地址中的 DNS 名不在 SAN 列表里, 校验会失败。
  • 叶子证书在剩余有效期不足 30 天时自动重新签发(install、start 或 --force 时检查)。 CA 保持不变,客户端无需更换信任文件。该行为只适用于 pgcli 生成的证书;使用 --tls-pem 时续期由你负责。

pgcli 创建的内容

单个实例的全部状态位于 <base_dir>/addon/kafka/<name>/:

宿主机路径 容器路径 说明
config/server.properties /mnt/shared/config(目录,只读) 生成的拓扑配置;镜像入口会把它合并进运行时配置
config/tls/ /mnt/shared/config/tls(只读) 仅 --tls:server.pem(证书链 + 私钥,0600,属主 uid 1000);由 pgcli 生成证书时还有 ca.crt(0644)、public.crt 与 private.key。使用 --tls-pem 时只写 server.pem——即你提供的合并 PEM 的规范化副本
data/ /var/lib/kafka/data 日志段、meta.properties 与 KRaft 元数据

生成的 server.properties 关键内容:

process.roles=broker,controller
node.id=1
listeners=PLAINTEXT://127.0.0.1:9092,CONTROLLER://127.0.0.1:19093
advertised.listeners=PLAINTEXT://127.0.0.1:9092
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
controller.quorum.voters=1@127.0.0.1:19093
log.dirs=/var/lib/kafka/data

启用 --tls 后,监听与安全配置变为:

listeners=PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093,CONTROLLER://127.0.0.1:19093
advertised.listeners=PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL
ssl.keystore.type=PEM
ssl.keystore.location=/mnt/shared/config/tls/server.pem
ssl.client.auth=none

三节点集群示例(展示 node 2 of prod;--listen 0.0.0.0):

process.roles=broker,controller
node.id=2
listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:19093
advertised.listeners=PLAINTEXT://192.0.2.12:9092
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
controller.quorum.voters=1@192.0.2.11:19093,2@192.0.2.12:19093,3@192.0.2.13:19093
log.dirs=/var/lib/kafka/data
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
transaction.state.log.min.isr=2
default.replication.factor=3
min.insync.replicas=2

voter 列表在三台主机上完全相同并按 node id 排序;只有本节点的 node.id 一行不同。 副本数为 min(3, voters),ISR 为 min(2, RF)。

再加上内部 topic 与自动创建 topic 的副本配置:单 voter 时 offsets.topic.replication.factor=1、transaction.state.log.replication.factor=1、 transaction.state.log.min.isr=1、share coordinator 的同名项、 default.replication.factor=1、min.insync.replicas=1。--config-option 覆盖项按 key 排序追加在托管配置之后。

容器以 --network host、--restart unless-stopped、30 秒停止超时运行,并显式传入 -e CLUSTER_ID=<id>,确保镜像用存储的 id 格式化数据目录(否则镜像会注入自己的固定 默认值)。

官方镜像以 uid/gid 1000(appuser) 运行。首次启动前,pgcli 用一次性的 podman unshare chown -R 1000:1000 把数据目录交给该用户。一旦 meta.properties 存在,说明目录属主已正确,后续 start 和 --force 重建都不会再递归遍历日志目录。 启用 --tls 时只把合并后的 config/tls/server.pem 交给 uid/gid 1000(镜像以 appuser 读取 PEM keystore);config 目录、CA 与原始私钥仍归宿主机所有,目录保持可遍历 以支持只读挂载。

Topic

pg addon kafka topic create demo --name k1
pg addon kafka topic create events --partitions 3 --replication-factor 1 --name k1
pg addon kafka topic create logs --config retention.ms=604800000 --name k1
pg addon kafka topic list --name k1
pg addon kafka topic list --exclude-internal --name k1
pg addon kafka topic describe demo --name k1
pg addon kafka topic delete demo --name k1 --yes
pg addon kafka topic retention demo 7d --name k1   # 设置 retention.ms
pg addon kafka topic retention demo --name k1      # 查看当前覆盖项
  • --partitions 默认使用 broker 的 num.partitions(上游默认 1)。 --replication-factor 0(默认值)表示"本实例的 RF"——单机为 1,集群为 min(3, voters)(三节点即 3)。
  • --config KEY=VALUE 可重复,直接透传给 kafka-topics.sh。
  • delete 默认要求确认,--yes 跳过;删除 topic 会删除其全部消息。
  • retention NAME [DURATION] 查看或设置 retention.ms。时长支持 ms/s/m/h/d/w 后缀(30m、12h、7d、4w)、纯数字毫秒, 或 -1 / infinite / forever 表示永久保留。不带时长时打印 topic 的 动态覆盖项;结果为空表示使用 broker 默认值。保留时间按时间清理旧 segment, 即使消息从未被消费也会被删除。
  • 所有 topic 命令都要求实例正在运行。

用容器内的 Kafka 脚本写入和读取(脚本位于 /opt/kafka/bin,不在 PATH 中):

pg addon kafka shell --name k1 -- bash -lc \
  'printf "hello\nworld\n" | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic demo'

pg addon kafka shell --name k1 -- bash -lc \
  '/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --topic demo --from-beginning --max-messages 2'

外部客户端使用打印出的 broker 端点作为 --bootstrap-server。对默认 loopback 监听, 宿主机客户端直接用 127.0.0.1:9092 即可。

集群中每个 broker 广播自己的地址,客户端可以从任意节点 bootstrap,随后被重定向到分区 leader。生产者和消费者可以运行在不同主机上:

pg addon kafka topic create e2e --partitions 3 --replication-factor 3 --name prod

# 在节点 1 上通过本地 broker 写入:
pg addon kafka shell --name prod -- bash -lc \
  'printf "alpha\nbeta\n" | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server 192.0.2.11:9092 --topic e2e'

# 在节点 3 上从节点 2 的 broker 读取:
pg addon kafka shell --name prod -- bash -lc \
  '/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 192.0.2.12:9092 --topic e2e --from-beginning --max-messages 2'

用 kcat 在宿主机测试

kcat 是轻量的原生生产/消费工具,适合在容器外做 冒烟测试(Debian/Ubuntu apt install kcat,macOS brew install kcat)。从任意节点 bootstrap 即可,客户端会自动跟随 advertised leader。示例使用上文创建的 e2e topic(3 分区、RF=3):

# PLAINTEXT
printf 'alpha\nbeta\n' | kcat -P -b 192.0.2.11:9092 -t e2e
kcat -C -q -b 192.0.2.12:9092 -t e2e -o beginning -e -c 2   # 前两条
kcat -C -q -b 192.0.2.13:9092 -t e2e -o -1 -e -c 1         # 最后一条

TLS 场景下把 kcat 指向集群 TLS 一节拼出的 CA bundle,并选择 SSL (TLS 监听端口为 9093):

printf 'gamma\n' | kcat -P -b 192.0.2.11:9093 -t e2e \
  -X security.protocol=SSL -X ssl.ca.location=kafka-ca-bundle.pem
kcat -C -q -b 192.0.2.12:9093 -t e2e -o -1 -e -c 1 \
  -X security.protocol=SSL -X ssl.ca.location=kafka-ca-bundle.pem
  • -o beginning / -o -1 分别从第一条 / 最后一条开始;-e 读到 EOF 退出, -c N 消费 N 条后退出;-q 只输出消息体。
  • -b 使用 advertised 地址即可,客户端会被重定向到分区 leader,无需逐分区配置。
  • 这些示例不加入消费组:kcat 直接读分区,不提交 offset,因此 pg addon kafka group list 里不会出现任何 group。要以消费组方式读取,加 -G NAME(topic 改为位置参数),见消费组。

消费组

kafka-consumer-groups.sh 的日常查询封装成了两个子命令:

pg addon kafka group list --name k1
pg addon kafka group describe demo-group --name k1
  • list 列出在本集群提交过 offset 的 group id。
  • describe 输出 group 状态、成员,以及每个分区的 CURRENT-OFFSET / LOG-END-OFFSET / LAG 和消费者 id、host。LAG 持续增长说明消费速度跟不上。
  • group 在第一次提交 offset 后才会出现;group.id 由客户端设置,pgcli 不创建 消费组。冒烟测试可以先带 --group 消费一次:
pg addon kafka shell --name prod -- bash -lc \
  '/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 192.0.2.11:9092 --topic e2e --group demo-group --from-beginning --max-messages 2'
pg addon kafka group list --name prod
pg addon kafka group describe demo-group --name prod

在宿主机上用 kcat 做同样的事则用 -G;组模式下 topic 是位置参数,不能用 -t:

kcat -C -q -G demo-group -b 192.0.2.11:9092 \
  -X auto.offset.reset=earliest -c 2 e2e
pg addon kafka group list --name prod
pg addon kafka group describe demo-group --name prod

上文各节的 console-consumer 和 kcat 示例刻意不带 group(读完即退出的冒烟 测试),因此不会创建消费组。

  • 两个命令都要求实例正在运行。消费者停止且未提交 offset 时 group 会留在 Empty 状态,offset 和 lag 仍可见,直到 offset 过期。

状态与 quorum

pg addon kafka status --name k1          # 容器状态、端点、id、路径
pg addon kafka cluster status --name k1  # KRaft 元数据 quorum 的 leader 与 voters
pg logs addon kafka --name k1 -f         # broker 日志(stdout/stderr)

cluster status 执行 kafka-metadata-quorum.sh --bootstrap-controller <本地 controller> describe --status;健康的单机会打印 cluster id、leader 和一个 voter。集群中它会报告 leader 以及所有 voter 的日志延迟和是否已追上——在任意节点执行均可:

ClusterId:              <推导出的 id>
LeaderId:               1
CurrentVoters:          [{"id": 1, ...}, {"id": 2, ...}, {"id": 3, ...}]

N 个 voter 容忍 (N-1)/2 台节点停机——三节点容忍一台,两节点一台都不能停。停掉多数派 会失去元数据 quorum:broker 的现有分区只能按 min.insync.replicas 允许的范围继续读写, 在 quorum 恢复前无法进行 leader 选举或元数据变更。

install 和 start 最多等待 90 秒,直到 kafka-broker-api-versions.sh 在本地 broker 端点返回结果(集群模式为 15 秒,因为完整 quorum 要等最后一个 voter 装好才形成)。 一直不就绪会让命令失败并打印最后几行日志;数据会保留。

配置

安装时可通过 server.properties 覆盖项补充配置:

pg addon install kafka --name k1 \
  --config-option num.partitions=3 \
  --config-option log.retention.hours=168

--config-option 替换已存储的覆盖表(不是合并)。覆盖项保存在 pg.yaml 的 addons.kafka.<name>.settings 下,每次启动都会重新渲染,因此重启和重建容器都不会丢失。 本版本没有单独的 config set 命令;修改覆盖项请带 --force 重新安装,并传入完整的 覆盖表。

key 允许字母、数字、点、下划线和连字符;value 不允许 NUL、CR、LF。pgcli 托管的 key 会被拒绝,而不是被静默覆盖。

生命周期

pg addon start kafka --name k1            # 启动,不存在则创建,并等待就绪
pg addon stop kafka --name k1
pg addon remove kafka --name k1           # 删除容器 + pg.yaml 条目,保留数据/配置
pg addon remove kafka --name k1 --clean-data   # 同时删除实例目录和自定义数据目录

不带 --clean-data 时,remove 会留下 <实例目录>(配置 + 数据)以及自定义 --data-dir。同名重装会采纳已格式化的存储:自动复用 meta.properties 里的 cluster.id 与 node.id,而不是生成新集群——因此重装可以恢复原有日志。

--clean-data 删除实例目录以及(若不同)自定义数据目录(uid 1000 的文件通过 podman unshare 删除),但不会删除已拉取的镜像。如果把实例指向属于其他 cluster id 或 node id 的数据目录,会在创建容器前被拒绝。

集群中 remove 只影响本机——pgcli 不会联系其他节点,它们的静态 voter 列表仍然引用 本节点。pg addon remove 会先打印 quorum 影响。三个 voter 时,移除(或停止)一个节点 集群仍可用;移除第二个会失去 quorum。之后重新加入必须使用相同的 --cluster、 --node-id 和 --peer 列表;--clean-data 后数据目录为空,broker 会用相同的推导 cluster id 重新格式化。

开机自启同样是按主机的——请在每个节点上启用:

pg autostart enable --kafka --name prod   # 在每台主机执行
pg autostart status
pg autostart disable --kafka --name prod

开机启动会先重新渲染 server.properties,并且是幂等的(pg start 也会启动 autostart 启用的 Kafka 实例)。

生产注意事项

  • 静态 quorum,不能在线扩缩。 --cluster 安装的是固定 KRaft quorum 中的一个节点, 没有在线增删 controller 的能力:扩容或缩容需要用新的 voter 列表重装节点。HA 请使用 3(或其他 ≥3 的奇数)个 voter;允许两 voter,但零容错。
  • 默认仍是单机。 不带 --cluster 时没有副本也没有 HA:broker 停止后该实例无法读写。
  • KRaft combined 模式。 broker 和 controller 共用一个进程,适合开发和小规模部署; 上游建议大型关键集群使用独立的 controller/broker 角色,本插件暂不提供。
  • 无认证;TLS 只负责传输加密。 PLAINTEXT 监听始终开启;--tls 增加的加密监听因 ssl.client.auth=none 而不认证客户端。能连到任一端口的人都可以读写所有 topic。 集群模式下 CONTROLLER quorum 与 broker 间流量同样是 PLAINTEXT——--tls 只覆盖客户端 连接。保持默认 loopback,或只在可信内网暴露。如需要真正的认证(SASL/mTLS),请自行在 Kafka 前放置代理/网关。
  • 没有 ZooKeeper。 Kafka 4.x 是 KRaft-only,不存在 ZooKeeper 容器; zookeeper.connect 作为托管 key 会被拒绝。
  • 副本不是备份。 topic 副本保障的是可用性,不能防误删或逻辑损坏。 离线备份请停止实例后复制数据目录;本版本没有 backup/restore 命令,且单个集群节点的 离线拷贝并不是完整的集群备份。
  • 升级。 --force --version <新版本> 会拉取新 tag 并在同一数据目录上重建容器。 Kafka 不保证新版本写入的数据能回退到旧版本——升级前请做离线拷贝。 --version 必须是真实存在的上游 tag。
  • JVM 堆。 镜像默认 -Xms1G -Xmx1G。节点承载大量分区/连接时用 --heap 调大,但要给 OS page cache 留足内存——Kafka 主要读 page cache,堆超过约 6–8 GB 通常弊大于利。集群中每节点独立设置;修改会用 --force 重建该节点容器, 逐台执行。
  • 数据属主。 数据文件属主为 uid/gid 1000。清理请用 --clean-data,不要手工删除; pgcli 会处理权限回退。
  • 端口。 broker 默认 9092;controller 默认 19093,避开 Alertmanager 默认的 9093;启用 TLS 时 SSL 从 9093 起分配并跳过已占端口。host 网络下它们就是普通 宿主机端口。
  • 平台。 Linux(host 网络);官方镜像多架构(amd64/arm64)。

命令行参数参考

pg addon install kafka

参数 默认值 说明
--name NAME kafka 实例名;[A-Za-z0-9][A-Za-z0-9_-]*,最多 48 字符
--version TAG 4.4.0 上游 docker.io/apache/kafka tag
--data-dir DIR <base_dir>/addon/kafka/<name>/data 绝对路径宿主机数据目录,挂载到 /var/lib/kafka/data
--listen ADDR 127.0.0.1 broker、SSL(启用时)与 controller 的监听地址;集群模式要求 0.0.0.0 或 advertise 地址
--advertise-host IP 127.0.0.1 对外广播的 broker 地址(也是本节点的 controller voter 地址);不能是通配地址;全新集群安装必须显式传入
--broker-port N 自动(基准 9092) broker 宿主机端口;0 = 自动分配
--controller-port N 自动(基准 19093) controller 宿主机端口;0 = 自动分配;全新集群安装必须显式传入,因为每个 peer 都会内嵌它
--cluster NAME — 多机集群名;所有主机相同;KRaft cluster id 由它推导
--node-id N 单机 1 1-based node id,集群内唯一;全新集群安装必填;单机模式会被拒绝
--peer ID@HOST:PORT — 另一个节点的 controller voter;每个其他节点各一条;所有主机传相同 peer 列表
--tls false 增加 SSL 监听;PLAINTEXT 监听保留
--tls-pem FILE — SSL 监听使用的合并 PEM(证书链 + 私钥);隐含 --tls;路径可为相对或绝对;集群模式下必须覆盖 --advertise-host
--tls-port N 自动(基准 9093) SSL 监听宿主机端口;0 = 自动分配;需要 --tls
--cluster-id ID 集群推导 / 单机生成 KRaft cluster id;集群模式下必须等于由 --cluster 推导出的值
--heap SIZE 镜像默认(1G) JVM 堆,以 KAFKA_HEAP_OPTS=-XmsSIZE -XmxSIZE 传入(512m、2g、4g);集群中每节点各自设置;修改需要 --force
--config-option K=V — 额外 server.properties 配置;可重复;替换已存储的覆盖表
--force false 重新渲染配置并重建容器,保留数据和 id;增删 TLS 必须使用;不能修改集群身份字段

pg addon kafka status / shell

参数 默认值 说明
--name NAME kafka 目标实例
-- COMMAND... /bin/bash 仅 shell:执行命令而不是打开交互式 shell

pg addon kafka topic create|list|describe|delete|retention

参数 默认值 说明
NAME — topic 名(create/describe/delete/retention 的位置参数)
DURATION — 仅 retention:新的 retention.ms(30m、12h、7d、4w、纯毫秒,或 -1/infinite);省略则查看当前覆盖项
--partitions N broker 默认(1) 仅 create:分区数
--replication-factor N 0 = 实例 RF(单机 1,集群 min(3, voters)) 仅 create:副本数
--config K=V — 仅 create:topic 配置;可重复
--exclude-internal false 仅 list:隐藏 Kafka 内部 topic
--yes false 仅 delete:跳过确认

pg addon kafka group list|describe

参数 默认值 说明
GROUP — 消费组 id(仅 describe 的位置参数)
--name NAME kafka 查询哪个实例的本地 broker

pg addon kafka cluster status

参数 默认值 说明
--name NAME kafka 查询哪个实例的本地 controller

生命周期、日志与开机自启

命令 参数 说明
pg addon start kafka --name NAME 启动(不存在则创建),等待就绪
pg addon stop kafka --name NAME 停止容器,保留数据
pg addon remove kafka --name NAME、--clean-data 删除容器与配置条目;--clean-data 同时删除实例目录和自定义数据目录
pg logs addon kafka --name NAME、-n N(默认 50)、-f 容器日志
pg autostart enable|disable --kafka、--name NAME 开关单个实例的 boot 启动

全局参数

参数 说明
-c, --config FILE 配置文件(默认 ~/.pgcli/pg.yaml)
-o, --output FILE 输出配置路径(默认与输入相同)
-i, --instance NAME PostgreSQL 实例选择器——Kafka 不使用,请用 --name

16.34 - RabbitMQ

以托管插件方式运行 RabbitMQ broker——单节点,或通过 etcd 自动发现组建多机集群;AMQP 与 management Web UI/API、可选 TLS(自动生成 CA 或自带证书)、以及队列管理(list/declare/info、publish/get、bind/unbind、purge/delete)

pg addon install rabbitmq 以托管容器方式运行 RabbitMQ 4.4.0,直接使用官方 docker.io/library/rabbitmq 的 -management 镜像(内含 Web UI 与 HTTP API)。和 Kafka、ClickHouse 一样,RabbitMQ 是顶层插件:pgcli 负责渲染 rabbitmq.conf、管理 管理员账号、绑定挂载数据目录,并以 host 网络启动容器。

插件支持单节点,或作为多机集群的一个成员。集群使用 etcd 自动发现 (rabbitmq_peer_discovery_etcd):每台主机以相同的 --cluster 名和 etcd endpoint 列表安装 一个成员,首次启动时成员自动注册并互相发现——不需要静态 peer 列表,也不需要手动 join_cluster。不配置 federation、shovel、MQTT 或 STOMP;vhost、用户、权限、策略、 exchange 等高级管理通过 management UI 或 pg addon rabbitmq shell 里的 rabbitmqctl 完成。常用队列操作已封装为 pg addon rabbitmq queue 子命令(见队列与消息)。 集群模式下 declare 默认创建 quorum 队列(见集群)。

认证始终开启。 AMQP 与 management UI 都需要 pgcli 创建的管理员账号(默认 admin,20 位自动生成密码)。--listen 默认 127.0.0.1。对外暴露时,密码就是安全 边界:TLS 只加密传输,不认证客户端(verify_none、fail_if_no_peer_cert = false)。

参数

参数 默认值 说明
--name rabbitmq 实例名(map key,也是子命令默认名字)
--version 4.4.0 上游镜像 tag,实际拉取 docker.io/library/rabbitmq:<version>-management
--admin-user admin 管理员账号,首启创建,每次启动对账
--admin-password 自动生成(20 位) 管理员密码;同时存入 pg.yaml 与 0600 secret 文件。集群模式拒绝明文,请用 --secrets-from-etcd 或 --admin-password-file
--admin-password-file FILE — 从 FILE 读取管理员密码(单行)。集群成员首次安装必填(使用 --secrets-from-etcd 时除外);单机实例也可使用
--data-dir <base_dir>/addon/rabbitmq/<name>/data 数据目录,挂载到 /var/lib/rabbitmq;必须是绝对路径
--listen 单机 127.0.0.1,集群 0.0.0.0 AMQP、AMQPS(启用 --tls 时)与 management 监听共用的绑定地址。0.0.0.0 会暴露所有已启用监听;集群模式不能是 loopback
--cluster NAME — 加入/创建名为 NAME 的 RabbitMQ 集群。空 = 单机;所有成员必须相同
--node-name NODE@HOST — 本成员的 Erlang 节点名(如 rabbit@192.0.2.11)。集群成员首次安装必填;必须唯一。host 为 IP 时无需名称解析;为主机名时必须在每个成员上可解析
--etcd m1,m2 — 用本地 etcd addon 成员作为发现 endpoint;与 --etcd-endpoints 二选一(后者优先)
--etcd-endpoints host:port,... — 外部 etcd 发现 endpoint;可重复或逗号分隔。仅支持明文 HTTP,可带 http:// 前缀(会被去掉)
--erlang-cookie-file FILE — 从 FILE 读取共享的 Erlang cookie。集群成员首次安装必填(使用 --secrets-from-etcd 时除外);值保存后不可变
--secrets-from-etcd false 从 pg addon rabbitmq secret generate 写入的 etcd key 读取两个共享密钥(推荐,无需在主机间复制文件)。不能与 --erlang-cookie-file 或 --admin-password-file 同时使用
--amqp-port 自动(基准 5672) 明文 AMQP 端口;0 = 从 rabbitmq_start_port 自动分配
--management-port 自动(基准 15672) Management HTTP UI/API 端口;0 = 从 rabbitmq_management_start_port 自动分配
--tls false 在明文监听之外增加 AMQPS 与 management HTTPS,使用 pgcli 自签 CA 签发的证书
--tls-cert FILE — 自带服务端证书(PEM 链);必须与 --tls-key 同时传入;隐含 --tls;--listen 不是 0.0.0.0 时必须覆盖该地址
--tls-key FILE — 自带私钥(PKCS#8/PKCS#1/SEC1),与 --tls-cert 配对
--tls-port 自动(基准 5671) AMQPS 端口;0 = 从 rabbitmq_tls_start_port 自动分配;需要 --tls
--config-option K=V — 额外的 rabbitmq.conf 配置(heartbeat=60、log.file.level=debug);可重复;替换已存储的覆盖表
--force false 重新渲染配置并重建容器;保留数据与已存储的密码。集群模式下 --cluster、--node-name 与 Erlang cookie 不可变,--etcd-endpoints 可以更新

RabbitMQ 不使用 --image,用 --version。--version 接受任意合法的上游 tag,但 tag 必须 真实存在,否则 pull 失败;镜像引用始终是 docker.io/library/rabbitmq:<version>-management。

安装

pg addon install rabbitmq --name rmq
✓ RabbitMQ "rmq" installed
  Version:    4.4.0
  Image:      docker.io/library/rabbitmq:4.4.0-management
  Container:  pgcli-rabbitmq-default-rmq
  AMQP:       amqp://127.0.0.1:5672
  Management: http://127.0.0.1:15672
  Admin:      admin
  Password:   <generated>
  Data dir:   /home/you/.pgcli/addon/rabbitmq/rmq/data
  Config:     /home/you/.pgcli/addon/rabbitmq/rmq/config/rabbitmq.conf

⚠️ 单机版创建的是持久化 classic 队列,但消息默认不持久化。classic 的消息持久化是 逐条消息的属性,必须由发布端在每次 publish 时设置(pg addon rabbitmq queue publish 加 --property delivery_mode=2,应用客户端设置 delivery_mode=2),不存在队列参数或 policy 级别的开关。未设置时 broker 重启后队列仍在、消息丢失。集群模式使用 quorum 队列, 会自动持久化所有消息。

第二个实例会从各自的端口池取下一个空闲端口(例如 AMQP 5673、management 15673、 EPMD 4370、distribution 25673):

pg addon install rabbitmq --name rmq2 --data-dir /srv/rabbitmq-data --listen 0.0.0.0

不带 --force 重复 install 是幂等的,只打印当前状态。带 --force 时,pgcli 会重写 rabbitmq.conf、重写 enabled_plugins、重建容器并重新对账管理员账号;数据目录与密码保留。

密码只在安装时打印一次,之后可随时取回:

pg addon password rabbitmq --name rmq

Management UI 与 API

Management Web UI 的地址:

单机: http://127.0.0.1:15672        (--listen 默认 127.0.0.1)
集群: http://<成员IP>:15672          (任意成员,如 http://192.0.2.11:15672)

执行 pg addon rabbitmq status --name rmq 可打印本实例准确的 Management: 地址(随 --listen 与 --management-port 变化)。浏览器打开该地址,用管理员账号登录,密码通过 pg addon password rabbitmq --name rmq 获取。集群模式下任意成员都提供相同的 UI, Overview → Nodes 可看到整个集群。

同一端口也提供 HTTP API:

pw="$(pg addon password rabbitmq --name rmq)"
curl -fsS -u "admin:$pw" http://127.0.0.1:15672/api/overview

CLI 未封装的能力——vhost、用户、权限、策略与 exchange 定义——都通过 UI 完成。 常用队列操作已提供 pg addon rabbitmq queue ... 命令(见队列与消息)。

端口

端点 默认 端口池
AMQP(明文) 5672 rabbitmq_start_port,默认 5672
AMQPS(TLS) 5671(启用 --tls 时) rabbitmq_tls_start_port,默认 5671
Management HTTP UI/API 15672 rabbitmq_management_start_port,默认 15672
Management HTTPS 15671(启用 --tls 时) rabbitmq_management_tls_start_port,默认 15671
Erlang EPMD 4369 rabbitmq_epmd_start_port,默认 4369
Erlang distribution 25672 rabbitmq_dist_start_port,默认 25672

每个池独立分配:实例按名字排序,显式端口不会被覆盖,宿主已监听的端口会被跳过。所有 RabbitMQ 实例的端口必须互不重复。

EPMD 与 distribution 是基础设施端口,不是客户端端点。单机实例以 RABBITMQ_NODENAME=rabbit@localhost、ERL_EPMD_ADDRESS=127.0.0.1 和独立的 EPMD/distribution 端口运行,rabbitmq.conf 里固定 distribution.listener.interface = 127.0.0.1——Erlang distribution 永不对外暴露,多个单机 实例可在同一宿主共存,也不会与宿主的 Erlang 安装冲突。

集群成员不同:每个成员固定使用 EPMD 4369 与 distribution 25672,distribution 监听 --listen(默认 0.0.0.0)。因此一台主机最多运行一个集群 RabbitMQ 实例,且 4369、 25672 必须能被其他成员访问(见集群)。

不启用 rabbitmq_prometheus 插件。 pgcli 挂载自己的 enabled_plugins(单机只有 rabbitmq_management,集群模式为 rabbitmq_management 与 rabbitmq_peer_discovery_etcd), 保证每个实例行为一致,也不会多开一个无认证的固定 metrics 监听。

集群

集群通过 etcd 自动发现组建,不使用静态 peer 列表。每台主机以相同的 --cluster 名和 etcd endpoint 列表运行一个成员;首次启动时成员在 etcd 中注册自己,并加入已经注册的节点。

前置条件:

  • etcd 3.4+,每个成员都能以明文 HTTP 访问。测试时单成员 etcd 即可,但生产环境的发现 存储本身应当冗余。本版本 pgcli 不支持该 etcd 连接的 TLS 或认证,请放在可信网络中。可以 用 pgcli 的 etcd addon(pg addon install etcd ...),也可以用 --etcd-endpoints 指向 外部 etcd。
  • 节点名唯一。 --node-name rabbit@host 在集群内必须唯一。host 部分可以是主机名或 IPv4;拒绝 loopback 与 IPv6。使用 IP 时成员直接互连,完全不需要名称解析——没有内网 DNS 时推荐这种方式。使用主机名时,每个成员都必须能解析它(DNS 或 /etc/hosts): etcd 发现只注册 rabbit@host,不提供地址。FQDN、IP 会让 RabbitMQ 自动使用 Erlang long names。
  • 共享密钥。 所有成员必须使用相同的 Erlang cookie 和相同的管理员密码。先在管理机 执行一次 pg addon rabbitmq secret generate:它生成本地 0600 文件并把内容写入 etcd; 各成员安装时用 --secrets-from-etcd 读取,无需在主机间复制文件。pgcli 会把两个值保存到 pg.yaml 和 0600 secret 文件,因此 start 与 autostart 不再依赖 etcd 或原始文件。 仍支持手动分发文件时使用 --erlang-cookie-file / --admin-password-file。这两个都是 集群级密钥,AMQP 客户端都不需要。
  • 版本一致。 每个成员的 --version 相同。
  • 集群名一致。 不同的 --cluster 会在同一个 etcd 中形成另一套集群;pgcli 通过 cluster_formation.etcd.cluster_name 隔离。

在管理机上生成并发布一次共享密钥:

pg addon rabbitmq secret generate --cluster pgcli-lab \
  --etcd-endpoints 192.0.2.10:2379

命令不会打印密钥值。它会写入 /tmp/rmq.cookie 与 /tmp/rmq.admin(权限 0600),并把 相同内容写入以下 etcd key(不是文件):

/pgcli/rabbitmq/clusters/pgcli-lab/secrets/erlang_cookie
/pgcli/rabbitmq/clusters/pgcli-lab/secrets/admin_password

这两个本地文件各有明确用途:

文件 用途
/tmp/rmq.cookie 共享 Erlang cookie。仅用于集群成员之间的 Erlang distribution 握手(EPMD 4369、distribution 25672),AMQP 与管理客户端从不使用。
/tmp/rmq.admin 共享的broker 管理员密码,用于创建/对账 admin 账号。它是安装输入,不是客户端凭据文件:应用应使用自己创建的 RabbitMQ 用户连接。需要以管理员身份登录时,用 pg addon password rabbitmq --name mq 取回已保存的密码即可,不必保留该文件。

这两个文件都是安装引导/分发用的中间产物。安装时每个成员都会把值保存到 pg.yaml 和 自己的 0600 secret 文件,start/autostart 不会读取 /tmp 下的文件,因此所有成员安装 完成后即可删除。若不用 --secrets-from-etcd 而选择手动分发,则把这两个文件复制到各主机 (内容必须一致),并通过 --erlang-cookie-file / --admin-password-file 传入;各主机的 路径可以不同。成员安装完成后绝不要重新生成 Erlang cookie——它不可变,cookie 不一致的 节点无法加入集群。

在各成员主机上安装(只有 --node-name 不同;用各主机 IP 可免去 /etc/hosts/DNS):

# 192.0.2.11
pg addon install rabbitmq --name mq --cluster pgcli-lab \
  --node-name rabbit@192.0.2.11 \
  --etcd-endpoints 192.0.2.10:2379 \
  --secrets-from-etcd

# 192.0.2.12
pg addon install rabbitmq --name mq --cluster pgcli-lab \
  --node-name rabbit@192.0.2.12 \
  --etcd-endpoints 192.0.2.10:2379 \
  --secrets-from-etcd

# 192.0.2.13
pg addon install rabbitmq --name mq --cluster pgcli-lab \
  --node-name rabbit@192.0.2.13 \
  --etcd-endpoints 192.0.2.10:2379 \
  --secrets-from-etcd

每个成员都读取相同的值,不需要手动复制文件。若倾向手动分发,可跳过 secret generate, 改用两个 --*-file 参数。

etcd 中这些值是明文的。任何能访问 etcd client 端口的人都能读到集群凭据,因此请把 2379 限制在成员主机与管理机之间;不要把密钥放进其他租户可读的共享 etcd。

第一个成员会启动一个只有自己的集群并打印提示;后续成员通过 etcd 发现并自动加入。发现只在 Mnesia 为空时执行——已加入的成员重启时联系 last-seen peers。

发现 key 位于

/rabbitmq/discovery/rabbitmq/clusters/<cluster-name>/nodes/<node-name>

因此只要 --cluster 名不同,多套集群可以共用一个 etcd。

队列语义

  • 集群模式下 pg addon rabbitmq queue declare 默认 --type quorum。Quorum 队列会在所有成员 上复制(默认 quorum size 为 3,以成员数为上限),是 RabbitMQ 4.x 唯一支持复制的队列类型。
  • --type classic 仍可使用,但 classic 队列不复制:它只存在于声明它的节点上,该节点 宕机时队列不可用。仅建议用于非关键或可重建的数据。
  • --durable=false、quorum + --auto-delete 等限制不变(见队列与消息)。
  • 每个节点都能服务客户端,management API 也会展示整个集群,因此客户端可以连接任意成员。 生产环境请配置多个 endpoint,或前置 TCP 负载均衡。

查看与下线成员

pg addon rabbitmq cluster status --name mq
pg addon rabbitmq cluster forget rabbit@192.0.2.13 --name mq --yes

cluster status 输出 rabbitmqctl cluster_status,包含所有 running nodes。cluster forget 用于把已停止、永久下线的成员从集群元数据中移除;它拒绝 forget 本机节点。移除成员后, 重新平衡 quorum 队列的副本分布:

pg addon rabbitmq shell --name mq
rabbitmq-queues rebalance quorum

只是临时停机的成员不需要任何操作:重启后会通过 last-seen peers 自动重新加入。不要在节点 仍计划恢复时执行 cluster forget——旧节点将无法重新加入,只能清空数据重建(或在对端 forget 后用空数据目录重新添加)。

集群约束

  • --cluster、--node-name 与 Erlang cookie 不可变。用不同的值执行 install --force 会失败;--etcd-endpoints 可以更新。
  • 单机实例不能转换为集群成员(反之亦然):用 --clean-data 删除后重新安装。集群首次安装 也要求数据目录为空。
  • --advertise-host、--node-id、--peer、--join 对 RabbitMQ 不适用;节点身份由 --node-name 唯一决定。集群模式拒绝明文 --admin-password。
  • 管理员密码是集群级凭据,所有成员必须保存相同的值。轮换时先在管理机执行 pg addon rabbitmq secret generate --force --keep-cookie ... 发布新密码(或改用 --admin-password-file ...),然后在每个成员上重新执行 install --force --secrets-from-etcd(或对应的 --admin-password-file ...)。 成员安装后绝不能重新生成 Erlang cookie:它不可变,变化会让 install --force 失败。
  • etcd 中 /pgcli/rabbitmq/clusters/<name>/secrets/ 下的密钥是明文。请把 etcd client 端口限制在成员主机与管理机之间。这两个密钥只属于集群:AMQP 客户端都不需要 cookie 或 管理员密码,业务应用应创建独立的 RabbitMQ 用户。
  • 集群互联流量(25672 上的 Erlang distribution)不加密;--tls 只保护客户端监听。 请把 4369 与 25672 限制在私有网络,且只对成员开放。

TLS

TLS 是增量的:--tls 保留明文 AMQP 与 management 监听,另外增加 AMQPS 与 management HTTPS。

pg addon install rabbitmq --name rmq --tls
pg addon install rabbitmq --name rmq --tls --tls-port 5671

install 与 status 会同时打印两套端点和 CA 路径:

  AMQP:       amqp://127.0.0.1:5672
  Management: http://127.0.0.1:15672
  AMQPS:      amqps://127.0.0.1:5671
  Mgmt TLS:   https://127.0.0.1:15671
  CA cert:    /home/you/.pgcli/addon/rabbitmq/rmq/config/tls/ca.crt

自动生成的证书由 pgcli 自签 CA(与 MinIO/silo/rustfs、Kafka 共用的 helper)签发,覆盖 localhost、127.0.0.1、::1、宿主所有网卡地址以及具体的 --listen 地址。客户端必须 信任 config/tls/ca.crt:

openssl s_client -connect 127.0.0.1:5671 \
  -CAfile "$HOME/.pgcli/addon/rabbitmq/rmq/config/tls/ca.crt" \
  -verify_ip 127.0.0.1 -verify_return_error </dev/null

pw="$(pg addon password rabbitmq --name rmq)"
curl -fsS --cacert "$HOME/.pgcli/addon/rabbitmq/rmq/config/tls/ca.crt" \
  -u "admin:$pw" https://127.0.0.1:15671/api/overview

启用 TLS 后的 AMQP URL(例如 Python/pika 或 Go 客户端):

amqps://admin:<password>@127.0.0.1:5671/
  • TLS 只负责传输加密。ssl_options.verify = verify_none 与 fail_if_no_peer_cert = false 表示客户端不提供证书;任何能连到已启用端口且知道密码的人 仍然可以访问。监听地址与密码才是安全边界。
  • 叶子证书在到期前 30 天内(安装、启动或 --force 时)重新签发,CA 保留,因此客户端 不需要换信任文件。该行为只适用于 pgcli 生成的证书。
  • --tls-port 没配 --tls 会被拒绝。用 --tls=false --force 关闭 TLS;生成的文件保留在 config/tls/,重新启用时复用。

自带证书(pg cert)

pg cert --host localhost,127.0.0.1,192.0.2.10 \
  --cert-file rmq.crt --key-file rmq.key

pg addon install rabbitmq --name rmq \
  --tls-cert rmq.crt --tls-key rmq.key

--tls-cert 与 --tls-key 必须成对传入,且隐含 --tls。在任何镜像拉取或容器操作之前, pgcli 会校验:

  • 证书可解析且当前有效;
  • 允许 server authentication,且不是 CA 证书;
  • 私钥与证书匹配且未加密;
  • 证书覆盖具体的 --listen 地址(不覆盖是硬错误;可改用 --listen 0.0.0.0 或把该地址 加入 SAN)。

文件会复制到实例的 config/tls/:证书为 public.crt、私钥为 private.key,证书副本同时 作为 ca.crt。pg cert 生成的自签叶子证书本身就是信任锚,客户端可直接信任 rmq.crt。 如果证书由其他 CA 签发,请改为信任该 CA 链:ca.crt 只包含你提供的文件,可能不包含根证书。

使用自带证书时,源文件不会被改动,pgcli 也不会自动续期——替换源文件后重新执行 install --force(或 --tls=false --force 回到明文)。

队列与消息

queue list、purge、delete 走容器内的 rabbitmqctl,不依赖 management HTTP 密码。 其余队列命令——declare、info、publish、get、bind、unbind、bindings——通过 management HTTP API 使用已保存的管理员凭据,要求实例正在运行且宿主能访问 management 端口。

# 声明与查看
pg addon rabbitmq queue list --name rmq
pg addon rabbitmq queue declare orders --name rmq
pg addon rabbitmq queue info orders --name rmq
pg addon rabbitmq queue info orders --name rmq --json

# 发布与消费
pg addon rabbitmq queue publish orders --payload '{"id":1}' --name rmq
echo '{"id":2}' | pg addon rabbitmq queue publish orders --payload-file - --name rmq
# 单机 classic 队列:让消息在 broker 重启后仍然保留
pg addon rabbitmq queue publish orders --payload '{"id":3}' --property delivery_mode=2 --name rmq
pg addon rabbitmq queue get orders --count 10             # peek:消息保留在队列
pg addon rabbitmq queue get orders --count 10 --consume   # ack:取出并删除
pg addon rabbitmq queue get orders --json

# 绑定(必须是真实 exchange;默认 exchange 不能显式绑定)
pg addon rabbitmq queue bind orders --exchange amq.topic --routing-key 'order.*' --name rmq
pg addon rabbitmq queue bindings orders --name rmq
pg addon rabbitmq queue unbind orders --exchange amq.topic --routing-key 'order.*' --name rmq

# 清理
pg addon rabbitmq queue purge orders --name rmq --yes
pg addon rabbitmq queue delete orders --name rmq --yes
pg addon rabbitmq queue delete orders --name rmq --if-empty --if-unused
  • list 打印 name、type、messages、ready、unacked、consumers;空 vhost 打印 no queues。
  • declare 在单机实例上默认创建持久化 classic 队列,在集群成员上默认创建 持久化 quorum 队列(见集群)。RabbitMQ 4.x 已移除 transient(非持久化且 非 exclusive)队列,因此 --durable=false 会在发出请求前直接报错。--type classic 或 --type quorum 可显式覆盖默认类型;quorum 队列必须持久化,且不能 auto-delete。 可重复的 --argument KEY=VALUE 设置队列参数;能按 JSON 解析的值保留 JSON 类型(如 --argument x-max-priority=10)。
  • 消息保留时间。 --message-ttl 30m(或毫秒数)设置 x-message-ttl:在队列中等待超过 该时间的消息会被丢弃;若队列配置了 x-dead-letter-exchange 则转死信。单条消息也可用 --property 'expiration="60000"' 设置自己的 TTL。
  • 自动删除。 --auto-delete 在最后一个消费者断开后删除队列。--expires 24h 设置 x-expires:队列连续闲置该时长后,队列本身连同其中剩余消息一起删除。只有存在 消费者、被重新声明、或通过 basic.get 访问才算“使用过”;单纯发布消息不会重置 计时器。该值必须为正数(0 会被 RabbitMQ 拒绝)。两者也可通过 --argument 设置。
  • info 显示 type、state、durable、auto-delete、消息计数、consumers、memory、policy、 arguments 与 node;--json 输出 management API 原始文档。计数由 RabbitMQ 统计模块异步 刷新,可能比 publish/get 滞后几秒。
  • publish 发送一条消息。--payload 与 --payload-file 必须二选一(- 表示 stdin)。 --exchange 默认 amq.default(默认 exchange,按 routing key 直接路由到同名队列); --routing-key 默认队列名。可重复的 --property KEY=VALUE 设置消息属性,例如 --property delivery_mode=2 --property 'content_type="application/json"'。消息未被路由时 只打印警告,命令仍成功。
  • 单机持久化。 单机实例默认声明持久化 classic 队列(不复制),但 durable 只作用于 队列本身,不作用于消息。classic 的消息持久化是每条消息的属性,必须由发布端在每次 publish 时设置:命令行加 --property delivery_mode=2,应用客户端则在 AMQP 发布代码中 设置 delivery_mode=2。队列参数或 policy 无法开启该行为(--argument delivery_mode=2 不生效)。未设置时消息是 transient,broker 重启(stop/start、 容器重建或宿主机重启)后队列还在、消息丢失。设置后只要实例数据目录保留(不要 remove --clean-data),消息即可恢复;磁盘损坏则无法恢复。集群的 quorum 队列不依赖 delivery mode,所有消息都会持久化并复制。
  • get 默认 peek(ack_requeue_true);--consume 确认并删除取出的消息 (ack_requeue_false)。--count 默认 1;--json 输出 API 原始响应。
  • bind / unbind 必填 --exchange,并拒绝 amq.default(默认 exchange 是隐式绑定)。 --routing-key 默认队列名。可重复的 --argument 支持基于参数(headers)的绑定。 unbind 不带 --argument 时删除该 exchange 下所有同 routing key 的绑定;带 --argument 时只删除参数完全一致的那条。
  • bindings 列出队列的绑定及其参数(--json 输出原始文档)。
  • purge 清空队列中的消息但保留队列;delete 删除队列本身。两者默认要求确认, --yes 可跳过。--if-empty / --if-unused(仅 delete)透传 RabbitMQ 的条件检查:队列 非空或有消费者时删除失败,而不是直接删掉。
  • 所有队列命令都接受 --name 与 --vhost(默认 /),并要求实例正在运行。

限制保留时间样例

不带保留参数的队列会无限期保留消息:默认没有 TTL。消息只会因为被消费、purge、 进入死信,或队列本身被删除而离开。常见限制方式:

# 队列级 TTL:发布后 24 小时过期
pg addon rabbitmq queue declare orders --message-ttl 24h --name rmq

# 单条消息 TTL:这条消息 60 秒后过期
# (expiration 是毫秒字符串属性)
pg addon rabbitmq queue publish orders --payload '{"id":1}' \
  --property 'expiration="60000"' --name rmq

# 队列闲置删除(不是消息 TTL):连续 1 小时无消费者、未被重新声明、
# 未被 get 访问后,队列本身及其中消息一起删除
pg addon rabbitmq queue declare scratch --expires 1h --name rmq

# 按容量而非时间限制:最多保留 10000 条消息
pg addon rabbitmq queue declare backlog --argument x-max-length=10000 --name rmq

# 过期消息转入死信交换机,而不是直接丢弃
pg addon rabbitmq queue declare dlq-demo --message-ttl 24h \
  --argument x-dead-letter-exchange=dlx --name rmq

--message-ttl 与 --expires 只在队列首次声明时生效。要修改已存在队列的保留策略而不 删除队列,使用 policy:

pg addon rabbitmq shell --name rmq
rabbitmqctl set_policy orders-ttl '^orders$' \
  '{"message-ttl":86400000,"max-length":10000}' --apply-to queues
rabbitmqctl list_policies

policy 按队列名正则匹配,对新建和已有队列都生效,并优先于同名的 x-* 队列参数。也可以 在 management UI 的 Admin → Policies 中创建。

同样的保留限制也可以不重新声明队列,直接通过 RabbitMQ policy(message-ttl、 expires、max-length、max-length-bytes)施加;pgcli 暂未封装 policy,请使用 management UI 或 rabbitmqctl set_policy。

pg addon rabbitmq shell 打开容器内的 /bin/bash;rabbitmqctl 与 rabbitmq-diagnostics 都在里面,可处理 CLI 和 API 未覆盖的操作。

状态与生命周期

pg addon rabbitmq status --name rmq       # 状态、端点、TLS、管理员、EPMD/dist、路径
pg addon start rabbitmq --name rmq        # 启动(缺失则创建),等待就绪
pg addon stop rabbitmq --name rmq
pg addon remove rabbitmq --name rmq       # 删除容器与 pg.yaml 条目,保留数据/配置/secret
pg addon remove rabbitmq --name rmq --clean-data   # 同时删除实例目录与自定义 data dir
pg logs addon rabbitmq --name rmq -f

install、start 与 --force 在单机实例上最多等待 90 秒,轮询 rabbitmqctl -q await_startup;集群成员最多等待 180 秒,覆盖首次 peer discovery 与 Mnesia 引导。集群模式下每次尝试上限 10 秒——有 peer 时该命令可能已执行成功却静默 不退出;若被空闲 watchdog 终止,pgcli 改为探测 AMQP 端口,监听器在启动末尾打开, 端口可连即视为就绪。就绪后 pgcli 用 rabbitmqctl change_password / add_user / set_user_tags administrator / set_permissions 对账管理员账号。

集群模式下 status 还会打印集群名、Erlang 节点名与 etcd endpoint; pg addon rabbitmq cluster status 展示 rabbitmqctl cluster_status 的实时视图。

不带 --clean-data 的 remove 会保留数据目录、生成的配置和 0600 密码 secret。同名重装会 复用该密码,因此 remove/reinstall 之后客户端配置无需修改。

开机自启:

pg autostart enable --rabbitmq --name rmq
pg autostart status
pg autostart disable --rabbitmq --name rmq

启动时会重写 rabbitmq.conf、启动容器并重新对账管理员账号。pg start 也会带上已启用 autostart 的 RabbitMQ 实例。

pgcli 创建的内容

单个实例的全部文件位于 <base_dir>/addon/rabbitmq/<name>/:

宿主路径 容器路径 说明
config/rabbitmq.conf /etc/rabbitmq/rabbitmq.conf(只读) 渲染后的监听、management、管理员 tags/权限与 distribution 配置
config/enabled_plugins /etc/rabbitmq/enabled_plugins(只读) 单机 [rabbitmq_management].;集群 [rabbitmq_management,rabbitmq_peer_discovery_etcd].
config/tls/ /etc/rabbitmq/tls(只读,仅 --tls) ca.crt(0644)、public.crt(含链,0644)、private.key(0600,chown 给 uid 999);ca.key 留在宿主侧且不挂载
data/ /var/lib/rabbitmq Mnesia、消息存储、definitions 与日志;集群模式额外包含 .erlang.cookie(0400,chown 给 uid 999)
secrets/admin_password 不挂载 0600,重装时复用

明文实例生成的 rabbitmq.conf:

listeners.tcp.default = 127.0.0.1:5672

management.tcp.ip = 127.0.0.1
management.tcp.port = 15672

default_user_tags.administrator = true
default_permissions.configure = .*
default_permissions.read = .*
default_permissions.write = .*

distribution.listener.interface = 127.0.0.1
distribution.listener.port_range.min = 25672
distribution.listener.port_range.max = 25672

--tls 时会额外写入 listeners.ssl.default、ssl_options.* 与 management.ssl.*。 集群成员改为绑定 0.0.0.0(或显式 --listen 地址),并追加发现配置:

cluster_formation.peer_discovery_backend = etcd
cluster_formation.etcd.endpoints.1 = 192.0.2.10:2379
cluster_formation.etcd.cluster_name = pgcli-lab

任何 secret 都不会写进 rabbitmq.conf。管理员密码通过容器环境变量传入 (RABBITMQ_DEFAULT_USER / RABBITMQ_DEFAULT_PASS 只用于播种空的 Mnesia,之后 pgcli 每次启动用 rabbitmqctl 对账),RABBITMQ_NODENAME 与 RABBITMQ_USE_LONGNAME 同样走 环境变量。集群模式下 pgcli 还会在容器启动之前把共享 Erlang cookie 写入 data/.erlang.cookie(权限 0400,属主 uid 999):官方 4.4 镜像忽略 RABBITMQ_ERLANG_COOKIE,任其自行生成随机 cookie 的节点无法与 peer 完成 distribution 握手。

容器以 --network host、--restart unless-stopped 和 30 秒 stop timeout 运行,身份是镜像的 uid/gid 999(rabbitmq);pgcli 只把 TLS 私钥与集群 cookie 文件交给该 uid(rootless 下 通过 podman unshare,root 下用 os.Chown)。数据目录由镜像入口脚本自行 chown。

配置覆盖

安装时可追加 rabbitmq.conf 条目:

pg addon install rabbitmq --name rmq --force \
  --config-option heartbeat=60 \
  --config-option log.file.level=debug

--config-option 替换已存储的覆盖表(不合并)。覆盖项保存在 pg.yaml 的 addons.rabbitmq.<name>.settings 下,每次启动重新渲染。键可包含字母、数字、点、下划线和 连字符;值不能包含 NUL、CR 或 LF。

pgcli 托管的键会被拒绝而不是被静默覆盖:listeners.*、ssl_options.*、management.*、 prometheus.*、cluster_formation.*、default_user、default_pass、 default_user_tags.*、default_permissions.*、loopback_users.*、 distribution.listener.*。

密码与账号语义

  • RABBITMQ_DEFAULT_USER / RABBITMQ_DEFAULT_PASS 只在首次启动、Mnesia 为空时播种。 因此 pgcli 每次启动都用 rabbitmqctl change_password 对账(用户不存在时退回 add_user),再设置 administrator tag 以及 vhost / 上的 .* / .* / .* 权限。
  • 在已有实例上修改 --admin-user 会创建/对账新账号,但不会删除旧账号。请通过 management UI 或 rabbitmqctl delete_user 清理。
  • 修改 --admin-password(或复用已保存的 secret)会在下一次 install --force 或 start 时生效。集群中的密码是共享的:所有成员必须保存相同的值,轮换时要在所有成员上同步操作。
  • 集群模式的共享密钥来自 --secrets-from-etcd(推荐,key 由 pg addon rabbitmq secret generate 写入),或来自 --admin-password-file 与 --erlang-cookie-file。明文 --admin-password 被拒绝。两个值随后都会持久化到 pg.yaml 与 0600 secret 文件。
  • 密码同时保存在 pg.yaml 与 0600 secret 文件中; pg addon password rabbitmq --name NAME 打印存储值。

生产注意事项

  • 单节点或一个集群成员。 单机实例没有 HA:容器停止即 broker 不可用。集群模式的可用性 来自跨成员复制的 quorum 队列;classic 队列不复制,会随所在节点一起不可用。不配置 federation、shovel、MQTT 或 STOMP。
  • 集群网络。 4369(EPMD)与 25672(Erlang distribution)只在私有网络中、仅对成员 开放。即使启用 --tls,distribution 流量也不加密;TLS 只覆盖客户端监听。一台主机只能有 一个集群实例。
  • 发现链路是明文的。 etcd endpoint 通过明文 HTTP 访问,没有认证。任何能访问 etcd 的人 都能往集群注册恶意节点,或读取 pg addon rabbitmq secret generate 写入的共享密钥; 请把 etcd 隔离在可信网络中。本版本不支持 etcd TLS/认证。
  • 不启用 Prometheus 插件。 enabled_plugins 固定为 management(集群模式另加 etcd 发现插件)。需要 metrics 时可通过 management API 采集,或自行在容器内启用插件——但要 注意多个实例会争抢同一个默认 metrics 端口。
  • TLS 不是客户端认证。 请使用 --listen 127.0.0.1,或在 broker 前加私有网络/防火墙。 Management UI 是完整的管理面,不要随意暴露。
  • TLS 是增量监听。 启用 --tls 后明文 amqp:// 与 http:// 仍然可用;需要加密的 客户端必须显式选择 amqps:// / https:// 端点。
  • 升级。 --force --version <new> 会拉取新 tag 并在原数据目录上重建容器。RabbitMQ 不保证 能被更新版本写入的数据可降级——升级前请先做离线拷贝。--version 必须是真实存在的上游 tag。
  • 备份。 停止容器后复制数据目录,或用 management API 导出 users/vhosts/policies 的 definitions。本版本没有 pg addon rabbitmq backup 命令。
  • 数据属主。 数据文件属 uid/gid 999。请用 --clean-data 删除,而不是手工 rm;pgcli 会通过 podman unshare 处理权限回退。
  • 平台。 Linux(host 网络);官方镜像为多架构(amd64/arm64)。rootful 宿主上需以 root 运行 pgcli 才能把 TLS 私钥交给 uid 999;rootless 下走 podman unshare。

CLI 参数参考

pg addon install rabbitmq

参数 默认值 说明
--name NAME rabbitmq 实例名;[A-Za-z0-9][A-Za-z0-9_-]*,最长 48 字符
--version TAG 4.4.0 上游 docker.io/library/rabbitmq tag;自动追加 -management 后缀
--admin-user USER admin 管理员账号;[A-Za-z0-9_.-]{1,64}
--admin-password PASS 自动生成 管理员密码(不能含 NUL/CR/LF);集群模式拒绝
--admin-password-file FILE — 从文件读取密码;集群成员首次安装必填(使用 --secrets-from-etcd 时除外)
--data-dir DIR <base_dir>/addon/rabbitmq/<name>/data 绝对宿主路径,挂载到 /var/lib/rabbitmq
--listen ADDR 127.0.0.1(集群 0.0.0.0) AMQP、AMQPS、management HTTP 与 management HTTPS 的绑定地址;集群模式不能是 loopback
--cluster NAME — 集群名;空 = 单机
--node-name NODE@HOST — 本成员的 Erlang 节点名;首次安装必填
--etcd M1,M2 — 用本地 etcd addon 成员做发现
--etcd-endpoints HOST:PORT,... — 外部 etcd 发现 endpoint;优先于 --etcd
--erlang-cookie-file FILE — 从文件读取共享 Erlang cookie;集群成员首次安装必填(使用 --secrets-from-etcd 时除外)
--secrets-from-etcd false 从 secret generate 写入的 etcd key 读取两个共享密钥;不能与两个 --*-file 同时使用
--amqp-port N 自动(基准 5672) 明文 AMQP 端口;0 = 自动分配
--management-port N 自动(基准 15672) Management HTTP 端口;0 = 自动分配
--tls false 增加 AMQPS 与 management HTTPS;明文监听保留
--tls-cert FILE — 自带证书链(PEM);必须与 --tls-key 配对;隐含 --tls
--tls-key FILE — 自带私钥;必须与 --tls-cert 配对
--tls-port N 自动(基准 5671) AMQPS 端口;0 = 自动分配;需要 --tls
--config-option K=V — 额外的 rabbitmq.conf 配置;可重复;替换已存储覆盖表
--force false 重写配置并重建容器,保留数据与密码;cluster/node/cookie 不可变

pg addon rabbitmq status / shell / cluster ...

参数 默认值 说明
status --name NAME rabbitmq 显示状态、端点、TLS、管理员、EPMD/dist、路径;集群模式附集群名/节点/etcd
shell --name NAME rabbitmq 打开容器内 /bin/bash
cluster status --name NAME rabbitmq 输出 rabbitmqctl cluster_status
cluster forget NODE --name NAME [--yes] — forget 一个已停止并下线的成员;拒绝本机节点,除非 --yes 否则要求确认

pg addon rabbitmq secret generate

参数 默认值 说明
--cluster NAME — 要发布密钥的集群名(必填)
--etcd M1,M2 — 保存密钥的本地 etcd addon 成员
--etcd-endpoints HOST:PORT,... — 保存密钥的外部 etcd endpoint;优先于 --etcd
--cookie-file FILE /tmp/rmq.cookie Erlang cookie 的本地副本(0600);仅集群节点使用,客户端不需要
--admin-password-file FILE /tmp/rmq.admin 管理员密码的本地副本(0600);安装输入,不是客户端凭据
--force false 覆盖已存在的文件与 etcd key
--keep-cookie false 与 --force 连用:保留现有 Erlang cookie,只轮换管理员密码(成员已安装后使用)

pg addon rabbitmq queue ...

所有队列子命令都接受 --name NAME(默认 rabbitmq)与 --vhost VHOST(默认 /)。

命令 参数 默认值 说明
list — — 列出队列及消息/消费者计数
declare QUEUE --durable true 声明持久化队列(RabbitMQ 4.x 拒绝 false)
--auto-delete false 最后一个消费者断开后删除队列
--type TYPE 单机 classic,集群 quorum classic 或 quorum
--message-ttl DURATION — 设置 x-message-ttl(如 30m、24h 或毫秒)
--expires DURATION — 设置 x-expires:队列闲置该时长后删除(必须 > 0)
--argument K=V — 额外队列参数;可重复;JSON 值保留类型
info QUEUE --json false 输出 management API 原始文档
publish QUEUE --payload TEXT — 消息体(与 --payload-file 互斥)
--payload-file FILE — 从文件读取消息体;- 表示 stdin
--exchange NAME amq.default 目标 exchange
--routing-key KEY 队列名 routing key
--property K=V — 消息属性;可重复;单机 classic 消息必须在每次 publish 时设置 delivery_mode=2 才能跨 broker 重启保留(不存在队列级开关);JSON 值保留类型
get QUEUE --count N 1 最多取出的消息数
--consume false ack 并删除消息(默认 peek 后重新入队)
--json false 输出 management API 原始响应
bind QUEUE --exchange NAME — 源 exchange(必填;拒绝 amq.default)
--routing-key KEY 队列名 routing key
--argument K=V — 绑定参数;可重复
unbind QUEUE --exchange NAME — 源 exchange(必填)
--routing-key KEY 队列名 routing key
--argument K=V — 仅删除参数完全一致的绑定;省略则删除所有匹配项
bindings QUEUE --json false 输出 management API 原始文档
purge QUEUE --yes false 跳过确认提示
delete QUEUE --yes false 跳过确认提示
--if-empty false 队列非空时失败而不是删除
--if-unused false 队列有消费者时失败而不是删除

生命周期、日志、autostart

命令 参数 说明
pg addon start rabbitmq --name NAME 启动(缺失则创建),等待就绪并对账管理员
pg addon stop rabbitmq --name NAME 停止容器,保留数据
pg addon remove rabbitmq --name NAME、--clean-data 删除容器与配置条目;--clean-data 同时删除实例目录与自定义 data dir
pg logs addon rabbitmq --name NAME、-n N(默认 50)、-f 容器日志
pg autostart enable|disable --rabbitmq、--name NAME 切换单个实例的开机自启
pg addon password rabbitmq --name NAME 打印存储的管理员密码

全局参数

参数 说明
-c, --config FILE 配置文件(默认 ~/.pgcli/pg.yaml)
-o, --output FILE 输出配置路径(默认与输入相同)
-i, --instance NAME PostgreSQL 实例选择器——RabbitMQ 不使用,请用 --name

16.35 - RocketMQ

以托管插件方式运行 Apache RocketMQ 5.x——三容器(NameServer + broker + proxy),支持单机或跨主机 controller 模式集群与 broker 自动主备切换,含 topic/消息命令、渲染配置与 host 网络

pg addon install rocketmq 以托管插件方式运行 Apache RocketMQ 5.x,使用官方 docker.io/apache/rocketmq 镜像。和 Kafka、ClickHouse、RabbitMQ 一样,它是顶层插件, 不代理某个 PostgreSQL 实例。

每个实例都是 host 网络上的三个容器:NameServer、broker 和 proxy。单机与 集群使用相同的形态,因此端口、命令、日志和生命周期行为保持一致:

  • 单机——一个不带 controller 的 NameServer、一个无副本的 async master broker,以及 proxy。这是最快能收发消息的模式。不使用 4.x 的 master/slave 或 DLedger broker 部署模式。
  • 跨主机集群——每个 NameServer 内嵌 DLedger(Raft)controller,broker 以 controller 模式运行:一个 broker 被选为 master,故障时另一个自动接管;proxy 作为 客户端入口。这是 RocketMQ 5.x 的自动切换部署模式。

没有认证,也没有 TLS。 RocketMQ 使用 host 网络监听,集群端口固定。任何能连到 NameServer、broker 或 proxy 端口的人都可以读写所有 topic。只应在可信内网或防火墙之后 运行,并限制 9876、9877、10911、10912、8080、8081。本插件也没有 Web UI 和 ACL。

参数

参数 默认值 说明
--name rocketmq 实例名(配置 map 的 key,也是子命令的默认实例名)
--version 5.5.0 上游镜像 tag,镜像为 docker.io/apache/rocketmq:<version>;只接受 5.x
--data-dir <base_dir>/addon/rocketmq/<name>/data 实例数据根目录:broker store 为 <dir>/broker,集群模式的 controller store 为 <dir>/controller;除相对 base_dir 外必须是绝对路径
--advertise-host 单机 127.0.0.1;集群必填 向客户端和对端通告的地址。集群模式必须是非 loopback 的具体 IPv4 地址;主机名、IPv6 和 0.0.0.0 都会被拒绝。单机若需远程客户端访问,应设置为宿主机的局域网 IP
--cluster NAME — 开启跨主机集群模式。每台主机必须使用相同名称;它同时作为 DLedger controller group 和 broker cluster/group 名。集群身份安装后不可变
--controller-id ID — 本节点的 controller 成员 id(n0、n1……)。集群首次安装必填,组内唯一,匹配 [A-Za-z][A-Za-z0-9]{0,31};单机模式拒绝
--peer ID@HOST — 另一个 controller 成员。按其他节点逐条重复,host 为裸的非 loopback IPv4、不带端口(controller 端口固定 9877)。每台主机都要传入其他成员的完整列表;本地成员会自动补全。单机模式拒绝
--namesrv-port 单机自动(9876) NameServer 主机端口;0 = 从 RocketMQ 端口池自动分配。仅单机可用,集群成员固定 9876
--broker-port 单机自动(10911) broker 监听端口;HA 端口为 broker_port + 1,VIP 端口为 broker_port - 2。仅单机可用,集群成员固定 10911
--proxy-port 单机自动(8080) proxy remoting 端口。仅单机可用,集群成员固定 8080
--grpc-port 单机自动(8081) proxy gRPC 端口(5.x SDK)。仅单机可用,集群成员固定 8081
--controller-port — 内嵌 controller 端口。单机模式不接受;集群成员固定 9877
--heap 512m broker JVM 堆,写入 JAVA_OPT_EXT=-Xms<size> -Xmx<size>
--namesrv-heap 256m NameServer JVM 堆
--proxy-heap 256m proxy JVM 堆
--config-option K=V — 追加到 broker.conf 的设置(flushDiskType=SYNC_FLUSH、brokerPermission=4);可重复,且替换已存储的覆盖列表。pgcli 托管的键会被拒绝(身份、监听、store 路径、controller 设置、复制设置、autoCreateTopicEnable)
--force false 重新渲染三个配置并重建三个容器,保留 broker store 和 controller store。集群身份和固定端口即使加 --force 也不能修改

--image、--listen、--tls、--tls-pem、--tls-cert、--tls-key、--node-id、 --cluster-id、--join 均不适用于 RocketMQ,会被拒绝。选择镜像请使用 --version。

安装单机实例

pg addon install rocketmq --name rmq

安装会按需拉取镜像、渲染配置、创建目录,按 namesrv → broker → proxy 启动,并等待 NameServer、broker 注册和两个 proxy 监听端口就绪:

✓ RocketMQ "rmq" installed
  Version:     5.5.0
  Image:       docker.io/apache/rocketmq:5.5.0
  Containers:  pgcli-rocketmq-rmq-namesrv, pgcli-rocketmq-rmq-broker, pgcli-rocketmq-rmq-proxy
  Namesrv:     127.0.0.1:9876
  Broker:      127.0.0.1:10911 (HA 10912, VIP 10909)
  Proxy:       127.0.0.1:8080 (remoting), 127.0.0.1:8081 (gRPC)
  Mode:        single-node (no controller, no replicas)
  JVM heap:    broker 512m, namesrv 256m, proxy 256m
  Data dir:    /home/you/.pgcli/addon/rocketmq/rmq/data
  Config:      /home/you/.pgcli/addon/rocketmq/rmq/config

若需要其他主机上的客户端访问,应通告宿主机的局域网地址(proxy 配置和 broker 都使用它):

pg addon install rocketmq --name rmq --advertise-host 10.0.0.11

第二个单机实例会从各端口池取下一个空闲端口,不会复用第一个实例的端口:

pg addon install rocketmq --name rmq2 --data-dir /srv/rocketmq-data

在内存较小的 VM 上,降低三个 JVM 的堆,避免三个进程超出内存:

pg addon install rocketmq --name rmq \
  --heap 384m --namesrv-heap 192m --proxy-heap 192m

不带 --force 重复执行 install 是空操作,只报告当前状态。带 --force 时,pgcli 会重写 namesrv.conf、broker.conf 和 rmq-proxy.json,重建三个容器,并保留 broker store 与 controller store。

多机集群

集群模式使用 RocketMQ 5.x 的 controller 模式:controller 以 DLedger(Raft)group 的 形式内嵌在每个 NameServer 中,controller 模式的 broker 选举 master 并在故障时自动切换。 每台主机各自执行一次 pg addon install rocketmq;pgcli 不会 SSH 到其他主机。

要求:

  • 每台主机的 --cluster 名称相同。
  • 每台主机使用唯一的 --controller-id(n0、n1、n2……)。
  • 本机的 --advertise-host 是具体的非 loopback IPv4 地址。
  • --peer ID@HOST 列出其他 controller 成员;每台主机都要传完整对端集合,本地成员会 自动补全。
  • 每台主机上的固定集群端口空闲:9876、9877、10911、10912、8080、8081。 一台主机只能运行一个集群 RocketMQ 实例。
  • 至少 3 个成员才有容错。 DLedger 的 quorum 是多数派,2 个成员无法容忍任何故障; pgcli 会为 2 成员集群打印 warning。

逐台安装。以 10.0.0.11、10.0.0.12、10.0.0.13 三台主机为例:

# 在 10.0.0.11 上(controller n0)
pg addon install rocketmq --name rmq --cluster prod --controller-id n0 \
  --advertise-host 10.0.0.11 \
  --peer n1@10.0.0.12 --peer n2@10.0.0.13

# 在 10.0.0.12 上(controller n1)
pg addon install rocketmq --name rmq --cluster prod --controller-id n1 \
  --advertise-host 10.0.0.12 \
  --peer n0@10.0.0.11 --peer n2@10.0.0.13

# 在 10.0.0.13 上(controller n2)
pg addon install rocketmq --name rmq --cluster prod --controller-id n2 \
  --advertise-host 10.0.0.13 \
  --peer n0@10.0.0.11 --peer n1@10.0.0.12

前两台安装预计会以 warning 结束:

[!] RocketMQ broker is running, but it has not registered with the controller yet
(broker not listed in clusterList) — install the remaining nodes with the same
--cluster and --peer values ...

这是正常现象:只有多数 NameServer 起来后 controller quorum 才会形成。安装仍会成功返回, 容器保持运行。第三台安装完成后,controller 选出 broker master,broker 完成注册。

逐台检查:

pg addon rocketmq status --name rmq
pg addon rocketmq cluster status --name rmq

status 显示三个容器状态、controller id、完整 peer 列表,以及集群可达时已注册的 broker 数和 master 数。cluster status 原样输出 clusterList 表格;controller 分配的 BID 列(旧版客户端里叫 BrokerId)中,当前 master 为 0,副本为非 0:

rocketmq "rmq" — cluster prod: 3 broker(s), 1 master(s)
#Cluster Name  #Broker Name  #BID  #Addr             #Version  #InTPS(LOAD)  #OutTPS(LOAD)
prod           prod          0     10.0.0.13:10911   V5_5_0    0.00(0,0ms)   0.00(0,0ms)
prod           prod          1     10.0.0.11:10911   V5_5_0    0.00(0,0ms)   0.00(0,0ms)
prod           prod          2     10.0.0.12:10911   V5_5_0    0.00(0,0ms)   0.00(0,0ms)

列布局和行顺序由 mqadmin 产生,不同 5.x 版本可能略有差异;pgcli 只依赖 cluster 名和 broker id 列。

主备切换

master broker 停止后,controller 在心跳超时后从 in-sync 副本中选出新 master。通过 proxy 或 NameServer 连接的客户端会自动重试。观察一次切换:

# 在 cluster status 中找到 BID 0 所在主机,然后在该主机上:
pg addon stop rocketmq --name rmq

# 在任一成员上轮询,直到出现新的 BID 0:
pg addon rocketmq cluster status --name rmq

# 重新启动旧 broker;它会以副本身份重新加入:
pg addon start rocketmq --name rmq

配置中渲染了 enableElectUncleanMaster=false,不在 in-sync state set 中的副本不会被选为 master。如果 controller quorum 本身失去多数派(例如三台 NameServer 中两台停止),就无法 选出 master,写入会一直阻塞到 quorum 恢复。停止或移除一个成员不会改写其他主机上的 peer 列表;pgcli 不会联系其他节点。

端口

端口 角色 单机 集群
9876 NameServer 客户端/RPC 自动,基准 9876 固定
9877 内嵌 DLedger controller 不使用 固定
10911 broker 监听 自动,基准 10911 固定
10912 broker HA 复制 broker_port + 1 固定
10909 broker VIP broker_port - 2 固定
8080 proxy remoting 自动,基准 8080 固定
8081 proxy gRPC 自动,基准 8081 固定

单机实例可以从端口池分配端口并连同派生的 HA/VIP 端口一起保留,因此一台主机上可共存多个 实例。集群模式所有端口固定,因为 controller 成员关系和 broker 地址会写入每个节点的配置。

客户端

两种模式下 proxy 都是客户端入口,并以 CLUSTER 模式运行:

  • 5.x gRPC SDK(推荐的 5.x 客户端):连接 <advertise-host>:8081。
  • Remoting SDK(4.x 风格客户端):连接 <advertise-host>:8080,或直接连接 broker <advertise-host>:10911。
  • NameServer 地址(9876、controller 9877)是集群内部通信地址,不要暴露给远程客户端。

单机默认 127.0.0.1,其他主机上的客户端需要使用设置了 --advertise-host 的安装。集群模式 每台成员都要求非 loopback 的 IPv4 --advertise-host,各主机上的 proxy 通告本机地址。

Topic 与消息

为兼容启用了 topic 自动创建(autoCreateTopicEnable=true),但建议显式创建,以保证队列数 可预期:

pg addon rocketmq topic create surge --queues 3 --name rmq
pg addon rocketmq topic list --name rmq
pg addon rocketmq topic describe surge --name rmq

topic create 在整个集群上创建 topic(-c <cluster-or-name>),--queues 同时设置读写 队列数(默认 3)。topic list 默认隐藏 RocketMQ 内部 topic(%RETRY%…、%DLQ%…、 TBW102、SELF_TEST_TOPIC、事务/调度/trace topic、5.x 的 rmq_sys_* / RMQ_SYS_*、 *_REPLY_TOPIC 以及以 cluster 名命名的内部 topic);加 --system 显示全部。 topic describe 输出 topicStatus 的按 broker 队列和 offset 表。

发送一条消息并按 key 查询:

pg addon rocketmq send surge "hello" --key k1 --tags demo --name rmq

pg addon rocketmq shell --service namesrv --name rmq -- \
  sh mqadmin queryMsgByKey -t surge -k k1 -n 127.0.0.1:9876

send 会打印 sendMessage 结果,包括 message id、发送状态和队列。用带边界的一次性调试消费者把消息拉回来:

pg addon rocketmq consume surge --count 10 --timeout 30s --name rmq

# 只读一个队列,从起始 offset 开始
pg addon rocketmq consume surge --queue 0 --offset 0 --count 50 --name rmq

# 按时间窗口读取(epoch 毫秒或 yyyy-MM-dd#HH:mm:ss:SSS)
pg addon rocketmq consume surge --begin 1793264528000 --end 1793264535000 \
  --count 0 --name rmq

consume 在一次性容器里运行 mqadmin consumeMessage,把消息打印到终端。它把 topic 各队列当前的消息拉完就退出;--timeout(默认 30s)是容器内的硬性安全上限,Ctrl-C 可提前结束。它只做拉取和打印,不删除消息,也不提交消费位点。

--count 是 mqadmin 的消息预算(默认 10);mqadmin 按队列计算,实际打印的条数可能略少于请求值,--count 0 表示不设预算。--queue/--offset 只读一个队列,broker 名默认取实例的单 broker 名(--broker 可覆盖)。--begin/--end 接受 epoch 毫秒或 yyyy-MM-dd#HH:mm:ss:SSS;mqadmin 会把时间边界换算成队列 offset,因此结束边界可能包含恰好在其后的一条消息。

删除 topic 会删掉它的所有消息,除非加 --yes,否则会先要求确认:

pg addon rocketmq topic delete surge --name rmq --yes

消息保留

消费不会删除消息。broker 以 commitlog 段(同一 broker 上所有 topic 共用的存储单位)为 单位删除:段年龄超过 fileReservedTime 小时,并且时钟到达 deleteWhen 指定的整点时 删除;磁盘占用超过 diskMaxUsedSpaceRatio 百分比时提前删除。保留策略是 broker 级别的, 不能按 topic 单独设置。

查看运行中 broker 的实际值并修改:

pg addon rocketmq retention show --name rmq

pg addon rocketmq retention set --hours 24 --delete-when "04;16" \
  --disk-ratio 80 --name rmq

默认值为 72 小时、04 点、75%。set 至少要给一个参数:--hours(1–8760)、 --delete-when(一个或多个 0–23 的整点,用 ; 分隔)、--disk-ratio(1–100)。 它通过 updateBrokerConfig 立即修改运行中的 broker,并把相同的值写入 pg.yaml 和 重新渲染的 broker.conf,因此 pg addon stop/start 后仍然生效,无需重建容器。 过期段不保证在整点立刻删除,而是在 broker 下一次调度清理时移除。把比例调高到当前磁盘 占用以上可以停止磁盘压力触发的提前删除。

保留策略按 broker 保存。集群中要在每台成员主机上分别执行;每个 broker 只清理自己的 commitlog。

投递与持久性

单机。 broker 是唯一节点(brokerId=0、brokerRole=ASYNC_MASTER),没有副本。消息写入 data/broker 下的 broker store,因此优雅停止/启动不会丢失。渲染的刷盘策略是 RocketMQ 默认的 ASYNC_FLUSH,宿主机掉电可能丢失最近的写入。需要更强持久性时渲染 SYNC_FLUSH(会降低 吞吐):

pg addon install rocketmq --name rmq --force \
  --config-option flushDiskType=SYNC_FLUSH

集群。 broker 以 controller 模式复制。pgcli 渲染 allAckInSyncStateSet=true,3 个及以上 成员时 minInSyncReplicas=2(2 成员时为 1),因此只有所有 in-sync 副本都接受消息后 produce 才会成功。刷盘默认仍是 ASYNC;如果要求所有副本同时掉电也不丢已确认写入,请使用 flushDiskType=SYNC_FLUSH。

复制保护的是 controller quorum 健康时的 broker 故障,并不意味着 controller store 可以 随意删除:data/controller 保存 DLedger 状态和 epoch 文件。删除它、清空成员,或用 --clean-data 重装,都会破坏该成员的 controller 状态,并可能破坏 quorum。被移除后重新加入 的成员必须以空数据目录启动,并使用相同的 cluster/controller/peer 身份。

pg addon remove rocketmq --clean-data 会删除实例目录(配置、broker store、controller store 和日志)以及独立的自定义数据目录。不带 --clean-data 时会保留数据和配置目录。

配置、升级与不可变性

  • 加 --force 可修改: 版本、JVM 堆、broker.conf 覆盖项,以及单机实例的监听端口。 --config-option 会替换已存储的覆盖 map,强制重装时需重新传入全部覆盖项。
  • 即使加 --force 也不可修改: 集群模式的 --cluster、--controller-id、 --advertise-host、--peer 和固定端口。这些值已写入每个对端的配置,原地修改会被拒绝。
  • 拓扑变更必须重建: 单机 ↔ 集群不是原地变更。使用 --clean-data 删除实例后,按目标 模式重新安装。
  • 自定义数据目录: 把 --data-dir 指向新路径会使用新的空 store;pgcli 不会迁移已有数据。

pgcli 托管的 broker.conf 键(在 --config-option 中会被拒绝)包括 brokerClusterName、brokerName、brokerId、brokerRole、brokerIP1/brokerIP2、 listenPort、haListenPort、namesrvAddr、enableControllerMode、controllerAddr、 所有 controller* 键、storePathRootDir、storePathCommitLog、storePathEpochFile、 autoCreateTopicEnable、allAckInSyncStateSet、minInSyncReplicas、 enableControllerInNamesrv、enableElectUncleanMaster、notifyBrokerRoleChanged。

生命周期、日志与开机自启

pg addon start rocketmq --name rmq
pg addon stop rocketmq --name rmq
pg addon remove rocketmq --name rmq --clean-data

pg logs addon rocketmq --name rmq --service broker -f
pg logs addon rocketmq --name rmq --service namesrv
pg logs addon rocketmq --name rmq --service proxy

停止按逆序(proxy → broker → namesrv),启动按正序(namesrv → broker → proxy)。集群模式 的启动不等待 controller quorum:多数派尚未可达时,broker 会保持等待状态,命令仍会成功返回。 其他成员起来后,用 pg addon rocketmq status 和 cluster status 确认。

开机自启与其他顶层插件一致:

pg autostart enable --rocketmq --name rmq
pg autostart status

它会重新渲染三个配置并按顺序启动容器。对于集群成员,自启不等待 controller quorum,因此各 成员可以任意顺序启动。pg addon start(全局自启启动器)行为相同。

限制

  • 没有认证、TLS 或 ACL。 请限制网络;proxy 和 NameServer 对任何能连到端口的人开放。
  • 没有 Web UI。 使用 pg addon rocketmq status、cluster status、topic 命令和日志。
  • 不支持在线扩缩容或滚动升级。 集群成员关系和固定端口不可变;成员变更需要有计划地重建。 成员仍在 quorum 中时不能删除其 controller store。
  • 2 成员集群没有容错能力。 生产环境请使用 3 个及以上成员。
  • 仅 Linux。 插件依赖 host 网络和官方镜像的 uid/gid 3000。
  • 不做消息迁移、consumer group 管理或 broker/storage 重平衡。 可通过 pg addon rocketmq shell 使用标准 RocketMQ 管理工具。

17 - 健康巡检

pgcli PostgreSQL 健康巡检报告指南

pg inspect 用一条命令回答一个问题:这个库健康吗,该先修什么? 它对 PostgreSQL 标准统计视图做一次单快照采样,产出确定性报告——0-100 的健康分,加上按 CRITICAL / WARNING / NOTE / GOOD 分级的诊断条目,每条带证据、修复建议,以及防止"自信地给出错误建议"的限定说明。

它严格只读:每条语句都包在 READ ONLY 事务里执行,绝不写入、除普通读取外不加任何锁,可随时对生产库运行。

适用场景

  • 每周 / 值班例行检查 —— 对所有实例跑一遍,只看分数不到 100 的;有没有退化一目了然,不用盯仪表盘。
  • “数据库感觉变慢了” —— 在翻 EXPLAIN 计划之前,先拿到 30 秒全貌:缓存命中率、耗时 Top SQL(需 pg_stat_statements)、膨胀、会话堆积。
  • 安装 / 升级之后 —— 确认没有危险参数混进配置(比如 fsync = off),autovacuum 确实跟得上。
  • 容量决策之前 —— 看清哪些索引从未被扫描(删掉能回收多少空间)、连接数是否在逼近 max_connections。
  • 审计与交接 —— --html 生成单个自包含文件,可直接发邮件或归档;--json 喂给脚本和监控 agent。

能提前发现的问题

问题 不巡检你会错过什么
事务 ID 年龄逼近冻结墙 一次被迫停机的 vacuum 紧急事故
连接数逼近 max_connections 高负载时登录失败
缓存命中率低 shared_buffers / effective_cache_size 配小了
超过 1 MiB 的从未使用索引 空间浪费与写放大
表膨胀、从未 vacuum 或关了 autovacuum 的表 扫描越来越慢,vacuum 风暴
大表重顺序扫描 / 非 HOT 更新 缺索引和热点更新模式
长事务与 idle in transaction 会话 清理被阻塞、锁等待
偏离编译默认值的危险参数 override 悄无声息的持久性或调优错误

各项检查独立降级——没装 pg_stat_statements 的库照样出完整报告,只多一条说明该板块为空的 NOTE。

用法

# 本地实例:带健康分的终端摘要
pg inspect -i myinst

# 远程数据库(输出中绝不显示密码)
pg inspect --dsn postgres://user:pass@host:5432/db

# 各板块原始表格:queries、indexes、vacuum/tables、settings、sessions
pg inspect -i myinst --full

# 自包含 HTML 报告,可选用浏览器打开
pg inspect -i myinst --html /tmp/report.html --open

18 - 配置调优

pgcli 实例与 Patroni 集群的 postgresql.conf 硬件感知调优

pg tune 把一台机器的硬件规格和一类工作负载,换算成一套自洽的 postgresql.conf 参数——目标感知,移植进 pgcli 原生实现。它同时服务 pgcli 纳管的两种部署:普通实例(写入 postgresql.conf 的托管调优块)和 Patroni 高可用集群(通过 edit-config 写入 DCS, 由 Patroni 滚动下发到每个成员)。

默认只打印建议,什么都不改。--apply 才落盘,--revert 移除已应用的调优。

适用场景

  • 新实例装完之后 —— 容器默认值是"安全"而非"快"。一条 pg tune -i <name> --apply 就能把内存划分、WAL 尺寸、planner 代价常量调到契合这台硬件。
  • 升 / 降配之后 —— RAM 和 CPU 变了,重跑同一条命令,派生值随之更新。
  • pg inspect 报参数可疑时 —— 巡检报告会指出 shared_buffers 偏小 / 缓存命中率 偏低,pg tune 就是那条修复路径。

输入

硬件规格默认探测命令所在的宿主机(Linux 从 /proc/meminfo 取总内存,CPU 数取自 运行时)。覆盖项:

参数 含义
--type 工作负载:web、oltp、dw、desktop、mixed(默认)
--hd 存储类型:ssd(默认)、hdd、san、nvme
--ram 总内存,如 16G——共享主机探测不准时用它覆盖
--cpus CPU 数——跨主机 Patroni 集群必须显式传,按最小成员规格算
--conns 覆盖 max_connections(默认按负载预设 200/300/40/20/100)
--db-size 库大小 vs RAM,如 200G(<RAM→装得下,≤4×RAM→中等,否则→远大于)

工作负载一句话选型:应用侧多为短查询 → web/oltp;分析型、大扫描 → dw;与库同机 的单用户应用 → desktop;拿不准 → mixed。

目标服务器本身会被探测 server_version_num 和每个参数的当前值,所以表格是 CURRENT 与 RECOMMENDED 并排;版本门控参数(jit、io_method 等)只在目标版本存在时才出现。

应用

普通实例(-i <name>)

pg tune -i myinst                       # 只出建议表
pg tune -i myinst --apply               # 写入 pgcli 托管块
pg tune -i myinst --revert              # 再次移除已应用的调优

--apply 把参数记录进实例配置,并写入 postgresql.conf 的托管调优块。该块在每次 pg start 都会重新施加,因此调优能挺过重启和容器重建。凡是用 ALTER SYSTEM 设过 的(即存在于 postgresql.auto.conf)会被跳过并计数——auto.conf 永远优先。

postmaster 级参数(shared_buffers、wal_buffers、max_connections 等)需要重启: 交互式下会 y/N 询问;非交互式下 pgcli 只打印 pg stop && pg start 提示,绝不擅自重启 数据库。其余改动直接 config reload 立即生效。

Patroni 集群(--ha <scope>)

pg tune --ha app                        # 从 leader 读规格,出建议表
pg tune --ha app --apply --auto-restart # 写入 DCS,Patroni 滚动下发
pg tune --ha app --revert --auto-restart

这里参数经 patronictl edit-config 进入集群 DCS 配置——在 Patroni 的世界里这是改 postgresql.conf 值的唯一正当途径。随后 Patroni 在每个成员上施加,遇 postmaster 级改动 滚动重启。因为这有破坏性,--apply 和 --revert 都会先确认;--auto-restart 跳过确认(脚本场景)。你拒绝确认时,DCS 里什么都不会写。

建议表按 pgcli 运行所在主机的规格计算——跨主机集群成员规格可能不同,所以要传 --ram/--cpus 对齐最小成员。

校验与 --apply-conf

pg tune -i myinst --apply-conf /tmp/tune.conf              # 把建议存成文件…
$EDITOR /tmp/tune.conf                                     # …手工编辑…
pg tune -i myinst --apply --apply-conf /tmp/tune.conf      # …再以该文件为源应用

--apply-conf 指定一个 key = value 文件(postgresql.conf 风格:# 注释、值可带引号和 单位)。不加 --apply 时写出该文件(首行头注释记录输入,便于复现);加 --apply 时读取该文件,以它为参数来源。

每条 apply 路径都先校验。 每个参数都对照目标的 pg_settings 检查:未知参数名 (如 PG15 上的 io_method)、非法 bool、超出枚举集合、整数/浮点越出 [min_val, max_val] ——逐条列出后整体拒绝。一批调优里只写进去一半是最坏结果。

--dsn:只出建议

pg tune --dsn postgres://user:pass@host:5432/db

远程目标只探测、只建议;--apply/--revert 会被拒绝——拿一套启发式去改别人的库,这个 决定 pgcli 不替你做。

--web-tune:交互式站点

pg tune --web-tune                  # 启动服务器,监听 http://127.0.0.1:10864
pg tune --web-tune --listen :8080   # 自定义端口
pg tune --web-tune --open           # 启动服务器并打开浏览器

启动 HTTP 服务器,提供交互式调优计算器。页面有完整的输入表单(版本、OS、负载、RAM+单位、 CPU、连接数、存储、库大小),实时重算,postgresql.conf / ALTER SYSTEM 输出带一键复制。 无网络、无依赖;宿主机 RAM/CPU/OS 会预填表单。页面内的计算器是 Go 算法的 JS 移植; 页面自带 Go 现算的 golden 样例,加载时自检,两套实现一旦对不上就显示红条警示。

按 Ctrl+C(或发送 SIGTERM)优雅关闭服务器。

数值怎么来的

公式忠实且确定(内部以 kB 计算,每步推导都取整):

  • shared_buffers = RAM/4(desktop /16);effective_cache_size = 3/4 RAM。
  • maintenance_work_mem = RAM/16(dw /8),封顶 8 GB(Windows ≤PG17 封顶 2047 MB)。
  • work_mem = (RAM − shared_buffers) / ((max_connections + max_worker_processes) × 3), 再按负载缩放(dw/mixed ½、desktop ⅙)与库大小缩放(装得下 ×1.3、远大于 ×0.9),下限 4 MB。
  • wal_buffers = shared_buffers 的 3%,夹在 [32 kB, 16 MB]。
  • random_page_cost:hdd 及 dw-on-SSD 取 4(并带上"会选到灾难性索引扫描"的警告), 其余 1.1;effective_io_concurrency 按存储类型(仅 Linux)。
  • 并行度按 CPU 数(<4 核不出并行块);autovacuum_max_workers 在 ≥16/≥32 核才加; 版本门控:jit=off ≥12、wal_compression on/lz4 ≥10/≥15、io_method/io_workers ≥18 (io_method 恒推荐 worker——io_uring 额外要求 --with-liburing 编译支持加内核支持, 不满足会导致实例启动失败,故不自动推荐)、 desktop 专属 wal_level=minimal + max_wal_senders=0。

逐参数注意事项(lz4 编译开关、DW 代价警告)在表内就近打印;离谱的机器规格 (<256 MB、>100 GB RAM)给全局警告。

19 - KV 缓存

PostgreSQL KV 缓存函数,轻量级 Redis 替代方案

pg kv 将一组 KV 缓存函数部署到 PostgreSQL 实例,将其转变为轻量级 Redis 替代方案,用于纯键值缓存。它不是完整的 Redis 替代——没有计数器、没有有序集合、没有发布/订阅——只有快速、原子化、基于 TTL 的键值存储,支持自动过期。

提供两个变体:BYTEA(二进制值,通用缓存)和 JSONB(结构化 JSON,支持字段级更新和包含查询)。两者可以一起或分别部署。

适用场景

  • 会话存储 —— 存储用户会话、OAuth 令牌或 API 密钥,自动过期。当应用已经连接 PostgreSQL 时,比 Redis sidecar 更简单。
  • 配置缓存 —— 缓存不频繁变更的应用配置或功能开关。JSONB 变体支持部分字段更新,无需重写整个对象。
  • 限流 —— 按用户/IP 跟踪请求计数或时间戳。UPSERT 原子性防止并发写入时的竞态条件。
  • 临时数据 —— 任务队列、锁令牌、一次性代码。UNLOGGED TABLE 意味着无 WAL 开销;对于临时缓存,崩溃丢数据可接受。
  • 原型开发 —— 避免为副项目启动 Redis。一条命令(pg kv -i myinst --database myapp)即可获得可用缓存。

不适用场景

  • 高吞吐(持续 >500 QPS)—— 使用 Redis 或 Memcached。PostgreSQL 未针对每秒数百万个小 KV 操作优化。
  • 有序集合、计数器、发布/订阅 —— 这些是 PostgreSQL 不复刻的 Redis 特性。使用 Redis 或应用层逻辑。
  • 大值(每个键 >1 MB)—— BYTEA 存储可行但效率低。使用对象存储(S3/MinIO)或文件系统。
  • 需要持久化 —— UNLOGGED TABLE 意味着崩溃或非正常关机时数据丢失。如果缓存丢失不可接受,使用普通表或带 AOF 的 Redis。

用法

# 部署 BYTEA 和 JSONB 两个变体(默认)
pg kv -i myinst --database myapp

# 仅部署 BYTEA(通用二进制值)
pg kv -i myinst --database myapp --type bytea

# 仅部署 JSONB(结构化 JSON,支持字段更新)
pg kv -i myinst --database myapp --type json

# 通过 DSN 目标远程数据库
pg kv --dsn postgres://user:pass@host:5432/otherdb --database myapp

# 预览 SQL 不执行
pg kv -i myinst --database myapp --dry-run

--database 标志是必填的——pgcli 不会自动创建数据库,目标必须已存在。命令是幂等的:多次运行是安全的(函数使用 CREATE OR REPLACE,表使用 IF NOT EXISTS)。

BYTEA 变体

通用二进制键值缓存。值存储为 BYTEA(任意二进制数据)。用于字符串、序列化对象或原始字节。

表

CREATE UNLOGGED TABLE kv_cache (
    key TEXT PRIMARY KEY,
    value BYTEA NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL
);

函数

函数 签名 返回值 描述
kv_set (p_key TEXT, p_value BYTEA, p_ttl_seconds INT DEFAULT 300) VOID 以 UPSERT 原子性写入缓存。默认 TTL 300秒。
kv_set_with_result (p_key TEXT, p_value BYTEA, p_ttl_seconds INT DEFAULT 300) BOOLEAN 与 kv_set 相同,成功返回 TRUE,错误返回 FALSE。
kv_get (p_key TEXT) BYTEA 读取缓存(自动过滤过期)。未找到或已过期返回 NULL。
kv_get_text (p_key TEXT) TEXT 读取缓存并转换为文本(convert_from UTF-8)。未找到返回 NULL。
kv_del (p_key TEXT) VOID 删除缓存条目。
kv_cleanup_batch (max_rows INT DEFAULT 1000) INT 删除最多 max_rows 条过期条目。返回实际删除数量。

示例

-- 写入缓存条目(文本转换为 bytea)
SELECT kv_set('session:abc123', 'user_id=42'::bytea, 3600);

-- 以文本读取
SELECT kv_get_text('session:abc123');
-- 返回: user_id=42

-- 以原始 bytea 读取(十六进制编码)
SELECT kv_get('session:abc123');
-- 返回: \x757365725f69643d3432

-- 检查键是否存在(过期或缺失返回 NULL)
SELECT kv_get('session:xyz') IS NOT NULL;

-- 删除键
SELECT kv_del('session:abc123');

-- 清理过期条目(通过 cron 或应用定期运行)
SELECT kv_cleanup_batch(1000);

JSONB 变体

结构化 JSON 缓存,支持字段级更新和包含查询。值存储为 JSONB,带 GIN 索引用于高效的 @> 查询。

表

CREATE UNLOGGED TABLE kv_cache_json (
    key TEXT PRIMARY KEY,
    value JSONB NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_kv_cache_json_gin ON kv_cache_json USING GIN (value);

GIN 索引服务于用户编写的临时查询(WHERE value @> '{"status":"active"}')。函数 API 使用主键查找,因此 json_get/json_set 不使用该索引。

函数

函数 签名 返回值 描述
json_set (p_key TEXT, p_value JSONB, p_ttl_seconds INT DEFAULT 300) VOID 以 UPSERT 原子性写入 JSON 缓存。默认 TTL 300秒。
json_set_with_result (p_key TEXT, p_value JSONB, p_ttl_seconds INT DEFAULT 300) BOOLEAN 与 json_set 相同,成功返回 TRUE。
json_get (p_key TEXT) JSONB 读取整个 JSON 对象。未找到或已过期返回 NULL。
json_get_field (p_key TEXT, p_field TEXT) TEXT 以文本读取指定字段(->> 操作符)。键或字段缺失返回 NULL。
json_get_field_json (p_key TEXT, p_field TEXT) JSONB 以 JSONB 读取指定字段(-> 操作符)。键或字段缺失返回 NULL。
json_contains (p_key TEXT, p_query JSONB) BOOLEAN 检查缓存的 JSON 是否包含查询子集(@> 操作符)。键缺失返回 FALSE。
json_update_field (p_key TEXT, p_field TEXT, p_new_value TEXT, p_ttl_seconds INT DEFAULT NULL) BOOLEAN 部分更新指定字段。智能类型转换(数字/布尔/数组/对象/字符串)。p_ttl_seconds NULL 保留原过期时间;非 NULL 重置它。p_new_value SQL NULL 返回 FALSE(防止意外字段删除)。键缺失返回 FALSE。
json_del (p_key TEXT) VOID 删除 JSON 缓存条目。
json_cleanup_batch (max_rows INT DEFAULT 1000) INT 删除最多 max_rows 条过期条目。返回实际删除数量。

示例

-- 写入 JSON 对象
SELECT json_set('user:42', '{"name":"Alice","age":30,"roles":["admin","user"]}'::jsonb, 3600);

-- 以文本读取字段
SELECT json_get_field('user:42', 'name');
-- 返回: Alice

-- 以 JSONB 读取字段(保留类型)
SELECT json_get_field_json('user:42', 'age');
-- 返回: 30(整数类型)

-- 读取整个对象
SELECT json_get('user:42');
-- 返回: {"age": 30, "name": "Alice", "roles": ["admin", "user"]}

-- 检查对象是否包含子集
SELECT json_contains('user:42', '{"age":30}'::jsonb);
-- 返回: t

-- 更新单个字段(保留其他字段,智能类型转换)
SELECT json_update_field('user:42', 'age', '31');
SELECT json_get_field('user:42', 'age');
-- 返回: 31(仍是整数,不是字符串)

-- 添加新字段
SELECT json_update_field('user:42', 'email', 'alice@example.com');

-- 更新字段并重置 TTL
SELECT json_update_field('user:42', 'age', '32', 7200);

-- 更新字段不改变过期时间
SELECT json_update_field('user:42', 'age', '33', NULL);

-- 删除键
SELECT json_del('user:42');

-- 清理过期条目
SELECT json_cleanup_batch(1000);

并发与原子性

两个变体都使用 ON CONFLICT(UPSERT)实现原子写入。对同一键的并发 kv_set 调用不会竞态——一个获胜,另一个覆盖。除单行外不持有锁。

读操作(kv_get、json_get)无副作用且永不阻塞。过期条目通过 WHERE expires_at > NOW() 过滤——它们不会物理删除,直到 kv_cleanup_batch 运行。

过期与清理

条目基于 TTL(默认 300 秒)过期。过期条目不会自动删除——它们保留在表中,直到调用 kv_cleanup_batch 或 json_cleanup_batch。

推荐的清理策略:

  1. 应用层 cron —— 每 5-10 分钟从应用或 cron 任务调用 kv_cleanup_batch(1000)。
  2. pg_cron 扩展 —— 如已安装,通过 cron.schedule 调度 SELECT kv_cleanup_batch(1000)。
  3. 惰性清理 —— 每 N 次写入后调用清理(例如 if (rand() < 0.01) kv_cleanup_batch(1000))。

max_rows 参数限制每次清理,防止长事务。每批 1000 行,即使有数百万过期条目,清理也只需 <10ms。

限制

  • 无持久化 —— UNLOGGED TABLE 意味着崩溃时数据丢失。缓存可接受,主数据不可接受。
  • 无复制 —— UNLOGGED TABLE 不被逻辑复制复制(它们被物理流复制复制,但仅表结构,非数据)。
  • 无分区 —— 所有键在单表中。对于超大缓存(>1000万键),考虑按键前缀分片或使用 Redis。
  • 无 TTL 索引 —— 过期条目未单独索引。清理使用普通 B 树索引扫描 expires_at < NOW()。

验证

部署后,验证函数是否存在:

# 检查函数数量(--type all 应为 15)
pg exec -i myinst -c "SELECT COUNT(*) FROM pg_proc WHERE proname LIKE 'kv_%' OR proname LIKE 'json_%'"

# 列出所有 KV 函数
pg exec -i myinst -c "SELECT proname FROM pg_proc WHERE proname LIKE 'kv_%' OR proname LIKE 'json_%' ORDER BY proname"

--type all 的预期输出:

         proname
-------------------------
 json_cleanup_batch
 json_contains
 json_del
 json_get
 json_get_field
 json_get_field_json
 json_set
 json_set_with_result
 json_update_field
 kv_cleanup_batch
 kv_del
 kv_get
 kv_get_text
 kv_set
 kv_set_with_result
(15 rows)

测试

scripts/test-kv-cache.sh 提供了测试脚本。覆盖所有 15 个函数(6 个 BYTEA + 9 个 JSONB),包括边缘情况(NULL 处理、TTL 保留、过期条目清理)。

# 对本地实例运行测试
bash scripts/test-kv-cache.sh myinst

# 对默认实例运行测试
bash scripts/test-kv-cache.sh

如果所有测试通过,脚本以代码 0 退出;如果有任何失败,以代码 1 退出。在 CI 或升级后使用它确认函数按预期工作。

卸载

没有 pg kv remove 命令。函数和表保留在数据库中,直到手动删除:

-- 移除 BYTEA 变体
DROP TABLE IF EXISTS kv_cache CASCADE;

-- 移除 JSONB 变体
DROP TABLE IF EXISTS kv_cache_json CASCADE;

CASCADE 子句自动删除依赖函数。

20 - 工具箱

扩展 pgcli 能力的独立工具,无需 PostgreSQL 实例

在核心实例管理之外扩展 pgcli 能力的工具与使用指南。

  • ZFS 存储 — 安装 ZFS 并管理 pool、dataset 和快照
  • PGMQ 消息队列 — 在 PostgreSQL 内部实现持久化消息队列
  • SQLite + Litestream — 管理宿主机 SQLite 数据库并持续备份到 S3
  • TimescaleDB — 在 PostgreSQL 上使用时序 hypertable 和 time_bucket 分析

20.1 - ZFS 存储管理

在 pgcli 中安装 ZFS 并管理 pool、dataset 和 snapshot

pg zfs 把一台 Linux 主机变成 ZFS 存储服务器:安装软件包、检查磁盘、创建带冗余的 pool,并管理 dataset 和 snapshot。它直接与内核交互,不读写 pg.yaml,也不依赖任何 PostgreSQL 实例。

命令按“资源 → 动作”组织:pg zfs pool create、pg zfs dataset create、 pg zfs snapshot rollback。没有封装的能力可通过 pg zfs raw 原样透传。

支持的系统

pg zfs install 从 /etc/os-release 识别发行版,并自动准备软件源后完成安装:

发行版 ZFS 来源 实测版本
Debian 12 (bookworm) Debian contrib,DKMS 2.1.11
Debian 13 (trixie) Debian contrib,DKMS 2.3.9
Ubuntu 24.04 (noble) Ubuntu universe 2.2.2,使用 Ubuntu 内核模块包
Ubuntu 26.04 Ubuntu universe 2.4.1
Fedora 44 OpenZFS release RPM,DKMS 2.4.4
Rocky Linux 10 OpenZFS release RPM + EPEL,DKMS 2.2.11

这些是安装器测试时上游仓库提供的版本,实际安装到的版本由仓库决定; pg zfs status 总会打印已安装的用户态与内核模块版本。

其他发行版只会被识别并给出手动安装指引,不会自动安装。仅支持 Linux。

安装

pg zfs install

命令零参数、幂等:

  1. 检查权限(root,或可用的 sudo -n)。
  2. 自动启用所需软件源(Debian contrib、Ubuntu universe、 Fedora/Rocky 的 OpenZFS release 仓库)。
  3. 安装 ZFS、内核头文件/模块和 ZED 守护进程。
  4. 加载模块并把 ZFS 的 systemd 单元设为开机自启。
  5. 打印用户态与内核模块版本。

在健康的机器上重复执行是 no-op。安装不提供 --version、--backports、 --force 等开关——需要特定 ZFS 版本时请用发行版包管理器升级,之后交由 pg zfs 管理。

随时查看状态:

pg zfs status
OS:            Debian GNU/Linux 12 (bookworm)
Family:        debian
ZFS:           installed
Userland:      zfs-2.1.11-1+deb12u1
Kernel module: 2.1.11-1+deb12u1 (loaded)
Kernel:        6.1.0-54-cloud-amd64
Privilege:     root
Pools:         none
Secure Boot:   disabled

RAID 级别与 ZFS 的对应关系

ZFS 没有标准的 RAID2 / RAID3 级别,对应关系如下:

--type ZFS 布局 冗余 类比
stripe(raid0) 条带 vdev 无 RAID0
mirror(raid1) 镜像 1 份以上副本 RAID1
raidz1 单校验 1 块盘 RAID5
raidz2(raid2) 双校验 2 块盘 RAID6
raidz3(raid3) 三校验 3 块盘 三校验 RAID

为方便起见也接受 raid0/raid1/raid2/raid3 别名。

快速开始

# 1. 安装 ZFS
pg zfs install

# 2. 查看哪些磁盘可用
pg zfs disk list

# 3. 两块整盘 -> 自动创建 mirror
pg zfs pool create tank /dev/vdb /dev/vdc

# 4. 确认 pool 在线
pg zfs pool list

# 5. 为备份创建 dataset
pg zfs dataset create tank/backups

# 6. 升级前创建快照
pg zfs snapshot create tank/backups@before-upgrade

# 7. 升级失败时回滚
pg zfs snapshot rollback tank/backups@before-upgrade

pool 挂载在 /tank,dataset 挂载在 /tank/backups。回滚会丢弃快照之后的所有 改动,执行前需要确认。

复用旧 pool 的磁盘

ZFS 会在盘上保留 vdev 分区和标签,export 不会清除它们,因此安全校验会拒绝 这些盘。创建新 pool 前先销毁旧 pool 并清理残留:

# 1. 旧 pool 的数据不再需要时,先导入再销毁
pg zfs pool import fast
pg zfs pool destroy fast --force

# 2. 清除 ZFS 标签和分区表
pg zfs raw zpool labelclear -f /dev/vdd
pg zfs raw zpool labelclear -f /dev/vde
wipefs -a /dev/vdd /dev/vde

# 3. 创建新 pool
pg zfs pool create tank /dev/vdd /dev/vde

如果整盘 labelclear 报错,改清成员分区(/dev/vdd1、/dev/vde1)。旧 pool 已被销毁时跳过第 1 步。

磁盘安全

pg zfs disk list 会列出每块整盘的挂载点、ZFS 归属以及 pgcli 是否认为它可以安全 交给 ZFS 使用。

创建 pool 时拒绝:

  • 根盘,或带有已挂载分区的磁盘;
  • 分区、loop、ROM、CD-ROM 设备;
  • 带有文件系统(ext4、xfs、swap、LVM、LUKS、mdraid 等)的磁盘;
  • 已有 ZFS 标签的磁盘;
  • 只读磁盘;
  • 重复设备(包括通过不同路径指向的同一块盘);
  • 已被其他 pool 使用的磁盘。

--force 只跳过确认提示,不会映射为 ZFS 的 -f,也不绕过任何安全检查。

创建 pool

自动布局

位置参数的数量决定布局:

盘数 布局
1 stripe(告警:无冗余)
2 mirror
3 raidz1
4 及以上 raidz2
pg zfs pool create tank /dev/vdb /dev/vdc              # mirror
pg zfs pool create data /dev/vdb /dev/vdc /dev/vdd     # raidz1
pg zfs pool create bulk /dev/vdb /dev/vdc /dev/vdd /dev/vde   # raidz2

用 --type 覆盖:

pg zfs pool create fast /dev/vdb /dev/vdc /dev/vdd /dev/vde --type mirror
pg zfs pool create safe /dev/vdb /dev/vdc /dev/vdd /dev/vde /dev/vdf --type raidz3

同一 vdev 内请尽量使用容量一致的磁盘(性能也应相近)。ZFS 不会拒绝混合容量, 但 vdev 的可用容量由最小的成员盘决定,大盘多出的空间会被浪费。同一 pool 的不同 vdev 之间可以容量不同。

多个 vdev

使用可重复的 --data 分组,并显式给出 --type。每个分组是一个 vdev,pool 在 这些 vdev 之上做条带:

# 两个 mirror vdev(条带化镜像)
pg zfs pool create fast --type mirror \
  --data /dev/vdb,/dev/vdc --data /dev/vdd,/dev/vde

# 两个 raidz2 vdev
pg zfs pool create bulk --type raidz2 \
  --data /dev/vdb,/dev/vdc,/dev/vdd,/dev/vde \
  --data /dev/vdf,/dev/vdg,/dev/vdh,/dev/vdi

--type 作用于每个 --data 组内部,而不是 pool 整体:每个组建成该布局的一个 vdev,pool 在这些 vdev 之上条带。所有组共用同一个 --type;pg zfs 不支持 一次创建中混用 mirror、raidz2 等布局,混合拓扑需使用 pg zfs raw zpool create。

pool 容量为各 vdev 可用容量之和;冗余按 vdev 计算,任一 vdev 失去超出其容错能力 的磁盘都会导致整个 pool 不可用。

SLOG、special、cache 与热备

pg zfs pool create tank /dev/vdb /dev/vdc \
  --type mirror \
  --special /dev/vdd,/dev/vde \
  --log /dev/vdf,/dev/vdg \
  --cache /dev/vdh \
  --spare /dev/vdi

--special 跟随 --type(这里是 mirror)。--log 两块及以上自动 mirror,单块为 stripe 并给出告警。--cache 与 --spare 接受独立的设备列表。

同一块盘只能出现在 data、special、log、cache、spare 中的一处。

建池后也可以用 pool add 追加热备盘:

pg zfs pool add tank --class spare /dev/vdi

默认属性

除非指定 --no-defaults:

对象 属性 值
pool ashift 12
pool autoexpand on
pool autotrim 所有数据盘均为非旋转盘且支持 discard 时为 on
根 dataset compression zstd
根 dataset atime off
根 dataset xattr sa
根 dataset acltype posixacl
根 dataset dnodesize auto

pool 属性用 --prop/-p 覆盖(对应 zpool -o),根 dataset 属性用 --fs-prop/-P 覆盖(对应 zpool -O):

pg zfs pool create tank /dev/vdb /dev/vdc \
  --prop ashift=13 --fs-prop compression=lz4 --fs-prop atime=on

-o 是 pgcli 的全局输出参数,所以 pool/dataset 属性使用 -p/-P。在 pg zfs raw 内部,ZFS 原生参数照常可用。

管理 pool

pg zfs pool list
pg zfs pool status tank
pg zfs pool status --brief          # 只显示有问题的 pool
pg zfs pool scrub tank
pg zfs pool scrub tank --stop

扩容与修复:

# 增加一个 mirror vdev
pg zfs pool add tank /dev/vdd /dev/vde --type mirror

# 把 /dev/vdd 附加到 /dev/vdc 所在的 mirror(2 盘扩成 3 盘)
pg zfs pool attach tank /dev/vdc /dev/vdd

# 再缩回 2 盘
pg zfs pool detach tank /dev/vdd

# 用新盘 /dev/vde 替换故障盘 /dev/vdc
pg zfs pool replace tank /dev/vdc /dev/vde

# 临时下线/上线
pg zfs pool offline tank /dev/vdc
pg zfs pool online tank /dev/vdc

attach 针对 mirror vdev。给 raidz vdev 增加磁盘(raidz expansion)需要 OpenZFS 2.3 及以上;Debian 12(2.1)和 Ubuntu 24.04(2.2)不支持。新版本上可用 pg zfs raw zpool attach 操作。

卸载与彻底删除 pool

pg zfs pool export 会卸载所有 dataset 并把 pool 从主机上分离,但数据仍完整 保存在成员盘上:

pg zfs pool export tank

已 export 的 pool 不会在开机时自动导入。重新接管时先列出可导入的 pool,再导入:

pg zfs pool import                  # 列出可导入的 pool
pg zfs pool import tank
pg zfs pool import tank --dir /dev/disk/by-id --no-mount

pg zfs pool destroy 会删除 pool、所有 dataset 和全部数据:

pg zfs pool destroy tank

destroy 只能作用于已导入的 pool。如果 pool 已 export,需要先导入再销毁:

pg zfs pool import tank
pg zfs pool destroy tank

不加 --force 时这两个命令都会要求确认;EOF 和默认答案均为“否”。非交互自动化 必须显式传 --force。--force 只跳过确认,绝不会映射为 ZFS 的 -f,也不绕过 任何安全校验。export 可逆,destroy 不可逆。

持久化与开机自动导入

pool 和 dataset 默认在重启后自动恢复,无需额外配置:

  • pg zfs install 会启用 zfs-load-module、zfs-import-cache、zfs-mount 和 zfs-zed(单元存在时)。
  • zpool create 会把 pool 记录到 /etc/zfs/zpool.cache;开机时 zfs-import-cache 据此导入。
  • ZFS 不使用 /etc/fstab。每个 dataset 的 mountpoint 是 ZFS 属性,开机时 由 zfs-mount 完成挂载。pool 的根 dataset 挂载在 /<pool>,子 dataset 挂载在其下(例如 /fast/data)。
  • pool 状态保存在成员盘上,异常断电或非正常关机后数据仍可导入。

检查配置:

systemctl is-enabled zfs-load-module zfs-import-cache zfs-mount zfs-zed
pg zfs raw zpool get cachefile fast
pg zfs raw zfs get mountpoint fast

重启后 pg zfs pool list 应能看到该 pool,findmnt /fast 应显示已挂载。

真实硬件上建议用 /dev/disk/by-id/... 等稳定路径创建 pool。/dev/sdX 名称可能在重启后变化,设备找不到时 pool 不会导入。把磁盘移到其他主机前先执行 pg zfs pool export tank,对方即可用 pg zfs pool import tank 接管。

Dataset 与 snapshot

pg zfs dataset create tank/data
pg zfs dataset create tank/a/b/c --parents
pg zfs dataset create tank/logs --prop quota=50G --prop recordsize=128K

pg zfs dataset list
pg zfs dataset list tank/data --props quota,recordsize,compression

pg zfs dataset set tank/data compression=lz4 quota=100G

pg zfs snapshot create tank/data@snap1
pg zfs snapshot create tank/data@weekly --recursive
pg zfs snapshot list tank/data
pg zfs snapshot rollback tank/data@snap1
pg zfs snapshot destroy tank/data@snap1

pg zfs dataset clone tank/data@snap1 tank/clone --prop compression=off
pg zfs dataset destroy tank/clone
pg zfs dataset destroy tank/a --recursive

带子 dataset 的 dataset 必须加 --recursive 才能销毁。

回滚

pg zfs snapshot rollback tank/data@snap1 把 dataset 恢复到快照记录的状态, 之后的所有改动都会被丢弃。如果存在更新的快照,ZFS 会拒绝回滚;加 --recursive 会先删除这些更新的快照。依赖该快照的 clone 也会阻止回滚——先删除 clone,或用 pg zfs raw zfs rollback -R(会一并删除依赖的 clone)。非交互运行需加 --force 跳过确认。

# 升级前创建快照
pg zfs snapshot create tank/data@before-upgrade

# 回滚(需要确认)
pg zfs snapshot rollback tank/data@before-upgrade

# 非交互自动化
pg zfs snapshot rollback tank/data@before-upgrade --force

# 存在更新的快照:--recursive 会一并删除
pg zfs snapshot rollback tank/data@before-upgrade --recursive --force

# clone 阻止回滚:-R 会一并删除依赖的 clone
pg zfs raw zfs rollback -R tank/data@before-upgrade

克隆

pg zfs dataset clone tank/data@snap1 tank/clone 从快照创建一个新的可写 dataset。未改动的数据块按写时复制共享,不会复制整份数据。用 --prop 设置新 dataset 的属性:

# 从快照创建可写副本
pg zfs dataset clone tank/data@snap1 tank/clone

# 为副本设置属性
pg zfs dataset clone tank/data@snap1 tank/clone \
  --prop compression=off --prop mountpoint=/tank/clone

# 提升 clone 断开依赖,之后可删除原快照
pg zfs raw zfs promote tank/clone
pg zfs snapshot destroy tank/data@snap1
pg zfs dataset destroy tank/clone --force

clone 依赖的快照在 clone 存在期间不能销毁。需要删除旧快照时,先用 pg zfs raw zfs promote tank/clone 提升 clone,使其独立于原 dataset。

raw 透传

pg zfs raw zpool status -v
pg zfs raw zpool set comment="backup pool" tank
pg zfs raw zfs create -o compression=off tank/scratch
pg zfs raw zpool create -f -O compression=gzip temp /dev/vdb

raw 绕过所有安全校验和确认,是未封装参数的逃生通道——请像以 root 身份直接运行 zpool/zfs 一样谨慎。二进制管道也可以工作,例如:

pg zfs snapshot create tank/data@send
pg zfs raw zfs send tank/data@send | pg zfs raw zfs receive tank/recv

权限模型

所有会调用 zpool 或 zfs 的命令都会先检查权限:

  • root:直接执行;

  • 非 root 且 sudo -n 可用:所有 ZFS 命令通过 sudo -n 执行;

  • 否则立即失败:

    ZFS management requires root: run "sudo pg zfs ..." or configure passwordless sudo for this user

pgcli 从不弹出 sudo 密码提示。pg zfs status 和 pg zfs disk list 在无权限时仍会 输出静态信息,并把 pool 详情标记为不可用。

命令参考

命令 用途
pg zfs install 安装 ZFS(零参数、幂等)
pg zfs status 发行版、软件包、模块与 pool 概览
pg zfs disk list [--all] 整盘、挂载、ZFS 归属与安全性
pg zfs pool create NAME DEVICE... 创建 pool
pg zfs pool list 列出 pool
pg zfs pool status [NAME] [--brief] 详细状态
pg zfs pool add NAME DEVICE... 增加 vdev
pg zfs pool attach/detach NAME DEVICE [NEW] 扩大/缩小 mirror
pg zfs pool replace NAME OLD NEW 替换磁盘
pg zfs pool offline/online NAME DEVICE 下线/上线磁盘
pg zfs pool export/import [NAME] 在系统间迁移 pool
pg zfs pool scrub NAME [--stop] 校验数据完整性
pg zfs pool destroy NAME 销毁 pool 及全部数据
pg zfs dataset create/list/set/clone/destroy dataset 生命周期
pg zfs snapshot create/list/rollback/destroy snapshot 生命周期
pg zfs raw zpool/zfs ARGS... 绕过封装的透传

故障排查

pg zfs install 找不到软件包。 检查发行版归档源是否可达、contrib/universe 是否已启用。安装器只修改发行版官方归档源(绝不动第三方源),并在每个被修改的文件旁 留下 *.pgcli.bak。

DKMS 构建失败。 确认当前内核的头文件已安装(Debian/Ubuntu 为 linux-headers-$(uname -r),Fedora/Rocky 为 kernel-devel-$(uname -r))。安装器 会给出构建日志位置;它不会重启或更换内核。内存很小的 VM 请先加 swap。

Secure Boot 已开启。 DKMS 模块需要签名。pgcli 只报告该情况,不会自动登记密钥; 请用 MOK 签名模块,或在测试机上关闭 Secure Boot。

模块未加载。 执行 modprobe zfs 后运行 pg zfs status。安装器会启用 zfs-load-module、zfs-import-cache、zfs-mount 和 zfs-zed,使 pool 开机 自动导入。

磁盘因分区或旧标签被拒绝。 如果确认磁盘空闲,清理残留:

pg zfs raw zpool labelclear -f /dev/vdb
wipefs -a /dev/vdb

在 OpenZFS 2.2 及更新版本上,若标签位于 ZFS 成员分区内,清理整盘会报 failed to clear label for /dev/vdb。此时改为清理该分区,再清分区表:

pg zfs raw zpool labelclear -f /dev/vdb1
wipefs -a /dev/vdb

绝不要对已导入 pool 的成员盘这样做——pg zfs disk list 会显示所属 pool,安全检查 也会拒绝这类磁盘。

SELinux(Fedora/Rocky)。 ZFS 挂载点使用标准文件上下文。若服务无法访问某个 dataset,请查看 ausearch/journalctl 中的 AVC 拒绝记录,而不是全局关闭 SELinux。

重启后 pool 没有自动导入。 以 root 运行 systemctl status zfs-import-cache 和 zpool import。用 /dev/sdX 名称创建的 pool 会受设备重命名影响;真实硬件上 建议使用稳定路径(/dev/disk/by-id/...)创建 pool。

20.2 - PGMQ 消息队列

用 pgmq 扩展在 PostgreSQL 内部实现持久化消息队列

pgmq 把 PostgreSQL 变成持久化消息队列:不需要 Redis、RabbitMQ 或后台进程, 只有 pgmq schema 下的一组表和 SQL 函数。生产者发送 JSON 消息,消费者按可见性 超时读取,消息在被显式删除或归档之前一直保留在队列里。

安装

pgcli 从 Pigsty 仓库安装扩展包,并在实例的默认数据库执行 CREATE EXTENSION IF NOT EXISTS pgmq:

pg extension install pgmq -i pg01

同一实例的其他数据库需要单独创建扩展:

pg psql -i pg01 -- -d appdb
CREATE EXTENSION IF NOT EXISTS pgmq;

查看扩展版本:

pg exec -i pg01 "SELECT extversion FROM pg_extension WHERE extname = 'pgmq'"

快速开始

-- 创建队列;对已存在的队列重复执行不会报错
SELECT pgmq.create('jobs');

-- 发送一条消息,返回消息 id
SELECT pgmq.send('jobs', '{"task": "resize", "image_id": 42}');

-- 读取最多 10 条,60 秒内对其他消费者不可见
SELECT msg_id, read_ct, message
FROM pgmq.read('jobs', 60, 10);

-- 确认一条消息
SELECT pgmq.delete('jobs', 1);

通过 pgcli 执行同样的语句:

pg exec -i pg01 "SELECT pgmq.send('jobs', '{\"task\": \"resize\"}')"
pg psql -i pg01

队列在磁盘上的形态

每个队列都是 pgmq schema 里的一张表:

对象 用途
pgmq.q_<name> 在队消息
pgmq.a_<name> pgmq.archive 写入的归档表
pgmq.meta list_queues 和 metrics 使用的队列表册

一条在队消息包含 msg_id、read_ct(投递次数)、enqueued_at、vt (再次可见时间)和 message jsonb。较新版本还增加了 last_read_at 和 headers。

消息就是表里的行,因此 pgmq.send 参与事务:随所在语句一起提交或回滚。 队列流量也是普通表流量——高频 send/delete 会像其他高写入表一样产生膨胀。

发送消息

SELECT pgmq.send('jobs', '{"task": "resize"}');

-- 30 秒后才可被读取
SELECT pgmq.send('jobs', '{"task": "cleanup"}', 30);

-- 批量发送:一条语句、一次原子写入
SELECT pgmq.send_batch('jobs', ARRAY[
  '{"task": "a"}',
  '{"task": "b"}',
  '{"task": "c"}'
]::jsonb[]);

send 返回新的 msg_id,send_batch 为每条消息各返回一个 id。批量场景应使用 send_batch,而不是循环调用 send——它只有一次网络往返,且整批原子写入。

消息必须是合法 JSON,存储类型为 jsonb:键会重排、重复键会被去掉。不要把大块 二进制内容放进队列。

较新版本支持 headers jsonb 参数,可携带 trace id、FIFO 分组键等元数据。

读取与确认

SELECT msg_id, read_ct, enqueued_at, vt, message
FROM pgmq.read('jobs', 60, 10);

read(queue, vt, qty) 最多返回 qty 条可见消息,并在 vt 秒内让它们对其他 消费者不可见。读取不会删除消息,每条都必须显式确认:

SELECT pgmq.delete('jobs', 42);                -- 单条
SELECT pgmq.delete('jobs', ARRAY[42, 43, 44]); -- 批量

pgmq.pop('jobs') 读取并删除一步完成;只有在消费者崩溃时丢一条在途消息可以接受时 才使用它。

长期运行的消费者不要用紧凑轮询:

SELECT msg_id, message
FROM pgmq.read_with_poll('jobs', 60, 10, 5, 100);

它最多等待 max_poll_seconds(5 秒),每 poll_interval_ms(100 毫秒)检查一次, 有消息到达就立即返回。

可见性超时与重试

可见性超时是 PGMQ 的投递模型:

  1. read 把 vt 设为 now() + timeout,并递增 read_ct;
  2. 在 vt 之前,其他消费者看不到这条消息;
  3. delete 或 archive 将其移除。若消费者崩溃或没有及时确认,消息在 vt 到期后 重新可见,并被投递给另一个消费者。

因此跨故障场景下投递语义是 at-least-once(至少一次):vt 要大于最坏处理 耗时,消费者要保证幂等。长任务可以在不重新读取的情况下续期:

SELECT pgmq.set_vt('jobs', 42, 300);            -- 单条
SELECT pgmq.set_vt('jobs', ARRAY[42, 43], 300); -- 批量

read_ct 可以直接当重试计数器使用。

归档与死信

delete 是永久删除。archive 把消息移到 pgmq.a_<queue>,便于保留、审计或 重放:

SELECT pgmq.archive('jobs', 42);
SELECT pgmq.archive('jobs', ARRAY[42, 43]);
SELECT * FROM pgmq.a_jobs;

一个简单的死信模式是归档重试次数过多的消息:

-- 把已投递 5 次及以上的消息移入归档表
SELECT pgmq.archive('jobs', ARRAY(
  SELECT msg_id FROM pgmq.q_jobs WHERE read_ct >= 5
));

没有这类规则时,毒消息会被无限重投。

监控

SELECT * FROM pgmq.list_queues();
SELECT * FROM pgmq.metrics('jobs');
SELECT * FROM pgmq.metrics_all();

metrics 返回 queue_length、queue_visible_length、最新/最老消息的年龄以及 total_messages。较旧的扩展版本返回的列更少。

也可以直接查询队列表:

SELECT count(*) FROM pgmq.q_jobs;

SELECT msg_id, read_ct, enqueued_at, vt, message
FROM pgmq.q_jobs
ORDER BY msg_id DESC
LIMIT 5;

queue_length 持续增长或 oldest_msg_age_sec 不断变大时应当告警:要么消费者 挂了,要么消费速度跟不上。

维护

SELECT * FROM pgmq.list_queues();

SELECT pgmq.create_unlogged('scratch');                    -- 不写 WAL,崩溃后丢失
-- 分区队列依赖 pg_partman:pg extension install pg_partman -i pg01
SELECT pgmq.create_partitioned('events', '10000', '100000');
SELECT pgmq.purge_queue('jobs');                           -- 清空消息,保留队列
SELECT pgmq.drop_queue('jobs');                            -- 删除队列和归档表
  • create_unlogged 用崩溃安全性换写入速度,只用于可重新生成的数据。
  • create_partitioned 依赖 pg_partman 扩展。它避免超高吞吐队列把单表撑大; 旧分区会自动丢弃。
  • purge_queue 清空但保留队列;drop_queue 彻底删除。
  • 队列名最长 47 个字符。

普通队列在高频 send/delete 下会产生 dead tuple。确认 autovacuum 能跟上;写入量 很大时优先考虑归档代替删除,或使用分区队列。

权限

PGMQ 函数以调用者的权限执行。应用角色需要 schema、函数和队列表的访问权限:

GRANT USAGE ON SCHEMA pgmq TO app;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON pgmq.q_jobs TO app;
GRANT SELECT, INSERT ON pgmq.a_jobs TO app;   -- 仅归档时需要

pgmq.create 应以队列所有者身份执行;建表权限通常不应授予应用角色。新建队列需要 单独授权,或预先配置 default privileges。

函数参考

函数 用途
pgmq.create(name) 创建队列
pgmq.create_unlogged(name) 创建 unlogged 队列
pgmq.create_partitioned(name, ...) 创建分区队列
pgmq.send(name, msg [, delay]) 发送单条消息,返回 msg_id
pgmq.send_batch(name, msgs jsonb[]) 原子批量发送
pgmq.read(name, vt, qty) 读取并隐藏消息
pgmq.read_with_poll(name, vt, qty, ...) 长轮询读取
pgmq.pop(name [, qty]) 读取并删除
pgmq.delete(name, id) / pgmq.delete(name, ids[]) 确认并移除消息
pgmq.archive(name, id) / pgmq.archive(name, ids[]) 移入归档表
pgmq.set_vt(name, id(s), seconds) 延长可见性超时
pgmq.list_queues() / pgmq.metrics(name) 查看队列状态
pgmq.purge_queue(name) 删除全部消息,保留队列
pgmq.drop_queue(name) 删除队列及其归档表

故障排查

extension "pgmq" is not available。 实例镜像里没有扩展包,或该数据库没有 对应构建。执行 pg extension install pgmq -i <instance>,并在目标数据库中创建 扩展。

消息被重复投递。 处理时间超过 vt,或消费者在 delete 前退出。增大 vt、 在长任务中调用 set_vt,并保证处理逻辑幂等。

队列长度不下降。 消费者失败或太慢。检查 read_ct、应用日志和 metrics().oldest_msg_age_sec。PGMQ 会无限重投失败消息,除非归档或删除它们。

性能随时间下降。 普通队列就是普通表。检查 dead tuple 与 autovacuum,批量 发送和读取;持续高吞吐场景考虑 create_partitioned。

permission denied for schema pgmq。 给应用角色授予 schema 的 USAGE、 函数的 EXECUTE,以及每个队列表的相应表权限。

20.3 - SQLite + Litestream

在宿主机上管理 SQLite 数据库,并用 Litestream 将 WAL 变更复制到 S3 兼容对象存储

pg sqlite 在 Linux 宿主机上管理 SQLite 数据库,并可选地用 Litestream 把 WAL 变更持续复制到 S3 兼容对象存储。 SQLite 直接以宿主机进程运行:不使用容器、不引入 Go SQLite driver,也不依赖 cgo。 所有 SQL 都由 sqlite3 命令行执行;每个数据库对应一个独立的 Litestream 后台进程。

复制是异步的(默认 sync interval 下 RPO 约 1 秒)。它属于持续备份,不是高可用: SQLite 仍然只有一个写入者,故障切换意味着要在别处恢复备份。

目前仅支持 Linux。

工作原理

  • 每个受管数据库在 pg.yaml 中有一条记录,并在 <base>/sqlite/<name>/ 下有独立 的状态目录。
  • pg sqlite create 会启用 journal_mode=WAL,该设置持久化在数据库文件头中。
  • pg sqlite replication enable 生成专属的 litestream.yml(权限 0600),并在 后台启动 litestream replicate。Litestream 跟随 WAL,把 LTX 对象上传到指定 bucket。
  • S3 凭据只写入该 litestream.yml,绝不会进入 pg.yaml,任何 pg sqlite 命令也不会打印它。

安装

pg sqlite install

命令是幂等的,会安装两个宿主机二进制:

  1. 如果 sqlite3 不在 PATH 上,则通过发行版包管理器安装(apt-get、dnf、yum、 pacman、zypper 或 apk)。只有这一步可能需要 root;pgcli 使用免密 sudo -n,不会交互式索要密码,失败时会给出明确提示。
  2. Litestream 0.5.17,从 GitHub 下载并校验官方 SHA256。可写时安装到 /usr/local/bin,否则安装到 ~/.local/bin。

常用参数:

pg sqlite install --install-dir /opt/bin      # 指定 Litestream 安装目录
pg sqlite install --force-litestream          # 强制重新下载受支持版本

Litestream 0.3.x 与生成的配置不兼容;安装器会忽略 PATH 上的旧版本,直接安装 0.5.x。

创建与管理数据库

# 默认路径:<base>/sqlite/appdb/appdb.db
pg sqlite create appdb
pg sqlite create analytics --path /srv/data/analytics.db

pg sqlite list
pg sqlite info appdb
pg sqlite info appdb --integrity full

pg sqlite exec appdb "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)"
pg sqlite exec appdb "INSERT INTO items (name) VALUES ('first')"

# 从 stdin 读取 SQL(不受命令行长度限制)
pg sqlite exec appdb - <<'SQL'
INSERT INTO items (name) VALUES ('second');
INSERT INTO items (name) VALUES ('third');
SQL

pg sqlite shell appdb
pg sqlite shell appdb -- -header -column

create 不会截断已有文件:指向一个已存在的 SQLite 数据库时会接管该文件、启用 WAL,并保留原有内容。--path 必须是绝对路径,且只移动数据库文件——配置、日志、 pid 和 Litestream 本地状态始终留在 <base>/sqlite/<name>/ 下。

info 读取 journal_mode、page size/count 和 freelist,默认执行 PRAGMA quick_check。大库可用 --integrity none 跳过检查,或 --integrity full 执行完整 integrity_check。

remove 只注销数据库并停止复制,数据库文件保留。加 --clean-data 才会删除 数据库、WAL 附属文件、Litestream 本地状态和日志(交互确认,--force 跳过)。 远端 S3 备份永远不会被自动删除。

启用 S3 复制

bucket 必须已经存在——Litestream 不会创建 bucket。请先用 mc mb、云控制台或 其他 S3 工具创建好。

本地 MinIO(测试用)

pgcli 可以直接用 MinIO addon 在宿主机上起一个对象存储:

# 安装名为 sqlite-s3 的 MinIO 实例;命令输出会打印 S3 API 端点。
# 端口空闲时默认端点为 http://127.0.0.1:9000。
pg addon install minio --name sqlite-s3

# root 用户默认为 admin,密码在安装时生成。
S3_SECRET=$(pg addon password minio --name sqlite-s3)

# 用内置的 mc 包装器注册端点并创建 bucket。
pg mc alias set local http://127.0.0.1:9000 admin "$S3_SECRET"
pg mc mb local/sqlite-backups

如果 9000 端口已被占用,请改用 pg addon list 中显示的实际端点。然后把复制指向 这个本地存储:

pg sqlite replication enable appdb \
  --s3-endpoint http://127.0.0.1:9000 \
  --s3-bucket sqlite-backups \
  --s3-access-key admin \
  --s3-secret-key "$S3_SECRET" \
  --s3-uri-style path

其他 MinIO 或 S3 兼容存储使用相同的 flags,替换成自己的端点和凭据:

pg sqlite replication enable appdb \
  --s3-endpoint http://127.0.0.1:9000 \
  --s3-bucket sqlite-backups \
  --s3-access-key minioadmin \
  --s3-secret-key minioadmin \
  --s3-uri-style path

AWS S3,使用默认凭据链(命令行不传 key):

pg sqlite replication enable appdb \
  --s3-bucket my-org-sqlite-backups \
  --s3-region eu-west-1 \
  --s3-uri-style virtual-host
参数 默认值 说明
--s3-bucket 必填 必须已存在
--s3-endpoint AWS 默认 完整 http(s)://host[:port],不能带路径或凭据
--s3-path sqlite/<name> 对象 key 前缀;前导 / 会被去掉
--s3-region us-east-1
--s3-access-key / --s3-secret-key 默认凭据链 必须同时提供或同时省略
--s3-uri-style path path 或 virtual-host
--s3-ca-file 无 通过 SSL_CERT_FILE 传给 Litestream,用于私有 CA
--s3-skip-verify false 关闭 TLS 校验(自签名端点)
--sync-interval 1s WAL 上传间隔
--snapshot-interval 24h 全量快照间隔
--retention 24h 快照保留时间
--no-start false 只写配置,不启动 Litestream
--force false 覆盖已有配置

enable 会先确认数据库处于 WAL 模式,解析可用的 0.5.x Litestream(或用 --litestream-binary PATH 指定),以 0600 写入 litestream.yml,清理本地 Litestream 旧状态,启动进程并检查它没有立即退出。若立即退出,会打印日志最后一行 并报错。

在复制运行中,用完全相同的非敏感配置再次执行 enable 是幂等的。配置不同则默认 拒绝;--force 会停止旧进程、重写配置、reset 本地状态并重启。

凭据存放位置

路径 内容 权限
<base>/pg.yaml 名称、路径、binary/version、endpoint、bucket、前缀、region、URI style、CA file、skip-verify —— 不含 key 0644
<base>/sqlite/<name>/<name>.db 数据库文件 应用默认
<base>/sqlite/<name>/litestream.yml 完整 S3 配置,含 access/secret key 0600
<base>/sqlite/<name>/litestream.log 进程日志 0644
<base>/sqlite/<name>/litestream.pid 运行中的 pid 0644
<base>/sqlite/<name>/.<db>-litestream/ Litestream 本地 LTX 状态 应用默认

备份或复制 litestream.yml 时要像对待其他凭据文件一样谨慎。 replication disable 会删除它,因此下次 enable 需要重新提供凭据。

复制日常操作

pg sqlite replication status appdb
pg sqlite replication status              # 所有数据库
pg sqlite replication logs appdb -n 100
pg sqlite replication logs appdb -f
pg sqlite replication stop appdb          # 最终 sync,最长 30s
pg sqlite replication start appdb
pg sqlite replication disable appdb       # 停止并删除 litestream.yml

status 结合进程状态与 litestream status -json(本地事务 id 和 WAL 大小), 并显示日志中最近的错误行。即使状态查询失败,仍会展示进程状态并正常退出。

检查远端备份文件

pg mc 是内置 MinIO 客户端的包装器,mc 自己的 flags 必须写在 -- 之后。使用 「本地 MinIO」示例中创建的 alias,可以列出 Litestream 上传到 --s3-path 下的对象:

pg mc ls local/sqlite-backups/sqlite/appdb -- --recursive

有数据时类似这样:

0000/0000000000000001-0000000000000001.ltx
0000/0000000000000002-0000000000000002.ltx
0001/0000000000000001-0000000000000002.ltx
0009/0000000000000001-0000000000000002.ltx

0000/ 存放逐事务的 WAL 帧;编号更大的层级(0001/、0009/ 等)是合并、 压缩后的文件。刚 enable 完时前缀为空只说明还没有写入被同步——插入一行数据, 等待至少一个 --sync-interval(默认 1s)后再列一次。replication status 显示的本地事务 id 应与这些文件名中的最大 txid 一致。

disable 保留数据库、本地 LTX 状态和日志,只删除 pg.yaml 中的 replication 块并删除 litestream.yml。远端备份不受影响。

开机自启

Litestream 是独立的宿主机进程,不会在主机重启后自行恢复。把数据库注册到 pgcli 的开机服务:

pg autostart enable --sqlite --name appdb
pg autostart status

--name 必填(SQLite 没有默认数据库名)。复制必须已经启用且 litestream.yml 仍然存在——开关命令会检查这两点,否则报错并提示重新 enable。开机时 pg start --autostart 会在对象存储 addon 之后重启 Litestream,因此同机的 MinIO/silo/rustfs/garage/SeaweedFS 已经先开始监听。开机路径只做启动:绝不 重写 litestream.yml,也不修改数据库文件。

pg autostart disable --sqlite --name appdb

pg sqlite replication disable 会自动清除该标志。systemd/launchd 服务细节见 开机自启。

恢复

Litestream 可以恢复最新状态或指定时间点。支持两种模式:

# 恢复到另一个路径;复制继续运行
pg sqlite replication restore appdb --target /tmp/appdb-restored.db

# 按 RFC3339 时间戳恢复(UTC,使用 Z 后缀)
pg sqlite replication restore appdb --target /tmp/appdb-old.db \
  --timestamp 2026-01-01T12:00:00Z --integrity full

# 按 Litestream 事务 id 恢复(十六进制,可带 0x 前缀)
pg sqlite replication restore appdb --target /tmp/appdb-tx.db --txid 00000000000000ff

# 只展示将要恢复的内容,不写任何文件
pg sqlite replication restore appdb --dry-run

--timestamp 与 --txid 互斥;都不传则恢复最新状态。--timestamp 表示绝对 时间点,请写成带 Z 后缀的 UTC 时间(如 2026-01-01T12:00:00Z),避免恢复 时间点受宿主机时区影响。--target 不能与 --restart 同用;目标已存在时必须加 --force,此时旧的 target 及其 -wal/-shm 会先被移入 <target>.pre-restore-<UTC>/。

时间点恢复演练

在做出想撤销的变更之前,先记录一个事务边界:

# 当前本地事务 id(输出中的 `local: ... txid=...` 行)
pg sqlite replication status appdb

# 或查看 S3 上已上传的 LTX 范围
pg mc ls local/sqlite-backups/sqlite/appdb -- --recursive

LTX 对象名为 <min-txid>-<max-txid>.ltx。按 --txid 恢复会包含该事务及之前 的状态。记录变更前的 txid(或一个安全早于变更的 RFC3339 UTC 时间戳),然后恢复 一份副本并验证:

pg sqlite replication restore appdb \
  --target /tmp/appdb-pitr.db \
  --txid 0000000000000004 \
  --integrity full

sqlite3 -readonly /tmp/appdb-pitr.db \
  "SELECT COUNT(*) FROM items; PRAGMA journal_mode;"

时间戳方式流程相同:

pg sqlite replication restore appdb \
  --target /tmp/appdb-pitr-ts.db \
  --timestamp 2026-01-01T12:00:00Z \
  --integrity full

不确定时先加 --dry-run:它只打印将要使用的事务范围,不写任何文件。恢复到 --target 不会停止复制,因此可以在检查副本的同时继续处理线上流量。建议再恢复 一次最新状态(不传 --timestamp/--txid),确认较新的数据存在;验证无误后, 再用原地恢复把副本正式替换上线。

原地恢复

不传 --target 时会替换配置中的数据库:

pg sqlite replication restore appdb --force

流程刻意保守:

  1. 停止 Litestream(会执行最终 sync);其他打开该数据库的应用必须提前自行停止。
  2. 当前数据库及 WAL 附属文件移入 <db>.pre-restore-<UTC>/(不加 --force 时 会先确认)。
  3. 备份先恢复到数据库同目录下的临时目录,因此恢复失败也不会影响已经保留的旧副本。
  4. 检查恢复出的数据库,必要时切回 WAL,然后通过同一文件系统内的原子 rename 移动到最终位置。
  5. 重置 Litestream 本地状态;若 reset 失败,则删除旧的本地状态目录。
  6. 默认保持复制停止;--restart 会重新启动。

恢复后把复制重启到相同的 --s3-path,可能把恢复出的时间线与远端旧数据混在一起。 优先用新的 --s3-path 重新 enable;使用 --restart 前请确认理解旧时间线。

如果数据库文件完全丢失,原地恢复不需要 --force;但每个数据库的 litestream.yml 必须仍然存在。

WAL 与应用侧设置

CLI 只把 PRAGMA journal_mode=WAL 持久化到数据库文件中。以下设置按连接在应用侧 调整:

PRAGMA busy_timeout=5000;   -- 等待锁而不是立即失败
PRAGMA synchronous=NORMAL;  -- WAL 下的推荐值(在持久性上有所取舍)
PRAGMA wal_autocheckpoint=1000;

注意事项:

  • WAL 模式要求本地文件系统,不要把数据库放在 NFS 或 SMB 上。
  • 同一时刻仍然只有一个写入者。长写事务会阻塞其他写入者;长读事务会阻止 WAL checkpoint,可能让 WAL 无限增长。
  • Litestream 复制的是 WAL,不是应用一致快照。涉及多条语句的不变量请使用事务。

故障排查

litestream exited during startup —— 执行 pg sqlite replication logs <name> -n 50。常见原因是 bucket 不存在、凭据错误, 或宿主机无法访问 endpoint。

no Litestream 0.5.x binary found —— 执行 pg sqlite install。PATH 上的 0.3.x 会被报告但不会使用。

连接 MinIO/RustFS 的 TLS 错误 —— 纯 HTTP 端点要写完整 http://host:port;自签名 HTTPS 用 --s3-ca-file 或 --s3-skip-verify。

database ... uses journal_mode=delete —— 数据库不是由 pg sqlite create 创建的,或被其他工具改过模式。在没有其他连接时执行 PRAGMA journal_mode=WAL;,再重试 enable。

litestream.yml not found —— start、status 和 restore 都依赖 enable 写出的配置;请重新执行 enable(凭据不在 pg.yaml 中)。

限制

  • 仅支持 Linux;macOS 的 host-binary 路径未实现。
  • Litestream 是异步备份,不是同步复制或高可用。默认 RPO 约 1 秒, stop/disable 时的最终 sync 最长可能耗时 30 秒。
  • 每个数据库一个 Litestream 进程、一个 WAL 写入者。
  • --clean-data 和原地恢复具有破坏性;默认会确认,保留的 .pre-restore-* 目录需要手动清理。

20.4 - TimescaleDB

在 PostgreSQL 内使用 TimescaleDB hypertable 和 time_bucket 处理时序数据

TimescaleDB 是面向时序负载的 PostgreSQL 扩展。它把普通表变成 hypertable: 数据按时间自动切分成 chunk,应用仍然使用普通 SQL、索引和事务。

pgcli 安装的是 Pigsty 的 TSL 构建(TimescaleDB 2.31.0)。Hypertable、 time_bucket、chunk 管理、列存/压缩、连续聚合和保留策略均可使用。 TSL 允许免费内部使用,但禁止对外提供时序数据库服务。

使用场景

TimescaleDB 适合以时间为主要访问维度、以追加写入为主的数据:

  • IoT 与设备遥测 — 按设备采集读数、查询最新值和按时间窗口聚合。
  • 基础设施与应用监控 — CPU、内存、延迟、请求量等指标,用于看板和告警。
  • 工业与能源计量 — 高频传感器数据流,保留周期长。
  • 金融行情数据 — tick、报价和成交;用 time_bucket 和 first/last 生成 OHLC 类聚合。
  • 产品与事件分析 — 用户事件、会话和 API 调用,按最近时间窗口统计。
  • 智能家居与楼宇自动化 — 传感器历史数据和状态变化。

典型形态是 (entity_id, time) 的追加型数据:查询过滤最近一段时间,过期数据按 整个 chunk 删除。

不适合没有时间维度的 OLTP、键值/文档存储和大对象存储。TSL 构建包含压缩、 连续聚合和保留策略,但 TSL 不允许对外提供时序数据库服务。

安装

pg extension install timescaledb -i pg01 --auto-restart

pgcli 会使用 Pigsty 的 TSL 包构建扩展镜像,把 timescaledb 放到 shared_preload_libraries 的首位,重启 PostgreSQL,并在实例的默认数据库执行 CREATE EXTENSION IF NOT EXISTS timescaledb。

同一实例的其他数据库需要单独创建扩展:

pg psql -i pg01 -- -d appdb
CREATE EXTENSION IF NOT EXISTS timescaledb;

查看版本和许可证:

pg exec -i pg01 "SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"
pg exec -i pg01 "SHOW timescaledb.license"

Pigsty 包返回 timescale。

快速开始

1. 创建实例

pg create -i tsdb
pg start -i tsdb

pg create 写入配置并打印管理员密码;pg start 启动 PostgreSQL 和备份容器。 默认数据库为 tsdb_db。

2. 安装扩展

pg extension install timescaledb -i tsdb --auto-restart

pgcli 构建扩展镜像,预加载 timescaledb,重启 PostgreSQL,并在 tsdb_db 中 执行 CREATE EXTENSION IF NOT EXISTS timescaledb。

3. 创建 hypertable

连接实例:

pg psql -i tsdb

创建普通表并转换为 hypertable:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision
);

SELECT create_hypertable('sensor', by_range('time', INTERVAL '1 day'));
CREATE INDEX ON sensor (sensor_id, time DESC);

也可以一步创建;此时使用默认 chunk 间隔,之后仍可调整:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision
) WITH (tsdb.hypertable);

SELECT set_chunk_time_interval('sensor', INTERVAL '1 day');

chunk 间隔按查询和保留窗口选择;后续调整只影响新 chunk。

4. 写入数据

INSERT INTO sensor
SELECT now() - (i * INTERVAL '1 minute'),
       'sensor_' || (i % 3 + 1),
       random() * 100
FROM generate_series(1, 1440) AS i;

SELECT time_bucket('1 hour', time) AS bucket,
       sensor_id,
       round(avg(value)::numeric, 2) AS avg_value
FROM sensor
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id
ORDER BY bucket DESC, sensor_id;

5. 日常运维

追加数据、查看 chunk 和后台任务、启用自动维护:

INSERT INTO sensor VALUES (now(), 'sensor_1', 42.0);

SELECT show_chunks('sensor');
SELECT * FROM timescaledb_information.jobs;

ALTER TABLE sensor SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id',
    timescaledb.compress_orderby = 'time DESC'
);
CALL add_columnstore_policy('sensor', INTERVAL '7 days');
SELECT add_retention_policy('sensor', INTERVAL '365 days');

常用运维命令:

pg status -i tsdb               # 实例与备份状态
pg logs -i tsdb -n 100          # 查看日志尾部
pg export -i tsdb -o sensor.dump

压缩、保留策略、连续聚合和 chunk 的完整操作见下文对应章节。

InfluxDB 兼容 API:influx2tsdb 插件

如果现有工具只支持 InfluxDB 协议,可用 influx2tsdb 插件 在 TimescaleDB 之上运行一个受管的 无状态代理,提供 InfluxDB 1.x HTTP API(Line Protocol 写入与 InfluxQL 查询):

  • 每个 InfluxDB 数据库对应一个 PostgreSQL schema,每个 measurement 对应一个 hypertable,time 为分区列;
  • CREATE / ALTER / DROP RETENTION POLICY 会被翻译成 TimescaleDB 自带的 add_retention_policy / add_compression_policy 任务——不需要 pg_cron, 且代理停机期间 chunk 清理照常进行;
  • 代理本身不存数据,备份、PITR、复制与 HA 仍然完全走 PostgreSQL。
pg addon install influx2tsdb --pg-name tsdb

代价是吞吐:请求需要翻译成 SQL,纯时序写入/查询性能低于原生 InfluxDB。代理 没有认证和 TLS,需要 nginx 前置或做网络隔离。参数、数据 模型、保留语义和测试脚本见 插件文档。

常用查询

每条序列的首末值:

SELECT sensor_id,
       first(value, time) AS first_value,
       last(value, time) AS last_value
FROM sensor
GROUP BY sensor_id;

每条序列的最新一行:

SELECT DISTINCT ON (sensor_id) sensor_id, time, value
FROM sensor
ORDER BY sensor_id, time DESC;

按指定时区做天级分桶:

SELECT time_bucket('1 day', time, 'Asia/Shanghai') AS day,
       count(*)
FROM sensor
GROUP BY day
ORDER BY day DESC;

补齐时间桶序列中的空档:

SELECT time_bucket_gapfill('1 hour', time) AS bucket,
       avg(value)
FROM sensor
WHERE time BETWEEN now() - INTERVAL '6 hours' AND now()
GROUP BY bucket
ORDER BY bucket;

time_bucket_gapfill 需要明确的起点和终点:可以作为第二、三个参数传入,或在 WHERE 中给出时间范围。

压缩与列存

较早的 chunk 可以转入列存(columnstore),压缩数据并加速分析查询:

ALTER TABLE sensor SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id',
    timescaledb.compress_orderby = 'time DESC'
);

-- 压缩 7 天前的 chunk,由后台任务执行
CALL add_columnstore_policy('sensor', INTERVAL '7 days');

查看压缩状态和空间占用:

SELECT * FROM hypertable_columnstore_stats('sensor');
SELECT * FROM chunk_columnstore_stats('sensor');

保留策略

超出保留窗口的 chunk 会自动删除:

SELECT add_retention_policy('sensor', INTERVAL '365 days');

-- 查看或移除策略
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';

SELECT remove_retention_policy('sensor');

这些任务由 TimescaleDB 自带的后台调度器执行,不需要 pg_cron。 influx2tsdb 插件 使用同一机制:其 CREATE / ALTER / DROP RETENTION POLICY 会被自动翻译成这些任务,代理停机期间 清理照常进行。

连续聚合

预先聚合最近的数据,并由后台任务持续刷新:

CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket,
       sensor_id,
       avg(value) AS avg_value
FROM sensor
GROUP BY bucket, sensor_id;

SELECT add_continuous_aggregate_policy('sensor_hourly',
    start_offset      => INTERVAL '3 hours',
    end_offset        => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour');

像普通表一样查询聚合结果:

SELECT * FROM sensor_hourly
ORDER BY bucket DESC
LIMIT 10;

策略只自动刷新 start_offset 到 end_offset 之间的近期窗口;回填更早的数据后 需要手动刷新。

管理 chunk

Chunk 间隔

创建时指定间隔,或之后调整(只影响新 chunk):

SELECT create_hypertable('sensor', by_range('time', INTERVAL '1 day'));

-- 已有 chunk 保持原范围;新 chunk 使用 6 小时
SELECT set_chunk_time_interval('sensor', INTERVAL '6 hours');

SELECT time_interval
FROM timescaledb_information.dimensions
WHERE hypertable_name = 'sensor';

timestamptz 列的 chunk 边界按 UTC 对齐。间隔要保证单个 chunk 能较轻松地放进 内存;间隔过小会产生大量 chunk,并增加查询规划开销。

查看 chunk

SELECT show_chunks('sensor');
SELECT show_chunks('sensor', older_than => INTERVAL '30 days');
SELECT show_chunks('sensor', newer_than => INTERVAL '1 day');

SELECT chunk_name, range_start, range_end
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC
LIMIT 10;

older_than / newer_than 按 chunk 的时间范围过滤,而不是按创建时间。

查看 chunk 大小

SELECT chunk_name,
       pg_size_pretty(pg_total_relation_size(
           format('%I.%I', chunk_schema, chunk_name)::regclass)) AS total_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC;

SELECT pg_size_pretty(hypertable_size('sensor'));
SELECT * FROM hypertable_detailed_size('sensor');

拆分 chunk

在 chunk 的时间范围内选一个时间点进行拆分;chunk 名称来自 show_chunks('sensor'):

CALL split_chunk('_timescaledb_internal._hyper_1_2_chunk',
                 '2026-10-26 12:00:00+00'::timestamptz);

split_chunk 是 procedure,必须用 CALL,不能用 SELECT。

删除 chunk

-- 完全早于 90 天、或晚于明天的 chunk
SELECT count(*) FROM drop_chunks('sensor', older_than => INTERVAL '90 days');
SELECT count(*) FROM drop_chunks('sensor', newer_than => now() + INTERVAL '1 day');

drop_chunks 直接删除文件,不产生 dead tuple。不要用大范围 DELETE 清理 历史数据。自动过期优先使用保留策略;只有需要自定义调度时才安装 pg_cron:

pg extension install pg_cron -i pg01 --auto-restart
SELECT cron.schedule('drop-sensor-chunks', '0 3 * * *',
  $$SELECT drop_chunks('sensor', older_than => INTERVAL '90 days')$$);

手动转换列存

配置好压缩参数后(见上文「压缩与列存」),可以不等策略而手动转换单个 chunk:

CALL convert_to_columnstore('_timescaledb_internal._hyper_1_1_chunk');
SELECT chunk_name, is_compressed
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC;

CALL convert_to_rowstore('_timescaledb_internal._hyper_1_1_chunk');

这两个 procedure 接收 chunk 名称,而不是 hypertable 名称。

元数据视图

SELECT * FROM timescaledb_information.hypertables;
SELECT * FROM timescaledb_information.dimensions;
SELECT * FROM timescaledb_information.chunks;
SELECT * FROM timescaledb_information.jobs;

jobs 中可以看到上面创建的列存、保留策略和连续聚合任务,以及内部的 job 历史 任务。

功能可用性

功能 状态
Hypertable、time_bucket、first/last、show_chunks、drop_chunks、hypertable_size、元数据视图 ✅ Apache
用 add_dimension / by_hash 做空间分区 ✅ Apache
列存与压缩(ALTER TABLE ... SET (timescaledb.compress)、CALL add_columnstore_policy) ✅ TSL
连续聚合(CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)) ✅ TSL
保留策略(add_retention_policy) ✅ TSL
time_bucket_gapfill ✅ TSL

标记为 ✅ Apache 的功能遵循 Apache-2.0;标记为 ✅ TSL 的功能来自 Timescale License,允许免费内部使用,但禁止对外提供时序数据库服务。

备份与恢复

逻辑备份和 pg export/pg import 正常工作。物理恢复(pg backup/pg restore 使用的 pgBackRest)还需要 TimescaleDB 的恢复钩子:

SELECT timescaledb_pre_restore();   -- 替换数据文件之前,条件允许时执行
-- 恢复实例
SELECT timescaledb_post_restore();  -- 恢复后的实例启动之后执行

pg restore 不会自动调用这两个函数,恢复完成后需要手动执行 timescaledb_post_restore()。正式依赖前请先演练一次。

权限

CREATE EXTENSION 需要超级用户;pg exec 以实例管理员身份连接。表的所有者 可以对自己的表执行 create_hypertable 并管理 chunk。应用角色只需要普通的表 权限。

故障排查

extension "timescaledb" is not available。 实例镜像里没有扩展包,或目标 数据库没有创建扩展。执行 pg extension install timescaledb -i <instance>,并在 该数据库中创建扩展。

timescaledb is not preloaded。 扩展已在 shared_preload_libraries 中,但 PostgreSQL 没有重启。pg extension install 会自动处理;可用 SHOW shared_preload_libraries; 检查并重启实例。

函数报许可证错误。 实例镜像中还是 Apache 构建(旧版 pgcli 或手动安装的 PGDG 包)。执行 pg extension install timescaledb -i <instance> --auto-restart 切换到 Pigsty TSL 包。

cannot create a unique index without the column "time"。 hypertable 上的 唯一索引和主键必须包含分区列:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision,
    PRIMARY KEY (sensor_id, time)
);

chunk 数量过多。 用 set_chunk_time_interval 调大间隔;已有 chunk 不会 重写。

旧数据没有被清理。 用 add_retention_policy 添加保留策略,或手动执行 drop_chunks;可在 timescaledb_information.jobs 中检查策略任务。

函数参考

函数 用途 状态
create_hypertable('t', by_range('time')) 把表转换为 hypertable ✅
by_range('time', INTERVAL '1 day') 指定时间分区和 chunk 间隔 ✅
set_chunk_time_interval('t', INTERVAL ...) 调整新 chunk 的间隔 ✅
time_bucket('1 hour', time [, timezone]) 时间分桶聚合 ✅
first(value, time) / last(value, time) 分组内首末值 ✅
show_chunks('t' [, older_than/newer_than => ...]) / drop_chunks('t', ...) 查看、过滤和删除 chunk ✅
hypertable_size('t') / hypertable_detailed_size('t') hypertable 总大小及明细 ✅
add_dimension('t', by_hash('col', N)) 添加空间分区 ✅
add_retention_policy(...) / remove_retention_policy(...) 自动过期清理 ✅ TSL
CALL add_columnstore_policy(...) 列存/压缩策略 ✅ TSL
CALL convert_to_columnstore(chunk) / CALL convert_to_rowstore(chunk) 手动转换 chunk 存储格式 ✅ TSL
hypertable_columnstore_stats('t') / chunk_columnstore_stats('t') 列存状态与空间统计 ✅ TSL
CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous) 连续聚合 ✅ TSL
add_continuous_aggregate_policy(...) 连续聚合的自动刷新策略 ✅ TSL
time_bucket_gapfill(...) 补齐缺失的时间桶 ✅ TSL

21 - 日志

查看 PostgreSQL 和插件控制台输出日志

pg logs 命令用于查看 PostgreSQL 实例和插件组件(如 PgBouncer 连接池、etcd 成员)的控制台输出日志。

PostgreSQL 实例日志

查看最近的日志

pg logs                              # 最后 50 行(默认实例)
pg logs -i proj01                    # 指定实例
pg logs -n 200                       # 最后 200 行

实时跟踪日志

pg logs -f                           # 跟踪模式(Ctrl+C 退出)
pg logs -i proj01 -f                 # 跟踪指定实例
pg logs -n 100 -f                    # 从最后 100 行开始,然后持续跟踪

显示所有可用日志

pg logs -n 0                         # 所有日志(无行数限制)

默认行为:不指定 -i 时,显示 default 实例的日志。

插件日志

插件日志(如 PgBouncer 连接池)使用 addon 子命令。

本地插件

查看附加到本地 PostgreSQL 实例的插件日志:

pg logs addon pgbouncer -i proj01    # proj01 的 PgBouncer 日志
pg logs addon pgbouncer -i proj01 -f # 跟踪 PgBouncer 日志
pg logs addon pgbouncer -i proj01 -n 100  # 最后 100 行

远程插件

查看针对远程数据库的独立 PgBouncer 实例的日志:

pg logs addon pgbouncer --pg-name my-pool
pg logs addon pgbouncer --pg-name my-pool -f
pg logs addon pgbouncer --pg-name my-pool -n 200

etcd 成员

etcd 属于顶层 infra 插件,用 --name 指定成员(省略时默认为 etcd):

pg logs addon etcd --name m1           # 成员 m1 的日志
pg logs addon etcd --name m1 -f        # 持续跟踪 m1
pg logs addon etcd -n 200              # 成员 "etcd",最后 200 行

PgDog 代理

pgdog 同样属于顶层 infra 插件,用 --name 指定代理(省略时默认为 pgdog):

pg logs addon pgdog --name pgdog       # 代理日志
pg logs addon pgdog --name pgdog -f    # 持续跟踪

PgDog 以结构化 INFO 行记录连接池事件(新建服务器连接、认证、客户端连接 / 断开)。

HAProxy 实例

haproxy 同样属于顶层 infra 插件,用 --name 指定实例(省略时默认为 haproxy):

pg logs addon haproxy --name lb        # 实例日志
pg logs addon haproxy --name lb -f     # 持续跟踪

HAProxy 会记录健康检查状态变化(Server lb_rw/node2 is UP/DOWN, reason: ...) 与连接事件,适合实时观察故障切换。

MinIO 实例

minio 同样属于顶层 infra 插件,用 --name 指定实例(省略时默认为 minio):

pg logs addon minio --name store       # 实例日志
pg logs addon minio --name store -f    # 持续跟踪

MinIO 日志走 stdout:启动行(其服务的 API: / Console: 地址)与请求错误。

Patroni 成员

Patroni 不是通过 pg addon 管理的组件——它是独立的顶层命令 pg ha 集群—— 因此有自己的简写形式 pg logs ha <scope> --member <name>:

pg logs ha app --member node1     # scope app 下 node1 的日志
pg logs ha app -m node1 -f        # 持续跟踪
pg logs ha app -m node1 -n 200    # 最后 200 行

pg logs ha 等价于 pg logs addon patroni --scope <scope> --name <member>; scope 必填,成员名在该集群内定位一个节点。

Patroni 成员每个周期都记录 HA 主循环(no action. I am (node1), a secondary, and following a leader (node3))以及 PostgreSQL 启动/恢复输出——适合发现卡住的 副本(反复出现 record with incorrect prev-link / waiting for WAL to become available)。

选项说明

选项 简写 描述
--follow -f 跟踪日志输出(类似 tail -f)
--tail N -n N 显示最后 N 行(默认:50,0 表示全部)
--instance NAME -i NAME 实例名称(默认:default)
--pg-name NAME 远程插件名称(用于远程 PgBouncer)
--name NAME etcd 成员 / PgDog 代理 / HAProxy / MinIO 实例名称(默认:etcd / pgdog / haproxy / minio);Patroni 成员名(与 --scope 搭配,必填)
--scope NAME Patroni 集群 scope(仅用于插件类型 patroni)
--member NAME -m NAME Patroni 成员名(仅用于 pg logs ha <scope>)

示例

# 检查最近的错误
pg logs -n 100 | grep ERROR

# 监控数据库活动
pg logs -i prod-db -f

# 调试 PgBouncer 连接问题
pg logs addon pgbouncer -i myapp -f

# 查看远程连接池日志
pg logs addon pgbouncer --pg-name analytics-pool -n 50

# 观察 etcd 成员的选主 / peer 事件
pg logs addon etcd --name m1 -f

# 观察 PgDog 连接池活动
pg logs addon pgdog --name pgdog -f

# 观察 HAProxy 健康检查 / 故障切换状态变化
pg logs addon haproxy --name lb -f

# 观察 MinIO 请求错误
pg logs addon minio --name store -f

# 观察 Patroni 成员 / 排查卡住的副本
pg logs ha app --member node1 -f

注意事项

  • PostgreSQL 日志包括查询执行、连接事件和系统消息
  • 插件日志显示连接池活动(连接、断开、池统计)
  • etcd 成员日志为 JSON 格式的 raft/选主/peer 事件,适合排查 quorum 问题
  • PgDog 代理日志为结构化 INFO 行,记录连接池事件(服务器连接、认证、客户端连接 / 断开)
  • HAProxy 日志记录健康检查状态变化与连接事件——可实时观察故障切换过程
  • MinIO 日志走 stdout,输出启动行与请求错误
  • 跟踪模式(-f)会保持连接直到按 Ctrl+C 中断
  • 远程插件使用 --pg-name 而不是 -i 来标识目标连接池;etcd 成员、PgDog 代理、HAProxy 实例和 MinIO 实例使用 --name
  • Patroni 成员不属于 pg addon 管理,用 pg logs ha <scope> --member <name> 查看(等价于 pg logs addon patroni --scope --name)

22 - 销毁

销毁实例并移除其配置

destroy 会停止并移除容器,然后从配置文件中删除该实例。默认情况下,主机的数据目录会被保留。使用 --clean-data 可以同时移除数据、WAL 归档和 pgBackRest 仓库的 stanza。

基本用法

# 销毁实例(保留数据目录)
pg destroy -i proj01

# 无需确认直接销毁
pg destroy -i proj01 --force

# 销毁并清理数据(全新开始)
pg destroy -i proj01 --clean-data

# 跳过确认并清理所有数据
pg destroy -i proj01 --clean-data --force

执行过程

运行 pg destroy 时:

  1. 停止容器 — PostgreSQL 优雅关闭
  2. 移除容器 — 删除 Podman 容器
  3. 移除配置 — 从 ~/.pgcli/pg.yaml 中删除该实例条目
  4. 保留数据(默认)— 主机数据目录 --base-dir/<instance> 保留

使用 --clean-data 时:

  1. 以上所有步骤,加上:
  2. 移除主机数据 — 删除数据目录
  3. 移除 WAL 归档 — 删除主机上的所有 WAL 文件
  4. 移除备份 stanza — 删除该实例的 pgBackRest 仓库 stanza

重建实例

销毁后,可以重新创建实例以获得全新开始:

# 销毁并清理所有数据
pg destroy -i proj01 --clean-data

# 使用相同名称重新创建
pg create -i proj01 --base-dir /data/pg

# 启动新实例
pg start -i proj01

重要: 不使用 --clean-data 时,旧的数据目录会被保留。当你重新创建实例时,PostgreSQL 会使用现有数据,而 init.sh(创建用户和设置管理员密码)不会再次运行。这可能导致以下问题:

  • 数据是用不同的用户或密码创建的
  • 你更改了配置中的默认用户
  • 你想要完全全新开始

需要彻底清理时使用 --clean-data。

确认提示

默认情况下,pg destroy 会在执行前询问确认:

$ pg destroy -i proj01
!  This will destroy instance "proj01":
   - Container: pgcli-pg-default-proj01
   - Data dir: /data/pg/proj01 (preserved)

Continue? [y/N]:

使用 --force 跳过提示:

pg destroy -i proj01 --force

使用场景

1. 配置更改后的干净重启

如果你更改了配置中的默认用户、密码或 PostgreSQL 版本,销毁并重建:

# 更新配置
# 编辑 ~/.pgcli/pg.yaml 更改 postgres.user 或 image_tag

# 销毁并清理数据
pg destroy -i proj01 --clean-data --force

# 使用新设置重新创建
pg create -i proj01 --base-dir /data/pg
pg start -i proj01

2. 移除测试实例

测试完成后,移除不再需要的实例:

pg destroy -i test-instance --force

3. 修复损坏的状态

如果实例处于异常状态(例如启动失败、数据损坏),销毁并重建:

pg destroy -i broken-instance --clean-data --force
pg create -i broken-instance --base-dir /data/pg
pg start -i broken-instance

4. 释放资源

销毁不再活跃使用的实例以释放:

  • Podman 容器(CPU 和内存)
  • 主机磁盘空间(使用 --clean-data)
  • 配置文件中的条目

与副本的关系

销毁副本时,主实例上的复制槽不会自动移除。你需要单独清理:

# 步骤 1:销毁副本
pg destroy -i ro1 --force

# 步骤 2:在主实例上删除复制槽
pg replica drop ro1 -i primary-instance

这种两步流程确保如果你计划稍后重新创建副本,不会意外丢失复制槽。

安全注意事项

  • 数据丢失:--clean-data 会永久删除该实例的所有数据、WAL 和备份。谨慎使用。
  • 无法撤销:销毁后,除非有外部备份,否则实例无法恢复。
  • 配置丢失:实例条目会从配置文件中移除。如果以后需要该配置,请先备份。

标志

标志 默认值 描述
--force false 跳过确认提示
--clean-data false 同时移除主机数据、WAL 归档和备份 stanza
-i, --instance default 要销毁的实例名称

相关命令

  • pg create — 创建新实例
  • pg start — 启动实例
  • pg stop — 停止实例(容器保留)
  • pg replica drop — 从主实例移除复制槽