11.7 实战:无中断演进订单模式
本节把模式发布做成一条可重复、可中止、可复核的证据链:
target guard
→ legacy fixture
→ physical-risk A/B
→ lock failure with unchanged catalog
→ compatible expand
→ old/new writer matrix
→ concurrent temporary index
→ controlled backfill interruption
→ exact resume
→ validate and SET NOT NULL
→ switch while retaining rollback shape
→ contract refusal
→ partition attach/detach rehearsal
→ model checksum and proposal review实验不把生产“无中断”承诺缩成一次本地脚本成功。它证明机制和状态关系;真实服务 SLI、复制水位与旧依赖清零仍需在目标 Pigsty 发布窗口完成。
11.7.1 加字段、回填、建约束与新旧版本共存
确认 disposable L1
准备权限受控的 service file:
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
psql -X -w \
--dbname='service=pg36-admin application_name=pg36-ch11-preflight' \
--command="
SELECT
current_database(),
session_user,
current_user,
current_setting('server_version'),
pg_is_in_recovery();
"只在已确认可写、可重建的 L1/本地目标继续。本章脚本再次验证:
database=pg36_shop
writable primary
effective role=pg36_owner
search_path=pg_catalog,shop_private
ch04-v1 marker
ch11 object marker无 service file、错误 database、recovery target、role/path/model 不符都会 fail closed。
全量入口:
cd static/labs/ch11
export PG36_EVIDENCE_DIR="$PWD/evidence/ch11/all-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all可独立运行:
setup | risk | expand | backfill | validate
partition | verify | review | reset | allrisk 到 validate 会重建专用 fixture;verify 只检查当前完整状态。
Legacy fixture
setup.sql 建立 50,000 行:
CREATE TABLE shop_private.ch11_order (
order_id bigint PRIMARY KEY,
order_ref text NOT NULL UNIQUE,
shipping_method text NOT NULL,
created_at timestamptz NOT NULL,
payload text NOT NULL,
CONSTRAINT ch11_order_shipping_method_check
CHECK (
shipping_method IN
('standard', 'express', 'pickup')
)
);以及单行 state:
migration_id=shipping-code-v1
phase=legacy
source_rows=50,000
target_rows=NULL
rows_migrated=0
batches=0
last_order_id=0shipping_code 必须不存在。setup 只在所有已存在同名对象 marker 匹配时才重建,防止误删用户对象。
先测 default/rewrite 风险
default-probe.sql 对另一张 50,000 行 disposable table 做 A/B:
ADD fast_flag integer NOT NULL DEFAULT 7
ADD volatile_stamp timestamptz
NOT NULL DEFAULT clock_timestamp()一次 raw result:
fast:
before_filenode=20116
after_filenode=20116
atthasmissing=true
attmissingval={7}
WAL=12088
volatile:
after_filenode=20129
atthasmissing=false
WAL=11488328review 只要求:
before_filenode == fast_filenode
fast_filenode != volatile_filenode
fast atthasmissing
not volatile atthasmissing
volatile WAL > fast WAL
row_count=50,000具体 OID/WAL 随环境变化。
Expand 前先让锁预算失败
holder:
ACCESS SHARE on ch11_order
waiter:
ADD COLUMN shipping_code text
requests ACCESS EXCLUSIVE
lock_timeout=4s
observer:
one pg_blocking_pids edge结果:
lock=55P03
requested=AccessExclusiveLock
blocker_edges=1
shipping_code remains absent
holder released by COMMIT只有这条 negative path 通过,才执行 expand.sql:
phase legacy required
ADD nullable shipping_code
install bridge function/trigger
ADD pair CHECK NOT VALID
phase=expanded in same transaction这验证 timeout 后可安全重新评估并重试,而不是声称所有 55P03 都可以自动循环。
旧/新版本共存
compatibility.sql 运行四条合同:
old insert:
method=standard, code omitted
→ standard/STD
old update:
existing legacy row method express
→ express/EXP
new insert:
method=pickup, code=PUP
→ pickup/PUP
bad dual write:
method=express, code=STD
→ 23514
→ constraint=ch11_order_shipping_pair_consistent
→ row absent此时:
total rows=50,002
legacy NULLs=49,999
pair constraint convalidated=false
attnotnull=false其中一条旧行已经被 old update 顺带填充,两个新 insert 都由 bridge/dual write 提供 code,所以 backfill target 不是 50,000,而是 49,999。审查器固定这个关系,防止统计口径漂移。
Temporary partial index
online-index.sh 在 transaction block 外:
CREATE INDEX CONCURRENTLY
ch11_order_shipping_missing_idx
ON shop_private.ch11_order (order_id)
WHERE shipping_code IS NULL;build 后验证:
indisready=true
indisvalid=true
indpred=(shipping_code IS NULL)
indrelid=exact ch11_orderbackfill 完成并验证后,再 exact:
DROP INDEX CONCURRENTLY
shop_private.ch11_order_shipping_missing_idx;最终 verify 要求该 migration-only index 不存在。
Backfill 与约束收紧
第一次:
batch size=5,000
max batches=2
exit=75
rows migrated=10,000
remaining=39,999
phase=backfilling续跑:
eight more batches
total migrated=49,999
total batches=10
last_order_id=50,000
remaining=0
mismatches=0
phase=migratedALTER TABLE ch11_order
VALIDATE CONSTRAINT
ch11_order_shipping_pair_consistent;
ALTER TABLE ch11_order
ADD CONSTRAINT ch11_order_shipping_code_nn
CHECK (shipping_code IS NOT NULL)
NOT VALID;
ALTER TABLE ch11_order
VALIDATE CONSTRAINT
ch11_order_shipping_code_nn;
ALTER TABLE ch11_order
ALTER COLUMN shipping_code SET NOT NULL;PostgreSQL 18.4 目录:
ch11_order_shipping_pair_consistent | c | valid
ch11_order_shipping_code_nn | c | valid
ch11_order_shipping_code_not_null | n | valid
pg_attribute.shipping_code | attnotnull=trueSET NOT NULL 前后 relfilenode 相同。PG14–17 review 分支不要求 relation contype=n。
Switch 保留回退形状
switch.sql 先做 full shadow comparison,再分别运行一个 old/new writer:
shadow mismatches=0
old writer after switch succeeds
new writer remains dual-write
phase=switched
shipping_method retained
bridge retained最终总行数:
50,000 baseline
+2 coexistence
+2 switch probes
=50,004读取可以切到 shipping_code,但数据库继续支持旧 application rollback。
11.7.2 注入锁等待和回填中断,验证中止与恢复
Evidence 不是最终一句 PASS
一次全量目录:
manifest.txt
preflight.txt
setup.txt
default-probe.txt
default-catalog.csv
lock-holder.stdout/stderr
lock-attempt.stdout/stderr
lock-graph.csv
lock-summary.json
expand.txt
index-build.txt
compatibility.txt
constraint-before.csv
backfill-interrupted.json
backfill-resumed.json
validate.txt
constraint-after.csv
index-drop.txt
switch.txt
contract-gate.stdout/stderr/exit
partition-prepare.txt
partition-attach-holder.txt
partition-detach.txt
partition-summary.json
verify.txt
model-verify-after.txt
review.txtraw evidence 保存动态值,review 比较稳定关系。
锁注入的 happens-before
协调器不是用 sleep 1 猜时序:
- interactive holder 完成
LOCK ... ACCESS SHARE; - holder 输出 readiness marker;
- 才启动 DDL waiter;
- observer 循环直到捕获 exact blocker edge;
- waiter 以 55P03 结束;
- 检查 column count=0;
- 向 holder 发送
COMMIT,不是强杀 backend。
这固定关键顺序而不固定 PID/时间。异常退出时 finally 只处理自己启动的 child process,并尝试 rollback。
回填中止的原子边界
--max-batches=2 在第二次 commit 后由 client 主动退出,模拟:
- maintenance window 到达停止线;
- safety waterline 要求暂停;
- orchestrator 有计划让出资源。
它证明 between-batch restart。每个 batch 内:
target updates
checkpoint updates
phase updates同事务,因此 disconnect/error 时一起 rollback。
审查器比较:
resume.start.last_order_id
== interrupted.end.last_order_id
resume.end.rows_migrated
== target_rows
resume.end.remaining_nulls
== 0
resume.end.last_order_id
== 50,000还要求 mismatches=0 与 total batches=10,不能仅看 phase 字符串。
Contract 是故意失败的验收项
全量流程运行:
psql --file=contract-gate.sql但不提供 token/target/observation。期望:
exit=3
SQLSTATE=P3612
message=contract refused随后 final verify 要求:
release=switched/contract:not-executed
compatibility=legacy-column+bridge-retained如果 contract 意外成功,verify.sql 会因 old column/bridge 缺失而失败。负向 gate 与正向 post-state 双重闭合。
分区实验的可逆边界
prepare standalone child + valid CHECK
→ ATTACH inside held transaction
→ observer captures SUE + AX
→ COMMIT
→ DETACH CONCURRENTLY in top-level command
→ verify rows and filenode retained
→ reattach一次结果:
server_version_num=180004
before child rows=20,000 / parent=0
after attach child is partition / parent=20,000
after detach child standalone / rows=20,000 / parent=0
final reattached / parent=20,000
same child filenode across all stateselapsed ms 被记录,但不参与 PASS。
最终数据库验收
verify.sql 要求:
active pg36-ch11 workers=0
release phase=switched
checkpoint=49,999 rows / 10 batches / last id 50,000
orders=50,004
NULL/mismatch=0
pair + nn CHECK valid
attnotnull=true
old column + bridge present
temporary index absent
default A/B relationship intact
partition attached / 20,000 rows再运行 ch05 model verify:
relation_checksum=
f8a7bfae59c6d16cd323abecfefe1014证明专用实验没有改变 shop.* 业务基线。
Reset 与负向安全
all 保留 fixture 供人工复核。删除属于 R2:
export PG36_RESET_TOKEN=RESET_CH11_RELEASE_LAB
export PG36_RESET_TARGET=pg36_shop/shop_private/ch11
./task.sh resetreset 前要求:
- action token exact;
- database/schema/chapter target exact;
- 所有同名 relation/function marker exact;
- 没有活跃
pg36-ch11-*worker。
成功:
status=ok
reset_target=pg36_shop/shop_private/ch11
remaining_ch11_relations=0
remaining_ch11_functions=0
business checksum unchanged错误 token、错误 target 或 active worker 必须 psql exit=3 且 state 保持。
11.7.3 把发布证据与新规则追加到规约
Review summary
一次 PostgreSQL 18.4 审查:
status=ok
risk=lock:55P03/schema-unchanged/
default:metadata-vs-rewrite
wal=fast:12088/volatile:11488328
compatibility=old+new/mismatch:23514/contract:P3612
backfill=interrupt:2x5000/
resume:49999-in-10/remaining:0
constraints=not-valid->valid->not-null/
catalog:pg18-pg_constraint+pg_attribute
partition=attach:SUE+AX/
detach-concurrently:180004/
retained:20000
proposal=0.1.0->0.6.0/
SAFE-MIGR-006+DEFAULT-VERS-010/
depends-on-v0.5
final=switched/contract:not-executed/
checksum:f8a7bfae59c6d16cd323abecfefe1014WAL 数字仅为该次观测;关系和规则才是 golden。
SAFE-MIGR-006 的 v0.6 change
baseline-v0.6-proposal.json 提议把原 statement:
类型收窄、列删除、表重写或约束收紧前完成
precheck、timeout、兼容、恢复和 post-state;
contract 与 application rollback 共同设计。收紧为:
跨应用版本 DDL 必须划分:
expand / migrate / validate / switch / contract
执行前验证 target,并声明:
lock + statement timeout
compatibility window
scan/rewrite/WAL/replica-lag budget
batch and stop conditions
restart semantics
post-state
contract only after:
old readers/writers zero
real observation evidence
lost semantics:
forward repair or restore
never an empty-shell down migration这不是把所有小 DDL bureaucratize。scope 是跨 application version 或 destructive/high-impact change;低风险变更仍按实际风险分级。
DEFAULT-VERS-010 的 runtime check
statement 不改,新增 check:
every migration phase has:
stable migration identity
monotonic queryable state
interruption:
resumes from committed checkpoint
cannot skip unresolved data本章证据:
- phase 与 DDL 同事务;
- checkpoint 与 batch 同事务;
- two-batch interruption;
- exact resume;
- zero unresolved before watermark;
- final catalog/data checksum。
Proposal 依赖链
v0.6 保存:
immutable v0.1 canonical checksum
v0.5 proposal canonical checksum
rule_changes
evidence paths
promotion conditionsreview.py 每次重算 canonical JSON checksum。依赖 artifact 改动后 checksum 不匹配,v0.6 fail;不能静默引用一个同名但内容已变的 proposal。
它仍是 candidate。晋升条件:
- ch11 suite 在 PG14–18 compatibility matrix 通过;
- PG18 NOT NULL catalog 分支与 PG14–17 分支都实测;
- 至少一个真实 Pigsty HA 发布窗口保存 lock/WAL/lag/disk/tail evidence;
- application owner 证明旧 read/write 清零;
- 约定 rollback observation window 真实经过;
- v0.2–v0.5 依赖先晋升;
- 再发布不可变 baseline v0.6 artifact。
本地 PASS 不冒充这些条件已经成立。
从实验模板迁移到真实 change
替换 fixture 时,不要只改 table name。必须重新设计:
domain mapping and reversibility
old/new application versions
row ordering and partitioning
batch size and watermarks
temporary/permanent indexes
constraint identity
Pigsty service/role
replica and disk thresholds
switch/rollback mechanism
consumer inventory
contract evidence and authority保留 harness 的结构:
context guard
negative lock case
compatibility matrix
controlled interruption
catalog before/after
final checksum/invariants
safe reset for disposable rehearsals
candidate rule evidence本章最终交付
本章完成后,读者不应只会写:
ALTER TABLE ...;而应能提交一个 change package:
intent + risk classification
version compatibility matrix
state machine
SQL/migration artifacts
backfill/checkpoint code
Pigsty observation plan
stop/rollback/forward-repair decisions
raw evidence + automated review
contract gate
post-state and governance proposal这才是“安全发布”可复用的工程能力。
本节验收问题
- 全量任务能否从 clean fixture 重复运行;
- default A/B 是否保存 physical 与 WAL 关系;
- lock case 是否有 exact edge、55P03 与 unchanged catalog;
- old/new/mismatch compatibility cases 是否全部通过;
- temporary index 是否事务块外 build/drop;
- interrupted/resumed JSON 的 checkpoint 是否连续;
- constraints before/after 与 PG version branch 是否正确;
- switch 后 old rollback shape 是否仍在;
- contract 是否因缺证以 P3612 拒绝;
- partition ATTACH locks 和 DETACH boundary 是否实测;
- worker、temporary object 与 business checksum 是否闭合;
- reset 是否双 token、marker、active-worker 三重保护;
- v0.6 checksum/dependency/rule IDs 是否由 review 验证;
- candidate 是否诚实保留生产 promotion conditions。
参考资料
上一节:发布窗口中的平台观察 · 返回本章目录 · 下一章:一气呵成:从数据库契约到后端服务 · 查看全书目录 · 查看索引中心