Files
sanguo_vnpy_v2/docs/three-env-code-promote.md
T
claude_dev 2ca2facce3
CI/CD / test (push) Successful in 9s
CI/CD / nas-deploy (push) Successful in 25s
CI/CD / nas-verify (push) Successful in 4s
fix(docker): requirements-docker 补组合/CTA 运行时依赖(治重建丢包后患)
容器是 docker commit 快照链(pre-init-rebuild 比 with-sqlite 多64包),
手动装的 bullet-trade/jqdatasdk/vnpy_ctastrategy/vnpy_sqlite 只活容器层,
从 base 重建会全丢。补进 requirements-docker.txt 钉到容器实证版(均PyPI可装):
- bullet-trade==0.9.2(组合框架,import bullet_trade)
- jqdatasdk==1.9.8(bullet_trade import 链必需)
- vnpy_ctastrategy==1.4.1(CTA引擎)
- vnpy_sqlite==1.1.3(vnpy sqlite驱动)
不补 pyecharts(0import残留)/jupyter栈(bloat)
docs §10 记可复现重建步骤;干净重建待 env 空闲验证
2026-08-01 12:23:53 +08:00

191 lines
9.5 KiB
Markdown

# 三机代码晋升 Runbook (Phase4)
> Mac(dev 源头) → NAS(test 镜像) → VPS(prod 生产) 单向代码晋升。
> 本文档只管 **代码**,不管数据(数据铁律见末节)。
## 1. 角色与流向
| 机器 | 角色 | 代码路径 | 工具 |
|------|------|----------|------|
| **Mac** | dev 源头(改代码) | `~/.openclaw/sanguo_projects/sanguo_vnpy_v2` | rsync/scp |
| **NAS** | test 镜像(只读副本) | `/volume1/stock/sanguo_vnpy_v2` | rsync(LAN 快) |
| **VPS** | prod 生产(实跑) | `C:\sanguo_vnpy_v2` | scp(无 rsync) |
```
改代码 rsync (Step1)
Mac dev ──────────────────► NAS test (镜像)
└─────── scp (Step2) ─────► VPS prod (生产)
```
> 两路都从 Mac 出发,NAS 是只读镜像(供回归),VPS 是生产实跑。**NAS 不是中转**,VPS 代码不经 NAS。
晋升是 **单向**(Mac → 外),NAS/VPS 永不回推 Mac。NAS 是镜像备份,VPS 是生产实跑。
## 2. 前置条件
- **Mac SSH key 连 VPS**: `~/.ssh/config` 已配 `Host 49.232.102.198` + key `~/.ssh/id_ed25519`
- 验证: `ssh 49.232.102.198 'echo VPS_OK'` 应直接通(免密)。
- **严禁 `ssh vps`**: 本机 config 无此别名,会被代理 fake-ip 劫持到 198.18.1.254 报 Connection reset。
- **NAS SSH**: `ssh sanguo-nas`(LAN 免密)。
- **工作目录**: 在 Mac 代码根 `~/.openclaw/sanguo_projects/sanguo_vnpy_v2` 下执行。
## 3. 使用
### 3.1 全量晋升(所有模块 + 根文件)
改完一批代码后:
```bash
cd ~/.openclaw/sanguo_projects/sanguo_vnpy_v2
bash scripts/nas_sync/promote.sh
```
推送内容:
- 模块目录(13 个): `sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor sanguo_live sanguo_orchestrator sanguo_portfolio sanguo_qmt_bridge sanguo_research sanguo_trader sanguo_web scripts config tests`
- 根文件(4 个): `pyproject.toml pytest.ini requirements-lock.txt run_web.py`
### 3.2 单模块快速补推
只想推刚改的一个模块(如 `sanguo_portfolio`):
```bash
bash scripts/nas_sync/promote.sh --module sanguo_portfolio
```
`--module` 模式: NAS+VPS 只推该模块,**不推根文件**(根文件改动需走全量)。
可用模块名见上方列表。脚本会在本地缺失时报错退出。
## 4. Reload 机制(重要)
VPS 进程模式是 **一次性任务**(回测 runner_backtest + 采集 schtask bs_eod/akshare),**无常驻 web 服务**。
| 场景 | reload 动作 |
|------|-------------|
| 采集 schtask(定时) | **无需操作**。下次 schtask 触发时自动加载新代码 |
| 回测任务 | **无需操作**。下次启动回测时自动加载新代码 |
| 立即让采集生效 | `schtasks /end sanguo-bs-eod && schtasks /run sanguo-bs-eod` |
> 一句话: 代码部署后,**下次启动自动生效**,不用 hot reload、不用重启服务。
立即生效命令(在 VPS 上执行,通过 ssh):
```bash
ssh 49.232.102.198 'schtasks /end sanguo-bs-eod'
ssh 49.232.102.198 'schtasks /run sanguo-bs-eod'
```
## 5. 落盘验证
脚本 Step3 自动验证 VPS 第一个推送模块的 `.py` 文件 mtime。手动深度核查:
```bash
# VPS: 查某模块文件列表+mtime
ssh 49.232.102.198 'powershell -NoProfile -Command "Get-ChildItem C:\sanguo_vnpy_v2\sanguo_portfolio -Filter *.py | Select Name,Length,LastWriteTime | Format-Table -Auto"'
# VPS: 查某文件是否含新代码标记
ssh 49.232.102.198 'powershell -NoProfile -Command "Select-String -Path C:\sanguo_vnpy_v2\sanguo_common\__init__.py -Pattern SOME_TOKEN"'
# NAS: 查某文件
ssh sanguo-nas "ls -la /volume1/stock/sanguo_vnpy_v2/sanguo_portfolio/"
```
> 教训(memory: commit≠部署VPS): 改完代码必须晋升,否则 VPS 跑旧代码。用 `Select-String`/`grep` 验证新代码落盘。
## 6. Rollback
代码部署出问题需要回退:
```bash
# 1. Mac 本地回退到上一个好版本
cd ~/.openclaw/sanguo_projects/sanguo_vnpy_v2
git log --oneline -5 # 找到好版本
git checkout <good_commit> -- sanguo_portfolio/ # 或整个目录
# 2. 重新晋升回退后的代码
bash scripts/nas_sync/promote.sh --module sanguo_portfolio
```
> NAS/VPS 没有 git,rollback = Mac git 回退 + 重新 promote。所以 **Mac git 历史是唯一真相源**。
## 7. 数据铁律(边界)
**只推代码,严禁推 `data/`。**
| 允许推 | 严禁推 |
|--------|--------|
| `sanguo_*/` 代码模块 | `data/`(42GB 数据库 + parquet) |
| `scripts/``config/``tests/` | `logs/``vnpy_v4.4.0/`(VPS 已有) |
| 根配置文件 | `vnpy_qmt_v0.3.3/``docker/``.git/` |
| | `__pycache__/``*.pyc``htmlcov/``*.log` |
脚本已通过两层防护:
1. **rsync(NAS)**: `--exclude='/data' --exclude='__pycache__' ...` 白名单式排除。
2. **scp(VPS)**: 按模块逐个推,天然隔离 `data/`(不在模块列表里)。
## 8. 常见坑
| 坑 | 解法 |
|----|------|
| `ssh vps` 连不上(fake-ip) | 用 `ssh 49.232.102.198`,config 无 vps 别名 |
| scp 路径反斜杠报错 | VPS 目标用**正斜杠**: `49.232.102.198:"C:/sanguo_vnpy_v2/"` |
| PowerShell 中文乱码 | 用 `-NoProfile`,查文件用 `Get-ChildItem`/`Select-String`,避免中文路径 |
| scp 某模块卡住 >60s | Ctrl-C 记录,先验证已通的模块,单独重推失败的 |
| 部署后 VPS 行为没变 | 代码是下次启动才加载;采集 schtask 需 `/end && /run` 立即生效 |
| macOS bash 3.2 空数组报错 | 脚本已用 `${#arr[@]} -gt 0` 守卫;如改脚本注意 `set -u` + 空数组 |
## 9. NAS 容器 --init 铁律(防后台回测被清)
NAS test 容器 `sanguo_vnpy_v2` **必须 `docker run --init` 创建**(tini 作 PID1)。
**根因**:无 `--init` 时 PID1=uvicorn(非 init/tini)。`docker exec ... nohup python &` 启动的后台回测进程,在 docker exec 会话(bash)结束后成孤儿,docker 清理该后台进程组 → **长任务读盘中途被清**(02 small_cap day0 选股 5128 只 fundamentals 需 ~4min 超会话存活被清,log 停 day0 无 error;03 纯量价 day0 秒级没被清)。一度误判 OOM,被 `dmesg` 空 + 前台跑过 day0 推翻。
**何时会丢 --init(复发风险)**:
- ci-cd `nas-deploy``docker restart`(**不重建,--init 保留** ✅)。
- ⚠️ **任何手动 `docker stop && rm && run` 必须带 `--init`** —— 这是唯一丢 --init 的路径。
**手动重建命令要点**:
```bash
D=/var/packages/Docker/target/usr/bin/docker # Synology docker 不在 PATH
# 1. 先 commit 当前容器(备份 + 含运行时手动装的 pip 包如 jwt)
$D commit sanguo_vnpy_v2 sanguo_vnpy_v2:pre-init-rebuild
# 2. stop && rm
$D stop sanguo_vnpy_v2 && $D rm sanguo_vnpy_v2
# 3. run --init 原参数(挂载/端口/env/network 原样保留)
$D run -d --init --name sanguo_vnpy_v2 \
-v /volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2:/app \
-v /volume1/stock:/volume1/stock -p 8000:8000 \
--restart unless-stopped sanguo_vnpy_v2:pre-init-rebuild
```
- ⚠️ **启动 image 用 `pre-init-rebuild`**(含 jwt 等运行时包)。**勿用 base `with-sqlite`** —— 缺 jwt,web 起 `ModuleNotFoundError`。(Dockerfile.nas 零 pip install,依赖全靠 base,故 jwt 只在 commit 层,重建从 base 会丢。)
- 验证:`$D inspect sanguo_vnpy_v2 --format '{{.HostConfig.Init}}'` = `true`;`ps -o comm -p $(...PID)` = `docker-init`(tini)。
**正式 nas-verify(重建后必跑,同 ci-cd 口径)**:login(token 字段) + `/api/v1/strategy/list` 200 非空 + dbbardata 600519 SSE >0。
## 10. NAS 镜像可复现构建(避免 commit 链依赖丢失)
**现状**:NAS 运行的 `sanguo_vnpy_v2:pre-init-rebuild`**docker commit 快照链**(with-sqlite → 手动 pip 装 bullet-trade/jwt/… → commit),非从 Dockerfile 构建。手动装的包只活在容器层,**任何从 base 重建都会丢**。实测 `pre-init-rebuild``with-sqlite` 多 64 个包(含 jwt/bullet-trade/baostock/jqdatasdk/vnpy_ctastrategy/pytest/整个 jupyter 栈)。
**已修(2026-08-01)**:`requirements-docker.txt` 补齐缺失的**顶层运行时**依赖(此前只在容器层手动装;transitive 会自动拉,不用列):
- `bullet-trade==0.9.2`(组合框架,import 名 `bullet_trade`)
- `jqdatasdk==1.9.8`(`import bullet_trade` 默认 provider=jqdata 触发 import,包须在;实跑用 LocalUnifiedProvider 不调 online)
- `vnpy_ctastrategy==1.4.1`(CTA 策略引擎,sanguo_backtest/sanguo_live 广用)
- `vnpy_sqlite==1.1.3`(vnpy sqlite 驱动,trader/database.py 运行时动态加载)
四个均已确认公共 PyPI 可装,版本钉死=容器实证版。**不补**:`pyecharts`(0 import 残留)、jupyter 栈(交互开发 bloat,非运行时)。
**可复现重建步骤**(⚠️**待空闲验证**——现运行容器 02 回测在跑,别动;这是验证步骤,不是修复本身,修复已落到 requirements):
```bash
D=/var/packages/Docker/target/usr/bin/docker
# 1. 建 base(docker/Dockerfile 装完整 requirements-docker.txt;Dockerfile.nas 的 FROM sanguo_vnpy:base 依赖它)
$D build -f docker/Dockerfile -t sanguo_vnpy:base .
# 2. 建 app 层(Dockerfile.nas FROM base + COPY 代码)
$D build -f docker/Dockerfile.nas -t sanguo_vnpy_v2:reproducible .
# 3. 起测试容器带 --init(见 §9),用新名不占 sanguo_vnpy_v2
$D run -d --init --name sanguo_repro_test -v <同挂载> -p 8001:8000 sanguo_vnpy_v2:reproducible
# 4. nas-verify(login+strategy/list+dbbardata) + 跑一次 03 短测确认组合链路(bullet_trade import 通)
# 5. 全过才换掉 sanguo_vnpy_v2;pre-init-rebuild 暂留回滚
```
**遗留**:① 干净重建尚未实测(requirements 已对齐容器 216 包里的顶层子集,理论可复现,待 env 空闲验证);② `requirements-lock.txt`(==钉版基线)未同步这 4 包,且本身"当前未验",待统一。