10 KiB
三机代码晋升 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 全量晋升(所有模块 + 根文件)
改完一批代码后:
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 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):
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。手动深度核查:
# 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
代码部署出问题需要回退:
# 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 |
脚本已通过两层防护:
- rsync(NAS):
--exclude='/data' --exclude='__pycache__' ...白名单式排除。 - 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 的路径。
手动重建命令要点:
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 等运行时包)。勿用 basewith-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 栈)。
构建走 docker/Dockerfile(单阶段自包含,2026-08-01 已验证+换装):FROM python:3.10-slim + 编译 TA-Lib + pip install -r requirements-docker.txt + COPY 代码。⚠️不是 Dockerfile.nas(它 FROM sanguo_vnpy:base 是从未建成的 2-stage 残留,别用)。NAS 容器 bind-mount /app 提供运行时代码,镜像只需 Python+依赖层。
已修(2026-08-01,均在 git):
requirements-docker.txt补缺失顶层运行时依赖:bullet-trade==0.9.2/jqdatasdk==1.9.8(bullet_trade import 链)/vnpy_ctastrategy==1.4.1/vnpy_sqlite==1.1.3。polars[rtcompat]==1.42.1:NAS CPU(Synology Celeron)无 AVX2,plain polarsimport即 SIGILL(exit132);[rtcompat] 拉 polars-runtime-compat 兜底。勿放回>=松约束(会拉到要 AVX2 的新版崩溃)。Dockerfilepip+apt 都补清华镜像(原 pypi.org/deb.debian.org 国内不稳);加.dockerignore(排 data_cache/frontend/venv 等,上下文 1.9G→~50MB)。
可复现重建步骤(已实证):
D=/var/packages/Docker/target/usr/bin/docker
CTX=/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2
# 1. 单阶段构建(setsid/nohup 防 ssh 杀;log 落盘)
ssh sanguo-nas "cd $CTX && setsid $D build -f docker/Dockerfile -t sanguo_vnpy_v2:reproducible . > /tmp/repro_build.log 2>&1 &"
# 2. 起测试容器带 --init(见 §9),新名+8001 端口不占 sanguo_vnpy_v2
$D run -d --init --name sanguo_repro_test -v $CTX:/app -v /volume1/stock:/volume1/stock -p 8001:8000 --network sanguo_vnpy_network -e DATA_DIR=$CTX sanguo_vnpy_v2:reproducible
# 3. 验证:nas-verify(8001 login+strategy/list+dbbardata) + 03 短测(bullet_trade 链路)
# 4. 全过才换主容器(见下);pre-init-rebuild image 留回滚
# 5. 进程检测用 /proc/<pid>/cmdline,别用 ps(此容器 ps 看不到 python,会误判)
换主容器:$D stop sanguo_vnpy_v2 && $D rm sanguo_vnpy_v2 → $D run -d --init --name sanguo_vnpy_v2(同 §9 挂载/端口/network/env) sanguo_vnpy_v2:reproducible → 8000 nas-verify。回滚:同命令换 image 为 sanguo_vnpy_v2:pre-init-rebuild。
验证结果(2026-08-01):03 momentum_timing 短测 total_return 15.62%/sharpe 1.59/6 持仓;web/login/dbbardata 全过;reproducible 镜像已换上主容器(Init=true)。⚠️02/01 未单独实测(用同栈,理论同),策略 session 首跑若异常即回滚。