Files
wordcloud/learning/runbooks/02-safe-update-data-preservation-runbook.md
T

307 lines
11 KiB
Markdown
Raw 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.
# 受控更新:如何在升级版本时保留数据(runbook)
> 对应你的问题:**“给我一个能够保留数据来更新的技术文档,从而实现运维的可控”**
> 一句话目标:**升级代码、重建容器、打补丁时,项目所有的用户数据都不丢、可回滚、可验证。**
这份文档是“按步骤照着做”的故障/操作手册。不是空谈原则,是每一步都有命令、都有验证点。
---
## 0. 先说结论:这个项目现在更新会丢什么
当前(2026-08-13)数据分两块:
| 数据 | 存放位置 | Docker 里会不会丢 |
|---|---|---|
| 任务产物(输入/输出/配置) | `backend/service_workspace/` | ✅ 已挂卷 `wordcloud_workspace`,不丢 |
| 贴纸/素材 | `backend/service_assets/` | ✅ 已挂卷 `wordcloud_assets`,不丢 |
| 工程项目 | `backend/service_projects/` | ✅ 已挂卷 `wordcloud_projects`,不丢 |
| 设计模板 | `backend/service_design_templates/` | ✅ 已挂卷 `wordcloud_design_templates`,不丢 |
| 上传字体 | `backend/service_fonts/` | ✅ 已挂卷 `wordcloud_fonts`,不丢 |
| **任务元数据(SQLite** | `backend/service_metadata/app.db` | ⚠️ **没挂卷,容器重建即丢** |
| **订单数据** | `backend/service_orders/` | ⚠️ **没挂卷,容器重建即丢** |
⚠️ 红色标记的两项是**当前最要紧的缺口**:
- `service_metadata/app.db` 是任务状态/事件的权威来源(`metadata_store.py`)。
它写进了容器的可写层,不在任何 named volume 里。
`docker compose up -d --build`(重打镜像、重建容器)时,这层会被清空 → **任务记录清零**
- `service_orders/` 是新增的运行时目录,同样没进卷。
其余 5 个目录因为有 named volume,容器重建时数据还在。
> 所以“保留数据来更新”的第一步不是写代码,是先**确认要保的数据都在卷里/都有备份**。
> 光指望 `docker compose down` 不删数据是不够的——重建容器才危险,而更新几乎必然会重建。
---
## 1. 数据全景:更新前必须认识的东西
### 1.1 文件数据(前端/后端所有运行时产物)
```text
backend/
├─ service_workspace/ # 任务目录,每个 job 一个 {job_id}/input|output|配置
├─ service_assets/ # 素材库,每个 {asset_id}/asset.* + meta.json
├─ service_projects/ # 工程项目
├─ service_design_templates/ # 设计模板 JSON
├─ service_fonts/ # 上传字体
├─ service_metadata/app.db # 业务元数据(SQLitejobs / job_events 两张表)★ 易丢
└─ service_orders/ # 订单数据 ★ 易丢
```
关键点:
- 产物文件(SVG/PNG/字体/Excel)存文件系统,**不是**数据库里。
- 一份任务结果有时自带一个 `word_locations.db`(词云算法自己的 SQLite 结果文件),在那任务目录里,跟 `service_metadata/app.db` 不是一回事。
### 1.2 元数据(数据库)
`service_metadata/app.db``metadata_store.py` 生成的 SQLite 单文件:
```text
表 jobs — 任务主表(job_id、status、stage、progress、params、artifacts
表 job_events — 任务进度事件(SSE 落库,重启可恢复)
```
这文件很小(几百 KB 级),但它是“任务可恢复”的关键。**更新时必须单独备份。**
### 1.3 不算项目数据的(别备份、也不要提交)
```text
backend/.venv/ # 本地 Python 虚拟环境
frontend/node_modules/ # npm 依赖
backend/.runtime/ # 运行时日志(可丢,日志另按需归档)
*.so / dist/ # 编译产物,重新构建即可
```
---
## 2. 受控更新流程(四条主线)
无论本地还是 Docker,都按 **备 → 查 → 更 → 验 → 回** 五步走:
```
① 备份数据 → ② 记录当前版本 → ③ 升级 → ④ 验证 → ⑤(出问题)回滚
```
下面的命令直接复制可用。
---
## 3. 第一步:备份(更新前必做,最好 5~10 分钟搞定)
### 3.1 备份文件数据
```bash
# 在项目根目录执行,生成带时间戳的备份包
TS=$(date +%Y%m%d_%H%M%S)
BACKUP_ROOT=$(pwd)/.backups
mkdir -p "$BACKUP_ROOT"
tar -czf "$BACKUP_ROOT/wordcloud-data-$TS.tar.gz" \
-C backend \
service_workspace service_assets service_projects \
service_design_templates service_fonts service_metadata service_orders
```
### 3.2 单独稳妥备份 SQLite 元数据库(推荐用 sqlite 的在线备份,避免写一半)
```bash
sqlite3 backend/service_metadata/app.db ".backup '.backups/metadata-$TS.db'"
```
> 如果服务正在跑,用 `.backup` 而不是直接 `cp`,因为 `cp` 可能拷到写一半的文件。
### 3.3 校验备份真的可读(光备份不看等于没备)
```bash
tar -tzf "$BACKUP_ROOT/wordcloud-data-$TS.tar.gz" | wc -l # 能看到条数
sqlite3 "$BACKUP_ROOT/metadata-$TS.db" "select count(*) from jobs;" # 能查到任务数
```
### 3.4 本地开发:备份产物
本地数据直接躺在 `backend/` 下,copy 出去即可:
```bash
cp -r backend/service_workspace backend/service_assets ~/wcd-backup-$TS
```
---
## 4. 第二步:记录当前版本(为了能回滚)
更新前记下“现在跑的是哪一版”,否则回滚都不知道回哪。
- Docker:记镜像 Digest 或 Tag
```bash
docker inspect wordcloud-backend --format '{{.Image}} {{.Config.Image}}'
docker image ls | grep backend
```
- 代码:记 commit
```bash
git rev-parse HEAD
```
把这些写进 `.backups/UPDATE-LOG-$TS.txt`
```text
更新前 commit: abc1234
更新前镜像: wordcloud-backend:latest (digest sha256:...)
更新时备份包: wordcloud-data-20260813_1015.tar.gz
```
---
## 5. 第三步:升级
### 5.1 Docker 部署(最常见,也最危险)
```bash
# 拉/构建新镜像并重建容器
docker compose up -d --build
# 容器重建后,立刻(不要拖)确认关键接口活着
curl -fsS http://localhost:8000/api/health
curl -fsS http://localhost:8000/api/maintenance/storage-summary | jq '{jobs_in_db, events_in_db}'
```
> `jobs_in_db` 应该是更新前的数字——如果变成 0,说明 `service_metadata` 没进卷、被重建清掉了 → 走“恢复”那一步。
### 5.2 本地开发
```bash
cd backend && ./start-dev.sh # 会重新编译 C++、起 uvicorn
# 或前端
cd frontend && npm install && npm run dev
```
本地 `uvicorn --reload` 重建的是代码不是数据目录,一般不丢数据,但仍建议更新大改动前备份。
---
## 6. 第四步:验证(更新的“可控”就体现在这一步)
升级完不是“能打开首页”就完事,要验证**数据层**:
```bash
# 1) 服务健康
curl -fsS http://localhost:8000/api/health
# 2) 数据还在(拿更新前记录的 baseline 对比)
curl -fsS http://localhost:8000/api/maintenance/storage-summary \
| jq '{job_dir_count, referenced_job_ids, jobs_in_db, events_in_db, reclaimable_bytes}'
# 3) 素材/列表有东西(随便挑一个列表接口)
curl -fsS http://localhost:8000/api/assets | jq 'length'
# 4) 能真正跑一个最小词云任务(端到端冒烟)
curl -fsS http://localhost:8000/api/jobs -X POST \
-H 'Content-Type: application/json' \
-d '{"text":"hello world","shape":"rectangle"}' \
&& sleep 3 && curl -fsS http://localhost:8000/api/jobs | jq '.[0]'
```
**通过的标准**(缺一不可):
- `jobs_in_db` / `events_in_db` > 0,且与更新前基线一致或更高。
- 素材列表数量没掉。
- 冒烟任务能 success。
---
## 7. 第五步:回滚(更新失败时)
更新最怕“上了下不来”。回滚分两个层面,先回数据、再回代码。
### 7.1 Docker:切回旧镜像 + 恢复数据
```bash
# 1) 停掉当前
docker compose down
# 2) 恢复文件数据(把备份解开到已挂卷的位置)
# 注意:named volume 可以通过临时容器写回,例如恢复 service_assets
docker run --rm \
-v wordcloud_assets:/target \
-v "$(pwd)/.backups":/bak \
alpine sh -c 'rm -rf /target/* && tar -xzf /bak/wordcloud-data-XXX.tar.gz -C /target service_assets 2>/dev/null; echo done'
# 3) 恢复 SQLite(如果它丢了)
cp .backups/metadata-$TS.db backend/service_metadata/app.db # 本地路径法
# 容器内恢复:挂载校验后重建容器时会重新读 /app/service_metadata/app.db
# 4) 用旧镜像/旧 tag 拉起
docker compose up -d --build # 或 docker compose pull 旧 tag 后 up
```
> 更省事、更可靠的开销小做法:**更新前对卷打快照**。
> 现在有组件还进不了卷时,快照是唯一能把它们一起保住的兜底。
### 7.2 代码回滚
```bash
git checkout <更新前commit>
# 或不改代码,直接复用旧镜像 tag
```
---
## 8. 把“易丢数据”变成“永不丢”的根治建议
上面 runbook 是“治标”——每次更新都备份。要“治本”,把下面两件事做了,第 0 节的红色缺口就消失:
### 8.1 让 `service_metadata` 和 `service_orders` 进 Docker 卷
在 `docker-compose.yml` 的 `backend` volumes 里加两行,并在 `volumes:` 声明:
```yaml
services:
backend:
volumes:
- wordcloud_workspace:/app/service_workspace
- wordcloud_assets:/app/service_assets
- wordcloud_projects:/app/service_projects
- wordcloud_design_templates:/app/service_design_templates
- wordcloud_fonts:/app/service_fonts
- wordcloud_metadata:/app/service_metadata # ← 新增
- wordcloud_orders:/app/service_orders # ← 新增
volumes:
wordcloud_workspace:
wordcloud_assets:
wordcloud_projects:
wordcloud_design_templates:
wordcloud_fonts:
wordcloud_metadata: # ← 新增
wordcloud_orders: # ← 新增
```
> 注意:`service_metadata` 当前安装在容器的 `/app/service_metadata``PROJECT_ROOT` 是 `/app`)。
> 加上卷后,`app.db` 会持久化到卷,容器重建不再清零。
### 8.2 定期备份(不只在更新时才备)
加一个 cron(示例每天 02:17,避开整点):
```cron
17 2 * * * cd /path/to/wordcloud && ./scripts/backup-data.sh
```
配套的 `scripts/backup-data.sh` 只需做第 3 步那三件事:打包文件 → sqlite `.backup` → 校验。
---
## 9. 更新风暴口诀
**更新五步:备份 → 记版本 → 升级 → 验证数据 → 能回滚。**
**最危险的不是 `docker compose down`,而是重建容器;重建前看卷、重建后看 `jobs_in_db`。**
---
## 10. 相关文件
- `docker-compose.yml`:数据卷挂载(改这里补缺口)
- `backend/service/metadata_store.py``service_metadata/app.db` 的来源
- `backend/service/storage_metrics.py`:任务清理审计(dry-run → `--apply`
- `docs/DEPLOYMENT.md`:部署与产物目录清单
- `docs/DATA_STORAGE_OPTIMIZATION.md`:任务存储现状与清理策略