跳至内容
20.7 实战:一次有证据的计划切换

20.7 实战:一次有证据的计划切换

这是一次真实运行过的实验,不是示例输出。

在第 19 章保留的本地四机沙箱中,本章对 pg-test 执行:

pg-test-1 -> pg-test-2 planned switchover
pg-test-2 -> pg-test-1 planned baseline restore

同时通过 Pigsty primary service 连续写 synthetic idempotency token,保存 PostgreSQL、Patroni、client 与 chapter-19 pre/postflight evidence。

安全结论先写在前面:

local disposable sandbox only
production data/traffic forbidden
planned transition only
no process/host/network/storage/DCS fault
no reinit
no cluster reset
fixture reset not executed
production approval remains pending

20.7.1 预检查、切换、客户端观察与数据核对

风险分级

actionrisk远端 mutation
captureL0
verifyL0
reviewL0
allL0
drill:switchoverL2synthetic fixture + 两次 planned switchover
reset:fixturedestructivedrop test.pg36_ch20 schema

本节只描述如何在指定 local sandbox 重现。生产环境即使也叫 pg-test,也 不能因此获得授权。

先读合同

确认:

target = pg36-l2-vagrant/pg-test
three exact members
initial/final leader = pg-test-1
forward candidate = pg-test-2
production_data_permitted=false
production_traffic_permitted=false

第 19 章基线仍是前提

正式运行前,wrapper 自动执行:

PG36_EVIDENCE_DIR="$run/preflight-ch19" \
  static/labs/ch19/task.sh all

必须看到:

status=ok
hosts=4-distinct
topology=pg-meta-1-primary+pg-test-1-primary-2-replicas
sandbox_l2=accepted-with-exceptions
production_ch19_gate=pending
mutation=none

若第 19 章失败,本章不“顺便修”。先解释 target/host/topology drift。

私密输入

需要第 19 章 private inventory,只用于生成临时 libpq service file:

mode=0600
not committed
not copied into evidence
password never printed

helper 从 exact pg-test user declaration 取 credential,输出:

[pg36-ch20]
host=10.10.10.11
port=5433
dbname=test
user=test
target_session_attrs=read-write
sslmode=prefer

此处故意不展示 password。临时文件是 mode 0600、拒绝 symlink/overwrite, wrapper 退出时删除。

sslmode=prefer 只因本地 5433 没有 TLS,形成命名例外;生产不可复制。

synthetic fixture

setup.sql 建立:

CREATE SCHEMA pg36_ch20;

CREATE TABLE pg36_ch20.write_probe (
    run_id        text        NOT NULL,
    attempt_no    integer     NOT NULL,
    token         text        NOT NULL UNIQUE,
    client_sent_at timestamptz NOT NULL,
    committed_at  timestamptz NOT NULL DEFAULT clock_timestamp(),
    PRIMARY KEY (run_id, attempt_no)
);

正式文件还验证 schema owner、column type/nullability、primary/unique key; 如果已存在但结构漂移,拒绝使用。

fixture 只含 synthetic token。清理不是 acceptance 前提,也不会被 drill 自动 drop。

preflight phase

在任何 DDL 前,底层脚本先 capture before

Patroni exactly one pg-test-1 primary
pg-test-2/3 streaming
one system identifier
SQL roles agree
two sender rows
replay gap <= 1 MiB
dynamic policy exact
pause=false

然后创建/验证 fixture、启动 probe,等第一个 acknowledged write,再 warm up 3 秒并 capture pre-switch

为什么要两次:

before
  证明 mutation 前 baseline

pre-switch
  证明 probe 已运行且 action 紧邻前 topology 仍合格

client probe

运行 24 秒,每 0.2 秒尝试:

INSERT ... RETURNING
  committed_at,
  pg_current_wal_insert_lsn(),
  current WAL filename timeline,
  backend pid,
  pg_is_in_recovery();

每次 token:

<run UUID>:<8-digit attempt>

事件立即 append JSONL 并 fsync 本地 evidence:

attempt start/end monotonic ns
acknowledged or unknown
SQLSTATE/error class without credential
WAL LSN/timeline for acknowledged event

probe 不把异常 token 当成一个新业务动作重试。

执行正式动作

