跳至内容

19.7 实战:L2 部署验收

这次实战不是模拟输出。本书在四台本地 Ubuntu VM 上用 exact Pigsty v4.4.0 部署 PostgreSQL 18,并执行了正式的只读验收。读者可以复跑同一 合同,但不能把硬件与 secret 路径照抄。

19.7.1 核对版本、拓扑、资源、端点和安全入口

风险分级

正常动作:

project / capture / verify / review / all
risk = L0 read-only against deployed service
local writes = selected evidence directory only

它们不会:

deploy packages
render remote config
restart/fail over service
run DDL/DML
take/restore backup
remove cluster
export secret values

reset:cluster 是独立 destructive action,不属于正常实验。

正式目标

target               pg36-l2-vagrant
platform             local Vagrant Linux sandbox
Pigsty               exact v4.4.0 tag
PostgreSQL           18.4 observed
hosts                4
PostgreSQL clusters  2
members              4
addresshostname after convergenceservice unitobserved role
10.10.10.10pg-meta-1pg-metaprimary
10.10.10.11pg-test-1pg-testprimary
10.10.10.12pg-test-2pg-teststreaming replica
10.10.10.13pg-test-3pg-teststreaming replica/offline

先读实验合同

入口:

先确认:

你控制的是 disposable local sandbox
没有生产数据/流量
inventory 是 private mode-0600 文件
四个地址可以 direct SSH
当前只执行 read-only acceptance

生产环境不能因命令“只读”就跳过访问授权;主机和数据库 fact 本身也可能是 内部信息。

准备私有输入与 evidence 路径

export PG36_CH19_INVENTORY=/absolute/private/path/pg36.yml
export PG36_EVIDENCE_DIR=/absolute/private/path/evidence/ch19-run
export PG36_SSH_USER=vagrant

要求:

inventory file mode = 0600
evidence path outside source control
inventory contains no production credential
SSH user has intended read/sudo ability on this sandbox

本章不提供正式 live inventory。公开的 inventory.example.yml 只有结构和 sentinel。

先做 secret-free projection

static/labs/ch19/task.sh project

预期:

status=projection-ok
secrets=redacted

检查:

python3 -m json.tool \
  "$PG36_EVIDENCE_DIR/inventory-projection.json"

只检查字段,不把输出粘到公共日志。关键值:

status=secret-free-projection
source.mode_octal=0600
source.secret_values_exported=0
version=v4.4.0
pg_version=18
pg_locale=C.UTF-8
host_count=4

direct SSH 避免 alias/forwarding 冒充目标

采集器固定:

ssh -F /dev/null \
    -o BatchMode=yes \
    -o UserKnownHostsFile=/dev/null \
    -o StrictHostKeyChecking=no \
    vagrant@10.10.10.10 ...

禁用本机 config 是本实验对已知 alias 陷阱的防御。StrictHostKeyChecking=no 只适用于这个 disposable、隔离 sandbox;生产必须维护可信 host key,不应 复制这个选择。

正式 evidence 保存 machine ID 的 SHA-256,不保存原值:

four 64-hex hashes
all distinct
address/hostname exact match

hash 只是避免直接扩散标识,不是强匿名化;仍应把 evidence 当内部资料。

host capture 内容

每台生成:

hosts/<address>.json

包含:

OS/kernel/architecture/virtualization
CPU/model/NUMA count
memory/swap/THP/overcommit
root and data backing filesystem/free bytes
timezone/NTP
addresses/DNS/firewall service state/listening ports
selected systemd services
selected package versions

采集器最初尝试跟随 /pg symlink,目标是 PostgreSQL-owned mode-0700 目录,非特权用户得到 PermissionError。修正后采集 /data backing filesystem,不跨越 PGDATA 权限边界。好的 audit 不应为了“读事实”扩大 权限或放松数据目录。

host acceptance 与真实例外

四台均:

Linux / Ubuntu 24.04.x / aarch64
Etc/UTC + NTP synchronized
swap = 0
THP = never
root free >= 8 GiB
required services active

资源实际值:

