WuBingzheng

交易系统的几种设计方案

交易系统,可以分成两个部分:账户管理(Account) 和 撮合引擎(ME)。账户管理,包括用户资金、持仓、挂单。撮合引擎,就是Order撮合。

方案1:一体

最简单的方案,是把Account和ME混合在一起。流程是:

用户下单 -> 撮合 -> 成交,直接更新taker和maker的资金、持仓

好处是简单,没有接下来要讲的很多问题。

坏处是只能单线程,有性能瓶颈。如果把网络IO、协议解析、日志等都放到其他线程中,而主线程专门执行上述流程,那么单线程能到20万qps。对于一般交易所应该足够,但对于大型交易所不太够。

方案2:ME多线程,Account加锁

ME部分,按照Symbol(币对、股票、合约等市场)拆分,并发执行。对Account的操作,加锁。

多线程并发执行,可以显著提高性能。但也有些问题:

Account加锁,对于现货交易(币对交易)是可行的,因为Account是 User+Currency 的,每个Account只有两个字段:Total和Frozen,对Account的更新也只是加加减减,加锁临界区非常小。 但对于合约(期货、期权交易),Account需要维护所有的持仓,并根据持仓、挂单、当前市场价来实时计算保证金,加锁临界区非常大,加锁不能接受。

多线程并发操作Account的另外一个问题是,在重放Log时,无法保证对Account的更新顺序。不过对Account的更新只是加加减减,是满足交换律的,所以最终结果是一致的。比如对一个账号先+100再-30,和先-30再+100,最终结果是一致的。这个最终一致性有什么具体影响?

方案3:Account全程加锁

为了解决上述方案的两个问题(加锁临界区大;不能保证Account更新顺序),可以在处理下单请求时,全程对Account加锁(而不是仅在有成交时)。

仍然是按照Symbol拆多线程,并发执行。在下单一开始就对Account加锁,并一直持续到下单请求结束。关键点在于,如何处理锁冲突(两个case):

这就解决了上个方案的两个问题。但maker的成交消息的方案,引入了新的问题,即一个请求不再是原子的了,而是包括很多条Log:一个请求Log,多个成交Log,和多个order结束Log。不是原子的,就可能导致状态不一致。比如记录了请求Log,而成交Log没有完全记录,程序就异常终止。但这种异常并不影响程序的继续执行。可以通过后台的对账来发现异常,并通过API来修复。

方案4:Account和ME分层

Account和ME分层,之间不直接调用,而是通过消息传输。

对于下单请求,先经过Account,冻结资金;然后发给ME做撮合;如果有成交,则发消息给Account做结算。

好处是,Account和ME完全独立,不用加锁,没有上面方案因为加锁带来的问题。不过仍然有非原子的问题。

坏处就是复杂。

有两种分层方案:1. 一个服务,拆线程,Account多个线程(按用户拆),ME多个线程(按Symbo拆);2. 拆服务。

一般来说,拆服务(分布式)的优点是:

  1. 性能可扩展。但现在CPU普遍64core的情况下,拆线程基本可以满足需求,所以拆服务方案的优势并不大。除非64core也不满足,需要很多个服务。那基本属于设计有问题了。全部流程,包括网络IO、协议解析、交易业务处理、记录日志等,设计和实现足够好的话,一个core应该可以达到5万qps。40个core就能到200万qps。
  2. 高可用。但这个场景下意义不大。如果一个ME节点失效,可能其余节点还可以正常运行;但如果一个Account节点失败,那么整个服务就都受影响。
  3. 可灰度发布。有小升级或大升级(比如重构Account或Symbol服务),都可以灰度发布。这个确实有意义,不过也进一步增加了复杂度。

缺点(也是相对于拆线程而言):

  1. 难以保证一致性。Account和ME之间的消息的 完整和有序 是很难完全保证的,容易出现不一致性。
  2. 运维复杂。
  3. 成本高,需要很多机器。

总结,这个方案优点是无锁,缺点是复杂。两个实现,1拆线程,略复杂;2拆服务,太过复杂。

总结

最终的选择,取决于具体的场景和决策者的口味。我个人喜欢1和3。

方案A:打快照

打快照有几种方案:

  1. 暂停服务。如果快照比较大,打快照时间比较长(超过10ms)就不能接受;
  2. 暂停服务,clone数据,恢复服务,然后基于clone的数据打快照。clone比打快照快很多,但也有上限;
  3. Fork进程,但实际上也会中断服务,可能超过几十ms;
  4. 基于Rocksdb等基于LSM结构的自带快照功能的db。但需要频繁写DB,并有写放大,另外还需要保证内存和DB里数据的一致性,代码复杂。

对于数量不大的内容,可以采用上述1或2。对于大数据,这里提供一个新的思路。以HashMap为例。

对于删除操作,再做讨论。

方案B:降低Raft延迟

交易系统有两类请求:1下单撤单等;2资金变更,包括外部划转和内部成交。

前者要求低延迟,但可以容忍极个别的不一致,比如程序异常导致的请求回滚;后者不能容忍不一致,但不关心延迟。

Raft的延迟,来源于节点间的消息和本地磁盘同步。能否这样,对于前类消息,在提交Raft Log后,不等Raft协议的确认,就立即执行并返回给用户;对于后类消息,则严格按照Raft协议来。