读者使用当前 Pigsty 时,可以先:

pig pt switchover --plan

本书 exact v4.4.0 target 的 formal executor 是:

patronictl -c /etc/patroni/patroni.yml \
  switchover pg-test \
  --leader pg-test-1 \
  --candidate pg-test-2 \
  --force

但不要手工绕过本章 guard。完整入口:

export PG36_CH19_INVENTORY=/absolute/private/path/pg36.yml
export PG36_EVIDENCE_DIR=/absolute/path/to/new-empty/ch20-run
export PG36_CH20_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH20_NONPRODUCTION=true
export PG36_CH20_PRODUCTION_DATA=false
export PG36_CH20_PRODUCTION_TRAFFIC=false
export PG36_CH20_CONFIRM=SWITCH_CH20_PG_TEST_1_TO_2_AND_BACK

static/labs/ch20/task.sh drill:switchover

所有 guard 都必须 exact match。output 已非空则拒绝覆盖。

forward completion

动作完成定义不是 CLI exit:

Patroni member set exact
pg-test-2 sole primary/running
pg-test-1 and pg-test-3 replica/streaming

然后 capture after-forward,等待 probe 完成并 reconcile token。

token reconciliation

SELECT attempt_no, token, committed_at
FROM pg36_ch20.write_probe
WHERE run_id = $1
ORDER BY attempt_no;

分类:

acknowledged_missing
unknown_committed
unknown_absent
duplicate_tokens
unreconciled_unknown

验收:

acknowledged_missing=0
duplicate_tokens=0
unreconciled_unknown=0
persisted = acknowledged + unknown_committed

恢复教学角色基线

probe 完成后:

patronictl ... switchover pg-test \
  --leader pg-test-2 \
  --candidate pg-test-1 \
  --force

再等:

pg-test-1 sole primary
pg-test-2/3 streaming

capture restored。这是第二次 planned switchover,不是 rewind timeline。

异常时的保守恢复

若 forward 后中途失败,脚本只在能确认:

pg-test-2 is the sole healthy leader

时尝试切回 pg-test-1

若 topology ambiguous:

stop automatic recovery
print inspection requirement
preserve evidence

它不会为了“清理实验”猜 leader。

postflight

角色恢复后重新运行第 19 章 all。正式 preflight 与 postflight 都通过。

证据树:

ch20-run/
├── preflight-ch19/
├── drill/
│   ├── phases/
│   │   ├── before.json
│   │   ├── pre-switch.json
│   │   ├── after-forward.json
│   │   └── restored.json
│   ├── forward-action.json
│   ├── restore-action.json
│   ├── client-events.jsonl
│   ├── client-probe.stderr
│   ├── reconciliation.json
│   ├── drill-manifest.json
│   ├── validation-report.json
│   └── negative-report.json
├── postflight-ch19/
└── review.txt

目录应放在 private evidence 存储,不进入 Git。

read-only 重验

export PG36_EVIDENCE_DIR=/absolute/path/to/ch20-run
static/labs/ch20/task.sh all

输出应含:

status=ok
counterexamples=10-rejected
acknowledged_commits=all-present
sandbox_planned_switchover=accepted-with-exceptions
unplanned_failure_drill=not-run
production_ch20_gate=pending
mutation=none

all 不移动 leader。

fixture reset

reset:fixtureDROP SCHEMA pg36_ch20 CASCADE,是独立 destructive action,需要完整 reviewed evidence、drained clients 和另一个 exact token。

正式实验没有执行 reset。保留 fixture 供后续查证没有问题;若确实清理, 先读合同,不要把它和 cluster reset 混淆。

20.7.2 测量实际 RTO、提交风险与恢复时间

先纠正这个标题

本次没有注入 failure,所以没有测量 failure-time “实际 RTO”。它测到的是:

planned action command duration
planned action -> Patroni stable duration
sampled client write gap
baseline restore duration

把这些都标成 RTO,会把检测、租约、fencing 和故障信息缺失从模型中删掉。

四个 monotonic 时钟

A  forward action starts
B  patronictl returns
C  Patroni topology becomes stable
D  first acknowledged client write after C

指标:

[ command = B-A ]

[ action_to_stable = C-A ]

