Modernization 2021 2024
ODTS 04 — 现代化 2021–2024:Strangler Fig、微服务实验与系统现状
业务背景 2021–2024
到 2021 年,中金 OTC 衍生品交易台不再是创业团队了。它是一个中国 OTC 衍生品 top-3 做市商,和中信、广发及其他主要券商直接竞争。
业务压力有四个来源:
1. 利率压缩 (Margin Compression)
雪球 15% 票息的黄金时代结束了。到 2022 年,雪球票息只有 5–7%。交易台需要量来维持收入。
2018 年收入模型:
雪球票息: 15%
对冲成本: 8%
利差: 名义本金的 7% → 10 亿 RMB 组合产生 7000 万 RMB
2023 年收入模型:
雪球票息: 6%
对冲成本: 4%
利差: 名义本金的 2% → 10 亿 RMB 组合产生 2000 万 RMB
(同样收入需要 3.5 倍的名义本金)
系统影响: 交易台需要用同样的团队处理更多交易、更快。自动化不再是可选的——是必须的。
2. 新竞争对手
到 2022 年,几家新进入者用现代技术栈(云原生、微服务、Kafka、React)从零搭建了 OTC 衍生品系统。中金 2016 年起步的技术栈开始显得老了。
3. 人民币产品扩张
从 2022 年起,交易台大力扩展到人民币计价的结构化产品——瞄准中国境内投资者(之前大部分量是通过香港的外资投资者)。
对系统的意义:
- 境内产品有不同的监管要求(CSRC vs SFC)
- 结算通过中央结算公司 (CCDC),不是 Euroclear
- 客户准入 (KYC) 需要中国内地特定的流程
- 系统一直是为香港/港币产品建的——加人民币意味着全新工作流
4. 2022 年雪球危机
2022 年,A 股又跌了。中证 500 从 7300(2021 年 12 月)跌到 5200(2022 年 4 月)——跌幅 29%。很多从 2021 年就开始跑的雪球敲入了 (knocked in)。
这是第二次生产危机:
- ~2000 个雪球合约需要障碍事件处理 (barrier event processing)
- EOD 批处理跑了 4+ 小时(正常 90 分钟)
- 初始保证金/变动保证金 (IM/VM) 飙升——有些对手方付不出来
- 系统的障碍事件处理器从未处理过一天 200 个事件(它是为一天 10–20 个设计的)
什么炸了: hedging-as/service/eod/BarrierCheckStep.java——原始实现是单合约逐个检查障碍。2000 个合约 × 5+ 个障碍位 = 10000 次检查 × ~100ms = 1000 秒。修复方案:并行化(Java parallel stream)。
业务代价: 交易员在关键时刻拿不到准确的保证金数据。对手方在等催缴通知 (margin call),系统在慢跑。一个对话:
交易员:“为什么 EOD 还没跑完?” 开发:“有 2000 个雪球要检查障碍事件,系统设计时没想过这种情况。” 交易员:“所以我们现在给对手方发催缴是盲发的?” 开发:”……差不多。”
又学到的教训: 每一次大的市场事件都会把系统的某个部分搞崩。系统从未在规模上被测试过——每次都是从生产里发现瓶颈。障碍处理应该默认并行,而不是串行。
两个前端的由来
这是一个关键的文化问题。
2021–2022 年,团队分成了两组:
A 组(“eds”团队):
- 维护
new-edsweb(Vue 2 + Element UI) - 拥有
eds-web-app(Spring Boot) - 专注交易簿记、确认书、报告
B 组(“期权”团队):
- 建了
odts-option-web(Vue 2 + KoiUI) - 维护
hedging-as(定价引擎) - 专注风险管理、Greeks、对冲
为什么另建一个前端: B 组想要”专业”的风控 UI。A 组的 new-edsweb 专注交易操作(簿记,不是风控)。KoiUI 组件库有更好的图表和仪表盘。两个组在不同办公室(上海 vs 香港)。
结果: 同一个后端,两个前端,没共享组件。两个团队各自建了:合约列表页、保证金展示页、交易搜索和过滤、Excel 导出。
重复现在开始实实在在地烧钱了。每个功能都要建两遍。
2022 年的解决方案尝试: 开了个会。“能不能共享 Vue 组件?“技术答案是”可以”,但实际答案是”不行”——两个项目有不同的构建流程、不同版本的 Vue 2、不同的设计语言 (Element vs KoiUI)。重构需要数周,大家都很忙。两个前端继续。
真正的原因: 没有任何一个团队的领导有权力命令另一个团队”你必须用我们的组件库”。没有跨团队的技术负责人。组织架构决定了系统架构。
Strangler Fig 迁移 (2022–2023)
到 2022 年,JSP 前端 (edsWeb) 已经”维护模式”3 年了。但它没死掉。用户还在用它做:历史交易查询(Vue 应用没有完整历史)、管理功能(用户管理、系统配置)、未迁移的报告。
方法: 不一次性迁移所有功能。新功能写 Vue,逐个扼杀 (strangle) 老的 JSP 页面。
- 对同一功能加新 Vue 页面
- 更新路由指向 Vue 页面
- JSP 页面保留(但没人用了)
- 6 个月后没人抱怨,删除 JSP 页面
JSP 页面的消亡:
- 2021: ~150 个 JSP 文件
- 2023: ~80 个(报告页面,它们反抗最激烈)
- 2024: ~50 个(有些永远不会死,见下文)
幸存页面: 自定义报告(用户热爱他们的 Excel 导出)、管理功能、系统日志查看器、小工具。
为什么它们活下来了: 报告是针对特定用户高度定制的。每个用户有自己的 Excel 格式。迁移意味着重新协商 20+ 个报告格式。管理功能很少用——不值得迁移。有些经理喜欢旧 UI,因为”我知道东西在哪”。
教训: 你可以扼杀一个单体应用,但你扼杀不了用户的 Excel 报告。自定义报告是遗留系统的小强——它们能挺过一切。
Odyssey 实验 (2023)
到 2023 年,团队决定现代化服务层。计划:把功能从 eds-web-app 单体重构到独立微服务。
想法是这样:
eds-web-app (单体) Odyssey (微服务)
│ │
├── 簿记 ──────────────────→ ├── booking-svc
├── 确认书 ────────────────→ ├── confirm-svc
├── 保证金展示 ────────────→ ├── margin-svc
└── 报告 ─────────────────→ └── report-svc
实际发生了什么:
“Odyssey”项目建了 3 个微服务,但都没有真正完成:
~/odts1/odsy-book-svc/ — 簿记服务(部分完成)
~/odts1/odsy-core-common/ — 共享领域模型
~/odts1/odsy-confirm-svc/ — 确认书服务(部分完成)
为什么叫 Odyssey(奥德赛): 名字是激励性的——漫长的回家之旅。后来变成了讽刺。
交付了什么: 共享领域模型(一堆类,不是运行的代码)、一个能处理简单股权互换的簿记服务、一个包装了现有确认书生成器的确认书服务。
没交付什么: 完整的簿记工作流(不能处理复杂产品)、与 hedging-as 的集成(仍然调老旧的 ActiveMQ 队列)、微服务的新前端(用户还是用 new-edsweb/odts-option-web)。
为什么卡住了:
- 业务不会停下来等架构。 Odyssey 在建时,交易台在催新产品(Bond TRS、Daily Accrual、Phoenix)。这些需求建在旧单体里,因为”等不了 6 个月 Odyssey”
- 团队分裂。 部分开发做 Odyssey,其他做需求。需求团队有更紧急的工作=更多资源。Odyssey 变成了”下班后”项目
- 微服务没有解决真正的问题。 真正的问题不是单体——是两个前端。但没人想讨论合并前端(太政治了)
- ActiveMQ vs Kafka。 Odyssey 想要 Kafka。现有系统用 ActiveMQ。新旧集成需要一个桥。没人想建和维护
事后的判断: Odyssey 是个好想法,但在错误的时间执行了。应该把精力花在:合并两个前端为一个 SPA 共享组件、用 Kafka 替换 ActiveMQ、加 CI/CD 和自动化测试。微服务应该是最后一步,而不是第一步。
EOD 批处理:系统最关键的路径
到 2023 年,日终批处理 (EOD batch) 处理:
| 步骤 | 功能 | 2021 耗时 | 2023 耗时 |
|---|---|---|---|
| 市场数据刷新 | 获取价格、波动率、利率 | 5 分钟 | 5 分钟 |
| Fixing | 处理所有浮动端的引用价 | 10 分钟 | 15 分钟 |
| 公司行为 | 应用分红、拆股等 | 5 分钟 | 10 分钟 |
| 障碍检查 | 检查所有路径依赖产品的敲入/敲出 | 15 分钟 | 30 分钟 |
| 估值 | 重新估值全部 ~5000 合约 | 30 分钟 | 60 分钟 |
| Greeks | 计算所有合约的 Greeks | 10 分钟 | 20 分钟 |
| 保证金 | 计算 IM + VM | 10 分钟 | 20 分钟 |
| 资金变动 | 生成支付指令 | 5 分钟 | 10 分钟 |
| 报告 | SAC、客户报告 | 10 分钟 | 20 分钟 |
| 合计 | ~90 分钟 | ~180 分钟 |
为什么 EOD 越来越慢: 更多合约、更复杂的产品、更多市场数据、串行处理设计(每一步必须等上一步完成)。
瓶颈: 估值和障碍检查在热点路径上都是单线程的。蒙特卡洛模拟不能完全并行化,因为随机数生成器是有状态的 (stateful)。
增量修复:
- 2022: 并行障碍检查——30 分钟 → 15 分钟
- 2023: 市场数据缓存(同一个标的不用重新取)——60 分钟 → 45 分钟
- 2023: 雪球预计算场景网格——45 分钟 → 30 分钟
EOD 失败率:
- 2019: ~3 次/月(主要是市场数据连接问题)
- 2021: ~1 次/月(偶尔定价溢出)
- 2023: ~1 次/季度(已经很稳定了)
尽管慢,EOD 是可靠的。团队知道每一种失败模式。每一步都有重试逻辑。
崩溃点总结:2021–2024 的三个关键时刻
- 2022 年雪球危机 → EOD 批处理超时,2000 个雪球同时触发障碍事件。系统设计从未考虑这种并发量。暴露出两个问题:定价引擎的单线程瓶颈、没有熔断机制 (circuit breaker)
- Odyssey 实验搁浅 → 团队花了大半年建微服务,但业务不等人。新产品还是写在旧单体里。微服务没交付就停了。根本原因:没有解决真正的问题(两个前端重复才是核心痛点)
- 两个前端重复的成本 → 每个功能建两遍,团队无法合并因为组织没有跨组技术负责人。这是整个系统历史上最大的隐性效率损失
系统现状 (2024)
项目全景
| 项目 | 行数 | 技术栈 | 状态 |
|---|---|---|---|
eds-web-app | ~15 万 | Spring Boot 2.x | 活跃 |
hedging-as | ~10 万 | JFinal | 活跃 |
new-edsweb | ~8 万 | Vue 2 + Element | 活跃 |
odts-option-web | ~6 万 | Vue 2 + KoiUI | 活跃 |
edsWeb | ~10 万 | JSP + jQuery | 衰落中 |
accessapp-new | ~3 万 | JSP + JDBC | 已停用 |
odsy-book-svc | ~2 万 | Spring Boot | 搁浅 |
odsy-confirm-svc | ~1.5 万 | Spring Boot | 搁浅 |
jzmq | ~1 万 | FIX + ZMQ | 在用 |
整个系统: ~56.5 万行生产代码,~35 个项目,20+ 个开发者历时 10 年建成。
未解决的问题
- 前端重复:未解决。 两个 Vue SPA,两个设计系统。合并的政治成本 > 维护两者的技术成本
- 测试缺口:未解决。 ~56.5 万行代码几乎没有自动化测试。每次部署都紧张
- 文档缺口:未解决。 本系列文章是解决方案的一部分。在此之前,系统的业务背景或架构没有任何文档
- EOD 调度:未解决。 批处理串行跑 3 小时。大部分日子可以接受,但波动的日子交易台想要盘中估值 (intraday valuation),EOD 不是为此设计的
- 中英文混写: 代码、注释、Protobuf 定义中英文混杂。变量名用拼音。非中文母语的开发上手困难
ODTS 系列完整。如果你已经读到这,你对这个系统的历史和架构的了解已经超过了大多数工作过 2 年的开发。代码在 ~/odts1/ 等着你。