pipeline-app/deploy/METHODOLOGY.md

11 KiB
Raw Blame History

测试 → 生产 增量部署规范

原则:生产上线永远是增量,没有全量。每次上线 = 一批「增量单元」(代码 + 数据库变更 + 配置), 每个单元必须可幂等重放、可验证、可回退。本规范固化四步:备份 → 操作 → 验证 → 故障回退。

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.pyparams_set/env 类型,或记录在迁移说明里人工核对

铁律

  1. 测试环境执行过的库变更,必须在提交测试代码的同时登记成迁移文件——没有登记就没有测试通过
  2. 生产执行迁移前必须先跑 ddiff.py,拿差异清单与迁移批次对照,清单里没有对应迁移的差异 = 遗漏
  3. 破坏性操作DROP/DELETE/TRUNCATE永远生成命令由人工在生产执行自动化只做到「打印 [NEEDS_APPROVAL]」。

2. 迁移单元格式migrations/mNNNN_名称.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 EXISTSALTER TABLE ... ADD COLUMN IF NOT EXISTSMySQL 8 用先查 information_schema 的方式dmig 会自动做存在性检查)、INSERT ... ON DUPLICATE KEY UPDATE
  • downup 的精确逆操作;无逆操作的步骤(如纯数据修复)down 写空数组并在 title 注明「不可逆」。
  • backup_tables 列出本次会改动的表——dmig.py apply 前自动备份这些表(生产备份由人工确认后执行)。

3. 四步流程

第 1 步:数据备份(上线前必做,不可跳)

# 生产机执行。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 生产实录)。

# 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 步:验证(不验证 = 没上线)

./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 步:故障回退(任一步失败时)

判断:回退的是「这次增量」,不是整个系统。定位到失败的批次号。

# 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. 台账表(自动创建)

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 张表在生产不存在时 前置检查会正确中止)。