Files
cma-management/docs/db-migration-runbook.md
wecom-fullstack b58513c48d
ci/woodpecker/push/main Pipeline was successful
docs(ddl): runbook 首步强制「迁移前像 dump 哈希」(--apply 之前)
CMA-DDL-RUNBOOK-20261005 / parent: CMA-KPI-20261003-D-T1
- docs/db-migration-runbook.md 新增 §3.0:任何 --apply 之前对每张受影响表落
  全表有序 (PK, 受影响列) dump,记录 sha256 + 行数 + 字节数,归档 backups/pre-ddl-<task>/
- 含可直接复制的一行命令(ORDER BY 主键保证字节级确定性)
- §4 发布 checklist 第 4 条(列级 ALTER)指向 §3.0

仅文档;零生产库/零部署/零重启。
2026-10-05 13:02:02 +08:00

102 lines
6.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CMA 数据库结构变更运行手册(DDL 与应用启动剥离)
- 任务:`cma-create-all-minpriv-rootfix-20260916`(P2)
- 事故档案:`wecom-project/_verification/2026-09-16-cma-release-incident-verification.md`
- 结论一句话:**DDL 只在部署链的 [4b] 特权迁移步发生,应用启动不再建表;运行库账号 `cma_api` 无需 CREATE。**
## 1. 三条不变式
| # | 不变式 | 落地位置 |
|---|---|---|
| 1 | 应用启动**不**执行 DDL(`create_all` 默认关,`CMA_AUTO_CREATE=1` 仅限开发/测试) | `backend/app/database.py::auto_create_enabled/init_db` |
| 2 | 结构变更由**有 DDL 权限的账号**在**应用重启前**执行(独立、幂等、可回滚、留痕) | `backend/scripts/db_migrate.py` + `deploy.sh [4b]` |
| 3 | 运行库账号保持最小权限(DML 表级白名单,**无** CREATE/ALTER/DROP) | DB 授权侧(由 ops/项目Bot 回收,见 §5) |
## 2. 部署链顺序(`deploy.sh`)
```
[0] 提交纪律检查(忽略未跟踪) ← 勿回退
[1] git pull origin main
[1b] 鉴权面审计闸门(B 类缺口即中止) ← 勿回退
[2] 前端构建 pnpm build
[3] 前端重铺 /var/www/cma
[4] 后端依赖
[4b] 数据库结构迁移(特权;重启前;幂等;失败即中止部署) ← 本单新增
[5] 重启 cma-backend + reload nginx
冒烟:就绪轮询 → BOT桥接 → schema_check → 登录/KPI
```
`[4b]` 失败时**不重启**后端 → 旧版本继续服务(不再出现「新模型建不出 + 服务被重启」的崩溃循环)。
## 3. 迁移步用法(`backend/scripts/db_migrate.py`)
### 3.0 强制前置步 —— 任何 `--apply` **之前**:迁移前像 dump 哈希
> 来源:`CMA-KPI-20261003-D-T1` 只读回读复核(2026-10-05)。落地前两侧(设计 10:11 / 执行 10:38)**只留了行数/计数基线**,
> 未留全表 `(PK, 受影响列)` 有序 dump 哈希 ⇒ **迁移前像不可得**,事后只能用「四时点基线谓词对拍 + 值域指纹 + 时间戳 +
> `MODIFY` 类型保持」四路**替代取证**。本步是事后自证「**存量取值零改写**」的**唯一直接依据**,不可省、不可推迟到 `--apply` 之后。
对本任务**每一张受影响表**,在 `--apply` **之前**落一次全表有序 `(PK, 受影响列)` dump,记录 `sha256 + 行数 + 字节数`;
产物归档 `backups/pre-ddl-<task>/`(与 `backups/pre-ddl-entity-dim-20260923/` 同约定),随任务件留痕。
可直接复制(把 `DB / TBL / COLS` 换成任务值;`COLS` **首位必须是主键**):
```bash
DB=cma TBL=treasury_limits COLS='id,status,limit_amount' D="backups/pre-ddl-$(date +%Y%m%d-%H%M%S)" sh -c 'mkdir -p "$D"; mysql -N -B --default-character-set=utf8mb4 "$DB" -e "SELECT $COLS FROM $TBL ORDER BY ${COLS%%,*}" > "$D/$TBL.before.tsv"; sha256sum "$D/$TBL.before.tsv"; wc -lc < "$D/$TBL.before.tsv"'
```
- `ORDER BY ${COLS%%,*}`(即主键)保证字节级确定性:同一份数据两跑,`sha256` 必须一致。
- 行数与 `SELECT COUNT(*)` 对账:`wc -l` 即行数(`-N` 无表头;末行缺换行符时 ±1 属正常)。
- 多表:每张受影响表各跑一次,逐表记录「表名 + sha256 + 行数 + 字节数」。
- 现成实现可复用 ops 探针 `wecom-ops/scripts/probe_cma_kpi_t1_readback_20261005.py --check`(自动落 `02b-current-id-tv.json` 全量行值基线)。
- 本步只用**只读**语句(`SELECT` + `sha256sum`),零生产写;`--apply` 与回滚命令见下。
```bash
cd /root/cma-management/backend
./venv/bin/python3 scripts/db_migrate.py --check # 只读预检(默认动作)
./venv/bin/python3 scripts/db_migrate.py --apply # 幂等建表(缺什么建什么)+ journal
./venv/bin/python3 scripts/db_migrate.py --status # 历史 journal
./venv/bin/python3 scripts/db_migrate.py --rollback latest # 回滚最近一次新建的表(先 dump 后删)
```
- **连接**:优先 `CMA_MIGRATE_DB_USER/CMA_MIGRATE_DB_PASS`(专用迁移账号,TCP);未设则走本机
`unix socket + root`(socket 认证,无口令落盘)。**显式拒绝**用应用运行账号(`cma_api`)执行本步。
- **幂等**:`create_all(checkfirst=True)` —— 已存在的表零动作;二跑 `created_new=0`。
- **journal**:`backend/scripts/backups/db-migrate-journal/<ts>-<status>.json`(含 db/git head/执行者/新建表清单)。
- **回滚**:只删该次 journal 记录为「新建」的表;表内有数据则**拒绝**(exit 3),`--force` 才放行;回滚前默认
`mysqldump` 备份(dump 失败即拒绝回滚,fail-closed)。
- **列级差异**:`create_all` **不**补列。`--check/--apply` 会报告「ORM 有列、库里没有」并提示需要独立
`ALTER` 迁移;部署链以此告警(不阻断)。
## 4. 新增模型的正确姿势(发布 checklist)
1. 在 `backend/app/models/` 加模型并确保被 `app/models/__init__.py` 导入。
2. 本地 `--check` 应能列出该表缺失(证明 ORM 已注册)。
3. 提交推送 → 部署链 `[4b]` 用特权账号建表 → 重启 → 冒烟 `schema_check` 通过。
4. 若模型**改列**(新增/改名/改类型):`create_all` 不处理 → 必须写独立迁移脚本/`ALTER`(参考
`backend/scripts/migrate_ext_input_ledgers_20260915.py`、`treasury_migrate.py` 的 `up()/down()` 模式);
**执行任何 `ALTER` 前先走 §3.0 迁移前像(全表有序 dump 哈希)**。
5. **不要**让运行库账号重新获得 CREATE 来「绕过」:那正是事故根因。
## 5. `cma_api` 的 CREATE 授权回收(项目Bot 执行)
2026-09-16 事故应急时授予了 `GRANT CREATE ON cma.* TO 'cma_api'@'localhost'/'127.0.0.1'`(保留至今)。
本单上线后(`[4b]` 已生效、应用启动不再建表)可安全回收:
```sql
REVOKE CREATE ON cma.* FROM 'cma_api'@'localhost';
REVOKE CREATE ON cma.* FROM 'cma_api'@'127.0.0.1';
SHOW GRANTS FOR 'cma_api'@'127.0.0.1'; -- 期望:仅 SELECT + 表级 DML,无 CREATE
```
回收后再发布一次(含任意新模型的版本)验证不中断即可闭环(证据:`[4b]` journal `created_new>=0` +
冒烟通过 + 无 `CREATE command denied`)。
## 6. 自验(可重复)
```bash
bash backend/scripts/tests/db-migrate-sim-e2e.sh # 需 root;scratch 库 cma_migrate_sim,零生产触碰
```
覆盖 17 项断言:负控制复现(`CREATE command denied` → 启动失败)、迁移幂等(76→0)、修复后启动零 DDL、
「新增模型未迁移」不中断 + 迁移补齐、回滚(含 dump 与有数据拒绝)。