pg-meta-1   2 vCPU, about 3.8 GiB RAM
pg-test-*   1 vCPU, about 1.9 GiB RAM each

原设计推荐 2 vCPU/2 GiB。没有把阈值悄悄改成“全部合格”,而是:

hard disposable-sandbox acceptance floor = 1 vCPU / 1.8 GiB
recommended teaching floor                = 2 vCPU / 2 GiB
EX19-LAB-RESOURCE-FLOOR                   = accepted exception

这允许验证部署关系,禁止容量推断。

PostgreSQL capture 内容

采集器通过 direct SSH,在每个 node:

sudo -n -iu postgres \
  psql -X -qAt --dbname=postgres --set=ON_ERROR_STOP=1

输入固定 postgresql-facts.sql,不拼接 live secret, 不读取业务 row。

生成:

postgres/<address>.json

验收:

PG 18
checksums on
block 8192
WAL segment 16777216
UTF8 / builtin / C.UTF-8
Etc/UTC
SCRAM verifier setting
SSL on
SQL recovery role
system identifier relation

Patroni 与 endpoint capture

patronictl list --format=json 生成:

patroni/pg-meta.json
patroni/pg-test.json

Patroni 表中的 healthy state 并不全叫 running

leader    state=running
replica   state=streaming

REST 根端点对 replica 可能返回非 2xx,同时带有效 Patroni JSON。采集器会 保存 HTTP status,并在 body 符合身份协议时标记 reachable,而不是把所有 HTTPError 都误判为网络失败。

endpoint 检查:

5432 direct postgres
5433 primary service
5434 replica service
5436 primary direct service
5438 offline service
8008 Patroni REST

只证明 TCP/identity 可观察,不登录业务 service,也不证明路由语义。

执行完整正常实验

static/labs/ch19/task.sh all

正式输出:

status=captured
target=pg36-l2-vagrant
hosts=4
secrets=redacted
production_approval=false

status=ok
target=pg36-l2-vagrant
deployment=pigsty-v4.4.0-postgresql-18
hosts=4-distinct
topology=pg-meta-1-primary+pg-test-1-primary-2-replicas
counterexamples=9-rejected
sandbox_l2=accepted-with-exceptions
production_ch19_gate=pending
mutation=none

任何缺行都不应靠人工补成通过。

19.7.2 从 SQL、主机与 Pigsty 三侧验证同一事实

实际是四个证据面

本节标题说“三侧”,但严谨验收还包括 declaration:

inventory  declared intent
host       machine and service substrate
SQL        PostgreSQL internal fact
Pigsty     Patroni/service implementation observation

四面回答同一问题:

问题inventoryhostSQLPigsty/endpoint
target 是谁address/grouphostname/machine hashcluster name/system IDmember/host
版本pg_versionpackageserver versionPatroni REST server version
当前 rolebootstrap pg_roleprocess/servicepg_is_in_recoveryLeader/Replica
replica healthymember declaredprocesses/portsrecovery identitystreaming
locale/checksumdesired vars/defaultsOS contextdatabase/settingsn/a
service existsdefinitionslistenersn/aTCP/REST

某一面冲突就停止,不做多数表决。

declaration 不能替代 observation

inventory 写:

pg_version: 18

可能出现:

package install failed
old server still running
wrong inventory targeted
host variable override
partial rerun

所以 SQL 必须返回 major 18。

同理,pg_checksum: true 或 role default 只表达意图,SHOW data_checksums 才表达运行 cluster 事实。

process/service active 不能替代 SQL

systemctl is-active patroni 返回 active,仍可能:

PostgreSQL startup failed and Patroni loops
member in catchup
wrong data directory
wrong cluster
timeline divergence
port occupied by another process

因此 service state 与 SQL/Patroni identity 同时要求。

SQL recovery role 不能替代 global coordination

两台各自执行:

SELECT pg_is_in_recovery();

如果都返回 false,SQL 只能说明两台都不是 standby;不能告诉你哪一台应当 获得流量,也不能自动 fence。Patroni membership/leader、DCS 和 service health 共同揭露冲突。

