54f9ab4c4f
vps-production-runbook(VPS运维一站式,已验证状态+拓扑+发布流水线+人类闸口+排障); env-version-matrix(三机Python/deps矩阵+Lock建议); scripts/ops(NAS bridge探针-容器无curl-+VPS→NAS备份脚本); nas-deploy-plan§3(修rsync --exclude语法,原会删万级staging parquet+破坏entrypoint启动).
401 lines
18 KiB
Markdown
401 lines
18 KiB
Markdown
# VPS 生产运维 Runbook
|
||
|
||
> **sanguo QMT bridge 生产环境运维手册。** VPS 已部署并在运行,本文档是"运维 + 已验证状态记录",不是从零搭建指南。
|
||
>
|
||
> 相关文档:
|
||
> - bridge 接口契约:[`sanguo_qmt_bridge/README.md`](../../sanguo_qmt_bridge/README.md)
|
||
> - 首次部署参考(历史):[`d-phase-windows-deploy.md`](./d-phase-windows-deploy.md) / [`windows-bridge-setup.md`](./windows-bridge-setup.md)
|
||
> - NAS 容器运维:[`nas-deploy-plan.md`](./nas-deploy-plan.md)
|
||
>
|
||
> **安全约定**:本文档进 git,**不硬编码 BRIDGE_TOKEN**。所有命令中 `<BRIDGE_TOKEN>` 为占位符,真实值见 Claude memory `windows-vps-access.md` 或 VPS 系统环境变量 `BRIDGE_TOKEN`。
|
||
|
||
---
|
||
|
||
## 1. 已验证生产状态(2026-07-14 巡检)
|
||
|
||
> 以下事实经实地验证,直接采纳。下次巡检时更新日期并重新核实。
|
||
|
||
| 项目 | 值 | 备注 |
|
||
|------|-----|------|
|
||
| **VPS** | `49.232.102.198` | 腾讯云轻量,Win Server 2022,4C/16G/180G SSD |
|
||
| **SSH** | `ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198` | Mac ~/.ssh/id_ed25519 免密 |
|
||
| **磁盘** | C: 24G 用 / 156G 空闲 | 充裕 |
|
||
| **Python** | `C:\Python310\python.exe` (3.10.11) | xtquant import OK |
|
||
| **miniQMT** | `C:\国金QMT交易端\` userdata `userdata_mini` | 模拟账户 `66639661` 已登录 |
|
||
| **bridge 代码** | `C:\sanguo_qmt_bridge\` | FastAPI + uvicorn :8765 |
|
||
| **bridge 启动脚本** | `C:\run_bridge.ps1` | 设环境变量 + BRIDGE_SESSION_ID + uvicorn |
|
||
| **Caddy** | `C:\caddy\` + `C:\run_caddy.ps1` | :443 → :8765 反代,Let's Encrypt 自动续期 |
|
||
| **schtasks** | `sanguo-bridge` + `sanguo-caddy` 均 Running | SYSTEM 账户 / onstart 自启 |
|
||
|
||
### 进程状态
|
||
|
||
| 进程 | 映像名 | 角色 |
|
||
|------|--------|------|
|
||
| XtMiniQmt | `XtMiniQmt.exe` | miniQMT 客户端,已登录模拟账户 |
|
||
| python | `python.exe` | uvicorn bridge :8765 |
|
||
| caddy | `caddy.exe` | HTTPS 反代 :443 → :8765 |
|
||
|
||
### bridge 端点验证(VPS 本地 127.0.0.1:8765)
|
||
|
||
| 端点 | 响应 |
|
||
|------|------|
|
||
| `GET /health` | `{"status":"ok","miniqmt_connected":true}` |
|
||
| `GET /account` | `{"ok":true,"cash":9997075.51,"frozen":9001769.0,"market_value":2901.0,"total":9999978.51}` |
|
||
| `GET /positions` | `{"ok":true,"positions":[...sh600000(200股), sz000001(100股)...]}` |
|
||
|
||
### 三条访问路径
|
||
|
||
| # | 路径 | 地址 | 场景 |
|
||
|---|------|------|------|
|
||
| ① | VPS 本地 | `http://127.0.0.1:8765` | 运维 RDP/SSH 内验证 |
|
||
| ② | 公网 HTTPS | `https://bridge.mysanguo.top` | 外部客户端(Mac 浏览器等)|
|
||
| ③ | NAS 容器经 Mac 隧道 | `http://192.168.2.101:8765` | NAS sanguo 容器调 bridge |
|
||
|
||
---
|
||
|
||
## 2. 拓扑图
|
||
|
||
```
|
||
┌───────────────────────────────────────────────────┐
|
||
│ Windows VPS · 49.232.102.198 │
|
||
│ Win Server 2022 · 4C/16G/180G SSD │
|
||
│ │
|
||
│ ┌───────────┐ xtquant ┌──────────────┐ │
|
||
│ │ miniQMT │◄──────────│ bridge │ │
|
||
│ │ 66639661 │ │ :8765 │ │
|
||
│ └───────────┘ └──────┬───────┘ │
|
||
│ │ 反代 │
|
||
│ ┌──────▼───────┐ │
|
||
│ │ Caddy │ │
|
||
│ │ :443 │ │
|
||
│ │ Let's Encr. │ │
|
||
│ └──────┬───────┘ │
|
||
└──────────────────────────────────┼────────────────┘
|
||
│
|
||
┌────────────────────────┼───────────────────┐
|
||
│ │ │
|
||
① VPS 本地 ② 公网 HTTPS ③ NAS→Mac→VPS
|
||
http://127.0.0.1:8765 https://bridge. http://192.168.2.101
|
||
(运维 RDP/SSH) mysanguo.top :8765
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌──────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ VPS 终端 │ │ 外部客户端 │ │ Mac Mini │
|
||
└──────────┘ │(Mac 浏览器等) │ │ 192.168.2.101│
|
||
└──────────────┘ │ 开发 + 隧道 │
|
||
│ │
|
||
│ ssh -N -L │
|
||
│ 0.0.0.0:8765:│──SSH:22──► VPS
|
||
│ 127.0.0.1:8765│
|
||
│ │
|
||
│ caffeinate │
|
||
│ -i -s 防睡眠 │
|
||
└──────┬───────┘
|
||
│
|
||
┌──────▼───────┐
|
||
│ NAS 容器 │
|
||
│ 192.168.2.154│
|
||
│ sanguo_vnpy │
|
||
│ _v2 (测试+备) │
|
||
└──────────────┘
|
||
|
||
⚠ 华为光猫拦截 NAS→VPS 的 80/443 → NAS 不能走路径②
|
||
→ NAS 容器走路径③:HTTP 到 Mac :8765 → Mac SSH 隧道 → VPS :8765
|
||
→ Mac 需常驻 SSH tunnel + caffeinate -i -s 防睡眠
|
||
```
|
||
|
||
### 三机分工
|
||
|
||
| 机器 | IP | 角色 | 能做什么 | 不能做什么 |
|
||
|------|-----|------|----------|-----------|
|
||
| **VPS** | 49.232.102.198 | 生产 | miniQMT + bridge + Caddy 全链路 | 无开发环境 |
|
||
| **Mac Mini** | 192.168.2.101 | 开发 + 隧道中继 | 写代码、git、scp 部署、SSH 隧道 | 跑不了 xtquant(Windows only) |
|
||
| **NAS** | 192.168.2.154 | 测试 + 备份 | Docker 容器跑集成测试、回测、每日备份 | 无法直连 VPS 80/443(光猫拦截) |
|
||
|
||
---
|
||
|
||
## 3. 日常运维操作
|
||
|
||
> 以下命令均从 **Mac Mini** 执行,通过 SSH 远程操控 VPS。
|
||
|
||
### 3.1 健康巡检(一条命令验三端点)
|
||
|
||
**快速健康(无需 token):**
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"curl -s http://127.0.0.1:8765/health"
|
||
```
|
||
|
||
期望:`{"status":"ok","miniqmt_connected":true}`
|
||
|
||
**完整三端点验证(含 account + positions,需 token):**
|
||
|
||
复杂引号场景用 stdin 喂 PowerShell(见 [§8 Windows 坑](#8-windows-跑命令的坑)):
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1'
|
||
Write-Host "=== health ==="
|
||
curl.exe -s http://127.0.0.1:8765/health
|
||
Write-Host "`n=== account ==="
|
||
curl.exe -s -H "X-Bridge-Token: <BRIDGE_TOKEN>" http://127.0.0.1:8765/account
|
||
Write-Host "`n=== positions ==="
|
||
curl.exe -s -H "X-Bridge-Token: <BRIDGE_TOKEN>" http://127.0.0.1:8765/positions
|
||
PS1
|
||
```
|
||
|
||
**检查 schtasks 状态 + 进程 + 磁盘:**
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1'
|
||
Write-Host "=== schtasks ==="
|
||
schtasks /query /tn "sanguo-bridge" /fo list | Select-String "Status"
|
||
schtasks /query /tn "sanguo-caddy" /fo list | Select-String "Status"
|
||
Write-Host "=== processes ==="
|
||
Get-Process python,caddy,XtMiniQmt -ErrorAction SilentlyContinue | Format-Table Name,Id,CPU -Auto
|
||
Write-Host "=== disk ==="
|
||
Get-PSDrive C | Format-Table Used,Free -Auto
|
||
PS1
|
||
```
|
||
|
||
### 3.2 bridge 重启(必须换 BRIDGE_SESSION_ID)
|
||
|
||
> **关键**:xtquant 的 `connect()` 复用旧 session 会返回 -1。每次重启 bridge 前必须换 `BRIDGE_SESSION_ID`,否则 bridge 起来但 miniQMT 连不上。
|
||
|
||
**步骤:**
|
||
|
||
1. **改 session_id** — RDP 或 SSH 编辑 `C:\run_bridge.ps1`,把 `$env:BRIDGE_SESSION_ID` 改为新值(如日期递增 `20260714` → `20260715` 或加后缀 `20260714b`):
|
||
|
||
```bash
|
||
# SSH 在线编辑(PowerShell 替换)
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1'
|
||
$path = "C:\run_bridge.ps1"
|
||
$content = Get-Content $path -Raw
|
||
# 把旧 session_id 替换为今天的(按实际值调整正则)
|
||
$newId = Get-Date -Format "yyyyMMddHHmm"
|
||
$content = $content -replace 'BRIDGE_SESSION_ID\s*=\s*"\d+"', "BRIDGE_SESSION_ID = `"$newId`""
|
||
Set-Content $path -Value $content -Encoding UTF8
|
||
Write-Host "session_id updated to $newId"
|
||
# 确认
|
||
Select-String -Path $path -Pattern "BRIDGE_SESSION_ID"
|
||
PS1
|
||
```
|
||
|
||
2. **重启 schtask:**
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"schtasks /end /tn \"sanguo-bridge\" & schtasks /run /tn \"sanguo-bridge\""
|
||
```
|
||
|
||
3. **等 5 秒后验证健康**(同 §3.1 快速健康命令)。
|
||
|
||
### 3.3 Caddy 重启
|
||
|
||
Caddy 无 session 状态问题,直接重启:
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"schtasks /end /tn \"sanguo-caddy\" & schtasks /run /tn \"sanguo-caddy\""
|
||
```
|
||
|
||
验证公网:
|
||
|
||
```bash
|
||
curl -s https://bridge.mysanguo.top/health
|
||
```
|
||
|
||
### 3.4 miniQMT 崩溃恢复(human-gated)
|
||
|
||
> miniQMT 是券商客户端 GUI 程序,崩溃后需要人工 RDP 登录。**无法自动化。**
|
||
|
||
**症状**:`/health` 返回 `{"miniqmt_connected": false}`,或 bridge 日志 xtquant connect 报错。
|
||
|
||
**恢复步骤(人工操作):**
|
||
|
||
1. RDP 登录 VPS(远程桌面 `49.232.102.198`)
|
||
2. 手动启动 miniQMT 客户端 → 登录模拟账户 `66639661`
|
||
3. 确认客户端进入极简模式/独立交易界面
|
||
4. **换 BRIDGE_SESSION_ID 后重启 bridge**(同 §3.2)
|
||
5. 验证 `/health` → `miniqmt_connected: true`
|
||
|
||
---
|
||
|
||
## 4. dev → test → prod 发布流水线
|
||
|
||
```
|
||
Mac Mini (开发) NAS (测试) VPS (生产)
|
||
───────────── ────────── ──────────
|
||
写代码 git pull scp bridge 代码
|
||
sanguo_qmt_bridge/ → 容器跑集成测试 → schtasks 重启
|
||
本地 unit test 验证通过 → 健康巡检
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
git push ──────────► gitea ──────► NAS pull ────► VPS deploy
|
||
(git.mysanguo.top) (Docker 容器) (scp + restart)
|
||
│
|
||
每日 VPS 状态
|
||
备份 → NAS
|
||
```
|
||
|
||
| 阶段 | 机器 | 动作 | 验证 |
|
||
|------|------|------|------|
|
||
| dev | Mac Mini | 改 `sanguo_qmt_bridge/` 代码 | 本地 unit test(`pytest tests/`)|
|
||
| push | Mac Mini | `git push origin master` | gitea 仓库更新 |
|
||
| test | NAS | 容器 `git pull` + 跑集成测试 | bridge_client 测试通过、数据管道 OK |
|
||
| prod | VPS | scp 变更文件 + 重启 bridge | `/health` + `/account` + `/positions` 三通 |
|
||
| backup | NAS | 每日 VPS 状态备份到 NAS | bridge 代码 + config 快照 |
|
||
|
||
> **注意**:NAS 测试只能验 bridge_client 端(发 HTTP 请求到 bridge),无法验 xtquant/miniQMT 端(Windows only)。bridge 服务端 + xtquant + miniQMT 的端到端验证只能在 VPS 上做。
|
||
|
||
---
|
||
|
||
## 5. 代码部署到 VPS(Mac → VPS scp + 重启)
|
||
|
||
### 5.1 scp 变更文件
|
||
|
||
bridge 代码在 Mac 的 `sanguo_qmt_bridge/` 目录,改完后 scp 到 VPS:
|
||
|
||
```bash
|
||
# 只传变更的 .py 文件(快速迭代)
|
||
scp -i ~/.ssh/id_ed25519 \
|
||
sanguo_qmt_bridge/bridge.py \
|
||
sanguo_qmt_bridge/xt_gateway.py \
|
||
sanguo_qmt_bridge/auth.py \
|
||
Administrator@49.232.102.198:C:/sanguo_qmt_bridge/
|
||
|
||
# 或整目录同步(含 requirements.txt 等)
|
||
scp -i ~/.ssh/id_ed25519 sanguo_qmt_bridge/*.py \
|
||
Administrator@49.232.102.198:C:/sanguo_qmt_bridge/
|
||
```
|
||
|
||
> **Windows scp 路径用正斜杠**:`C:/sanguo_qmt_bridge/`(不是反斜杠)。
|
||
|
||
### 5.2 换 session_id + 重启
|
||
|
||
```bash
|
||
# 1. 换 BRIDGE_SESSION_ID(同 §3.2 步骤 1)
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1'
|
||
$path = "C:\run_bridge.ps1"
|
||
$content = Get-Content $path -Raw
|
||
$newId = Get-Date -Format "yyyyMMddHHmm"
|
||
$content = $content -replace 'BRIDGE_SESSION_ID\s*=\s*"\d+"', "BRIDGE_SESSION_ID = `"$newId`""
|
||
Set-Content $path -Value $content -Encoding UTF8
|
||
Write-Host "session_id → $newId"
|
||
PS1
|
||
|
||
# 2. 重启 schtask
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"schtasks /end /tn \"sanguo-bridge\" & schtasks /run /tn \"sanguo-bridge\""
|
||
|
||
# 3. 等 5 秒,验证
|
||
sleep 5
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"curl -s http://127.0.0.1:8765/health"
|
||
```
|
||
|
||
### 5.3 如改了 requirements.txt(依赖变更)
|
||
|
||
```bash
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \
|
||
"cd C:\sanguo_qmt_bridge && C:\Python310\python.exe -m pip install -r requirements.txt"
|
||
# 然后同 5.2 重启
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 人类闸口清单
|
||
|
||
> 以下操作无法自动化,必须人工执行。自动化脚本碰到这些步骤应明确报"需人工干预"。
|
||
|
||
| 操作 | 为什么只能人做 | 频率 |
|
||
|------|---------------|------|
|
||
| **VPS 开通 / 重置密码** | 云厂商控制台操作 | 极低(首次/安全事件) |
|
||
| **miniQMT 券商客户端登录** | GUI 程序 + 可能需验证码/密码 | 崩溃后 / VPS 重启后 |
|
||
| **BRIDGE_TOKEN 轮换** | 需同时在 VPS + NAS + Mac 三处同步更新 | 定期(安全策略) |
|
||
| **Let's Encrypt 证书异常处理** | Caddy 自动续期,但 Rate Limit / DNS 异常需人工介入 | 极低(自动续期正常时无需干预) |
|
||
| **华为光猫 / 网络配置变更** | 运营商设备,SSH 碰不到 | 极低 |
|
||
| **VPS 计划任务创建/修改** | 首次配置 `schtasks /create` 需 RDP | 低(配置变更时) |
|
||
|
||
### BRIDGE_TOKEN 轮换流程(人工)
|
||
|
||
1. 生成新 token:`python -c "import secrets; print(secrets.token_urlsafe(32))"`
|
||
2. VPS:更新 `C:\run_bridge.ps1` 里的 BRIDGE_TOKEN(或系统环境变量)
|
||
3. NAS:更新 sanguo 容器环境变量 + `docker restart sanguo_vnpy_v2`
|
||
4. Mac:更新 `sanguo_trader/bridge_client.py` 引用的配置或环境变量
|
||
5. VPS:换 session_id + 重启 bridge(§3.2)
|
||
6. 全链路验证三端点
|
||
|
||
---
|
||
|
||
## 7. 排障表
|
||
|
||
| 现象 | 根因 | 排查 / 修复 |
|
||
|------|------|-------------|
|
||
| `/health` → `miniqmt_connected: false` | miniQMT 未登录 / 崩溃 / userdata 路径错 | RDP 检查 miniQMT 客户端状态(§3.4),确认 `MINIQMT_USERDATA` 指向正确 userdata_mini |
|
||
| bridge 启动 xtquant connect 返回 -1 | **BRIDGE_SESSION_ID 与旧 session 冲突** | 换 session_id 后重启(§3.2)——这是最常见坑 |
|
||
| 401 token 无效 | VPS 与 NAS/Mac 的 `BRIDGE_TOKEN` 不一致 | 核对三处 token 值一致 |
|
||
| 公网 `https://bridge.mysanguo.top` 502 | Caddy 没启 或 bridge 没启 | 先验 VPS 本地 `curl 127.0.0.1:8765/health`;本地通=bridge OK→查 Caddy(§3.3);本地不通→查 bridge |
|
||
| 公网 DNS 解析到错误 IP(如 198.18.1.244) | **Mac 本地代理(Clash/Surge)DNS 劫持到 fake-IP**。公网 DNS(8.8.8.8)解析正确到 49.232.102.198,但代理层拦截 | 修复①:`/etc/hosts` 加 `49.232.102.198 bridge.mysanguo.top`;修复②:代理规则里该域名直连(bypass proxy) |
|
||
| NAS 容器访问 `bridge.mysanguo.top` 超时 | **华为光猫拦截 NAS→VPS 的 80/443** | 走路径③:Mac SSH 隧道 `http://192.168.2.101:8765`(Mac 需常驻 tunnel + caffeinate) |
|
||
| Mac SSH 隧道断了 → NAS 容器连不上 bridge | Mac 睡眠 / SSH 进程退出 | Mac 跑 `caffeinate -i -s &` 防睡眠;用 autossh 或 launchd 守护 SSH tunnel |
|
||
| 下单报 `[120141][证券交易未初始化]` | **非交易日**(miniQMT 交易日才初始化交易通道)| 等交易日。这是 miniQMT 设计,不是 bug |
|
||
| 影子下单未触发 | `live.enabled` 未开 / 当日无成交 / BRIDGE_TOKEN 未设 / bridge_url 不对 | 逐项检查 NAS `config/data_platform.yaml` + 环境变量 |
|
||
| 重复下单 | — | 不会。`paper_shadow_orders` 表 `UNIQUE(account_id, trade_id)` 幂等去重 |
|
||
| cmd 中文/emoji 乱码 | cmd 默认 GBK 编码 | 用 PowerShell + `chcp 65001`,或 python 加 `-X utf8`(§8) |
|
||
| scp 中文路径失败 | Windows 中文目录 + SSH 编码 | 用正斜杠路径 + ASCII 变量名,中文路径用搜索代替字面量 |
|
||
|
||
---
|
||
|
||
## 8. Windows 跑命令的坑
|
||
|
||
> Windows SSH 远程跑命令有三个经典坑:编码、引号、中文路径。
|
||
|
||
### 8.1 编码(GBK → UTF8)
|
||
|
||
cmd 默认 GBK,中文输出和 emoji 会乱码。
|
||
|
||
```bash
|
||
# PowerShell 设 UTF8(代码页 65001)
|
||
ssh ... "powershell -Command \"[Console]::OutputEncoding = [Text.Encoding]::UTF8; chcp 65001; <你的命令>\""
|
||
|
||
# Python 加 -X utf8
|
||
ssh ... "C:\Python310\python.exe -X utf8 script.py"
|
||
```
|
||
|
||
### 8.2 引号嵌套 → 用 stdin
|
||
|
||
SSH → cmd → PowerShell 三层引号极易出错。复杂脚本用 stdin 喂:
|
||
|
||
```bash
|
||
# powershell -Command - 从 stdin 读脚本
|
||
ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1'
|
||
# 这里写 PowerShell,引号无需转义
|
||
curl.exe -s -H "X-Bridge-Token: <BRIDGE_TOKEN>" http://127.0.0.1:8765/account
|
||
PS1
|
||
```
|
||
|
||
`<<'PS1'` 的单引号防止 Mac shell 展开变量,PowerShell 在 VPS 侧原样执行。
|
||
|
||
### 8.3 中文路径
|
||
|
||
`C:\国金QMT交易端\` 等中文路径在 SSH 传输中可能因编码错乱。
|
||
|
||
- scp 目标用正斜杠:`C:/sanguo_qmt_bridge/`(ASCII)
|
||
- 需引用中文路径时,在 PowerShell 内用变量拼接或 `Get-ChildItem` 搜索,不写字面量:
|
||
|
||
```powershell
|
||
# 不写 "C:\国金QMT交易端\...",用搜索
|
||
$qmt = Get-ChildItem C:\ -Directory | Where-Object Name -like "*QMT*"
|
||
```
|
||
|
||
### 8.4 curl vs Invoke-WebRequest
|
||
|
||
PowerShell 里 `curl` 默认是 `Invoke-WebRequest` 的别名(参数语法不同)。要用的标准 curl:
|
||
|
||
```powershell
|
||
curl.exe -s http://... # 显式 .exe 绕过别名
|
||
```
|
||
|
||
cmd 里 `curl` 直接就是 `curl.exe`,无此问题。
|