Communication Architecture
ODTS-43: 系统通信架构——七种协议 + 一个元协议
目标读者:想要理解 CICC OTC 衍生品系统中 15+ 个服务如何”对话”的 BA/PM
数据来源:eds-web-app、hedging-as、eds-utility、tradedesign、odyssey 各项目的代码扫描 + 配置文件
一句话结论:这不是一个微服务架构,这是一个协议拼盘(protocol polyglot)——7 种通信协议 + 1 个元协议(tradedesign)共存的架构反映了 10 年演进过程中”每一代人用当时最好的方案”的历史事实。
相关文档:ODTS-49 详细解释了 tradedesign 协议注册中心。
概述
CICC 的 OTC 衍生品系统有 15+ 个独立服务进程。它们之间的通信不是通过统一的 API 网关或 Service Mesh,而是通过 7 种不同的协议,每种协议服务于特定的通信场景。
但在所有 7 种协议之下,存在一个第零协议——tradedesign:一个 1,709 个 Proto 文件组成的消息契约仓库,定义了所有服务之间交换的每一条消息的精确格式。详见 ODTS-49。
┌─────────────────────────────────────────────────────────────┐
│ 第零层:tradedesign 协议注册中心(15,014 commits) │
│ 定义 37 个子系统的全部消息格式(1,709 个 .proto 文件) │
├─────────────────────────────────────────────────────────────┤
│ 7 种通信协议 │
│ │
│ ActiveMQ → 服务间事件总线(Protobuf 编码) │
│ MINA+PB → 客户端-服务端实时双向通信(核心业务操作) │
│ Redis RPC → 后端服务间的同步调用 │
│ ZeroMQ → 行情数据实时推送 │
│ Kafka → 异步数据流(对外系统接口 + CDC) │
│ REST → Web API + 文件操作 + 遗留系统桥接 │
│ Feign → Odyssey 微服务间调用 │
└─────────────────────────────────────────────────────────────┘
关键事实:这不是过度工程。每种协议解决的是不同的通信需求——从毫秒级的行情推送到小时级的批处理数据提取,不存在”一个协议通吃所有场景”的方案。而 tradedesign 作为元协议,确保了无论底层是 ActiveMQ、MINA 还是 REST,消息的格式定义始终来自同一个权威来源。
协议一:ActiveMQ(JMS)——服务间事件总线
作用
ActiveMQ 是系统的中枢神经。当 eds-web-app 创建一个合约、更新一个参数、完成一笔交易时,它不直接调用其他服务,而是向 ActiveMQ Topic 发布一条消息,让感兴趣的订阅者自己拉取。
技术配置
broker: failover:(tcp://192.168.148.121:61616)
stomp: 192.168.148.94:61613
环境隔离: 所有 Topic 以数据库用户名作为前缀(如 EDS_DEV-)
关键消息流
| Topic | 生产者 | 消费者 | 传递内容 | 业务含义 |
|---|---|---|---|---|
{DB}-eds-model-parameter | eds-web-app (MqSender) | hedging-as | 模型参数 ProtoBuf | 通知定价引擎:合约/参数有变更,请重新计算 |
{DB}-eds-hedging-data | hedging-as (HedgingEntry) | eds-web-app → UI | 对冲结果 ProtoBuf | 定价引擎把计算完的对冲结果发回给 UI |
{DB}-eds-refresh | eds-web-app | STOMP → Web UI | EdsRefreshNotify | 告诉浏览器:数据变了,刷新视图 |
{DB}-risk_rule | eds-web-app | hedging-as | TextMessage (ID) | 风控规则变更,请刷新缓存 |
{DB}_AppRefreshTopic | eds-web-app | hedging-as (ClosePriceSubscriber) | 刷新类型 | 收盘价更新,通知定价引擎 |
{DB}-CurrencyRateRefreshTopic | eds-web-app | hedging-as | RefreshType | 汇率更新通知 |
核心代码
eds-utility/src/com/cicc/activeMQ/MqSender.java 封装了发送逻辑:
// 参数通知(核心消息)
sendNotifyMessage(type, deskId, uuid)
→ BytesMessage with PbMessageHead.Message (protobuf编码)
→ 属性: deskId, uuid, type=BOOKING_AS
// 刷新通知
sendRefreshByDeskEntity(deskEntityId)
→ EdsRefreshNotify protobuf
// CGM通知
sendCGMNotifyMessage(...)
→ 同上格式,用于CGM模块
消费者端由 HedgingAS.java 启动时初始化:
HedgingEntry.getInstance()
.setSender(new Sender(ActiveMQ))
.setSubscriber(new RiskRuleSubscriber())
.setSubscriber(new ClosePriceSubscriber())
.setSubscriber(new CurrencyRateSubscriber())
为什么用 Topic 而不是 Queue?
观察发现所有 ActiveMQ 通道都使用 Topic(发布订阅模式),而非 Queue(点对点模式)。这意味着:
- 一个消息可以被多个消费者同时消费
- 如果消费者不在线,消息会丢失(非持久订阅)
- 没有消息堆积风险——代价是消费者重启后可能错过中间的消息
这适合”状态同步”场景——系统不需要保证每一条消息都被处理,但需要保证”当前状态”是正确的。
协议二:MINA + Protobuf(TCP)——实时双向通信
作用
这是 CICC OTC 系统最核心的通信模式——所有的业务操作(开仓、平仓、询价、风控计算)都是通过这个通道完成的。它不是 HTTP 请求-响应模式,而是客户端和服务端之间建立的持久 TCP 连接,双向发送 Protobuf 编码的消息。
为什么需要专用 TCP 协议?
在 2015-2018 年系统设计时,REST/HTTP 的吞吐量和延迟不足以支持交易系统的实时交互需求。MINA(Java 高性能 NIO 框架)+ Protobuf(紧凑二进制序列化)的组合在当时是金融系统的标准选择——类似国内其他券商的定制的 TCP 协议。
架构
Web UI (odyssey前端 / new-edsweb)
│
│ MINA TCP (Protobuf编码)
▼
AccessApp (网关层, 127.0.0.1:52277)
│
│ MINA TCP (Protobuf编码, 服务端网关)
▼
hedging-as (主要服务端, 192.168.166.111:54000)
│
│ Redis RPC (见协议三)
▼
eds-web-app (BookingApp, 辅助后端)
服务端配置
comm.properties 中定义了三种协议编码器:
clienttype1.codec=ProtoBufCodecFactory (UI客户端)
clienttype2.codec=MessageDecorationProtobufServerFactory (后端服务网关)
clienttype3.codec=ProtoBufCodecFactory (GUI服务)
每个客户端类型使用不同的编码器,因为通信的消息格式不同(UI 的消息需要额外的路由头部字段)。
消息路由
每个 MINA 消息都通过 GatewaySessionManager 路由。这是一个ConcurrentHashMap<String, GatewaySession>,key 是 session ID,value 包含了 session 的元数据和输出通道:
GatewaySessionManager.sendMD() // 发送市场数据
GatewaySessionManager.sendDynamicToBookingAs() // 发送动态请求到定价引擎
GatewaySessionManager.putMessage() // 存入消息队列
这个架构的代价
这个定制的 TCP 协议虽然性能好,但代价很高:
- 调试困难:不能像 REST 那样用 curl 或浏览器测试,必须有专用的客户端库
- 连接管理:长连接需要心跳、重连、会话恢复逻辑
- 版本兼容:Protobuf schema 的升级需要所有客户端同步更新
- 新手学习曲线陡峭:新开发者需要理解 MINA 的 IoSession、IoHandler、ProtocolCodecFactory 等概念
这也是后来 odyssey 微服务转向 REST/Feign 的原因之一。
协议三:Redis RPC(Linda 框架)——后端服务同步调用
作用
当 eds-web-app 需要对冲引擎计算某个合约的风险指标,或者 hedging-as 需要把日终风控结果写回数据库时,它们通过 Redis RPC 通信。这是一个轻量级的类 RMI 框架,使用 Redis 作为注册中心和传输层。
配置
rpc.properties:
namespace=dev1
redisHost=192.168.148.177
redisPort=6379
password=ImsGwOms
database=1
服务接口
| 接口 | 提供方 | 调用方 | 典型方法 |
|---|---|---|---|
HedgingRpcService | hedging-as | eds-web-app | calcVolatility(), runEodRiskCheckByDeskEntityId(), refreshContract(), refreshParameter() |
BookingService | eds-web-app | hedging-as | setEodRiskCheckResult(), updateContract() |
有趣的回调模式
eds-web-app 调用 hedging-as.calcVolatility()
↓
hedging-as 计算结果
↓
hedging-as 回调 BookingService.setEodRiskCheckResult()
↓
eds-web-app 收到结果
这是一个双向 RPC——调用链上的服务可以互相调用,而不是传统 Client → Server 的单向模式。因为 hedging-as 计算完后需要把结果持久化,而持久化的入口在 eds-web-app 的数据层。
初始化
// hedging-as 启动时
RpcEntry.initServer(SenderType.HEDGING_AS)
.register(HedgingRpcService.class, new HedgingRpcServiceImpl())
.initClient(SenderType.HEDGING_AS)
.register(BookingService.class)
.startUp();
// eds-web-app 启动时
// 类似的初始化,方向相反
协议四:ZeroMQ(PUB/SUB)——行情数据推送
作用
ZeroMQ 专门用于市场数据(行情)的实时推送——这对 OTC 衍生品来说不是实时交易所行情,而是计算后的中间价、估值、对冲持仓等需要推送到 UI 的数据。
架构
hedging-as (行情计算服务)
│
│ ZMQ PUB (topic + protobuf bytes)
▼
┌─────────────────────────────────────┐
│ ZmqServer (PUB socket, singleton) │
│ zmqaddress: tcp://192.168.162.197:5510 │
│ zmqSnapaddress: tcp://192.168.162.197:5501 │
└─────────────────────────────────────┘
│
├──→ Web UI (订阅者1)
├──→ Web UI (订阅者2)
└──→ ...
主题格式
C^ODTS^MNT^{exchId}^{contractId}^
│ │ │ │ │
│ │ │ │ 合约ID
│ │ │ 交易所ID
│ │ 监控类型 (Monitor)
│ OTC 衍生品
类别前缀
数据内容
MQPushService.pushContractMonitor() 构建一个 CICCDataFeed protobuf 消息,包含合约持仓、盯市盈亏、保证金等实时计算数据。
ZMQ 的使用表明系统对行情推送有低延迟要求——ActiveMQ 的 JMS 协议在消息吞吐量和延迟上不如 ZMQ 的原生 PUB/SUB 模型。
协议五:Kafka——异步数据流
作用
Kafka 是系统中最晚引入的通信协议(约 2023 年后),用于与外部系统的数据交换和数据库变更捕获(CDC)。
配置
kafka.host=10.21.237.13:9364,10.21.237.13:9365,10.21.237.13:9366
kafka.security_protocol=SASL_PLAINTEXT
kafka.sasl_mechanism=SCRAM-SHA-256
数据流
| Kafka Topic | 方向 | 数据内容 |
|---|---|---|
CICC_ODTS_CONTRACT_POSITION_TOPIC | eds-web-app → 外部系统 | 合约持仓快照 (Protobuf) |
odts.eod.dev | eds-web-app → 外部系统 | OTC 持仓调整指令 |
margin_report_done | eds-web-app → 外部系统 | 保证金报告完成通知 |
ofa-linear-order-topic-dev | eds-web-app → PB 系统 | PB 订单数据 (JSON) |
ofa-linear-contract-generate | eds-web-app → PB 系统 | 合约生成事件 |
ofa-linear-dma-account-import-dev | eds-web-app → PB 系统 | DMA 账户导入 |
riskdata-restricted-list | 外部系统 → eds-web-app | 受限黑名单数据 |
CICC_FE_PB_NOTES_POSITION_TOPIC | 外部系统 → eds-web-app | PKS 票据持仓同步 |
CICC_PB_HEDAGE_POSITION | 外部系统 → eds-web-app | PKS 对冲持仓同步 |
CICC_PB_HEDAGE_POSITION | 外部系统 → eds-web-app | PKS 对冲持仓同步 |
odts-cdc-* | 外部系统 → eds-web-app | 数据库 CDC 事件(TA资质等) |
为什么引入 Kafka?
Kafka 解决了 ActiveMQ 的两个问题:
- 消息持久化:Kafka 的日志结构保证了消息不丢失,消费者可以回溯消费
- 外部系统集成:ActiveMQ 是内部防火墙后的,Kafka 可以和外部系统(如 PB 系统、PKS)共享
协议六:REST(HTTP)——Web API 层
作用
REST 是系统的用户可见层——前端(new-edsweb、odts-linear-web)通过 REST API 与后端交互。
eds-web-app REST 端点
eds-web-app 在 localhost:8087 暴露约 20+ 个 @RestController:
| Controller | 路径前缀 | 功能 |
|---|---|---|
ClosePriceController | /closePrice | 收盘价管理 |
RiskController | /risk | 风险分析 |
ReportController | /report | 报表(中证、做市、估值) |
SacReportController | /sacReport | SAC 报送审核 |
ValProcessController | /valProcess | 估值处理 |
PBRateConfigManagerController | /pbRateConfigManager | PB 利率配置 |
GeminiMailController | /geminimail | 邮件发送 |
edsWeb(旧)REST 端点
旧系统 edsWeb 暴露的 REST 接口被 Odyssey 微服务通过 Feign Client 调用:
| 端点 | 调用方 | 用途 |
|---|---|---|
POST /sacReport/submitSacReport | odyssey-report-processing-service | SAC 报告提交 |
POST /sacReport/precheckSacReportData | odyssey-report-processing-service | SAC 预检查 |
/closePrice/* | HttpClientHelper | 收盘价查询 |
协议七:Feign(Spring Cloud)——Odyssey 微服务间调用
作用
在 Odyssey 微服务体系内,服务间使用 Spring Cloud OpenFeign 进行声明式 REST 调用,通过 Nacos 做服务发现和配置管理。
客户端定义
| Feign Client | 目标服务 | 所属微服务 |
|---|---|---|
FuxiAuthLoginFeignClient | odyssey-fuxi-auth-login | internal-gateway |
FuxiAuthVerifyFeignClient | odyssey-fuxi-auth-verify | internal-gateway |
OdtsWebClient | edsWeb(遗留系统) | report-processing-service |
DemoFeign | fuxi-job-demo-service | cash-manager, quotation |
新旧桥接模式
Odyssey 到遗留系统(edsWeb)的调用是整个架构中最复杂的集成点。以 SAC 报告提交流程为例:
用户操作(Odyssey UI)
│
│ HTTP
▼
odyssey-report-processing-service
│
│ Feign (OdtsWebClient)
▼
edsWeb (旧系统 / 遗留 REST API)
│
│ MINA/Protobuf (DynamicProtoBufSacClientService)
▼
后端 (hedging-as 或 sac-as)
这意味着 Odyssey 的前端每次 SAC 操作都经过了三层协议转换:
- HTTP → 2. REST (Feign) → 3. MINA/Protobuf
每一层转换都增加了延迟、错误风险和调试复杂度。
通信架构演进时间线
2015-2016 (系统奠基)
│ ActiveMQ + MINA/Protobuf + Redis RPC
│ "一切用定制的 Java 协议,性能优先"
│
2017-2019 (功能扩张)
│ 加入 ZeroMQ 处理行情推送
│ REST 出现,但主要用于文件上传和报表查询
│
2020-2022 (前端独立化)
│ REST 成为前端 (Vue) 的主要接口
│ 但后端之间的"硬核操作"仍然走 MINA/Protobuf
│
2023-2025 (Kafka + Odyssey)
│ 引入 Kafka 处理外部系统数据交换
│ Odyssey 使用 Feign + Nacos
│ 新旧系统通过 edsWeb REST API 桥接
│
2026 (现状)
│ 七种协议并存
│ 没有"替换旧协议"的计划——每种协议仍有其不可替代的场景
外部系统通信映射 / External System Protocol Map
上述七种协议描述了 ODTS 内部服务之间的通信。在与外部系统交互时,使用的协议和模式有所不同:
| 外部系统 | 协议 | 模式 | 方向 | 数据格式 | 关键服务 |
|---|---|---|---|---|---|
| IPMP 资金划拨 | HTTP | 异步+轮询 | 双向 | XML | Odyssey cash-manager-service |
| Cert App 数字证书 | HTTP REST | 同步请求-响应 | 双向 | JSON | eds-web-app (DocRest接口) |
| Doc Agent 文档生成 | HTTP REST | 同步请求-响应 | 请求→生成← | JSON | eds-web-app / odyssey |
| JUMS 通知服务 | HTTP REST | 同步请求 | 单向→ | JSON | Odyssey cash-manager-service |
| HCP 对象存储 | HTTP (PUT/GET) | 同步 | 双向 | 二进制 | eds-web-app (HcpHelper) |
| SacAS 监管报送 | HTTP | 同步批量 | 正向→ | Protobuf | eds-web-app (SacReportAction) |
| Dipper / Pulsar | Protobuf over TCP | 异步数据同步 | 单向→ | Protobuf → CSV | 数据平台 |
| SFTP 文件交换 | SSH/SFTP | 文件推送+拉取 | 双向 | CSV / XML | 批处理脚本 |
协议叠加 — 以结算流程为例
一笔结算操作跨越了多种协议:
运营点击"批准转账" (HTTP REST)
→ eds-web-app / odyssey (Feign)
→ cash-manager-service 组装 IPMP XML
→ HTTP POST 到 IPMP (XML)
→ 轮询 IPMP 状态 (HTTP GET, XML)
→ 转账完成
→ cash-manager-service 调用 JUMS (HTTP REST, JSON)
→ JUMS 发送邮件+SMS通知客户和运营
→ 更新 CashTransfer 状态 (JDBC/数据库)
→ 通过 ActiveMQ 通知其他服务状态变更
一笔操作涉及 5 种不同的协议,这也是为什么”全链路追踪”在 ODTS 中几乎不存在——没有一种工具可以横跨 HTTP、XML-over-HTTP、JSON-over-HTTP、JDBC 和 ActiveMQ 做端到端追踪。
外部系统的错误处理模式
| 外部系统 | 超时配置 | 重试策略 | 幂等性 |
|---|---|---|---|
| IPMP | connectTimeout=30000, readTimeout=60000 | 状态轮询自动重试 | 通过 ftOrderNo 保证幂等 |
| Cert App | 默认 HTTP 超时 | 5 次重试 + 退避 | 通过 signatureId 保证 |
| Doc Agent | 默认 HTTP 超时 | 3 次重试 | 无(需要运营确认生成结果) |
| JUMS | 默认 HTTP 超时 | 1 次重试+回退 | 可能存在重复通知 |
| SacAS | 批量提交无超时 | 人工重试 | 通过 bussiDataSeq 保证 |
运维启示
故障排查的复杂度
当系统出现问题时,问题可能发生在七种协议的任意一层:
| 症状 | 可能的原因 |
|---|---|
| UI 数据显示”等待中” | ActiveMQ 消息丢失(非持久 Topic,消费者不在线) |
| 某个合约的定价不更新 | Redis RPC 调用超时或服务端线程池满 |
| 行情不刷新 | ZMQ PUB 连接断开或 socket 缓冲区满 |
| 结算数据不一致 | Kafka 消费偏移提交失败导致重复/丢失消费 |
| 页面加载慢 | REST API 的数据库查询慢(与协议无关) |
为什么没有统一?
对于外部观察者,最自然的问题是:为什么不用 gRPC 统一所有通信?
答案在与这些协议的引入时间:
| 协议 | 引入时间 | 当年的”最佳选择” |
|---|---|---|
| MINA + Protobuf | ~2015 | gRPC 还没发布(2016 年才出 1.0) |
| ActiveMQ | ~2015 | 当时 Java 生态最成熟的消息中间件 |
| Redis RPC | ~2016 | 简单的服务间调用,无需注册中心 |
| ZeroMQ | ~2017 | 当时最快的内存级消息队列 |
| REST | ~2018 | 前端独立化的必然选择 |
| Kafka | ~2023 | 外部系统集成需求驱动 |
| Feign | ~2024 | Odyssey 带来 Spring Cloud 生态 |
这是一个10 年演进的时间胶囊。每一层都是当时最合理的选择,没有任何一代人做出了”错误”的决定。
十、这些协议出问题时的业务代价
ActiveMQ Topic 重启丢消息——EOD 风控空跑
某次 hedging-as 在 EOD 开始时重启(运维升级),重启期间 ActiveMQ 发了 3 条模型参数变更通知。因为 Topic 是非持久订阅,重启后的 hedging-as 没有收到这 3 条通知——它不知道某个雪球产品的波动率参数在下午被更新了。
EOD 批处理:
hedging-as 用旧的波动率参数跑完了全部定价
→ EOD 风控报告显示所有风险指标"正常"
→ 实际参数已经变了 3 小时,风险值应该不同
业务影响:
→ 第二天交易员用 EOD 报告做开盘决策——基于错误的数据
→ 直到下午才发现"昨天的 EOD 报告参数对不上"
→ 重新跑 EOD 花了 3 小时(资源受限,白天不能跑全量)
→ 交易台在错误的风险视图下做了半天交易
技术反思:
Topic 的非持久订阅是"设计时就知道的取舍"。
但业务方不知道——他们以为系统重启后一切正常。
PM/BA 在评审这种架构选择时应该问:
"Topic 丢消息的概率多大?业务上能接受吗?"
MINA 连接耗尽——交易员无法下单
MINA 的长连接模式要求客户端连接数在服务端设置的线程池范围内。某次新前端上线时,连接数从 50 翻到了 300。hedging-as 的 MINA IoHandler 线程池(默认 128)被打满。
现象:
→ 部分交易员的客户端连不上
→ "卡在登录界面"或者"点击交易按钮没反应"
→ 但不是所有交易员都受影响——看谁的 session 先被挤掉
业务影响:
→ 15 分钟内有 3-5 个交易员无法操作
→ 此时市场有快速变动,无法及时减仓
→ 后续排查发现是连接数超限,紧急重启 + 扩容线程池
→ 15 分钟的"部分不可用"对交易台来说非常漫长
技术原因:
这个线程池参数(128)是 2017 年设定的。
没有监控来告警连接数的增长趋势。
新前端上线没有做连接数压测。
Redis RPC 超时——跨服务调用雪崩
Redis RPC 在后端服务之间做了大量同步调用。某次 Redis 实例高负载(因为有另一个团队在同一个 Redis 实例上跑了一个大查询),RPC 调用的响应时间从 1-2ms 涨到了 200-300ms。
雪崩链条:
eds-web-app 调用 hedging-as.calcVolatility() → Redis RPC 超时(300ms)
↓
eds-web-app 的请求线程被阻塞等待
↓
阻塞线程数超过 tomcat 连接池
↓
新的交易簿记请求被拒绝(503)
↓
连锁效应:所有依赖 eds-web-app 的前端也无法操作
业务影响:
→ 交易簿记暂停约 10 分钟
→ 排查发现 Redis RPC 的超时设置是 5 秒——太长了
→ 一个 RPC 慢导致了整个系统的级联故障
教训:
同步 RPC 调用是架构中最危险的耦合。
一个服务慢 → 调用方也慢 → 调用方的调用方也慢。
这是"通信协议选择"后面隐藏的架构风险。
数据目录
通信配置:
odts1/eds-web-app/config/comm.properties → ActiveMQ, Kafka, ZMQ, MINA 配置
odts1/eds-web-app/config/rpc.properties → Redis RPC 配置
odts1/hedging-as/config/comm.properties → ActiveMQ, ZMQ, MINA 配置
odts1/hedging-as/config/rpc.properties → Redis RPC 配置
odyssey/odyssey-report-processing-service/.../application.yml → Nacos + Feign
核心代码:
odts1/eds-utility/src/com/cicc/activeMQ/ → ActiveMQ 封装
odts1/hedging-as/src/com/cicc/communication/ → MINA/Protobuf + Gateway
odts1/hedging-as/src/com/cicc/service/infoQutation/zmq/push/ → ZMQ 行情推送
odts1/hedging-as/src/com/framework/rpc/ → Redis RPC 接口定义
odts1/eds-web-app/src/com/cicc/wsmq/ → STOMP WebSocket 推送
Kafka 代码:
odts1/eds-web-app/src/com/cicc/kafka/ → 生产者/消费者实现
odyssey/odyssey-report-processing-service/.../kafka/ → Odyssey Kafka 监听器