[ conservative\ write\ gap =D-lastAckBefore(A) ]

probe 使用 monotonic clock,避免 wall clock adjustment 影响 duration。UTC timestamp 只用于人类关联日志。

正式数据

formal wrapper start        2026-07-29T19:39:33Z
forward start               2026-07-29T19:39:46Z
forward command             2734.741 ms
forward stable              2026-07-29T19:39:52Z
action to stable            5822.678 ms
conservative write gap      6007.080 ms
maximum adjacent ack gap    5207.690 ms
restore command             2851.349 ms
restore action to stable    6018.921 ms
wrapper end                 2026-07-29T19:40:17Z
wrapper elapsed             44 s

wrapper elapsed 还包括:

chapter-19 preflight/postflight
four phase captures
24-second client probe
positive/negative validation
review

不能和 service interruption 比较。

为什么保守 gap 大于 command

command exit       2.735s
topology stable    5.823s
client gap         6.007s

差额来自:

Patroni role convergence
old member rejoin
HAProxy health cycle
connection/pool recovery
probe sampling interval

这正说明用 CLI 耗时报告 RTO 会偏乐观。

probe resolution

observed interval:

0.206990 s

采样 gap 至少包含一个 probe interval 的不确定性。若业务 QPS、driver backoff、transaction time 不同,观测会变。

生产 measurement 应:

  • 从真实外部 client vantage;
  • 按 transaction class;
  • 报告 distribution;
  • 记录 retry/backoff;
  • 分开 read/write;
  • 包含 detection;
  • 关联 topology/fence;
  • 说明 measurement resolution。

提交结果

attempts                 120
acknowledged              95
unknown                   25
persisted rows            95
acknowledged missing       0
unknown committed          0
unknown absent            25
duplicate tokens           0
unreconciled unknown       0

身份等式:

[ events=acknowledged+unknown=95+25=120 ]

[ persisted=acknowledged+unknownCommitted=95+0=95 ]

所有 token list length 也与 count 交叉验证。

unknown 全 absent 代表什么

本次 transition window 中的 25 个异常尝试,在最终 chosen history 查不到。 它们可能在连接建立前失败,也可能发送但未提交;对应用而言,先统一归为 unknown,再以 token lookup 得到 absent。

它不意味着未来 unknown 都 absent。系统必须支持:

unknown committed -> return existing result, do not duplicate
unknown absent    -> apply business retry policy

acknowledged 全在代表什么

可以严格说:

all 95 writes that returned success in this run exist after forward
switchover and baseline restore

不能说:

the asynchronous cluster has zero RPO

因为 healthy switchover 会协调 caught-up candidate;没有突然永久丢失 primary WAL tail。

timeline token

acknowledged event 从 WAL 文件名记录:

00000005
00000006

证明 client writes 横跨 forward transition 的两个 timeline。restore 在 probe 结束后执行,所以 client probe 不要求看到 7;restored phase 的 SQL 与 Patroni 证明 timeline 7。

objective

事前合同:

minimum acknowledged attempts = 30
maximum conservative gap      = 15000 ms
missing acknowledged          = 0
duplicates                    = 0
unreconciled unknown          = 0

正式通过:

95 >= 30
6007.080 <= 15000
0 / 0 / 0

这个 15 秒目标是教学 sandbox acceptance,不是生产 SLO。

建立真正 RTO 需要什么

另行授权的 unplanned drill 要定义:

fault injection time
failure domain
old-primary fence evidence
lease/detection
candidate eligibility
new authority
service route
client transaction
backlog recovery

并重复足够次数,报告 percentile 和失败 run。第 33 章再做,不在这里偷换。

20.7.3 输出拓扑证据、时间线和改进项

topology evidence

phaseleaderreplicasPatroni timelineprimary WAL timeline
beforepg-test-1pg-test-2/355
pre-switchpg-test-1pg-test-2/355
after-forwardpg-test-2pg-test-1/366
restoredpg-test-1pg-test-2/377

所有 SQL phase 的 system identifier 相同;公开 summary 只记录 one unchanged identifier,不把机器/cluster identity 无必要地扩散。

sender、receiver 与 slot

每个 stable primary:

