如何使用golang来搭建高性能内存撮合交易系统
股票交易大家都接触过,买卖双方都会将自己的买入价和卖出价委托给证券商,证券商又把这些数据发到交易所,这里面的核心引擎就是撮合交易系统,一般的撮合交易系统规则是这样的:
撮合成交的前提是买入价必须大于或等于卖出价。当买入价等于卖出价时,成交价就是买入价或卖出价,这一点大家是不会有疑义的。问题是当买入价大于卖出价时成交价应该如何确定?
计算机在撮合时实际上是依据前一笔成交价而定出最新成交价的。如果前一笔成交价低于或等于卖出价,则最新成交价就是卖出价;如果前一笔成交价高于或等于买入价,则最新成交价就是买入价;如果前一笔成交价在卖出价与买入价之间,则最新成交价就是前一笔的成交价。下面不妨以例明之。
买方出价1399点,卖方出价是1397点。如果前一笔成交价为1397或1397点以下,最新成交价就是1397点;如果前一笔成交价为1399或1399点以上,则最新成交价就是1399点;如果前一笔成交价是1398点,则最新成交价就是1398点。
这种撮合方法的好处是既显示了公平性,又使成交价格具有相对连续性,避免了不必要的无规律跳跃。
中金所计算机交易系统在撮合成交时,基本原则按价格优先、时间优先的原则进行(有的情况下为了控制风险的需要,会采取在价格相同情况下,平仓单优先)。
了解撮合规则后我们来讲讲如何用go语言实现高并发的分布式交易撮合。架构如下:
使用golang为交易系统的开发语言,go的优点大家都清楚,简单高并发。系统内部的通信则使用基于http2的grpc。
不建议大家使用go net/http作为高性能系统内部的通信协议,有几个影响性能的点,第一点数据协议的序列化消耗,第二点net/http连接池的锁竞争问题。go net/http作为client的最大qps输出也就4w左右,多开net/http连接池提升也是有限。大家可使用pprof和trace来分析下。而使用grpc配合连接池可压力输出30w+的qps。
介绍下该架构的各个组件:
gateway 入口网关
用来接收用户的请求,协议上可以是restful api和grpc。该gateway不单单是承担交易系统的网关,基本上除了行情推送业务都要走gateway网关。我们的部署方案是kubernetes,所以该gateway还承担了类似ingress的功能职责。
trade-router 交易路由
交易路由功能是对于trade-server及me-cluster的服务发现和状态回馈。
trade-server 交易系统
主要是管理委托和订单的逻辑,比如验证委托,冻结资产,创建订单等等。
me-cluster 撮合系统
把整个交易系统所支持的交易对切分多个分组,每组me集群承担一定数量的交易对。每组me集群通过raft来选举及维持数据的一致性。关于raft库这里使用了跟consul一样的hashicorp raft库,且把内置的msgpack rpc改为grpc协议。
rocketmq 消息集群
一方面用作事务消息,另一方面做行情的数据发布。通过事务消息的确认可以保证多个操作的一致性。作为发布订阅功能来说,在多topic和partition下rocketmq比kafka有更高的性能,主要体现在disk io消耗情况。
kafka每个分区都为一个文件,使用o_append模式来顺序io追加日志。但当请求密集时,顺序io转变为随机io。而rocketmq的消息数据都存在一个commitlog的集合里,consumerQueue只是存消息的offset。根绝操作系统page cache及脏数据flush的特性,尽量多的程度来保证磁盘做顺序io。
其他组件
数据库方面使用了postgres。缓存是大家熟悉的redis cluster。
撮合引擎系统的设计 ?
大家看了上面的架构图后应该会对撮合引擎me感兴趣,整个交易系统的核心其实是撮合系统引擎的设计。撮合系统是在股票和数字货币交易系统中必有的概念。
撮合系统的目的在于对用户的委托实现撮合匹配交易。比如已经有a b c三个卖家,他们以太坊ETH的价格分别是300, 200, 500。用户d发起了以太坊ETH买委托,价格为250。那么这个用户d的买委托会跟卖价格200的用户b撮合成交。
那么设计撮合系统你要考虑数据怎么存? 怎么用?
先需要把委托存在mysql里,然后建立价格及时间的索引,为了性能要分别建立买和卖的委托表。下面是撮合的伪操作过程。实现是没有问题,但是性能很差,匹配查找的合适委托过程过慢,毕竟要走磁盘。当同一交易对委托请求过多时会出现阻塞排队问题。下面的过程中来回这么多次的网络io,大概率造成一些延迟。
其他都好说,撮合系统时最大的性能瓶颈就是在如何高效匹配委托 !!!
begin; select * from entrust where symbol = btc and market eth and side = sell and price < xxx order by create_time asc; 在程序里进一步判断是否符合 delete from entrust where status = init and id in (xx, xx); insert into order .... commit;
网友评论0