docs: 记录共享 etcd 与外置 PostgreSQL 部署和接入边界

This commit is contained in:
2026-09-25 19:31:14 +00:00
parent 235288189d
commit acd4b55722
4 changed files with 450 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
# Homelab shared etcd
独立于 PostgreSQL 与 k3s 的三成员协调服务。Ansible 管主机、证书文件和 etcd 账号;
独立 Terraform root 管现有 Bao PKI 下的签发角色与 policy,不管理 CA 或秘密值。
旧 Ansible Vault 不在本次迁移范围。
状态:2026-09-25 已部署三成员 etcd,启用 Bao mTLS 和 RBAC;三个端点健康检查通过。
PVE 两个 LXC 的 rootfs 已通过 `pct move-volume` 从 local-lvm 迁入 `pve-rg` SSD DRBD 池,
保留容器与 etcd 数据。当前 PVE LXC 迁卷要求停止容器,维护入口按成员串行执行,回归健康后再处理下一个。
| 成员 | 承载 | 地址 | 资源 |
| --- | --- | --- | --- |
| etcd-laptop | laptop systemd | 192.168.10.127 | MemoryHigh 256 MiB / MemoryMax 384 MiB |
| etcd-pve1 | pve1 新无特权 LXC 150 | 10.60.0.20 | 1 core / 1536 MiB / 8 GiB SSD DRBD |
| etcd-pve2 | pve2 新无特权 LXC 151 | 10.60.0.21 | 1 core / 768 MiB / 8 GiB SSD DRBD |
资源限额是初始测试预算,不是已测容量。LXC 已增加 PG standby/备份的独立 HDD mp0 与内存上限,生产 PG standby 与备份仓库已启动。
etcd 自身的 MemoryHigh/MemoryMax 仍为 256/384 MiB。labnet 依赖现有 VyOS 路由;切换 bare-metal 时复用服务角色,更换主机 inventory。
## 接入方式与认证
客户端使用三个 `https://地址:2379` endpoint,校验 Bao 中央 CA,出示独立客户端证书。
成员间 2380 也开启 mTLS,额外限制 peer CN 为 `homelab-etcd-peer`。
管理员证书 CN 为 `root`,只保存在受管成员 root 可读的文件中;对应 etcd root 用户不设置密码。
服务进程只能读取自己的 server/peer 私钥,不能读取管理员私钥。
Patroni 走 etcd3 gateway,使用无 CN 的客户端证书及独立 username/password 完成 RBAC。
实测带 CN 的证书会被 gateway 拒绝;管理员 CN=root 证书仅用于原生 etcdctl。
成员另有无 CN 的 gateway 证书供内部 gateway→gRPC 连接及密码核对使用,不能将管理员证书复用到 HTTP API。首个消费者为
`patroni-pg-prod`,只允许 `/homelab/patroni/pg-prod/`。其 Bao KV v2 路径为
`kv/infra/etcd/consumers/patroni-pg-prod`,包含 username/password/prefix;不在本文复制实际值。
配置 Patroni 时对应 `namespace: /homelab/patroni/` 与 `scope: pg-prod`。
第一次接入应由管理流程交付 CA、客户端证书及秘密引用,然后使用自己的身份对自己的 prefix
执行一次 put/get/delete。跨 prefix 应被拒绝;不要把 root 证书复制给应用。
生产消费者证书已签发,Patroni 已接入并验证主从自动切换。
## 安装与维护入口
需要 Ansible、community.crypto,以及目标机 Python/systemd。当前二进制固定为 etcd 3.7.2
linux-amd64,并固定官方发布资产 SHA-256;版本升级必须单独评估兼容性、备份与 quorum。
Terraform 使用已登录的 Vault 兼容 provider 身份;Ansible 读取控制端 `BAO_TOKEN` 环境变量,
token 不下发目标机。`homelab-etcd-provisioner` policy 不自动绑定到现有身份,先由 Bao 管理流程授权。
```bash
cd infrastructure/etcd/terraform
terraform init
terraform plan
# 审阅计划后 apply;只新增此服务的 PKI roles/policies。
cd ../ansible
ansible-galaxy collection install -r requirements.yml
ansible-playbook lxc.yml --check
ansible-playbook lxc.yml
ansible-playbook site.yml
ansible-playbook bootstrap-auth.yml -e etcd_bootstrap_auth=true
ansible-playbook consumers.yml
ansible-playbook verify.yml
```
Terraform 使用现有 S3 tfstate bucket,独立 key `etcd/terraform.tfstate`,启用原生 lockfile;不复用 Bao 的 state。
从仓库根目录运行 `python3 infrastructure/etcd/run.py terraform <子命令>` 或
`python3 infrastructure/etcd/run.py ansible <playbook.yml>`,复用当前 Bao 会话并在内存中取得所需凭据。
`lxc.yml` 是创建入口,不接管已有未知 VMID,也不自动调整运行中容器的网络与容量;配置漂移会报错。
部署前核对模板与 `pve-rg` 存储可用性、IP 冲突、SSH host key、laptop sudo 和网络访问控制。
数据库角色不会创建/删除这些 LXC 或 etcd 成员。
`site.yml` 管安装、CSR 本地生成、Bao 签发、配置和逐成员激活。全体健康检查通过才记录激活指纹;
上次中断后重跑仍能识别尚未激活的磁盘配置。已有集群每个成员激活后检查健康,再处理下一个。
首次建群先让三个服务启动,再检查 quorum。`bootstrap-auth.yml` 与日常收敛分开,认证已启用时不重建。
首次认证启用前应限制客户端网络,不能把“已有中央 CA 签名”当作业务授权。
`consumers.yml` 首次生成 48 字符随机密码并以 KV v2 CAS=0 写入,之后只复用。
元数据存在但数据被删除、Bao 403/超时、或 etcd 用户存在但秘密丢失时均失败,不自动换密码。
并发 CAS 冲突会中止此次执行,重跑读取胜出的版本。既有额外角色或不同 prefix 权限也报错,
不默默扩大权限。账号删除、prefix 迁移和密码轮换均不包含在普通收敛里。
密码使用 stdin 送给 etcdctl,涉及秘密的任务 `no_log: true`;禁止用 `--diff` 打印未来消费者秘密文件。
证书有效期 60 天,`site.yml` 在剩余不足 14 天时续签;需要 Bao 的控制端身份。
续签 IaC 已实现于 `terraform/renewal.tf`、`controller/` 和 `ansible/controller.yml`:
独立 cert auth 挂载信任中央 CA,并限制 laptop 的 DNS SAN;用现有 peer 客户端证书登录,
换取 10 分钟 token,仅允许签发 server/peer/admin/gateway 四类证书,不授予 KV 读取权限。
控制端每日运行现有健康检查与串行部署,结束撤销 token;不保存长期 token,不依赖人工 OIDC 会话。
控制程序安装为 root 管理的固定副本,以 panxiao81 身份复用现有 Ansible/SSH/sudo 环境。
**机器身份和 timer 已启用**(2026-09-25):维护者恢复 OIDC 登录后,Terraform 新增三个
续签资源,`controller.yml` 登录预检通过。首次实际运行三成员均 `changed=0`,服务 Result=success。
Bao cert auth 需要非空 CN 作为 identity alias,不能使用无 CN 的 gateway 证书;因此登录使用
同一主机已有 peer 证书,仍由 `allowed_dns_sans=etcd-laptop` 限制身份,未扩大到其他成员。
手动触发 `systemctl start homelab-etcd-renew.service`,检查 journal 和
`/var/lib/homelab-etcd-controller/last-success`;每日 timer 04:10 UTC 加随机延迟。
本流程也续签自身登录所用 peer 证书;若超过有效期仍未修复,则需要管理身份重新签发。
成员启动本身仅用本地证书,不实时依赖 Bao。
## 备份与故障处理
每个成员有每日一次的 `homelab-etcd-snapshot.timer`,快照保存在各自
`/var/backups/homelab-etcd`,root-only,保留最近 3 个成功快照;snapshot status 验证成功才淘汰旧快照。
首备可执行 `systemctl start homelab-etcd-snapshot.service`,用 journal 和 `etcdutl snapshot status`
检查,不根据文件名推断备份成功。三处本地快照不等于异地备份。
quota 初始 256 MiB,按 1 小时保留进行自动 compaction;defrag 另择维护窗口逐成员做,
不能三个成员同时执行。metrics 绑定 loopback 与成员内网地址的 2381 端口,独立 listener 不提供 KV API;
与既有 LAN exporter 一样通过内网 HTTP 采集,禁止将端口映射到公网。
现有 VictoriaMetrics 使用 `platform/observability/metrics/scrapes/shared-etcd.yaml` 采集,
对应 VMRule 覆盖成员不可采集、少于两个可采集成员、无 leader、容量、WAL fsync 和频繁选举。
采集失败不等于 quorum 丢失,需结合管理员 endpoint health 判断。
备份年龄、证书到期与续签任务告警尚待补齐。既有 Alertmanager 已接入 Telegram,
维护者已收到本次成员无 leader 与频繁选举通知;接收器配置由现有监控栈维护。
### DRBD I/O 故障导致成员根卷只读
`SharedEtcdNoLeader` 是单成员指标,不等于全体没有 leader。先检查三个 endpoint,
并检查宿主机内核的 DRBD quorum、PingAck、I/O error 和 ext4 journal 日志。
`findmnt` 显示 `rw,emergency_ro` 仍表示文件系统已因错误停止写入;DRBD 恢复 quorum 不会自动修复它。
2026-09-25 17:16 UTC 起,CT150 根卷发生 quorum 短暂丢失、journal I/O 错误和 emergency_ro,
另外两个 etcd 成员保持健康。期间两个新 HDD 数据卷初始同步伴随多个 DRBD 资源 PingAck 超时。
只对本项目两个新数据卷设置 `DrbdOptions/PeerDevice/c-max-rate=10240`(10 MiB/s),
入口为 `../shared-postgresql/ansible/limit-resync.yml`,创建卷流程也复用该任务。
限速后短期未见新增超时,尚不构成唯一根因证明;未调整全局协议、quorum 或超时。
恢复顺序:确认另外两个成员健康并保存快照,正常停止故障容器,确认根卷卸载、DRBD quorum
和副本 UpToDate,再执行 `pct fsck 150 --device rootfs --force 1`。fsck 返回 1 表示已修复,
PVE 包装命令仍会报非零;需再次检查返回 0 才启动容器。不能在已挂载卷上执行 fsck,不能直接强制 remount。
本次修复后 CT150 启动,三个 endpoint 健康且 Raft term/index 一致;两个容器根卷与 mp0 均可写。
频繁选举告警含 15 分钟历史窗口,应核对最新计数和健康状态,不通过关闭规则消除通知。
- 单成员故障:先确认其他两成员仍健康,修复原成员;普通部署不删数据目录。
- 成员永久丢失:需单独执行 member remove/add 流程并同步 inventory,不用重新 init 覆盖。
- 全集群丢失:先恢复 Bao/信任根与恢复凭据,隔离全部旧成员,用同一有效快照恢复新逻辑集群。
开放客户端前逐个协调消费者状态。Patroni 必须核对真实主库、timeline、lease/DCS 状态;
不因旧快照宣称某节点为主就允许它写入。完整跨服务灾难恢复演练尚未完成。
## 验证
```bash
# 本机 loopback 三节点,临时测试 CA 与假的 Bao HTTP 服务,不接触线上秘密。
ETCD_TEST_BIN=/path/to/etcd-v3.7.2-linux-amd64 python3 tests/integration.py
cd terraform && terraform validate
cd ../ansible && ansible-playbook --syntax-check site.yml
```
集成测试验证实际模板启动、mTLS、认证初始化幂等、消费者幂等、CAS 首次创建、秘密缺失失败关闭、
prefix 隔离、gateway 账号登录和单成员故障。假 Bao 不验证真实 PKI 签发或中央 policy 的权限,
也不证明 PVE SSD DRBD 在业务负载下的延迟、systemd 内存预算、在线证书轮换和生产网络符合要求。
源码边界与 PostgreSQL 后续设计见 [研究记录](../shared-postgresql/RESEARCH.md)。
2026-09-25 验证结果:Terraform validate 通过;Ansible lint 零文件级问题;隔离三成员测试通过,
认证初始化与消费者第二次执行均 `changed=0`,包含 gateway 写入、密码漂移拒绝、快照离线恢复、
停止一成员后继续写入。测试低负载 RSS 约 36.6–40.4 MiB/成员,仅作开销参考。
现场验证:Bao PKI roles/policies 已应用,三成员已签发证书并启动;认证初始化与
`patroni-pg-prod` 账号创建成功,随机密码已保存 Bao,实际 gateway 登录验证通过。
两台 LXC 的 rootfs 均为 `pve-rg`,运行状态正常;迁移后全部端点成功提交健康探测。
现有监控已接入;自动续签已启用;备份/证书年龄告警尚待补齐。PostgreSQL 部署状态见其 README。
监控接入验收(2026-09-25):三个 `up{job="shared-etcd"}` 均为 1,六条规则 health=ok,
三端点健康检查通过。新增续签调度四项失败/成功路径测试通过,Terraform validate 与 Ansible lint 通过,
隔离三成员测试再次通过。监控 CR 已现场应用且已加入 Kustomization,IaC 通过独立 PR 发布,尚待合并;
GitOps 来源关联仍待完成。备份/证书年龄告警不包含在这六条规则中。
+116
View File
@@ -0,0 +1,116 @@
# k3s 外共享 PostgreSQL
面向 homelab 的 PG 18 + Patroni + pgBackRest,复用现有 Bao、shared etcd、VyOS 和监控栈。
开发与生产共用 laptop,但使用独立 OS 用户、数据集、端口与配置。没有部署 Pigsty 全栈。
**2026-09-25 已上线新生产主从、独立开发实例、稳定入口与本地备份。**
现有 CNPG 和应用数据未迁移。Ayatori 按维护者决定沿用独立控制面规划,本轮只备齐接入材料,
未向现有 k3s 部署 Ayatori 或 Instance CR,也不能宣称 Instance Ready。
## 入口与拓扑
| 承载 | 角色 | 数据目录/入口 | 初始预算 |
| --- | --- | --- | --- |
| laptop | prod 正常主库 / Patroni | `/var/lib/homelab-postgresql/prod`,192.168.10.127:5432 | ZFS quota 32 GiB;PG MemoryMax 1 GiB |
| pve1 LXC150 | prod standby,与 etcd 共置 | 同 prod 目录,10.60.0.20:5432 | HDD mp0 16 GiB;容器 1536 MiB |
| laptop | dev 独立实例 | `/var/lib/homelab-postgresql/dev`,pg-dev.ad.ddupan.top:5433 | ZFS quota 8 GiB;MemoryMax 384 MiB |
| pve2 LXC151 | pgBackRest 仓库,与 etcd 共置 | `/var/lib/homelab-pgbackrest` | HDD mp0 32 GiB;容器 768 MiB |
| VyOS | 生产稳定 TCP 入口 | pg-prod.ad.ddupan.top:5432 → 192.168.10.2:5432 | 独立 HAProxy service,MemoryMax 64 MiB |
两个 LXC 的 rootfs 保持 `pve-rg` SSD DRBD,数据盘为 `pve-rg-hdd`。
laptop ZFS 使用 recordsize=16K、lz4、atime=off。预算不是容量或 SLA 承诺,standby 的 16 GiB
小于主库 quota,扩容应以较小容量为约束。PG prod/dev 分别为 `pgprod`/`pgdev`,配置和 socket 隔离。
初始 prod shared_buffers=128 MiB / max_connections=80;dev 为 32 MiB / 30。
现场版本为 PostgreSQL 18.6、Patroni 4.1.5、pgBackRest 2.59.1。
客户端必须使用自己的业务凭据及 `sslmode=verify-full`。本轮仅建立 Ayatori 非 superuser
管理账号(CREATEDB/CREATEROLE),未交付普通应用账号。公开中央 CA 位于 `ayatori/ca.crt`。
例如,在已获管理身份的受控终端用交互密码提示验证,不在命令行填写密码:
```bash
psql 'host=pg-prod.ad.ddupan.top hostaddr=192.168.10.2 port=5432 dbname=postgres user=ayatori sslmode=verify-full sslrootcert=ayatori/ca.crt' -W
```
生产采用异步复制,自动切换可能损失未复制的已提交事务;最大候选落后阈值 1 MiB 不是精确 RPO。
Patroni REST 绑定内网 8008,写操作有独立认证。HAProxy 校验 `/primary`,摘除旧主时关闭旧会话;
角色变换与代理收敛期间可能出现短暂连接错误或只读响应,应用应使用适当重试。
没有硬件 watchdog/fencing,尚未验收进程冻结、网络分区或整站故障,不能把服务停止演练当作覆盖所有故障。
## IaC 与秘密
本目录 Terraform 独立 state 为 `shared-postgresql/terraform.tfstate`,使用现有 S3 `terraform`
身份;`run.py` 在内存读取 `kv/k8s/seaweedfs-s3` 的 AK/SK。首次 PKI/JWT role/policy 管理由有效
Bao 管理会话 plan/apply,业务部署改用受限 SPIFFE role `homelab-postgresql`。
```bash
python3 run.py terraform plan
PG_BAO_SPIFFE_ROLE=homelab-postgresql python3 run.py --spiffe ansible credentials.yml
```
`--spiffe` 从本机 Workload API 取得 JWT-SVID,交换短期 Bao token,执行后撤销;失败不回退到
旧 CLI token,不写 `~/.vault-token`。专用 role 绑定 `spiffe://ddupan.top/dev/panxiao81`,只允许
所需签发路径、实例 KV 与 Ayatori 交付路径,既有 etcd 消费者只读;没有 PKI/auth 管理或 KV 删除权限。
Terraform provider 直接使用包装器的 token(skip_child_token),不另外创建子 token。
原始实例秘密位于 `kv/infra/postgresql/{prod,dev}`;`initialize-secrets.yml` 仅在元数据明确不存在、
数据目录尚未初始化时 CAS=0 创建。403、软删除、密码缺失或漂移均不能触发隐式重置。
Ayatori 单独使用 `kv/infra/postgresql/ayatori/{prod,dev}`,仅有 username/password,由
`ayatori-credentials.yml` CAS 创建及回读核对。未来 ESO 只读这两个路径,不读取完整实例秘密。
涉及秘密的任务 no_log,禁止对凭据 play 使用 diff。旧 Ansible Vault 未迁移。
## 部署与维护入口
1. `storage.yml` / `pve-storage.yml` 准备专属卷和预算;`preflight.yml` 拒绝未知数据或错误挂载。
2. `packages.yml` 预装软件,阻止 apt 自动初始化默认 main;只刷新 Ubuntu 与本项目 PGDG 源,
仍校验签名,不接管 laptop 无关第三方仓库故障。
3. Terraform 准备 PKI 与身份,`initialize-secrets.yml -e pg_allow_initialize=true` 首次创建密码。
4. `site.yml` 写入实例配置和证书;不初始化、不自动重启。`backup.yml` 建立专属仓库与 SSH 传输。
5. `bootstrap.yml -e pg_allow_initialize=true` 显式初始化空目录,先 laptop 后 standby;未知数据不接管。
6. `bootstrap-backup.yml` 验证 WAL 与首备成功后启用每日 timer;`proxy.yml` 要求唯一 primary 才发布入口。
7. DNS 由 `infrastructure/dns/records.yml` 与 Samba `provision-dc.yml --tags dns` 管理。
8. `controller.yml` 安装 PG 证书每日续签调度,预检通过才启用。
9. Ayatori 独立控制面就绪后按 [接入说明](ayatori/README.md) 注册;CNPG 迁移另行按应用验收。
普通 `site.yml` 不代表所有配置已在线激活;尚未实现通用参数变更的 rolling restart。
专用 `renew-certificates.yml` 只处理证书,60 天有效期、剩余不足 14 天续签;逐主机检查服务,
需要时签发并向 Patroni/PostgreSQL 主进程发送 HUP,最后检查 SQL。`homelab-postgresql-renew.timer`
每日 04:30 UTC 加随机延迟;日志由同名 service journal 查询。程序与 Ansible 副本位于
`/opt/homelab-postgresql-controller`,由 root 管理,以本机开发身份取短期 token。
续签依赖现有 SPIRE(在 k3s)和 Bao;数据库与 etcd 的正常运行仅用本地证书,不实时依赖 k3s。
## 备份与恢复
仓库主机发起备份,发现实际 primary;每天 03:40 UTC 加随机延迟执行 full,保留 3 个 full,
本地连续归档 WAL。WAL archive_timeout=300s,未设置超限丢弃 WAL 的选项。
生产两节点各持有独立 SSH key,仓库和数据库账号相互授权,固定 host key,关闭 SSH 自动追加 host keys(UpdateHostKeys=no),不允许 PTY/转发;
专用账号仍能运行所需远端命令。密钥在目标文件系统原地生成,避免 ext4 属性复制到 ZFS 失败。
查看仓库 `homelab-postgresql-backup.timer/service` 与
`pgbackrest --config=/etc/homelab-pgbackrest/repository.conf --stanza=prod info/check`。
首次实际 full 的 DB 22.7 MB、仓库备份集约 2.7 MB,仅代表当前空实例,不是未来业务容量估算。
独立的 dev 实例当前不备份。异地备份按维护者决定后置;本地 PVE 仓库不是异地或离线副本。
`tests/live_restore.py --run` 从真实仓库恢复到新临时目录,用私有 Unix socket 验证恢复实例可写与
Ayatori 角色,随后停库清理;不覆盖生产 PGDATA。全站恢复及接管旧 CNPG 不由此脚本自动执行。
## 监控与验收
现有 VictoriaMetrics/Alertmanager 已采集生产两节点 Patroni 原生指标,规则位于
`platform/observability/metrics/{scrapes,rules}/shared-postgresql.yaml`。覆盖成员不可采集、无可见主库、
多主、replica 未流复制及 PG 进程停止。没有部署另一套监控栈;数据库 SQL 级 exporter、dev 运行告警、
备份年龄与证书到期告警尚待补齐,不能宣称监控完整。
2026-09-25 已验证:
- 真实 prod 主从、dev 初始化;Ayatori 管理账号 verify-full TLS/权限校验;生产稳定入口 SQL 写入。
- 配置第二次执行两主机 `changed=0`;备份配置三主机与代理重跑均 `changed=0`;实例秘密及 Ayatori 独立凭据重复执行不变更。
- 真实 SSH WAL 归档、首个 full、每日 timer;真实备份恢复到隔离实例并验证可写。
- `tests/live_failover.py --run` 停止主库服务,自动提升/入口写入、旧主重加入、计划回切后复制、探针清理全部通过;
完整通过的一次恢复写入约 9.2 秒(首次观察 15.1 秒),是低负载演练结果,不是 SLA。
- 隔离 Docker 测试使用实际模板,另覆盖时间点恢复、DCS 多数派丢失后拒绝写入及管理权限合同。
- Terraform 最终 plan 无变更、Ansible lint、短期会话撤销测试与 Ayatori Kustomize 渲染通过。
- PG 续签 systemd 首次运行 Result=success,生产两主机证书检查与 SQL 验收均 `changed=0`;尚未强制演练证书临期轮换。
新 HDD 卷同步限制为各 10 MiB/s,入口 `limit-resync.yml`;同步已完成,副本 UpToDate。
此前 CT150 根卷 DRBD quorum 抖动导致 emergency_ro,已离线修复;详见 [etcd runbook](../etcd/README.md)。
限速后未再观察到同类故障,长期稳定性与唯一根因尚未确认。
@@ -0,0 +1,150 @@
# Shared PostgreSQL:Pigsty 裁剪研究
日期:2026-09-25。状态:源码研究与方案建议,尚未部署、压测或决定迁移。维护者已确定 etcd 按全 homelab 共享基础设施设计,其余实现细节仍为建议。
研究基线:Pigsty `v4.5.0`,commit `dab5dba333a070d96fde1f9feb41761148f2be8c`,浅克隆位于 `/tmp/pigsty-source-review`。下文相对源码路径均指此版本。保留上游 Apache-2.0 许可及适用的版权声明。
## 结论
可以裁剪,优先保留 PostgreSQL + Patroni + etcd + pgBackRest,复用 pg_exporter 和经过筛选的规则。无需带入仓库服务器、PgBouncer、Supabase、Grafana/VictoriaMetrics 全栈、门户和 MinIO。
但这不是一份关闭功能开关的 inventory 就能完成的工作。主要改造点是:实例与宿主机解耦、移除对宿主机的全局接管、重做初始化权限、补齐可验证的备份和切换流程。建议维护独立的精简 Ansible 实现,明确记录取自上游的文件和基线;不保留无用组件及其配置兼容负担。
现阶段没有实测 RSS、HDD fsync 延迟和故障恢复时间,不能宣称任何 HA 方案已经满足资源预算。
## 已知需求与现状
- 数据库独立于 k3s;生产自动故障转移;开发不要求 HA。
- laptop ZFS SSD 为正常生产 primary 所在地;PVE HDD LXC 承担 standby。
- 开发与生产共用物理机器,但使用独立 PGDATA、端口、操作系统账号和资源约束。
- IaC 管宿主资源、实例、HA、备份、监控;Ayatori 管业务数据库、账号和凭据;Backstage 管门户。
- 异地备份暂缓,首期仍包含本地备份与恢复验证。
- 本会话先前只读检查:现有 CNPG 为 PostgreSQL 18.3,数据库合计约 185 MiB,PGDATA 约 748 MiB,其中 WAL 约 561 MiB;活跃 10 GiB PVC 的文件系统使用量约 795 MiB。这不是备份压缩后的体积,也不是新的持续监控结果。
- Gitea 数据库约 43 MiB、NetBox 约 29 MiB;NetBox 可能退役。Git 仓库、LFS、附件等不包含在数据库体积里。
## 源码发现与处理
| 范围 | 源码依据 | 结论与处理 |
| --- | --- | --- |
| 最小入口 | `slim.yml` | 已跳过 INFRA、监控栈和仓库服务,但仍执行 NODE、HAProxy、ETCD、PGSQL;不能直接当共享宿主部署入口。 |
| 生命周期 | `pgsql.yml`、`roles/pgsql/tasks/main.yml` | 初始化与日常收敛不是同一件事。拆成 provision/configure/upgrade/recover,禁止普通配置更新重跑 bootstrap。 |
| 同机多实例 | `roles/pgsql/tasks/install.yml`、`config.yml`;全仓检索 `pg_instances` | `pg_instances` 仅见注释/元数据,没有实例循环实现。硬编码 `/pg`、Patroni/PG service、pgBackRest/exporter 配置与全局环境文件;改端口或 inventory alias 不够。 |
| 宿主机接管 | `roles/pgsql/tasks/install.yml:58` 起、`roles/node/tasks/main.yml` | Debian 清理逻辑会停止默认 PG、删除发行版 systemd unit,包含立即停库与 kill 回退;NODE 管 DNS、软件源、调优等。删除这些逻辑,仅管理自己声明的资源。 |
| 包依赖 | `roles/node_id/vars/d12.aarch64.yml` 等发行版映射 | `pgsql-common` 仍包含 PgBouncer、vip-manager、backup exporter;`pgsql-main` 也含多种扩展/语言包。关闭服务不等于减少安装,必须改为明确的包清单并固定版本策略。 |
| 调优 | `roles/node_id/tasks/main.yml`、`roles/pgsql/templates/tiny.yml:14` 起 | `tiny` 默认仍为 250 connections,内存按宿主计算,work_mem 下限 16 MiB。共享 laptop/LXC 应按实例预算计算,不能把整机资源分配给每个实例。 |
| HA 替换 | `roles/pgsql/tasks/main.yml`、Patroni 模板与恢复脚本 | 禁用 Patroni 并不会得到另一套完整的初始化/HA 实现;换 pg_auto_failover 是重新接生命周期和恢复流程,不能按少一个组件直接判定更便宜。 |
| 路由 | `roles/pgsql/templates/service.cfg` | 已有 Patroni HTTP 角色检查、PostgreSQL 直连目标与摘除旧会话的逻辑,可抽取接既有 HAProxy。自动选主必须同时解决稳定入口与客户端重连。 |
| 初始化权限 | `roles/pgsql/tasks/patroni.yml:103` 起、`pg-init-roles.sql`、`pg-init-template.sql` | `pg_provision: false` 只跳过业务 provisioning,不跳过 bootstrap SQL。默认角色和 template1 修改仍会发生;替换为最小初始化,不能直接给 Ayatori 默认 superuser DBA。 |
| 备份 | `roles/pgsql/tasks/pgbackrest.yml`、`roles/pgsql/templates/pgbackrest.conf` | stanza/首备错误被忽略;全局 initial.done;模板仅渲染 repo1;按整机 CPU 计算并行度。需要实例化路径、明确失败门禁、保守并行度。 |
| 调度 | `roles/pgsql/defaults/main.yml:25`、`files/postgres/pg-backup` | 默认 pg_crontab 为空;备份脚本虽然检查 primary,但硬编码 `/pg` 与默认配置。部署成功不代表周期备份存在。 |
| 监控接入 | `roles/pg_monitor/tasks/register_victoria.yml` 等 | 上游注册到自己的 infra 目录。只取 exporter 部分,改为本仓现有 VMStaticScrape/VMRule 接入方式,不部署新的监控服务。 |
上游固定版本入口:[slim.yml](https://github.com/pgsty/pigsty/blob/dab5dba333a070d96fde1f9feb41761148f2be8c/slim.yml)、[pgsql tasks](https://github.com/pgsty/pigsty/tree/dab5dba333a070d96fde1f9feb41761148f2be8c/roles/pgsql/tasks)、[监控规则](https://github.com/pgsty/pigsty/blob/dab5dba333a070d96fde1f9feb41761148f2be8c/files/victoria/rules/pgsql.yml)。
## 建议拓扑
| 故障域 | 首期部署 |
| --- | --- |
| laptop | prod primary + Patroni;dev 单实例;etcd 成员 A;exporter。prod/dev 分别使用 ZFS dataset。 |
| PVE 主机 A 的 LXC | prod standby + Patroni;etcd 成员 B;exporter。 |
| PVE 主机 B 的 LXC | etcd 成员 C;本地 pgBackRest 仓库可与此共置,不额外引入对象存储服务。 |
| 既有入口 | HAProxy 按 Patroni `/primary` 健康检查转发生产写连接,开发使用独立入口。 |
| 既有监控 | vmagent 抓取;现有 vmalert/Alertmanager 评估与发送告警。 |
首期只配一个 PG standby;第二个 PVE LXC 可以只承担投票成员和备份仓库。两个 PVE LXC 必须跨实际物理主机,否则不能算两个故障域。etcd 不复用 k3s 的 DCS。
## Shared etcd 的管理边界
维护者于本轮明确:etcd 做成全 homelab 共享服务。原方案将 etcd 列入数据库部署角色;调整后 etcd 拥有独立的 inventory、部署入口、升级、快照和恢复流程,PostgreSQL 是第一个消费者。建议代码归属 `infrastructure/etcd/`,数据库只声明端点、客户端凭据和自身的 DCS prefix。物理共置方式可沿用上表,不因此增加节点。
- 为每个消费者分配独立账号与 key prefix,并用 etcd RBAC 约束读写范围。Patroni 示例:`namespace: /homelab/patroni/`、`scope: pg-prod`,只授权 `/homelab/patroni/pg-prod/`。路径命名本身不是权限隔离。参考 [etcd RBAC](https://etcd.io/docs/v3.6/op-guide/authentication/rbac/)。
- 客户端配置三个直接可达的 TLS endpoints;etcd 维护不依赖生产 PostgreSQL、Ayatori 或 k3s 的可用性。etcd root 仅用于基础设施管理,不交给消费者。
- 维护者明确 mTLS 证书使用 OpenBao 中央 CA 签发,删除 Pigsty 自建 CA 的部署依赖。建议分别定义 etcd server、peer、consumer 签发角色,限制名称、SAN、用途和签发权限;peer 需要 serverAuth/clientAuth,消费者凭据按服务独立。中央 CA 签名本身不等于获准加入 peer 网络,必须限制 peer 身份及网络入口,必要时使用中央 CA 下的专用中间 CA。证书私钥在目标机生成并以 CSR 签发;IaC 管角色和续签流程,秘密不进入 Git/state。
- 已签发证书与信任链保存在本地,etcd 正常启动不要求即时连接 Bao;提前续签、校验证书并按成员轮换,告警覆盖过期风险。Bao 不迁入此 etcd,也不依赖 PostgreSQL;灾难恢复沿用先恢复信任根的边界。Bao 暂时不可用时,现存有效证书仍可完成 TLS 验证。
- Patroni etcd3 使用 gRPC gateway,不能依赖客户端证书 CN 映射来完成 RBAC 身份认证;隔离实测还要求 gateway 客户端证书省略 CN;配置独立 username/password,并保留 TLS 校验。凭据由现有秘密管理提供。Pigsty tiny 模板已包含 username/password,但默认密码可退化为集群名,必须替换。参考 [Patroni etcd 配置](https://patroni.readthedocs.io/en/latest/ENVIRONMENT.html#etcd)。
- 维护者确定 Patroni etcd 密码随机生成并存入 Bao。实施时首次创建使用 KV v2 CAS 防止覆盖已有值,之后收敛复用同一 secret;读取失败不能当成不存在而重置密码。显式轮换需同步 etcd 账号与 Patroni 配置。Ansible 在执行时读取、以 no_log 和受限文件权限下发,不新增 Ansible Vault 副本。共享 etcd 实施阶段已生成并写入该线上 secret,gateway 登录验证通过;Patroni 尚未部署。
- 仓库配置检查显示 Ansible Vault 尚未整体退出:OpenBao、Samba AD、Proxmox 的 ansible.cfg 仍引用根目录 `.vault_pass`;OpenBao/Samba AD 仍有 vault.yml 路径,OpenBao README 仍将 OIDC client secret 归入该文件。未解密这些文件,也未查询 Bao 中是否存在对应副本。既有秘密迁移另行盘点;Bao 自身 bootstrap/恢复材料须保留独立恢复途径,不能只存在需要它们才能恢复的 Bao 中。
- 共享的是小规模配置、服务协调和选主能力。key 权限不隔离磁盘、写入负载、compaction 或集群故障;新增消费者须控制写入量、value 大小与历史保留,监控全局容量和延迟。暂不迁移 k3s 的内部 datastore。
- 快照、compaction、defrag 和成员维护统一由共享 etcd IaC 管理。卸载 PostgreSQL只清理其授权范围内的资源,不删除 etcd 成员或恢复整个集群。
- etcd 全集群恢复会回退所有消费者状态,不能拿整集群 snapshot 当单个应用的回滚。恢复 runbook 要协调 Patroni 的实际数据库角色、lease 与 DCS 状态,并为其他消费者保留恢复步骤。
这项决策进一步支持保留 Patroni:etcd 的运行与维护成本由多个使用者共享,无需为每个数据库集群再建一套 DCS。
PVE 改为 bare-metal 目前只是维护者表达的后续倾向,不构成本轮迁移决定。etcd/PG 角色面向普通 Linux systemd 主机,LXC 创建及宿主资源配置单独管理,以便未来更换承载方式时复用。
三个 etcd 成员不是三个额外的大 VM,可与上述角色共置。etcd 数据很小,但对磁盘延迟敏感;必须观察 PVE HDD 在备份/数据库负载下的 fsync 和选举稳定性,低数据量不自动代表低延迟。
开发默认不使用 Patroni/etcd,不做 standby,进一步减少常驻资源。若另建 laptop 容器,能保留更多上游单机单实例代码,但会引入容器生命周期、网络和挂载管理;因此首选直接实例化 systemd service,容器为已有成熟运行时可复用时的备选。
生产 PG、Patroni 和 exporter 数量分别为 2、2、2;开发再加一个 PG 和 exporter;etcd 共 3 个成员。pgBackRest 可通过 timer/SSH 工作,不必常驻对象存储服务器。这是角色数量,不是 RSS 估算。
## 自动切换的边界
建议先采用异步复制,使常态 SSD 写延迟不被 HDD 提交路径限制。故障切换可能丢失尚未复制的已提交事务。`maximum_lag_on_failover` 是候选资格阈值,不能宣传成精确 RPO;官方还计入最近 TTL 窗口的 WAL。见 [Patroni replication modes](https://patroni.readthedocs.io/en/latest/replication_modes.html)。
自动化包含:故障检测、候选提升、入口切换、旧主恢复后 rewind/rejoin。不自动抢回 laptop 主角色,避免反复抖动;正常时优先让 laptop 承担 primary,故障后先稳定运行于 HDD。是否需要 laptop 恢复后经过稳定窗口自动回切,须另定义策略,不能用循环抢主代替。
必须区分“进程死亡”和“进程暂停/内核卡死”。上游 watchdog 默认 off,DCS 多数派本身不构成对卡死旧主的物理隔离。LXC 不应随意透传宿主 `/dev/watchdog`,否则可能重启整台 PVE。首期可评估软件降级与入口隔离的 best-effort 边界,但不能宣称所有故障下零双主;暂停 Patroni、网络单向中断和旧连接的行为必须进入演练。必要时再增加受控的宿主级 fencing,而不是先部署一套重型编排。见 [Patroni watchdog](https://patroni.readthedocs.io/en/latest/watchdog.html)。
上游 tiny 开启 DCS failsafe:现有 primary 在满足与所有已知 PG 成员通信等条件时可继续服务,但没有 DCS quorum 不能把它当作通用选主替代。见 [DCS failsafe](https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html)。
既有 VyOS/HAProxy 仍是端到端可用性的依赖;复用它不等于数据库入口已经消除单点。要验证现有 VyOS 故障恢复及角色感知检查,不能沿用只检查 TCP 可连接的逻辑。
## 备份首期设计
- pgBackRest 备份仓库放在 laptop 之外的 PVE HDD,使用普通文件系统仓库与 SSH/TLS 访问,不需要 MinIO。两台生产 PG 都能归档到同一 stanza/repository。
- 以每日 full、保留最近 3 个成功 full 及恢复所需 WAL 为初始本地策略;这不等于严格覆盖任意时刻之前 72 小时。若要求完整 72 小时窗口,应增加锚点备份及相应 WAL。异地策略另行决定。
- 周期任务跟随实际 primary 或由仓库端发现 primary;切换之后必须继续备份,不能把 inventory 中初始 primary 当作永久主库。
- 备份并行度先从 1 开始,设置任务超时、磁盘配额与成功时间告警;初始备份失败即判部署验收失败。
- 连续 WAL 归档也应明确 `archive_timeout`,低写入量下不能把“配置 archive_command”当作 WAL 已及时落到仓库。初始候选为 5 分钟,结合生成量与恢复目标实测调整。
- 上游固定 `archive-push-queue-max=4GiB`。超过限制时 pgBackRest 可丢弃归档并破坏该区间 PITR,不能照抄后声称持续可恢复;先明确可用性与保留 WAL 的取舍,并补积压、归档失败和磁盘告警。见 [pgBackRest 配置](https://pgbackrest.org/configuration.html#section-archive/option-archive-push-queue-max)。
- 备份文件存在不算验收:隔离恢复到指定时间点并执行一致性检查;避免恢复出来的开发实例向生产仓库归档。
## exporter 与规则:可以裁得很小
对固定版本 `files/victoria/rules/pgsql.yml` 做 YAML 解析:共 **402 条 recording rules、16 条 alerts**。排除 4 条 PgBouncer alerts 和依赖整机综合压力的 `PostgresPressureHigh`,剩余 **11 条 alerts 仅依赖 8 条 recording rules**(按表达式中的 recording metric 递归追踪)。没有必要搬完整 402 条。
需要保留的 recording metrics:`pg:cls:partition`、`pg:db:age`、`pg:db:conn_limit`、`pg:db:conn_usage`、`pg:db:ixact_backends`、`pg:db:num_backends`、`pg:ins:lag_bytes`、`pg:ins:lag_seconds`。这只是依赖分析,不代表这些告警已通过运行验证。
仍需修正:
- 使用 Pigsty 的 `pg_exporter` 指标体系,不能直接替换为另一款 `postgres_exporter` 而照搬规则。
- 保持或显式转换 `cls/ins/ip/job`;同 IP 多实例按实例端口和标签识别,禁止把宿主 CPU/内存用量重复算成每个数据库的用量。
- `pg:cls:partition = count(... == 0)` 在没有 primary 时可能返回空向量,后续 `!= 1` 不足以保证无主告警;增加预期集群基线/缺失检测。
- exporter 完全不可抓取时不能指望 exporter 自己的 `*_up` 指标告警;增加 scrape `up == 0` 与 target 消失检查。
- `PostgresReplicationBreak` 用 streaming 数量的变化检测,不能覆盖所有持续缺副本情况;补预期 replica 数与持续复制失败。
- severity 从上游 `CRIT/WARN/INFO` 映射为既有路由使用的 `critical/warning/info`,去掉指向未部署 Pigsty UI 的链接。
- 从 collector 白名单开始,避免默认启用所有逐库/逐查询采集。一些 collector 依赖 `monitor` schema/辅助函数;删 bootstrap SQL 时要一起裁 collector。监控辅助 SECURITY DEFINER 与 Ayatori 管理 API 是不同权限问题,不为兼容 exporter 保留整套 DBA 权限。
- 补本地备份年龄、WAL 归档失败/积压、磁盘余量、etcd quorum/延迟,以及数据库端到端读写探测。
## Ayatori 与 IaC 的边界
IaC 仅建立实例运维必需的账号、复制账号、最小监控权限和 Ayatori 管理账号;业务数据库及 owner 不再同时放入 Pigsty `pg_users/pg_databases`。
Ayatori 使用既定的非 superuser `CREATEDB/CREATEROLE` 管理账号,按现有契约验证 PostgreSQL 18 的角色成员关系与所有权操作。应用凭据走 OpenBao/ESO,不进入 inventory 明文、日志或 CR。凭据发放、轮换和删除能力是否已可用要以 Ayatori 当前实现验收,不能把 Instance 注册成功当作 provisioning 已完成。
Backstage 不属于本裁剪仓库。PG/etcd/systemd 的创建与恢复也不塞进 Ayatori。
## 推荐实现切分与验收
1. 建立最小 inventory 模型,区分 host 与 instance;声明 prod/dev 的 uid、dataset、端口、socket、配置路径、systemd slice 和预算。所有控制文件、锁文件、日志及 pgBackRest spool 都实例化。
2. 独立实现 shared etcd 的部署与消费者账号管理,再提取数据库 package、instance、Patroni、backup、exporter 角色;移除 upstream NODE 全局操作。初始化有前置条件,空目录才能 init,变更部署与恢复入口分开。
3. 先在隔离环境验证同宿主 prod/dev:重复执行不重新初始化、不重启无关实例、不改变全局 PG unit、DNS 或调优;资源统计使用 cgroup/RSS/PSS,不能把 PostgreSQL 各进程共享内存重复求和。
4. 加入两数据节点与三投票成员,验证 primary 故障、任一 etcd 故障、网络分区、进程暂停、旧主回归、入口旧连接回收和客户端重试;记录实际 RTO 与丢失事务。
5. 触发主库切换后再次备份,并从备份恢复;模拟仓库不可达、任务失败、WAL 积压,验证报警及恢复链边界。
6. 连接现有监控与 Ayatori,验证 create/rotate/retain/delete 契约,再安排 CNPG 分应用迁移。Gitea 迁移同时协调应用停写与其非数据库存储,但不将其文件迁移混入本次 DB 基础设施研究。
pg_auto_failover 保留为候选,其官方主分支安装文档声明支持 PG 13–18,但依赖带 PG 的 monitor;是否更省资源需要在同预算、同故障场景下测量。当前不建议为了组件名字少一点而舍弃已经能提取的 Patroni 配置、路由与恢复经验。见 [官方安装文档](https://github.com/hapostgres/pg_auto_failover/blob/main/docs/install.rst)。
本研究阶段未修改现有数据库;后续 shared etcd 已以独立 IaC 部署,详见下方实现状态。设计边界已同步到 homelab-wiki 的 `architecture/constraints.md` 与 `services/shared-postgresql.md`,文档直接发布到 main,IaC 通过独立 PR 发布;现有 CNPG 服务状态未发生变化。
## 实现入口
共享 etcd 的首轮实现与验证见 [etcd README](../etcd/README.md),包括独立 Bao PKI/policy Terraform、
LXC 与成员 Ansible、消费者秘密与 RBAC、快照和隔离三成员测试。三成员已部署,PVE 两个 LXC 使用 `pve-rg` SSD DRBD;数据库 IaC 与隔离切换/恢复测试已具备,生产主从、开发实例、稳定入口和本地备份已上线,真实切换与隔离恢复通过;现有 CNPG 应用迁移未开始。具体状态见 [部署 README](README.md)。
旧 Ansible Vault 迁移按维护者要求后置,不阻塞新增消费者直接使用 Bao。
2026-09-25 后续只读前置核查:laptop 为 Ubuntu 24.04.4,ZFS `data` 池当时约 526 GiB 可用,
5432/5433/8008 未监听;CNPG 声明镜像为 `18.3-system-trixie`。新实例继续按 PG 18 主版本准备。
此核查没有创建 PG dataset、安装数据库或迁移应用;容量与端口使用可能随后改变,执行前须再核对。
@@ -0,0 +1,23 @@
# 独立 Ayatori 控制面接入材料
按维护者决定,本目录仅备齐声明,不应用到现有 k3s,不部署 Ayatori。
来源合同为 Ayatori main `f4deb98` 的 Instance API 与 manager flags。
`instances.yaml` 注册 prod/dev 两个 cluster-scoped Instance。`admin-credentials.yaml`
将专用 Bao 路径的 username/password 同步为管理 Secret,不引用含 superuser 密码的实例秘密。
`ca.crt` 是公开中央 CA;`kustomization.yaml` 生成 `homelab-postgresql-ca` ConfigMap。
独立控制面就绪后,由其部署流程完成:
1. 安装 Ayatori CRD 和 ESO,创建 controller namespace(当前预设 `ayatori-system`)。
2. 创建 namespaced `homelab-postgresql-admin` SecretStore,使用该控制面的专用身份访问
`https://bao.ad.ddupan.top:8200`、KV v2 mount `kv`;权限限于 `eso-policy.hcl`,不复用部署身份。
auth mount/role 和集群信任绑定须根据真实控制面配置,故此处不虚构可直接应用的 Store。
3. 挂载 `homelab-postgresql-ca` 的 `ca.crt`,设置 manager
`--database-secret-namespace=ayatori-system` 与 `--database-root-cert=<挂载路径>/ca.crt`。
namespace 有变更时同步修改 kustomization、SecretStore 和 manager 参数。
4. 渲染并审查 `kubectl kustomize <本目录>`,确认目标 kubecontext 为独立控制面后应用。
5. 等待两个 ExternalSecret 同步和两个 Instance Ready;不要将本文 SQL/TLS 验证当作 controller Ready。
首次独立管理凭据由 `ansible/ayatori-credentials.yml` CAS=0 创建;重复执行只核对已有值,
不自动轮换密码。控制器完整 Database/Tenant 供应链路仍按 Ayatori 自身实施与验收。