跳至内容
11.7 实战:无中断演进订单模式

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

riskvalidate 会重建专用 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=0

shipping_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=11488328

review 只要求:

before_filenode == fast_filenode
fast_filenode != volatile_filenode
fast atthasmissing
not volatile atthasmissing
volatile WAL > fast WAL
row_count=50,000

具体 OID/WAL 随环境变化。

Expand 前先让锁预算失败

run_lock_case.py

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_order

backfill 完成并验证后,再 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=migrated

validate.sql

ALTER 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=true

SET 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.txt

raw evidence 保存动态值,review 比较稳定关系。

锁注入的 happens-before

协调器不是用 sleep 1 猜时序:

  1. interactive holder 完成 LOCK ... ACCESS SHARE
  2. holder 输出 readiness marker;
  3. 才启动 DDL waiter;
  4. observer 循环直到捕获 exact blocker edge;
  5. waiter 以 55P03 结束;
  6. 检查 column count=0;
  7. 向 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 双重闭合。

分区实验的可逆边界

partition_lab.py

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 states

elapsed 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 reset

reset 前要求:

  • 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:f8a7bfae59c6d16cd323abecfefe1014

WAL 数字仅为该次观测;关系和规则才是 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 conditions

review.py 每次重算 canonical JSON checksum。依赖 artifact 改动后 checksum 不匹配,v0.6 fail;不能静默引用一个同名但内容已变的 proposal。

它仍是 candidate。晋升条件:

  1. ch11 suite 在 PG14–18 compatibility matrix 通过;
  2. PG18 NOT NULL catalog 分支与 PG14–17 分支都实测;
  3. 至少一个真实 Pigsty HA 发布窗口保存 lock/WAL/lag/disk/tail evidence;
  4. application owner 证明旧 read/write 清零;
  5. 约定 rollback observation window 真实经过;
  6. v0.2–v0.5 依赖先晋升;
  7. 再发布不可变 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

这才是“安全发布”可复用的工程能力。

本节验收问题

  1. 全量任务能否从 clean fixture 重复运行;
  2. default A/B 是否保存 physical 与 WAL 关系;
  3. lock case 是否有 exact edge、55P03 与 unchanged catalog;
  4. old/new/mismatch compatibility cases 是否全部通过;
  5. temporary index 是否事务块外 build/drop;
  6. interrupted/resumed JSON 的 checkpoint 是否连续;
  7. constraints before/after 与 PG version branch 是否正确;
  8. switch 后 old rollback shape 是否仍在;
  9. contract 是否因缺证以 P3612 拒绝;
  10. partition ATTACH locks 和 DETACH boundary 是否实测;
  11. worker、temporary object 与 business checksum 是否闭合;
  12. reset 是否双 token、marker、active-worker 三重保护;
  13. v0.6 checksum/dependency/rule IDs 是否由 review 验证;
  14. candidate 是否诚实保留生产 promotion conditions。

参考资料


上一节:发布窗口中的平台观察 · 返回本章目录 · 下一章:一气呵成:从数据库契约到后端服务 · 查看全书目录 · 查看索引中心

最后更新于