工程与管理交易系统架构安全工程教训

Shadow Mode:新功能上线的零风险验证模式

翻最近 10 天的 git 记录,127 个 commit 里 “shadow” 出现了超过 20 次:

feat: SGR ... shadow-mode
feat: ARE Phase 1 ... shadow-mode
feat: DCB Detector ... shadow-mode
feat: EV Engine Phase 1 ... shadow recording
fix: SGR shadow mode bypass riskGate for signal recording
fix: HedgeLock recovery v2 — shadow-mode bypass P1

不是巧合——这不是某个工程师的个人偏好,而是一个系统化的工程模式。


什么是 Shadow Mode

新功能上线时,不直接接管生产流量,而是在旁路运行,只记录数据和决策日志,不产生实际副作用。

生产流量 ──┬──► 旧逻辑(真实执行)

           └──► 新逻辑(Shadow:只记录,不执行)


              shadow_log 表
              ├─ 验证决策是否正确
              ├─ 验证无异常报错
              └─ 验证 PnL/指标符合预期

验证通过后,切开关让 shadow 逻辑接管。

这个模式和广告系统的 AB 实验原理完全一样:新策略先在少量流量上跑,不真正出价,只记录 would_have_bidwould_have_won。确认 CTR 没跌再切。


这次 10 天里,哪些功能用了 Shadow

功能Shadow 记录什么跑了多久
SGR 网格策略虚拟订单 PnL + 触发条件数小时
ARE 风控引擎决策 ALLOW/DENY + 决策理由当天
DCB 死猫反弹检测检测信号 + 5 状态机进行中
EV Engine期望值统计 + 状态向量数小时
Fracture v4MFE/MAE + 信号质量数天

每个新功能,不管多小,都先在 shadow 模式下跑。

为什么这么偏执? 因为三次痛过:

  1. 多头保护策略(history):新策略直接上线,误杀了正常交易,当天亏损。如果先跑 shadow,一眼就能看出 DENY 率异常。
  2. PnL 单位错误(本次):SGR 的 recordPnl 把百分比当 USD 写。如果直接实盘,风险敞口计算全错。Shadow 日志里一看就知道 PnL 数字不对。
  3. 数据库字段名不匹配(本次):ev_shadow_log 列名不符合 ORM 约定,实盘会静默写不进去。Shadow 模式下发现日志为空,立刻排查。

Shadow 不是多此一举,是每次事故前少做的那一步。


Shadow 的三种形态

1. 纯信号记录(SGR、DCB)

// Shadow: 只记录信号,不下单
shadowLog.record(signal);
// 真实: 在风险检查后下单
riskGate.check(order) → placeOrder(order);

最简单。信号质量是核心指标——信号频率、信号方向的胜率、触发后价格走势。跑几天数据就够判断策略是否值得上线。

2. 决策对比(ARE)

// 旧风控: 各策略自己的检查
oldResult = strategy.check(order);

// 新引擎 Shadow: 旁路计算
newResult = areRiskGate.check(order);

// 对比日志
shadowLog.record(oldResult, newResult, diff);

ARE 的 shadow 不只是记录自己的决策,还和旧逻辑对比。新旧不一致 → 要么旧逻辑有遗漏,要么新逻辑有误杀。 两边都要看。

3. 指标验证(EV Engine、Fracture)

// Shadow: 旁路统计
evEngine.collect(stateVector, outcome);

// 不产生交易决策,但产出的 EV 统计被后续策略引用

这种最微妙——shadow 阶段的数据质量必须高,因为别的模块会消费这些统计结果。Shadow 数据的消费者不关心你是不是 shadow,它只关心数据对不对。


Shadow 的风险:阴影也会犯错

Shadow 模式虽然不产生实际交易,但它也有风险:

  1. Shadow 本身崩溃:如果 shadow 逻辑有 NPE,会不会拖垮主流程?不会——所有 shadow 调用都包在 try-catch 里,异常只记日志,不影响主流程。
  2. Shadow 数据污染下游:EV Engine 的统计在 shadow 阶段如果被别的策略消费,错误数据会导致错误决策。解法是:shadow 阶段的数据打标记 source=SHADOW,消费者可以选择忽略。
  3. Shadow 忘记关闭:Fracture v4 跑了 shadow 好几天没切,因为没有人明确负责”验证通过后切换”这个动作。这个后来改了流程——每个 shadow 功能上线时,assign 一个 owner 和 due date。

元结构映射:广告系统的三阶段发布

阶段广告系统交易系统 Shadow
离线验证历史日志回放单元测试
旁路验证1% 流量记录 would_have_bidShadow mode
小流量5% 流量真实出价单标的实盘
全量100%全量标的

本质相同:任何新逻辑在接管生产流量前,必须在旁路证明自己。 跳过这一步就是在赌。


教训

  1. 每个新功能默认 shadow first。 不是”重要的跑 shadow,不重要的直接上”,而是所有新逻辑不例外。因为”不重要”的判断往往是错的——那次 PnL 单位错误就出在一个”不重要”的改动里。
  2. Shadow 不是”记录一下就完了”。 必须有明确的验证标准和切换时间。跑了 shadow 三天没人看 = 没用。
  3. Shadow 数据要打标记。 下游消费者要知道这个数据来自 shadow,可以选择信任或不信任。

一句话:Shadow mode = 新功能的安全网。跳过 shadow 直接上线,等于不开降落伞跳伞——可能没事,但有事就是大事。