two async streaming senders
application name and client address exact
replay gap inside 1 MiB lab bound
two active physical slots named for nonleaders
no WAL receiver

每个 standby:

one streaming WAL receiver
sender_host=current primary
sender_port=5432

这使“old primary rejoined”不是只靠 Patroni label。

checkpoint timeline 反例

正式结束:

cluster current timeline                7
pg-test-3 checkpoint_timeline_id        3
pg-test-3 Patroni state                 streaming
pg-test-3 WAL receiver upstream         pg-test-1

validator 接受 checkpoint 3 <= 7,不要求相等。若早期实现把 pg_control_checkpoint().timeline_id 命名为 timeline_id 并强制等于 current,实验会误报。

这是本章最值得保留的“测量修正”:发现反例后改 evidence schema,再重跑 正式实验,而不是改解释去迁就旧字段。

manifest

schema=pg36-ch20-drill-manifest-v1
run UUID present
mode=planned-switchover-and-planned-baseline-restore
production_approval=false
unplanned_failure_injected=false
secret_values_exported=0
17 source input files hashed

review.py 对比 current source。source 改过以后,旧 run 不能冒充新实现的 证据。

十个反例

caseexpected rejection
sandbox 冒充生产E_PRODUCTION_CLAIM
未评审 candidateE_ACTION
忽略 command failureE_ACTION
外来 system identifierE_LINEAGE
两个 leaderE_TOPOLOGY
timeline 未推进E_TIMELINE
旧主未 rejoinE_TOPOLOGY
acknowledged token 丢失E_COMMIT_EVIDENCE
unknown 未核对E_COMMIT_EVIDENCE
gap 超过 15 秒E_WRITE_GAP

正式 10/10 按预期失败。一个 validator 若只在 happy input 上返回绿色, 还没有证明 guard 分支存在。

accepted decision

sandbox_planned_switchover=accepted-with-exceptions
unplanned_failure_drill=not-run
production_ch20_gate=pending

十个 exception ID 与第 19 章/本章合同 exact match。

不通过的生产问题

shared hypervisor
single etcd
single backup target
virtual storage unqualified
temporary inventory secret lifecycle
three guests below recommended resource floor
async replication
watchdog off
external probe path without TLS
planned-only drill

改进项按 gate 排序

HA production gate

independent failure-domain placement
production DCS quorum
tested fencing/watchdog
approved sync/degrade policy
unplanned process/host/network/DCS drills
repeatable RTO/RPO distribution
manual failover runbook and authority

Recovery gate

independent repository
archive continuity
restore + PITR
timeline history
data/business validation

第 21 章负责。

Client gate

DNS/VIP/HAProxy path
pooling behavior
driver timeout/backoff
session reset
idempotency/reconciliation
read consistency and replica fallback

第 22 章负责。

Security gate

TLS verify-full or equivalent
credential authority/rotation
least privilege
host key trust
audit

第 23 章负责。

ADR

ha-adr.md 记录:

accepted: planned switchover behavior
rejected: automatic failover claim
rejected: command duration as RTO
rejected: no missing tokens as zero RPO
rejected: checkpoint timeline as current standby timeline
rejected: all reruns live mutation

这让将来升级时能判断是事实变化,还是 decision boundary 被悄悄扩大。

正式运行摘要

drill-run.json 是 secret-free reference account:

versions
times
topology relation
client counts/metrics
gates
exceptions/decision

它不是完整 evidence 的替代品;完整 bundle 留在 private path。

20.7.4 记录把本章迁移到新版本基线的工时

为什么记录 effort

版本迁移成本不只有:

change version string

还包括:

contract review
CLI/API drift
config schema drift
evidence query drift
lab provisioning
safe live run
counterexample maintenance
prose/source verification

没有记录,就无法判断下一版是“十分钟 bump”还是“重新认证 HA 行为”。

不虚构 human effort

本次可精确测得:

formal wrapper machine elapsed = 44 seconds

不能从 wall-clock timestamp 反推出:

human authoring seconds
operator active attention
review effort

因此 migration-effort.json 明确:

{
  "formal_machine_elapsed_seconds": 44,
  "human_authoring_seconds": null,
  "human_authoring_measurement": "not-instrumented-do-not-infer"
}

null 比一个看似精确但捏造的数字更专业。

