# 实战：无中断演进订单模式

LLMS 索引： [llms.txt](/llms.txt)

---

本节把模式发布做成一条可重复、可中止、可复核的证据链：

```text
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 加字段、回填、建约束与新旧版本共存 {#item-11-7-1}

### 确认 disposable L1

准备权限受控的 service file：

```bash
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/本地目标继续。本章脚本再次验证：

```text
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。

全量入口：

```bash
cd static/labs/ch11
export PG36_EVIDENCE_DIR="$PWD/evidence/ch11/all-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all
```

可独立运行：

```text
setup | risk | expand | backfill | validate
partition | verify | review | reset | all
```

`risk` 到 `validate` 会重建专用 fixture；`verify` 只检查当前完整状态。

### Legacy fixture

[setup.sql](/labs/ch11/setup.sql) 建立 50,000 行：

```sql
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：

```text
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](/labs/ch11/default-probe.sql) 对另一张 50,000 行 disposable table 做 A/B：

```text
ADD fast_flag integer NOT NULL DEFAULT 7
ADD volatile_stamp timestamptz
    NOT NULL DEFAULT clock_timestamp()
```

一次 raw result：

```text
fast:
  before_filenode=20116
  after_filenode=20116
  atthasmissing=true
  attmissingval={7}
  WAL=12088

volatile:
  after_filenode=20129
  atthasmissing=false
  WAL=11488328
```

review 只要求：

```text
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](/labs/ch11/run_lock_case.py)：

```text
holder:
  ACCESS SHARE on ch11_order

waiter:
  ADD COLUMN shipping_code text
  requests ACCESS EXCLUSIVE
  lock_timeout=4s

observer:
  one pg_blocking_pids edge
```

结果：

```text
lock=55P03
requested=AccessExclusiveLock
blocker_edges=1
shipping_code remains absent
holder released by COMMIT
```

只有这条 negative path 通过，才执行 [expand.sql](/labs/ch11/expand.sql)：

```text
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](/labs/ch11/compatibility.sql) 运行四条合同：

```text
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
```

此时：

```text
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](/labs/ch11/online-index.sh) 在 transaction block 外：

```sql
CREATE INDEX CONCURRENTLY
    ch11_order_shipping_missing_idx
ON shop_private.ch11_order (order_id)
WHERE shipping_code IS NULL;
```

build 后验证：

```text
indisready=true
indisvalid=true
indpred=(shipping_code IS NULL)
indrelid=exact ch11_order
```

backfill 完成并验证后，再 exact：

```sql
DROP INDEX CONCURRENTLY
    shop_private.ch11_order_shipping_missing_idx;
```

最终 verify 要求该 migration-only index 不存在。

### Backfill 与约束收紧

第一次：

```text
batch size=5,000
max batches=2
exit=75
rows migrated=10,000
remaining=39,999
phase=backfilling
```

续跑：

```text
eight more batches
total migrated=49,999
total batches=10
last_order_id=50,000
remaining=0
mismatches=0
phase=migrated
```

[validate.sql](/labs/ch11/validate.sql)：

```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.6 目录：

```text
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](/labs/ch11/switch.sql) 先做 full shadow comparison，再分别运行一个 old/new writer：

```text
shadow mismatches=0
old writer after switch succeeds
new writer remains dual-write
phase=switched
shipping_method retained
bridge retained
```

最终总行数：

```text
50,000 baseline
+2 coexistence
+2 switch probes
=50,004
```

读取可以切到 `shipping_code`，但数据库继续支持旧 application rollback。

## 11.7.2 注入锁等待和回填中断，验证中止与恢复 {#item-11-7-2}

### Evidence 不是最终一句 PASS

一次全量目录：

```text
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 内：

```text
target updates
checkpoint updates
phase updates
```

同事务，因此 disconnect/error 时一起 rollback。

审查器比较：

```text
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 是故意失败的验收项

全量流程运行：

```bash
psql --file=contract-gate.sql
```

但不提供 token/target/observation。期望：

```text
exit=3
SQLSTATE=P3612
message=contract refused
```

随后 final verify 要求：

```text
release=switched/contract:not-executed
compatibility=legacy-column+bridge-retained
```

如果 contract 意外成功，`verify.sql` 会因 old column/bridge 缺失而失败。负向 gate 与正向 post-state 双重闭合。

### 分区实验的可逆边界

[partition_lab.py](/labs/ch11/partition_lab.py)：

```text
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
```

一次结果：

```text
server_version_num=180006
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](/labs/ch11/verify.sql) 要求：

```text
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：

```text
relation_checksum=
  f8a7bfae59c6d16cd323abecfefe1014
```

证明专用实验没有改变 `shop.*` 业务基线。

### Reset 与负向安全

`all` 保留 fixture 供人工复核。删除属于 R2：

```bash
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。

成功：

```text
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 把发布证据与新规则追加到规约 {#item-11-7-3}

### Review summary

一次 PostgreSQL 18.6 审查：

```text
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:180006/
          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](/labs/ch11/baseline-v0.6-proposal.json) 提议把原 statement：

```text
类型收窄、列删除、表重写或约束收紧前完成
precheck、timeout、兼容、恢复和 post-state；
contract 与 application rollback 共同设计。
```

收紧为：

```text
跨应用版本 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：

```text
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 保存：

```text
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。必须重新设计：

```text
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 的结构：

```text
context guard
negative lock case
compatibility matrix
controlled interruption
catalog before/after
final checksum/invariants
safe reset for disposable rehearsals
candidate rule evidence
```

### 本章最终交付

本章完成后，读者不应只会写：

```sql
ALTER TABLE ...;
```

而应能提交一个 change package：

```text
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。

## 参考资料

- [实验合同](/labs/ch11/lab-contract.md)
- [PostgreSQL 18：ALTER TABLE](https://www.postgresql.org/docs/18/sql-altertable.html)
- [PostgreSQL 18：Table Partitioning](https://www.postgresql.org/docs/18/ddl-partitioning.html)
- [Pigsty：Service](https://pigsty.io/docs/pgsql/service/)
- [Pigsty：Dashboard](https://pigsty.io/docs/pgsql/dashboard/)

---

[上一节：发布窗口中的平台观察](../06/) · [返回本章目录](../) · [下一章：一气呵成：从数据库契约到后端服务](/database-to-service/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
