Files
cma-management/docs/db-migration-runbook.md
T
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

6.5 KiB
Raw Blame History

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 首位必须是主键):

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 与回滚命令见下。
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] 已生效、应用启动不再建表)可安全回收:

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 backend/scripts/tests/db-migrate-sim-e2e.sh   # 需 root;scratch 库 cma_migrate_sim,零生产触碰

覆盖 17 项断言:负控制复现(CREATE command denied → 启动失败)、迁移幂等(76→0)、修复后启动零 DDL、 「新增模型未迁移」不中断 + 迁移补齐、回滚(含 dump 与有数据拒绝)。