effort 维度

下次迁移记录:

categorymeasurement
researchrelease notes、docs、known issuesactive human time
source adaptationAPI/query/config changediff + active time
environmentbuild/deploy/convergemachine + operator
validationcapture/positive/negativemachine time
live drillguarded transitionexact start/end
diagnosisfailed run/root causeactive + elapsed
writingprose/citations/diagramsactive time if instrumented
reviewtechnical/safety/editorialreviewer time

active 与 elapsed 分开:

machine waits 30 minutes
operator active 3 minutes

二者都可能影响排期,但含义不同。

新版本迁移顺序

  1. 固定目标 release/commit,不用 moving branch;
  2. 阅读 PostgreSQL、Patroni、Pigsty release notes;
  3. diff inventory、Patroni dynamic/local config、service definitions;
  4. 检查 SQL catalog/function 字段变化;
  5. 更新 requirements.json,不要先改 validator 放宽;
  6. 更新 capture schema 与 source allowlist;
  7. 用 synthetic evidence 跑 normal/negative;
  8. 在新 disposable target 重做第 19 章;
  9. 做只读 chapter-20 capture;
  10. 获得 L2 authority 后跑 planned drill;
  11. 保留失败 candidate run,不覆盖;
  12. 更新 reference outcome 与正文;
  13. 独立 review;
  14. 只在证据支持时改变 decision。

兼容性问题清单

PostgreSQL:
  server_version_num, catalog/view/function fields
  WAL/timeline semantics
  pg_rewind prerequisites
  sync commit behavior

Patroni:
  CLI flags/output
  dynamic config
  sync/failsafe/watchdog semantics
  REST health endpoints

Pigsty:
  inventory schema
  default service ports/selectors
  wrapper commands
  monitoring labels
  component versions

client:
  libpq/psycopg behavior
  TLS/service file
  target_session_attrs

baseline result不能机械复制

新版本即使同样输出:

status=ok

也要检查:

validator 是否仍在验证同一事实
字段是否变成 null/新语义
默认配置是否改变
错误分支是否仍拒绝
exception 是否增加/消失

升级最危险的不是 test fail,而是旧 test 在新语义下无意义地继续 pass。

版本迁移 stop conditions

source release not exact
private inventory leaks
unknown config migration
old/new evidence schema mixed
member identity/topology ambiguous
client path differs without contract update
reset/reinit needed but not separately authorized
production target substituted for sandbox

遇到即停止,不用“先跑一次看看”跨过 authority。

当前 effort 记录的范围

正式文件只承诺:

work stream start timestamp recorded
formal wrapper start/end recorded
machine elapsed=44s
human authoring not instrumented
scope note prevents inference

这为以后建立更完整度量留下可比较 schema,同时不美化本次过程。

本章最终复核

[x] failure model before operation
[x] exact nonproduction target
[x] chapter-19 pre/postflight
[x] private credential never exported
[x] named leader and candidate
[x] client probe through service
[x] one system identifier
[x] timeline 5 -> 6 -> 7
[x] old primary rejoined
[x] acknowledged/unknown reconciled
[x] ten negative cases rejected
[x] temporary service file removed
[x] production gate remains pending
[ ] unplanned failure
[ ] fencing/watchdog
[ ] DCS quorum failure
[ ] zero RPO
[ ] production RTO/SLO

小结

本次实验真正证明的是:

在 exact Pigsty v4.4.0 / PostgreSQL 18.4 / Patroni 4.1.3 本地沙箱、 异步复制、单 etcd、无 watchdog 的记录条件下,一个健康且明确命名的 candidate 完成计划切换,旧主重新加入,客户端在约 6 秒的采样间隙后 恢复写入;95 个已确认 token 全部存在,25 个结果未知 token 全部核对 为未提交,最终角色基线恢复。

它明确没有证明:

自动故障转移、旧主硬隔离、DCS quorum、零 RPO、生产 RTO、灾难恢复或 生产安全。

把两段话一起交付,才是完整的 HA evidence。

权威参考


上一节:交付并观察 HA 集群 · 返回本章目录 · 下一章:未雨绸缪:备份体系与恢复演练 · 查看全书目录 · 查看索引中心

最后更新于