加密货币交易所 二:整体架构

分层不是为了画图好看。这一篇把五层的职责和一条更要紧的界线对上:加机器能扛住接入层和数据层,扛不住撮合引擎——一个交易对的订单簿只能有一个权威副本。

位置
第 02 篇 / 共 12 篇
预计
7 分钟

一张分层架构图能画出五个方框,但它回答不了这个系统里最要紧的那个问题:加机器能解决什么,不能解决什么。

答案很不对称。接入层、业务逻辑层、数据存储层都能靠加机器扛住流量,因为它们要么无状态,要么状态可以按用户切开。核心交易层里的撮合引擎不能:一个交易对的订单簿只能有一个权威副本,所有针对它的下单和撤单必须按同一个顺序处理。两台机器同时撮合 BTCUSDT,各自算出一份成交,谁的账是对的?

所以这套分层的真正含义,是把能水平扩展的东西和不能水平扩展的东西分开,然后让不能扩的那一块尽可能小、尽可能快。剩下的四层存在的意义,一半是业务功能,另一半就是替撮合引擎挡住它不该管的事。

五层各自决定什么

这一层决定什么 典型组件
接入层 请求从哪进来、谁被限流、实时数据怎么推出去 负载均衡器、API 网关、WebSocket 服务器
业务逻辑层 一笔下单在进撮合之前要过哪几道校验 用户服务、账户服务、订单服务、风控服务
核心交易层 订单按什么顺序成交、成交之后账怎么记 撮合引擎、清算系统、行情系统
数据存储层 哪些数据必须落盘、哪些只需要留在内存里 关系型数据库、时序数据库、缓存
基础设施层 服务怎么部署、消息怎么传、出事怎么被发现 容器编排、消息队列、监控告警

分层的收益不在「看起来清楚」,而在每一层能按自己的瓶颈单独扩容。接入层的瓶颈是连接数,撞上了就加 WebSocket 服务器;业务逻辑层的瓶颈是 CPU,撞上了就加实例;数据存储层的瓶颈是 IO,撞上了就换存储或者加副本。如果这三件事挤在同一个进程里,任何一个撞墙都得整套一起加机器,而加机器解决不了撮合那一块。

前提是服务本身无状态。状态一旦落进某个实例的内存里,请求就必须回到那台机器,加机器不再等于加吞吐 —— 这条对上面三层都成立,唯独对撮合引擎不成立,因为撮合引擎的状态就是它存在的理由。

撮合引擎是唯一加不了机器的那一块

订单簿是有状态的,而且状态的演化顺序本身就是业务语义。同一个价位上先挂的单先成交,这是交易所对用户的承诺;如果两个线程能同时改一个订单簿,这个承诺就没法兑现了。

主流做法是让一个交易对的撮合跑在单线程里,订单簿完全放在内存中,外部请求排成一条队列依次喂进去。听起来像是把性能拱手让人,实际正相反:没有锁、没有跨核心的缓存同步、没有一次磁盘 IO 落在关键路径上,单线程反而是这条路径上最快的形态。撤掉的那些并发能力,本来也用不上 —— 顺序是硬要求。

真要扩,只有一个方向:按交易对分片。BTCUSDT 一台、ETHUSDT 一台,互不相干。这条路的天花板很明确:单个交易对再热,也没法再切成两半。第五篇会具体讲订单簿的结构和分片的边界。

一致性按操作分级,不是全局开关

在保证最终一致性的同时,对关键操作(如下单、撮合)实现强一致性。这句话拆开看是两条相反的工程决策同时生效。

一笔下单从接入层进来,经过风控校验、冻结保证金、进入撮合,这条链上任何一环都不能「大概成功」。保证金冻结了但订单没进撮合,用户的钱凭空少了一笔;订单成交了但保证金没扣,交易所替他垫了钱。这两种状态在账上都是不可接受的,所以这条链必须强一致。

而行情推送晚到 200 毫秒、历史订单查询读到 3 秒前的副本、成交明细同步进数据仓库延迟一分钟 —— 没有人因此受损。这些走最终一致,换来的是它们可以自由地加副本、加缓存、异步化。

全局强一致在交易所里付不起:它意味着每一次行情推送都要等一轮共识,而行情的更新频率比下单还高一个量级。分级的代价是每个新功能上线前都要有人回答一句「这个操作属于哪一级」,答错了要么白白牺牲性能,要么埋一个对不上账的隐患。

微服务换来独立部署,代价是调试变难

微服务架构增加了系统的复杂性,难以调试和维护。一笔下单在单体应用里是一条调用栈,在微服务里是六个服务之间的九次网络调用,其中任何一次超时都会以「订单状态卡在 pending」的形态出现在用户面前。

链路追踪和服务网格是补这个洞的,不是加分项 —— 没有它们,上面那句「哪次调用超时了」根本无从查起。这是一笔明账:拆成微服务换来的是每个服务能独立发版、独立扩容、独立选型,付出的是一整套原本不需要的可观测性基础设施,以及每个工程师脑子里那张必须随时更新的调用关系图。

小团队做这个取舍要谨慎。撮合引擎必须单独部署是硬需求,但用户服务、账户服务、订单服务在早期完全可以待在同一个进程里,等某一块真的顶不住了再拆。反过来做 —— 先按教科书拆成十几个服务 —— 得到的是十几份部署脚本和一个没人说得清的故障边界。

这套架构真正难的地方

撮合是单点,分片切不动热门交易对。 按交易对分片是唯一的横向扩展手段,而流量在交易对之间是极度不均的。BTCUSDT 一个交易对的订单量可能超过后面几十个的总和,它撞上单机上限的时候,架构层面没有别的招可出,只能回去优化那一个进程。

内存里的订单簿和落盘的数据总是有偏差。 撮合在内存里完成,持久化异步跟上,中间那段时间里数据库看到的世界是旧的。任何一个直接读数据库来判断「这单成交没有」的服务,都会在某个时刻读到错的答案。这也是为什么成交要以撮合引擎发出的事件为准,而不是以某张表的状态为准。

跨服务的失败只能补偿,不能回滚。 保证金已经冻结、订单已经进了撮合、然后下游某个服务挂了 —— 分布式事务在这条延迟预算里用不起,剩下的办法是每一步都准备一个反向操作,出事了逐步退回去。补偿逻辑的代码量常常和正向逻辑一样多,而且它平时不跑,出问题的时候才第一次被真正执行。

这篇是加密货币交易所的第 2 篇。前一篇是加密货币交易所 一:合约交易基础概念,后一篇是加密货币交易所 三:核心业务概念和实现