反例:

declare-two-live-leaders -> E_TOPOLOGY

validator 要求每个 cluster 恰好一个 leader,并与每台 SQL recovery 状态 一致。

system identifier 连接 SQL 与 topology

假设:

三个 Patroni member 名和 address 都对
但 pg-test-3 是错误初始化的新 data directory

role/status 可能短暂看似正常。system ID 检查会拒绝:

members of one cluster have different system identifiers

另一个反例是 pg-metapg-test 共用同一 system ID,也会拒绝。

endpoint open 不等于正确 service

本章所有声明端口 reachable。这是必要条件,不是充分条件。

第 22 章还要验证:

5433 write reaches current primary
5434 is read-only and uses intended replicas
5438 selects offline members
PgBouncer pooling mode/session state
TLS/auth/database/role
drain/reconnect/failover behavior

把端口扫描写成“service verification passed”会过度声明。

source checksum 防止验收脚本漂移

capture-manifest.json 保存 capture 时每个 lab source file 的 SHA-256。 review.py 再对当前 source 计算。

这样拒绝:

先 capture
后改 requirements/validator
用旧 evidence 宣称新规则通过

修改 lab source 后必须重新 capture。

positive test 与 negative test 是两类证据

正向:

当前环境满足规则

反向:

规则会拒绝我们关心的已知坏状态

九个反例:

caseexpected code
sandbox 声称 production SLOE_PRODUCTION_CLAIM
两地址复用 machine identityE_HOST_IDENTITY
混用 OSE_HOST_UNIFORMITY
clock 未同步E_CLOCK
memory 低于 hard floorE_RESOURCE_FLOOR
checksum offE_PG_INIT
两 leaderE_TOPOLOGY
inventory mode 0644E_SECRET_FILE_MODE
evidence 导出 secret valueE_SECRET_EXPORT

negative-cases.json 只修改内存中的 evidence 副本,不破坏 live cluster。

单独复核已捕获 evidence

export PG36_EVIDENCE_DIR=/absolute/existing/evidence/ch19-run

static/labs/ch19/task.sh verify
static/labs/ch19/task.sh review

verify 重新运行 policy;review 检查:

source checksums
positive report identity/counts/decision
negative code set
four distinct identities
resource exception exact hosts
endpoint evidence
sanitized deployment account
production boundary
reset_executed=false

人工 review 仍不可省

自动 validator 不知道:

  • owner placeholder 是否已变成真实责任;
  • laptop 是否处于受控物理环境;
  • business classification 是否正确;
  • exception 接受者是否有权;
  • package supply chain 是否可信;
  • future production SLO 是否合理;
  • 当前证据是否用于允许的目的。

机器验证 consistency,人类承担 judgment。

19.7.3 产出基线清单、风险例外与 reset:cluster

evidence bundle

一次 all 产出:

inventory-projection.json
capture-manifest.json
hosts/
  10.10.10.10.json
  10.10.10.11.json
  10.10.10.12.json
  10.10.10.13.json
postgres/
  <four member facts>
patroni/
  pg-meta.json
  pg-test.json
endpoints.json
validation-report.json
negative-report.json
review.txt

不包含:

live inventory
password/token/private key
customer rows
full deployment private log
raw machine ID
production approval

evidence directory应按内部运维资料管理并设置 retention。

sanitized deployment account

deployment-run.json 保存:

exact source tag/commit
generator command
inventory mode and secret export count
bootstrap/Ansible version
deploy command and safe recap
observed acceptance summary
explicit non-claims
reset_executed=false

它方便读书,不替代 fresh capture。host role、package 和 endpoint 会漂移。

六个风险例外

exception事实阻止的生产结论
shared hypervisor全部 VM 共用 laptop四个独立 failure domains
single etcdDCS 单节点control-plane HA
single backup targetMinIO/control 共置DR independence
virtual storage未做 IOPS/latency/durability qualificationcapacity/durability
inventory secrets临时 0600 inventoryproduction secret lifecycle
resource floor三节点低于推荐 2C/2Gsizing/concurrency

