部署与运维
rocky-bot 跑在 EC2 上 systemd-user 服务里,由 30 个由 mint-30.sh 一次性 mint 出来的账号驱动。本文讲完整的"从零到运行 + 持续监控 + 故障复盘"。
一、初次 provisioning:mint 30 个账号
源码:rocky-bot/scripts/mint-30.sh
cd /Users/ubuntu/Desktop/Rocky/rocky-bot
bash scripts/mint-30.sh > .keys.json
脚本做的事(~1.5 分钟,全部 SSH 进 EC2):
- 生成 30 个 role manifest(python3 内联生成 JSON)
- 12 个 BUY ladder(5/6/7/8/9/10 bps × L1 + 15/20/25/30 bps × L2 + 50/100 bps × L3)
- 12 个 SELL ladder(对称)
- 1 个 anchor
- 5 个 taker
- 逐个调用 EC2 的
mint_api_keyRust binary- 在 rocky-backend repo 里跑
cargo run -p api-gateway --bin mint_api_key -- --new-user --label '<id>' - 后端会创建一个 auth.api_keys 行 + 一个新用户 user_id
- 返回
user_id / api key / secret
- 在 rocky-backend repo 里跑
- 每个新用户立即 seed $100 USDC
- 调 backend
/v1/deposits/seed接口
- 调 backend
- 聚合最终
.keys.json输出到 stdout
.keys.json 结构示例:
{
"rocky_fapi_url": "https://demo.rocky.exchange",
"accounts": [
{
"id": "mm-l1-buy-05bps",
"role": "ladder",
"side": "BUY",
"offset_bps": 5,
"user_id": "5cfb031b-5936-4467-9533-cd2df576dbb8",
"key": "...",
"secret": "..."
},
...30 总数
]
}
安全注意:.keys.json 含 30 对 (key, secret),是高敏感。
.gitignored,永不进 git- 仅在 deploy 时通过 scp 复制到 EC2
- 本地需要保留一份用于重新 deploy
1.1 SSH 多路复用
mint-30.sh 用 ControlMaster 复用单个 SSH 连接,避免开 30 次 SSH 触发服务器 MaxStartups 限速:
SSH_OPTS=(-i "$SSH_KEY" -o ControlMaster=auto -o ControlPath="$CTRL_PATH" -o ControlPersist=60)
历史上没加这一行时,第 8 个连接就被 sshd 拒绝("connection reset by peer")。
二、Deploy:上线 / 升级 bot 代码
源码:rocky-bot/deploy.sh
cd /Users/ubuntu/Desktop/Rocky/rocky-bot
./deploy.sh
做的事:
rsync把rocky_bot/源码同步到 EC2~/rocky-bot/scp .env(环境配置)+scp .keys.json(账号凭证)- SSH 执行
uv venv --python 3.12 --allow-existing && uv pip install -e . - SSH 执行
systemctl --user restart rocky-bot
rsync 配 --exclude .env --exclude .keys.json 让敏感文件走单独的 scp 通道。
2.1 systemd 单元
# ~/.config/systemd/user/rocky-bot.service
[Unit]
Description=rocky-bot — volume generator for demo.rocky.exchange
After=network.target
[Service]
Type=exec
WorkingDirectory=%h/rocky-bot
ExecStart=%h/rocky-bot/.venv/bin/python -m rocky_bot.main
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
控制命令:
systemctl --user start rocky-bot
systemctl --user stop rocky-bot
systemctl --user restart rocky-bot
systemctl --user is-active rocky-bot
journalctl --user -u rocky-bot --since "5 min ago" --no-pager
三、Reset:清空状态 + 重启
源码:rocky-bot/scripts/reset.sh
bash scripts/reset.sh
5 步:
systemctl --user stop rocky-bot- SQL reset 30 个 funnel 账号的状态:
UPDATE ledger.positions SET qty=0, locked_margin=0 WHERE user_id IN (funnel users);UPDATE ledger.accounts SET available = available + locked, locked = 0WHERE asset='USDC' AND user_id IN (funnel users);DELETE FROM ledger.orders_open WHERE user_id IN (funnel users);
pkill -f 'target/release/matching-engine'+ nohup 重启 ME(让内存订单簿与刚清空的orders_open同步)sleep 3systemctl --user restart rocky-bot
3.1 为什么要 restart matching-engine
历史教训:早期 reset 只动 DB 没动 ME,ME 内存里还有旧订单引用着已被 DELETE 的 orders_open 行。新 bot 一启动就和这些"幽灵订单"撮合 → backend apply.rs 找不到对应 order → 走 leverage=1 fallback → margin 计算错误。
加这一步后,ME 重启 时从(已清空的)orders_open 重新装载,本来就是空的。
四、监控(运行时)
四个关键指标:
4.1 max(locked) — 是否有账号接近爆仓
ssh ... 'docker exec rocky-backend-stack-postgres-1 psql -U rocky -d rocky -c "
SELECT round(max(locked)::numeric, 2) AS max_l,
round(avg(locked)::numeric, 2) AS avg_l,
count(*) FILTER (WHERE locked > 50) AS over_50,
count(*) FILTER (WHERE locked > 80) AS over_80
FROM ledger.accounts a JOIN auth.api_keys k ON k.user_id = a.user_id
WHERE a.asset = '\''USDC'\''
AND (k.label LIKE '\''mm-%'\'' OR k.label LIKE '\''taker-%'\'')
"'
健康标准(基于 leverage-fix 后的实测):
max_l < 50USDCover_80 == 0avg_l ~ 20-30USDC
异常:max_l 接近 100 表示 cap 没起作用 → 严重事故。
4.2 -2010 错误数 — backend 拒绝挂单的频率
ssh ... 'journalctl --user -u rocky-bot --since "30 min ago" --no-pager 2>&1 | grep -c "\-2010"'
-2010 = "insufficient balance",发生在 backend 余额不足以锁定挂单 margin 时。健康标准:
- 30 分钟内
< 30(平均 1/min 以下) - 完美状态:
0
异常:> 100 表示账号被锁满 → 一般伴随 max_l 接近 100。
4.3 recent trades — 是否还有成交活动
ssh ... 'docker exec rocky-backend-stack-postgres-1 psql -U rocky -d rocky -c "
SELECT symbol, side, price, qty, ts FROM ledger.trades ORDER BY ts DESC LIMIT 3
"'
健康:最新一笔 ts 在 1 分钟内。
异常:ts 超过 5 分钟 → bot 没在工作(可能挂了 / cap 锁满了)。
4.4 invariant logger — backend 计算正确性
ssh ... 'grep -c "invariant violated" /tmp/rocky-services/internal-ledger.log'
健康:0 或稳定低数值(仅 startup 噪声)。
异常:持续增长 → backend 撮合层有 bug。
五、5 个 margin-leak 修复轮次
完整的事故复盘(编年史),帮助理解为什么当前架构是这个样子:
Round 1:bot-side position cap(commits 077887d / 526cac5 / 97d23a6 / 1ae6f1a)
症状:30 分钟内 max(locked) 涨到 $99。 修复:给 LadderMakerLoop / AnchorMakerLoop 加 position-cap gate(策略详解 §2.4)。 结果:减缓但未根治。
Round 2:phantom-trade refusal(commits e67b63f / fd6b9e2)
症状:cap gate 生效后,仍有 leak。诊断发现 apply_trade_matched 在找不到 orders_open 行时 fallback 到 leverage=1。
修复:apply.rs 早期 return + 发 OrderCancelled NATS 让 ME 删除幽灵订单。
结果:减少但仍有 leak。
Round 3:invariant instrumentation(commit e32ae17)
症状:仍 leak,但找不到具体在哪一笔交易出错。
修复:在 apply_trade_matched 末尾加 tracing::error! 检查 accounts.locked == sum(positions.locked_margin) + sum(orders_open.margin_locked)。
结果:30 分钟收集 956 条违规记录。数据特征:pos_sum 中含 142857 循环(即 1/7)→ leverage 被错算成 7。
Round 4:reset.sh ops 修复
症状:reset 流程经常半失败(matching-engine 没被重启 / SQL heredoc 因 ssh 引号被吞)。 修复:reset.sh 完整重写,加 ME pkill + restart + stdin pipe SQL。 结果:清理变可重复,但 leak 本身还没修。
Round 5:LEVERAGE_V1 常量(commit dd653e6)
根因:apply.rs 用 notional / order_margin 反推 leverage,partial fill + price drift 后会 round 到 7/9/11。
修复:用 const LEVERAGE_V1: u32 = 10; 替代推导,与 api-gateway 早就 hardcode 的 leverage: 10 对齐。
结果:100% 修复。部署后 63 分钟运行 max_locked 稳定在 $27,0 个 invariant violation,0 个 -2010 错误。
六、故障排查 checklist
bot 似乎没在工作
systemctl --user is-active rocky-bot— 服务还活着吗journalctl --user -u rocky-bot --since "1 min ago"— 看最近日志- 看 max(locked) 是否接近 $98 — 如是,账号锁满
- 看 recent trades 是否新鲜 — 如不,可能 BinanceFeed 断了
CB 反复跳闸
- 看 journal 里 "CircuitBreaker opened: reason=..." — API errors / max loss
- 如是 API errors:backend 是否 healthy?
curl https://demo.rocky.exchange/api/perp/markets - 如是 max loss:人为 reset CB(重启 bot)OR 增加 RiskCaps.max_loss_usdc
invariant violation 重新出现
- 查具体哪一笔:
grep "invariant violated" /tmp/rocky-services/internal-ledger.log | head -3 - 检查 violation 数据里的
diff数值 + leverage 推断 - 如果是 leverage 相关,回到 round 5 的修复路径
- 如果是 new pattern,可能是 backend 新代码引入新 leak → 开 spec 重新走诊断流程
七、相关阅读
- 做市机器人概览 — 整体架构
- 策略详解 — 三种策略的内部逻辑
- 风控与杠杆 — RiskCaps / CircuitBreaker / position cap / LEVERAGE_V1
- 仓库 spec 文档(完整事故复盘):
rocky.interface/docs/superpowers/specs/2026-05-25-rocky-bot-position-cap-fix-design.mdrocky.interface/docs/superpowers/specs/2026-05-25-phantom-trade-fix-design.mdrocky.interface/docs/superpowers/specs/2026-05-25-margin-leak-instrumentation-design.mdrocky.interface/docs/superpowers/specs/2026-05-25-leverage-derivation-fix-design.md