- Dockerfile 腾讯源 + BuildKit 缓存(backend pip / frontend npm ci),gitea 锁 1.27.1 - .dockerignore 纳入版本管理(防密钥进镜像) - 新增 migrate_prod.sh:24 个迁移分 4 批执行,含镜像新鲜度 + DB 起点校验 - cb07d6b1df01 移除 search_tsv 回填(延迟到 g0h1i2j3k4l5 统一全量回填) - 模型 journal_iso/volume/issue/pages → Text(对应迁移 95c18ebf31e4 / 55105f0bb1d7) - 搜索优化:tsvector 主路径 + COUNT 截断(10000) + pub_date 排序,admin 聚合单查询 - statement_timeout 30s→60s
159 lines
10 KiB
Markdown
159 lines
10 KiB
Markdown
# 17. 生产数据迁移实施方案
|
||
|
||
> **日期:** 2026-08-09 **版本:** v1.0
|
||
> **关联:** [16-部署运维方案.md](16-部署运维方案.md)(deploy.sh 部署流程)、[10-生产部署文档.md](10-生产部署文档.md)(操作手册)
|
||
> **状态:** 待执行——生产 DB 落后本地 24 个迁移,本方案拆分 4 批,大数据量批次延后手工执行
|
||
|
||
---
|
||
|
||
## 1. 背景与现状
|
||
|
||
生产服务器(123.207.9.209)的数据库 schema 停留在 **2026-07-17**(alembic head = `6b662a8c5235`),而本地开发代码已演进到 **2026-07-30**(head = `55105f0bb1d7`),**中间有 24 个迁移待执行**。
|
||
|
||
关键数据规模(影响迁移耗时与锁表风险):
|
||
- `global_literature`:**123 万行**
|
||
- `global_literature_tags`:**348 万行**
|
||
- `global_tags`:2.1 万行
|
||
|
||
**⚠️ 核心约束:** 24 个迁移形成**线性链**(01→24 顺序依赖),无法跳过任何一个直接跑到 head。所以"分开做"= 把链切成 4 段,每段执行到中间 checkpoint,**大数据量/锁表操作集中在后段**,由人工控制执行时机。
|
||
|
||
---
|
||
|
||
## 2. 迁移链全景(24 个迁移,生产 head → 本地 head)
|
||
|
||
| # | Revision | 内容 | 数据量/风险 |
|
||
|---|---|---|---|
|
||
| 01 | `cb07d6b1df01` | 19 列 JSON→JSONB(全表重写)+ 3 新列 + entry_terms + search_tsv 触发器重建 + 8 索引(B-tree×4 + GIN×4) | 🔴 **重:全表重写** |
|
||
| 02 | `6492b887b779` | `meshed_date` 列 + UPDATE 回填(`date_completed` 非空行) | 🟠 中:全表回填 |
|
||
| 03 | `1421ea169bb6` | 5 个日期字段 TIMESTAMPTZ→DATE | 🔴 **重:锁表** |
|
||
| 04 | `f3b135d62407` | pipeline_runs.processed_date | 🟢 轻 |
|
||
| 05 | `0ee585329fc6` | pipeline_runs.metadata | 🟢 轻 |
|
||
| 06 | `db1cc822f2da` | global_journals.nlm_subsets | 🟢 轻 |
|
||
| 07 | `34bc08516f3a` | global_literature.is_preprint | 🟢 轻 |
|
||
| 08 | `d641e2f7a4ee` | auid_data / cois_statement / vernacular 相关 | 🟢 轻 |
|
||
| 09 | `52ec204acfd9` | pharmacological_actions JSONB | 🟢 轻 |
|
||
| 10 | `38bb4e1f8498` | investigators / personal_name | 🟢 轻 |
|
||
| 11 | `cac545862583` | 新建 user_saved_filters 表 | 🟢 轻 |
|
||
| 12 | `d8f6c3563587` | 筛选列 B-tree×4 + mesh_headings GIN | 🟠 中:大表建索引 |
|
||
| 13 | `a4b7c8d9e0f1` | journal_iso trgm GIN | 🟠 中:大表建索引 |
|
||
| 14 | `e5f6a7b8c9d0` | search_tsv 触发器函数(作者 simple 词典) | 🟢 轻 |
|
||
| 15 | `d9e7c8b1a2f3` | authors trgm GIN | 🟠 中:大表建索引 |
|
||
| 16 | `e0f1a2b3c4d5` | search_tsv 触发器重建(+chemical/gene,无回填) | 🟢 轻 |
|
||
| 17 | `f1a2b3c4d5e6` | author_names_text 列 + UPDATE 回填 + trgm 索引 | 🔴 **重:全表回填** |
|
||
| 18 | `g0h1i2j3k4l5` | search_tsv 全量回填(+mesh/keywords)+ 触发器 | 🔴 **重:全表回填** |
|
||
| 19 | `e0764f6d7c21` | tag_ids ARRAY + BRIN×2 + 部分索引×4 + tag_ids GIN | 🟠 中:大表建索引 |
|
||
| 20 | `b01b8f27c596` | **tag_ids 回填**(348 万行 array_agg 聚合) | 🔴 **重:大数据量回填** |
|
||
| 21 | `af4a8b2ec873` | search_tsv 全量回填(修复 mesh_headings key)+ 触发器 | 🔴 **重:全表回填** |
|
||
| 22 | `d166cde6083b` | volume/issue VARCHAR(50)→VARCHAR(200) | 🟢 轻 |
|
||
| 23 | `95c18ebf31e4` | volume/issue VARCHAR(200)→Text | 🔴 **重:锁表** |
|
||
| 24 | `55105f0bb1d7` | journal_iso/pages VARCHAR(100)→Text | 🔴 **重:锁表** |
|
||
|
||
---
|
||
|
||
## 3. 分段原则
|
||
|
||
1. **链是线性的**(01→24),不能跳段;分段 = 每批执行到中间 checkpoint,不改变迁移内部顺序。
|
||
2. **大数据量放后边**:🔴 重活集中在批次 1(链头,无法避免,见下)和**批次 4**(全部延后)。
|
||
3. **新代码依赖前置 schema**:批次 1 的 JSON→JSONB / 日期类型变更,本地模型代码已经按 JSONB/Date 定义——**在批次 1 完成前,新代码无法运行**。这是必须最先执行、无法延后的部分。
|
||
4. **批次 4 = 最重**:含 348 万行 tag_ids 回填 + 3 次全量 search_tsv 回填 + 2 次锁表类型变更,**全部延后到深夜低峰手工跑**。
|
||
|
||
---
|
||
|
||
## 4. 批次划分与目标 Checkpoint
|
||
|
||
| 批次 | 覆盖迁移 | 目标 checkpoint | 脚本参数 | 内容摘要 | 建议时段 |
|
||
|---|---|---|---|---|---|
|
||
| 1 | [01]–[03] | `1421ea169bb6` | `./migrate_prod.sh 1` | JSON→JSONB 全表重写 + meshed_date 回填 + 日期锁表 | ⚠️ **必须与发新代码同窗口**(含 destructive,见下) |
|
||
| 2 | [04]–[11] | `cac545862583` | `./migrate_prod.sh 2` | 纯加列/新表,additive | 可提前,任意时段 |
|
||
| 3 | [12]–[16] | `e0f1a2b3c4d5` | `./migrate_prod.sh 3` | 索引(大表建)+ 触发器重建,additive | 可提前,建议低峰 |
|
||
| 4 | [17]–[24] | `head`(`55105f0bb1d7`) | `./migrate_prod.sh 4` | **大数据量回填 + 类型锁表(含 destructive)** | **新代码已部署后,深夜低峰** |
|
||
|
||
> **⚠️ destructive 窗口纪律(2026-08-10 修正,防止批次 1 单独跑崩老代码):**
|
||
> - **批次 1 含 `1421ea169bb6`(5 日期字段 TIMESTAMPTZ→DATE)——类型收窄,属 destructive**。老代码把 `date_completed`/`pubmed_revised` 等读成 datetime,迁移后 DB 返回 `date`,`datetime` 专属调用会崩。**因此批次 1 绝不能脱离代码更新单独跑**——必须和 build 新镜像 + 发新代码同一维护窗口(迁移完几秒内新镜像接管),或新代码先发再跑批次 1(但新代码依赖 JSONB,见 §3 约束,建议同窗口)。
|
||
> - **批次 2/3 纯 additive**(加列/新表/索引/触发器重建),老代码完全兼容,**可在部署前任意时段提前跑**,缩小维护窗口。
|
||
> - **批次 4 含 `95c18ebf31e4`/`55105f0bb1d7`(VARCHAR→Text)**——类型加宽、老代码兼容,但按 destructive 判定仍算;**必须在新代码已部署后、深夜低峰跑**(大数据量回填 + 锁表)。
|
||
> - 推荐执行序:**批次 2/3(提前)→ 维护窗口:build 新镜像 → 批次 1 → 发新 backend/worker/frontend → 深夜:批次 4**。批次 1 与发代码之间的窗口必须控制在分钟级内。
|
||
|
||
---
|
||
|
||
## 5. 前置条件(执行迁移前必须完成)
|
||
|
||
以下由部署流程(docs/16 §2 deploy.sh)或人工准备:
|
||
|
||
1. **新 backend 镜像已构建**(必须包含 24 个新迁移文件)。生产镜像当前是 07-17 旧版,**用旧镜像跑迁移会报 `Can't locate revision '6b662a8c5235'`**(2026-08-09 事故根因)。验证方式:
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml build backend
|
||
```
|
||
> **构建加速已落地(2026-08-10):** backend Dockerfile 已加腾讯 pip/apt 源 + BuildKit 缓存,frontend Dockerfile.prod 已加腾讯 npm 源 + `npm ci`。**首次 build 约几分钟**(pip 全量从腾讯源装),**之后代码级 build 秒~1 分钟**(只 `COPY . .`)。构建前提:宿主机 daemon 已配腾讯 registry-mirrors(✅ 已配)、BuildKit 需 Docker 22.06+/23.05+(✅ 29.6.1)。
|
||
2. **⚠️ 批次 1 必须与发新代码同窗口**(见 §4 destructive 纪律):批次 1 的日期收窄迁移会让老代码读崩,**不能提前单独跑**。批次 2/3 可提前 additive 跑。
|
||
2. **数据库快照**(安全网,回滚用):部署前拍全量 `pg_dump -Fc`。
|
||
3. **postgres 容器 healthy**。
|
||
4. 脚本位于 `/root/scilit/`(与 `docker-compose.prod.yml` 同目录)。
|
||
|
||
---
|
||
|
||
## 6. 实施步骤(生产服务器手工执行)
|
||
|
||
### 6.1 脚本位置与用法
|
||
|
||
脚本:`backend/scripts/migrate_prod.sh`(已随代码提交,部署时同步到 `/root/scilit/`)
|
||
|
||
```bash
|
||
cd /root/scilit
|
||
chmod +x migrate_prod.sh # 首次
|
||
./migrate_prod.sh 1 # 批次 1
|
||
./migrate_prod.sh 2 # 批次 2
|
||
./migrate_prod.sh 3 # 批次 3
|
||
./migrate_prod.sh 4 # 批次 4(最重,深夜)
|
||
```
|
||
|
||
脚本内置安全网:
|
||
- **镜像新鲜度检查**:执行前确认 backend 镜像含本地 head(`55105f0bb1d7`),否则报错并给出修复指引(防 08-09 事故重演)。
|
||
- **起点校验**:检查 DB 当前 revision 是否等于该批次的预期起点,防止乱序/重复。
|
||
- **幂等**:DB 已在目标 checkpoint 时直接跳过。
|
||
- **退出码门控**:迁移命令失败(非 0)即中止,`set -euo pipefail`。
|
||
|
||
### 6.2 每个批次的验证点
|
||
|
||
```bash
|
||
# 执行后确认当前版本
|
||
docker compose -f docker-compose.prod.yml run --no-deps --rm backend alembic current
|
||
```
|
||
|
||
预期结果(逐批):
|
||
|
||
| 批次 | 执行后 `alembic current` |
|
||
|---|---|
|
||
| 1 | `1421ea169bb6` |
|
||
| 2 | `cac545862583` |
|
||
| 3 | `e0f1a2b3c4d5` |
|
||
| 4 | `55105f0bb1d7 (head)` |
|
||
|
||
---
|
||
|
||
## 7. 注意事项
|
||
|
||
1. **执行序(destructive 窗口纪律)**:**批次 2/3(additive)可提前跑 → 维护窗口内:build → 批次 1 → 发新代码 → 深夜:批次 4**。批次 1(日期收窄)与发代码必须同窗口(分钟级内衔接),批次 4(大数据量回填 + Text 加宽)须在新代码已部署后深夜跑。
|
||
2. **锁表窗口**:批次 1(日期锁表)、批次 4(volume/issue/pages→Text 锁表)期间,对 `global_literature` 的写入被阻塞。建议选业务低峰。123 万行上的回填(02/17/18/20/21)预计每步数秒到数分钟,总计约 10-20 分钟。
|
||
3. **批次 4 完成后,DB 即升级到本地最新 head**——代码与 schema 完全对齐。批次 4 未跑时新代码可运行,但 `tag_ids` 为空、搜索 tsvector 覆盖不全(回填数据缺失),功能受限但不报错。
|
||
4. **失败处理**:若某批次迁移失败,脚本退出非 0;DB 停在失败前一个 revision,可重跑该批次(alembic 只应用未执行的迁移)。涉及数据回退时用 §5 快照(`pg_restore --clean --if-exists`,见 docs/16 §2 rollback.sh)。
|
||
|
||
---
|
||
|
||
## 8. 回滚预案
|
||
|
||
- **迁移失败 / 需回退数据**:使用部署前快照 `pg_restore --clean --if-exists --no-owner -d scilit <快照>` 恢复(docs/16 §2 rollback.sh 完整流程)。
|
||
- **代码回滚**:换回旧镜像 tag + `up -d --no-deps`(docs/16 §1 双 tag 原子回滚)。
|
||
- **迁移本身是单向的**:alembic 各迁移均有 `downgrade()`,但**破坏性迁移(类型变更/回填)downgrade 不恢复数据**——正式回滚走快照,不走 downgrade。
|
||
|
||
---
|
||
|
||
## 9. 执行记录
|
||
|
||
| 日期 | 批次 | 执行结果 | 备注 |
|
||
|---|---|---|---|
|
||
| (待填) | 1 | | |
|
||
| (待填) | 2 | | |
|
||
| (待填) | 3 | | |
|
||
| (待填) | 4 | | |
|