exception 必须有:

ID
scope
reason
impact
owner/acceptor
expiry/review
remediation
claim blocked

本章 JSON 记录前四类核心字段;真实生产审批系统还要补责任与到期。

验收决策

正式结果:

{
  "sandbox_l2": "accepted-with-exceptions",
  "production_ch19_gate": "pending",
  "next_gate": "ch20-ha"
}

不是:

production-ready
HA passed
RPO/RTO achieved
backup verified
capacity approved
security compliant

reset:cluster 为什么存在

可重复实验必须说明如何回到起点。但删除 cluster:

停止服务
删除 PostgreSQL data
可能删除 backup state
改变 DCS/service/monitor registration
不可由正常 validation 自动恢复

所以 reset 是 destructive exercise,不是 cleanup convenience。

本章提供 reset-cluster.sh,但正式运行没有执行:

deployment-run.boundary.reset_executed=false

reset 多重 guard

需要显式设置:

export PG36_RESET_TARGET='pg36-l2-vagrant/pg-meta+pg-test'
export PG36_CONFIRM_RESET='RESET_CH19_L2_SANDBOX'
export PG36_NO_PRODUCTION_DATA=yes
export PG36_CLIENTS_DRAINED=yes
export PG36_PIGSTY_HOME=/absolute/exact/pigsty-v4.4.0
export PG36_CH19_INVENTORY=/absolute/private/pg36.yml
export PG36_MACHINE_ID_ALLOWLIST=/absolute/private/machine-ids.json
export PG36_RESET_EVIDENCE_DIR=/absolute/new/pre-reset-evidence

然后才可能:

static/labs/ch19/task.sh reset:cluster

脚本还会:

  1. 检查 exact v4.4.0 marker;
  2. 检查 inventory mode 0600;
  3. 要求新 evidence path;
  4. 先执行 fresh read-only all
  5. 将四台当前 machine hash 与预先 review 的 allowlist 比较;
  6. 要求 /dev/tty 再输入 exact token;
  7. 先移除 pg-test,后移除 pg-meta

任何 guard 缺失,exit 77,什么都不删除。

machine allowlist 不能由 reset 当场自我批准

machine-identity-allowlist.example.json 只有 sentinel。真实 allowlist 应:

从一次已接受 evidence 提取
由 operator review address/hostname/asset
存放 source control 外
限制权限
在 VM rebuild 后重新批准

若 reset 先读取当前机器、再自动把当前值当 allowlist,identity guard 就没有 外部 authority。

不要在 production override safeguard

Pigsty removal playbook支持 pg_safeguard。production inventory 应显式 启用保护。override 是 emergency/destructive authority,不应复制到普通 runbook、CI 或 alias。

本书脚本的 guard 也不能让 production reset 变安全:

target classification、data ownership、recovery、change approval 与现场 判断仍然优先。发现任何可能是 production,立即停止。

本章结束时保留环境

为了第 20–22 章:

do not reset
do not fail over yet
do not restore
do not benchmark to saturation

保留 accepted baseline,下一章才能比较 fault 前后。

handoff 包含:

exact source/deployment account
fresh accepted evidence
six exception IDs
known bootstrap role
system-ID relation
no reset performed
pending kernel reboot observation
production gate pending

读者自检

完成本章后,应能回答:

  1. 为什么 deployment rc=0 不等于 production ready?
  2. 哪些初始化事实必须从 SQL读取?
  3. 为什么四个 IP 需要 machine identity?
  4. 为什么 one leader + two replica 还不能给出 HA RTO?
  5. 为什么 endpoint open 不证明 routing?
  6. 如何在不泄露 inventory 的情况下 review topology?
  7. 六个 exception 各阻止什么结论?
  8. 为什么 reset 不属于 all

若任何答案只能是“因为 Pigsty 默认这样”,还没有完成环境基线。


上一节:用声明式清单交付两个服务单元 · 返回本章目录 · 下一章:狡兔三窟:高可用拓扑与容灾目标 · 查看全书目录 · 查看索引中心

最后更新于