# 测试 → 生产 增量部署规范 > 原则:生产上线永远是增量,没有全量。每次上线 = 一批「增量单元」(代码 + 数据库变更 + 配置), > 每个单元必须可幂等重放、可验证、可回退。本规范固化四步:备份 → 操作 → 验证 → 故障回退。 ## 0. 目录约定(在 pipeline-app 仓库) ``` deploy/ ├── METHODOLOGY.md # 本文档 ├── dmig.py # 迁移引擎:台账表 + up/down + 幂等 ├── ddiff.py # 环境差异比对:代码模块版本 / schema / 参数 ├── dbackup.py # 增量表备份(mysqldump)+ 恢复命令生成 └── migrations/ ├── m0001_xxx.json # 迁移单元(编号递增,字典序 = 执行序) └── ... ``` ## 1. 增量单元(一次上线携带什么) 每次从测试推向生产,变更分成三类,各走各的通道,**不允许混**: | 类型 | 载体 | 通道 | |---|---|---| | 代码 | 各模块仓库提交 | 生产机 `git pull --ff-only` + `pip install`(非 editable 模块)+ 清 `__pycache__` + 重启 | | 数据库变更 | `deploy/migrations/mNNNN_*.json` | `dmig.py`(测试先跑 → 生产复核后执行) | | 系统配置 | `params` 表 / 环境变量 | 走 `dmig.py` 的 `params_set`/`env` 类型,或记录在迁移说明里人工核对 | **铁律**: 1. 测试环境执行过的库变更,必须在提交测试代码的同时登记成迁移文件——**没有登记就没有测试通过**。 2. 生产执行迁移前必须先跑 `ddiff.py`,拿差异清单与迁移批次对照,**清单里没有对应迁移的差异 = 遗漏**。 3. 破坏性操作(DROP/DELETE/TRUNCATE)永远生成命令由人工在生产执行,自动化只做到「打印 [NEEDS_APPROVAL]」。 ## 2. 迁移单元格式(migrations/mNNNN_名称.json) ```json { "id": "m0001", "title": "说明(一句话:这次改了什么、为什么)", "backup_tables": ["sd_projects"], "up": [ {"op": "sql", "sql": "ALTER TABLE ..."}, {"op": "create_table", "name": "xxx", "columns": "...完整 CREATE TABLE 语句..."}, {"op": "params_set", "key": "tender_api_base", "value": "http://..."} ], "down": [ {"op": "sql", "sql": "DROP TABLE xxx"}, {"op": "params_del", "key": "tender_api_base"} ] } ``` 规则: - `id` 唯一且递增;执行顺序 = 文件名字典序。 - `up` 的每一步都必须**幂等**:`CREATE TABLE IF NOT EXISTS`、`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`(MySQL 8 用先查 information_schema 的方式,dmig 会自动做存在性检查)、`INSERT ... ON DUPLICATE KEY UPDATE`。 - `down` 是 `up` 的精确逆操作;无逆操作的步骤(如纯数据修复)`down` 写空数组并在 title 注明「不可逆」。 - `backup_tables` 列出本次会改动的表——`dmig.py apply` 前自动备份这些表(生产备份由人工确认后执行)。 ## 3. 四步流程 ### 第 1 步:数据备份(上线前必做,不可跳) ```bash # 生产机执行。dmig 会自动列出本次批次涉及的表;也可手工指定。 cd /d/doit/pipeline-app ./py3/bin/python deploy/dbackup.py backup --batch # 备份本批次所有 backup_tables ./py3/bin/python deploy/dbackup.py backup --tables sd_projects,pipeline_tasks # 手工补充 # 备份文件:/d/doit/pipeline-app/backups/__<日期时间>.sql,同目录生成 RESTORE 命令文件 ``` 检查点: - [ ] 备份文件存在且非空(`ls -lh backups/`) - [ ] 备份文件头部有 `CREATE TABLE` 且行数与线上一致(`dbackup.py verify `) ### 第 2 步:操作(按序执行增量) **顺序铁律:先库后码再重启**。新代码可能引用新列/新表,代码先上会在重启后 `Unknown column` 崩溃(2026-09-01 生产实录)。 ```bash # 2.1 差异核对(必做):打印生产与测试的差异清单,与本次批次对照 ./py3/bin/python deploy/ddiff.py --checklist # 2.2 数据库增量先行(迁移引擎) ./py3/bin/python deploy/dmig.py status # 看台账:哪些已执行、哪些待执行 ./py3/bin/python deploy/dmig.py plan # 预演:打印本批次将执行的 SQL(不落库) ./py3/bin/python deploy/dmig.py apply # 执行(自动先备份本批次表,逐条落库,逐条记账) # 2.3 代码增量(库就绪后才上代码) cd /d/doit/pipeline-app/pkgs/ && git pull --ff-only origin main cd /d/doit/pipeline-app && ./py3/bin/pip install pkgs/ find py3/lib/python3.10/site-packages/ -name __pycache__ -exec rm -rf {} + # 2.4 最后重启(生产四进程:逐 pidfile kill 再 start,见 pipeline-prod-operations 技能; # 禁止 pkill -f "pipeline-app" ——会误杀自己的远程 shell) nohup bash restart-pipeline.sh logs/restart.log 2>&1 & sleep 15; curl -s -o /dev/null -w "health:%{http_code}\n" http://127.0.0.1:9090/ ``` `dmig.py apply` 的行为: - 每条语句执行前查存在性(表/列/索引已有 → 跳过,保证幂等); - 执行成功才写台账 `pipeline_deploy_ledger`(批次、步骤、状态、时间); - 任一步失败 → 停止后续步骤,打印已完成步骤清单和对应 `down` 操作(供回退),**不自动回滚**。 ### 第 3 步:验证(不验证 = 没上线) ```bash ./py3/bin/python deploy/ddiff.py --checklist # 差异清零(或只剩本批次明确豁免项) ./py3/bin/python deploy/dmig.py status # 台账:本批次全部 applied curl -s -o /dev/null -w "health:%{http_code}\n" http://127.0.0.1:9090/ # 服务 200 grep -a "poller started" logs/pipeline.log | tail -5 # 各 poller 正常 # + 功能验收:真实浏览器走一遍本次变更影响的功能(铁律:功能验收必须真浏览器执行) ``` 检查点: - [ ] ddiff 差异清零 - [ ] dmig status 全 applied、无 failed - [ ] 服务健康 + poller 正常 - [ ] 功能浏览器实测通过(记录证据:页面/请求/数据三选一) ### 第 4 步:故障回退(任一步失败时) **判断**:回退的是「这次增量」,不是整个系统。定位到失败的批次号。 ```bash # 4.1 库回退:用迁移自带的 down ./py3/bin/python deploy/dmig.py rollback m0003 # 执行 m0003 的 down + 台账置 rolled_back # 4.2 数据回退:down 不足以恢复时,用第 1 步的备份 ./py3/bin/python deploy/dbackup.py restore --file backups/pipeline_sd_projects_20260901_120000.sql --confirm # 注意:恢复会先 DROP 再导入该表——执行前人工确认没有比备份点更新的写入(或先导出增量) # 4.3 代码回退 cd /d/doit/pipeline-app/pkgs/ && git log --oneline -5 # 找到上线前的提交 git checkout <上线前提交> && cd ../.. && ./py3/bin/pip install pkgs/ && 重启 ``` 回退后重新走第 3 步验证,确认系统回到上线前状态。 ## 4. 台账表(自动创建) ```sql CREATE TABLE IF NOT EXISTS pipeline_deploy_ledger ( id VARCHAR(32) NOT NULL, batch VARCHAR(16) NOT NULL, -- 迁移 id:m0001 step INT NOT NULL, -- up 内步骤序号 status VARCHAR(16) NOT NULL, -- applied / rolled_back / skipped backup_file VARCHAR(255), detail VARCHAR(4000), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_ledger_batch_step (batch, step) ); ``` ## 5. 测试环境的角色 测试环境是**迁移的演练场**:每个迁移文件必须在测试环境 `dmig.py apply` 跑通(含幂等重跑验证) 才允许进入生产批次。测试环境的台账独立(同库不同环境实例),生产台账只反映生产执行记录。 ## 6. 常见坑(历史教训) 1. **改库没登记** → 生产是老库。8/31-9/1 连续发现:`pipeline_agent_instances` 缺表、 `sd_projects.directory_name` 缺列、`pipeline_deliverables.created_by` 还是 32 宽。 根治就是本规范:改库必须落成迁移文件。 2. **只在测试跑 DDL 口头说"生产一样"** → 必然漂移。用 `ddiff.py` 强制对账。 3. **回退没有 down** → 不可逆变更(DROP 列、删数据)必须有备份兜底,备份在操作前。 4. **破坏性语句进自动化** → 永远 `[NEEDS_APPROVAL]`,人工执行。 ## 7. 配置数据同步(dsync.py,2026-09-09 用户定夺:数据一条通道) **职责边界**:dmig 只管表结构(DDL)与环境参数;业务配置数据(模型网关/定价/产品/ 折扣/供应商/记账配置)**不走 git 迁移**,一律由 `dsync.py` 按域同步(m0016 种子迁移已撤销—— 同一份数据两条通道毫无意义)。事务数据(usage/balance/detail/bill/subscription)永远不同步。 **域定义** `deploy/dsync_domains.json`:每域 = 表清单 + 机构过滤列 + order 拓扑序; `gate` 段 = 全库悬空引用门禁 SQL(模型→供应商/账号/模板/ppid、供应商→机构注册+记账开户、 产品→分类/资源实体、折扣→产品……返回行 = 失败)。加新域 = 加一段配置 + 补对应门禁。 **流程**: ``` 测试机: ./py3/bin/python deploy/dsync.py export -o /tmp/pkg.json # 全部域 传输: ssh 管道 cat 直传(包内含 llm_account.api_key 密文——禁入 git,禁落中间盘) 生产机: ./py3/bin/python deploy/dsync.py import pkg.json --dry-run # 预演 生产机: ./py3/bin/python deploy/dsync.py import pkg.json # 正式 生产机: ./py3/bin/python deploy/dsync.py gate # 日常体检(只读) ``` **安全语义**: - 只增改不删:按主键 upsert,目标多出的行保留(生产专属数据绝不误删) - 机构过滤:只带 org ∈ {'0','*',''} ∪ 目标环境 organization 实有机构(测试 test_* 机构数据跳过) - 密钥直迁:两环境 password_key sha256 一致(已核对),api_key 密文原样搬运 - 前置检查:目标缺表直接中止(提示先跑 create_tables.py) - 导入前强制 dbackup 备份涉及表;全部导入 + 24 条门禁在**单事务**内,任一悬空 → ROLLBACK(绝不落地半份数据);全绿才 COMMIT - 门禁失败 exit 2 / 一般失败 exit 1 / 成功 exit 0 **实测记录(2026-09-09 测试机自导自入 + 负向验证)**:241 行全 updated 幂等; 篡改 vendor_id 为悬空值 → 门禁精准拦截「模型→供应商 1 行悬空」→ ROLLBACK → 复跑 gate 全绿(零污染)。踩坑:information_schema 主键约束名是 `PRIMARY` 不是 `PRIMARY KEY` (查空 → upsert 全走 INSERT 撞 1062,首版即崩,测试机负向验证抓到)。 **生产导入时机**:必须晚于 create_tables.py 建表(llm_* 等 13 张表在生产不存在时 前置检查会正确中止)。