基于 golang 实现的分布式撮合交易系统 – 峰云就她了 基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图1

峰云就她了

专注于 Golang、Python、DB、cluster

基于 golang 实现的分布式撮合交易系统

rfyiamcool2020 年 2 月 6 日

前言:

去年花费不少时间在扩展优化撮合交易系统,对交易系统的分布式及高性能有些心得,借着新冠状疫情带来的空闲时间,写此文章分享下交易架构的经验。

这里谈及的分布式交易系统属于股票和数字货币类的架构,跟在线电商是有所差异的,因电商无撮合引擎的概念。考虑到交易系统的业务复杂性,该文着重讲撮合系统的架构实现。后期会专门写文章来描述交易系统的业务上的设计和实现。

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图2

该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=6429

架构图:

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图3

使用 golang 为交易系统的开发语言,go 的优点大家都清楚,简单高并发。系统内部的通信则使用基于 http2 的 grpc。

不建议大家使用 go net/http 作为高性能系统内部的通信协议,有几个影响性能的点,第一点数据协议的序列化消耗,第二点 net/http 连接池的锁竞争问题。go net/http 作为 client 的最大 qps 输出也就 4w 左右,多开 net/http 连接池提升也是有限。大家可使用 pprof 和 trace 来分析下。而使用 grpc 配合连接池可压力输出 30w + 的 qps。

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图4

介绍下该架构的各个组件:

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,大概率造成一些延迟。

其他都好说,撮合系统时最大的性能瓶颈就是在如何高效匹配委托 !!!

  1. // xiaorui.cc
  2. begin;
  3. select \* from entrust where symbol = btc and market eth and side = sell and price < xxx order by create\_time asc;
  4. 在程序里进一步判断是否符合
  5. delete from entrust where status = init and id in (xx, xx);
  6. insert into order ....
  7. commit;

那么 Redis 是否可以? 使用 redis sorted set 是可以对委托单进行排序,可以通过价格 + 时间的格式对其方式存储,这样就满足了价格及时间优先的排序标准。

但有几个问题? 首先,如何实现可靠的持久化?redis 的 aof 写入是异步的,不是主线程写入,由另一个异步功能线程来日志写入,有 crash 后丢失的概率。另外,redis 的主从架构同步数据方式为异步,mysql 最少还有个半同步及加强版半同步 😅。

上面说的是 redis 怎么存,那么怎么来保证撮合的一致性? 不要实现,倒是可以用分布式锁来关联委托主键 id。但后面带来的问题也不少。

高性能撮合引擎设计

上面说了 mysql 及 redis 的方案可能存在的问题,那么业界多是怎么搞的? 基本是内存撮合!在程序内存里用红黑树来存放委托单,这样规避了网络和磁盘 io 的消耗。既然用内存红黑树结构存放委托,那么必然要考虑内存的特性。

如果程序 crash 怎么办?

撮合引擎需要设计 recovery 机制,可以从 postgres 数据库中把把未操作的委托放到撮合引擎系统里。委托数据存两份,一份是内存中,一份是数据库里。

为了规避撮合引擎由于频繁上线及 crash 后的 recovery 恢复耗时,所以我们又设计了 raft 节点一致性。这样当某个节点 crash 后,follower 会提升为主并迅速接管撮合交易。在这里使用数据库 postgres 作为 recovery 的兜底方案,当撮合引擎所有节点都进行重新实例化时,那么就会走该兜底方案。

既然有了数据库作为兜底,那么 raft 节点产生的日志和快照都使用内存来存取。

为何根据交易对进行撮合集群分组?

golang 社区里比较靠谱的 raft 库就两个,一个是 etcd 的,一个是 consul 的。但他们的 raft 库实现是单一分组实现,不支持 multi raft 分组。所以把撮合引擎直接在架构上拆分了多组,每个分组由 3 个 raft 节点构成。在 trade-router 来分配交易对及分组的关系。

后来在 github 中找到了 golang 支持 multi raft 的库,经过一番测试,性能还不如分组部署。

这里说下压测并发下的交易数据,每秒交易 qps 可以到 8w,最后的瓶颈在数据库的磁盘压力。😅

trade-server 是否每次转发都需访问 trade-route?

不需要!因 trade-server 自己也会有一层映射缓存。当 me-group 产生扩容时,必然要迁移分片中的交易对过去,如何保证迁移时的安全及一致性? 简单说就是阻塞交易对,然后迁移,等迁移成功后,做删除并解除阻塞状态。

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图5

leader 跟 follower 同步数据的过程 ?

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图6

性能调优

1 需要对订单表进行分库分表。

2 raft 做节点同步时根据突发情况,进行批量同步,以减少 syscall 调用.

3 使用 redis 做查询订单的缓存,缓存算法为先更新 db 再淘汰缓存.

4 能批量的都批量.

5 golang 使用 sync.pool 减少堆对象生成,时间轮来减少锁竞争,分段锁 map 等等

总结:

关于交易系统的分布式和高性能实现就先说这么多了,对于更多的细节会在后面的文章补上。另外,国内外不少证券和数字货币交易所的设计更是复杂,使用了比如机房直连、udp 组播、原子钟等技术。

在这日益严重的疫情期间,大家要保护自己!武汉加油,中国加油🇨🇳 。

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图7


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址 xiaorui.cc

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图8

Golang 交易系统, 分布式撮合交易系统, 撮合引擎, 撮合系统 Bookmark

Previous Article 源码分析 grpc graceful shutdown 优雅退出

Next Article go1.14 基于 netpoll 优化 timer 定时器实现原理

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图9

公告

我的 github 地址: github.com/rfyiamcool

有问题可邮件沟通: rfyiamcool@163.com

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图10

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图11

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图12

标签

ansible ansible api docker docker api elasticsearch etcd gevent golang golang channel golang gc golang redis golang runtime grafana gunicorn happybase Influxdb InfluxDB 性能 kibana kibana4 mysql mysql 索引 peewee python python ctypes python elasticsearch python epoll python etcd python gevent python happybase python hbase python influxdb python multiprocessing python peewee python redis python requests python rq python thrift python zabbix redis redis cluster redis lua saltstack tokudb zabbix zabbix 二次开发

近期文章

文章归档

© 2021 峰云就她了. All rights reserved.

hello world, github.com/rfyiamcool

京 ICP 备 15061109 号 - 1

基于golang实现的分布式撮合交易系统 – 峰云就她了 - 图13

http://xiaorui.cc/archives/6429