Files
backend/docs/17-生产数据迁移.md
T
34047007@qq.com 3c85ded216
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run
feat: 生产部署准备 — 构建加速 + 24迁移链分批脚本 + 搜索性能优化
- 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
2026-08-10 00:33:16 +08:00

159 lines
10 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 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(日期锁表)、批次 4volume/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 | | |