一、架构选择:从单体到微服务的演进之路
做交易系统这么多年,我见过太多团队一上来就搞微服务,结果把自己搞崩了。说实话,暗池交易引擎的架构选择,没那么玄乎。
1.1 单体架构:简单粗暴,但够用
先说说单体架构。说白了就是把所有功能都塞进一个进程里。订单管理、撮合逻辑、风控检查、行情推送……全在一个大锅里煮。
单体架构的优点:
- 开发简单,调试方便,一个进程搞定所有
- 延迟极低,没有网络开销,函数调用而已
- 数据一致性天然保证,不用考虑分布式事务
我在2018年做过一个项目,刚开始就是单体架构。撮合引擎跑在单机上,延迟能做到5微秒以内。嗯,这个数字在当时已经相当不错了。
但是,单体架构的坑也很明显:
- 扩容困难,只能垂直扩展(换更好的机器)
- 一个模块挂了,整个系统都崩
- 技术栈绑定,想换语言?重写吧
1.2 微服务架构:拆开来看,各司其职
微服务架构,就是把大系统拆成多个小服务。每个服务独立部署、独立扩展、独立维护。
拿暗池交易引擎来说,我一般会拆成这样:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 订单网关 │ │ 撮合引擎 │ │ 风控服务 │
│ Order Gateway│ │ Matching │ │ Risk Control│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────┐
│ 消息总线 (Kafka/Pulsar) │
└─────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 行情服务 │ │ 清算服务 │ │ 监控告警 │
│ Market Data │ │ Settlement │ │ Monitoring │
└─────────────┘ └─────────────┘ └─────────────┘
你想想看,每个服务各管一摊,出了问题只影响局部。我曾经遇到过一次风控服务挂了,但撮合引擎还在跑,订单照常处理——只是少了风控检查而已。
我的建议:刚开始做暗池引擎,别一上来就搞微服务。先拿单体架构跑通核心逻辑,等业务量上来了,再逐步拆分。我见过太多团队,微服务还没搞明白,先把系统搞死了。
二、事件驱动架构(EDA):让系统活起来
事件驱动架构,说白了就是「有事说事,没事别吵」。每个组件只关心自己感兴趣的事件,收到事件就处理,处理完再发新事件。
2.1 为什么需要EDA?
传统的请求-响应模式,在交易系统里有个致命问题:耦合太紧。订单服务调用撮合服务,撮合服务调用清算服务……一个环节卡住,整条链都堵死。
EDA的解决思路很简单:
- 订单来了,发一个「新订单事件」
- 撮合引擎订阅这个事件,处理完发「成交事件」
- 清算服务订阅「成交事件」,开始清算
- 行情服务也订阅「成交事件」,更新行情
每个服务都是独立的,互不依赖。这就是解耦。
2.2 事件总线的选择
我这些年用过不少消息中间件,说说我的感受:
| 中间件 | 吞吐量 | 延迟 | 持久化 | 适用场景 |
|---|---|---|---|---|
| Kafka | 极高 | 毫秒级 | 磁盘持久化 | 日志、审计、大数据 |
| Redis Streams | 高 | 微秒级 | 内存+磁盘 | 实时撮合、行情推送 |
| ZeroMQ | 高 | 微秒级 | 无持久化 | 内部进程通信 |
| RabbitMQ | 中 | 毫秒级 | 磁盘持久化 | 任务调度、异步处理 |
我的经验:在暗池撮合引擎里,核心撮合路径我用的是ZeroMQ,因为它延迟最低。但审计日志和风控事件,我会走Kafka,因为它能保证数据不丢。
三、内存数据库 vs 持久化存储
这个选择其实不复杂。一句话:热数据放内存,冷数据放磁盘。
3.1 内存数据库:快,但贵
暗池撮合引擎的核心,就是订单簿。订单簿必须放在内存里,因为撮合需要在纳秒级别完成。
我常用的内存方案:
- Redis: 适合存订单状态、用户会话、临时数据
- Aerospike: 适合存账户余额、持仓信息
- 自研内存结构: 订单簿、价格队列,这些必须自己写
// 一个简单的内存订单簿结构
struct OrderBook {
// 买单价格队列,按价格降序
std::map<double, OrderList, std::greater<double>> bids;
// 卖单价格队列,按价格升序
std::map<double, OrderList> asks;
// 订单ID到订单的映射
std::unordered_map<uint64_t, Order> orders;
};
注意:内存数据库不是万能的。我曾经遇到过Redis宕机,丢了3000多笔订单状态。从那以后,我强制要求所有内存数据必须做持久化备份。
3.2 持久化存储:稳,但慢
持久化存储用来存什么?
- 成交记录(必须永久保存)
- 审计日志(监管要求)
- 用户账户信息
- 系统配置
我推荐用PostgreSQL或者ClickHouse。PostgreSQL适合OLTP,ClickHouse适合OLAP查询。
3.3 混合架构:两全其美
实际项目中,我用的都是混合架构:
┌─────────────────────────────────────────────┐
│ 应用层 (撮合引擎) │
│ ┌─────────────────────┐ │
│ │ 内存数据库 (Redis) │ │
│ │ + 自研内存结构 │ │
│ └─────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 持久化存储 (PG) │ │
│ │ + 归档 (ClickHouse)│ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────┘
说白了就是:撮合的时候用内存,成交之后写磁盘。这样既保证了速度,又保证了数据安全。
四、低延迟网络设计要点
做交易系统,网络延迟是最大的敌人。我见过太多团队,代码写得飞快,结果网络拖了后腿。
4.1 物理层:能近就近
这个其实没啥技术含量,就是物理距离。交易所的服务器和撮合引擎的服务器,最好放在同一个机柜里。我见过最极端的案例,两家公司把服务器放在同一个数据中心,中间用光纤直连,延迟只有0.3微秒。
4.2 协议层:能省则省
TCP?太慢了。三次握手、拥塞控制、重传机制……这些都是延迟杀手。
我推荐用:
- UDP + 应用层可靠传输: 自己实现ACK和重传,比TCP灵活得多
- RDMA: 绕过内核,直接内存访问,延迟能降到1微秒以下
- 共享内存: 同一台机器上的进程通信,用共享内存最快
// 共享内存示例(C++)
// 创建共享内存区域
int shm_id = shmget(IPC_PRIVATE, 1024 * 1024, IPC_CREAT | 0666);
// 映射到进程地址空间
void* shm_addr = shmat(shm_id, NULL, 0);
// 直接用指针读写
Order* order = (Order*)shm_addr;
order->price = 100.5;
order->quantity = 1000;
4.3 软件层:能快则快
网络栈的优化,我总结了几点:
- 零拷贝: 数据从网卡到应用,不要经过内核缓冲区
- CPU亲和性: 把网络处理线程绑定到特定CPU核心
- 中断合并: 减少中断次数,批量处理网络包
- DPDK: 用户态网卡驱动,彻底绕过内核网络栈
避坑指南:我曾经在一个项目里,用了DPDK之后延迟降到了2微秒,但CPU占用率飙升到90%。后来发现是中断合并参数没调好。嗯,调优是个细活。
五、架构全景图
说了这么多,咱们用一张图把整个架构串起来:
这张图基本概括了暗池交易引擎的核心架构。从上到下,依次是接入层、核心撮合层、数据层、监控运维层。每一层各司其职,通过事件驱动的方式串联起来。
最后说一句:架构设计没有银弹。单体架构有单体架构的好,微服务有微服务的妙。关键看你的业务场景和团队能力。我个人的习惯是:先跑通,再优化,最后才考虑拆分。别为了微服务而微服务,那是最蠢的做法。
公众号:蓝海资料掘金营,微信deep3321