pipeline-app/deploy/METHODOLOGY.md

205 lines
11 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.

# 测试 → 生产 增量部署规范
> 原则:生产上线永远是增量,没有全量。每次上线 = 一批「增量单元」(代码 + 数据库变更 + 配置),
> 每个单元必须可幂等重放、可验证、可回退。本规范固化四步:备份 → 操作 → 验证 → 故障回退。
## 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/<db>_<table>_<日期时间>.sql同目录生成 RESTORE 命令文件
```
检查点:
- [ ] 备份文件存在且非空(`ls -lh backups/`
- [ ] 备份文件头部有 `CREATE TABLE` 且行数与线上一致(`dbackup.py verify <file>`
### 第 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/<module> && git pull --ff-only origin main
cd /d/doit/pipeline-app && ./py3/bin/pip install pkgs/<module>
find py3/lib/python3.10/site-packages/<module_pkg> -name __pycache__ -exec rm -rf {} +
# 2.4 最后重启(生产四进程:逐 pidfile kill 再 start见 pipeline-prod-operations 技能;
# 禁止 pkill -f "pipeline-app" ——会误杀自己的远程 shell
nohup bash restart-pipeline.sh </dev/null > 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/<module> && git log --oneline -5 # 找到上线前的提交
git checkout <上线前提交> && cd ../.. && ./py3/bin/pip install pkgs/<module> && 重启
```
回退后重新走第 3 步验证,确认系统回到上线前状态。
## 4. 台账表(自动创建)
```sql
CREATE TABLE IF NOT EXISTS pipeline_deploy_ledger (
id VARCHAR(32) NOT NULL,
batch VARCHAR(16) NOT NULL, -- 迁移 idm0001
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.py2026-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 张表在生产不存在时
前置检查会正确中止)。