交易所项目重构总结

前言

18年9月,公司决定重构合作方的比特币交易所的项目。

原有项目主要使用 Java Spring Cloud 和 CPP 构建,用到的组件主要有 ActiveMQ、Redis、MariaDB、Nginx 等。在运行了一年左右出现了各种各样的问题:

  • 产品快速迭代,缺乏合理的架构合计
  • 原有系统性能出现瓶颈,生产事故也比较多
  • 数据库优化方面做的较差,索引不合理、慢查询较多
  • 缺乏功能完善的运营后台管理系统
  • 开发流程混乱,没有做好密码管理

在了解了具体的业务和问题后,我们开始了该项目的重构。

重构

架构设计

新架构延续微服务的设计,基于 gRPC 进行开发。鉴于组内人员技术栈的原因,部分子服务使用了 Python , 其余子服务使用 Golang 。新架构主要分为以下四层:

  • 统一网关层: 主要做限流、负载均衡等
  • 对外接口层: 主要对前端提供 HTTP、Websocket 等相关接口,还有对外的 Open API
  • 基础服务层: 包含用户管理、资产管理、币币交易、撮合引擎、行情处理、充提管理、钱包、行情推送、订单结算、内容管理、后台管理等子服务
  • 基础组件层: RabbitMQ、MySQL、Redis 等基础组件

数据库使用 MySQL,采用分库分表的策略,按照不同的业务分库,再按交易对、币种、时间分表。使用 alembic 做数据库版本管理。

技术选型

  • 微服务框架:采用原生 gRPC,因为同时使用 Go 和 Python 的原因,放弃了 micro 、kit 等框架
  • Web 框架:gin、Django
  • 前端:Vue、TradingView
  • 数据库:MySQL 8.0, 后续打算使用 AWS Aurora
  • 消息中间件:主要使用 RabbitMQ,部分使用 ActiveMQ(兼容原有钱包程序)
  • 缓存/NoSQL: AWS Redis 集群
  • API Gateway: Kong
  • Websocket: gorilla/websocket、gobwas/ws
  • 异常采集:Sentry
  • 日志相关:zap、loguru、logrotate
  • 配置解析:go-ini
  • CI/CD: gitlab-ci、ansible、tox、supervisor
  • 异步任务:Celery
  • 依赖管理:Python 使用 pipenv,Go 使用 dep (后续打算使用 go modules)
  • 其他组件:AWS SES、VPC、S3、ELB、CloudWatch、云片、极验等

子服务说明

撮合引擎

撮合引擎主要就是在内存中维护了一套 OrderBook,买卖单根据不同的顺序排列。买单列表按价格由高到低排列,卖单列表按价格由低到高排列。当收到下单消息的时候,检查 OrderBook,卖单从买单列表中匹配,买单从卖单列表中匹配。若匹配不到,就插入到 OrderBook 中排队。逻辑比较简单,但这只是简单的现货限价交易,如果是市价交易的话,撮合的时候会稍微复杂一点。主要是市价单需要处理一下价格到数量的换算问题。如果是期货的话会更复杂。

内存中撮合,OrderBook 里存的是整型的大数,因为整型的运算速度快,所以下单消息再进入撮合引擎后做了统一精度的放大。主要使用到了 bigInt 来储存(以太坊项目中大量用到了 big)。

撮合还需要注意的是高可用的问题,怎么持久化 OrderBook、怎么主从切换。

撮合最主要的是准确和效率。运算逻辑其实不难,做到准确是可以的。所以难点就在提升性能上,原有系统每秒能处理的订单数大概在 100 单左右,火币、OKey、币安等基本都是上万的。

行情处理

订单从撮合引擎成交后,撮合会发送对应的成交消息、深度更新消息到 MQ,行情处理程序通过启动多个 Worker 并发对行情进行更新。主要是更新深度、Kline、最新成交和 ticker。

Kline 需要同时更新不同时间段的。比如分钟线、小时线、日线、周线等。这个地方可以使用 goroutine 并发更新。因为不同K线之间没有依赖关系。kline 在 Redis 中的结构是哈希表,每个交易对、每种类型独占一张表。为了提升性能可以对 Kline 的内容做压缩再储存。

深度更新要稍微麻烦些,因为深度是有序的。所以在 Redis 中采用了 ZSet+Hash 的结构。Zset 保证顺序,Hash 用来存取更新数值。但后来觉得这种方式太过复杂,或许有更好的机制。比如直接从撮合引擎的 OrderBook 中截取,为什么还要单独在 Redis 中费劲维护一份呢?

最新成交比较简单,在 Redis 中维护一个固定长度的 List, 每次更新的时候 Lpush 一下,然后 LTrim 裁剪就行。

行情推送

行情在更新完毕后,都通过 RabbitMQ 的 Pub/Sub 推送到相应的 Exchange 上。由 Exchange 广播到对应的 Sub 端。行情是通过 WebSocket 推送的。刚开始用 gin 开发的 Web Server 来做 ws 服务器,使用 gorilla/websocket 这个库。给每个客户端只开一个 ws 连接,客户端通过发送消息订阅。

客户端与服务端通过 Ping 消息做心跳检测,2~3 个心跳没有回复的时候就主动断开连接。

程序刚开始跑自己没发现什么问题,后来测试环境跑了几天后发现貌似有内存泄露的问题。搞的我有点小紧张,通过 pprof 检测发现是 gorilla/websocket 这个库的 bufio 占用很高。没有释放,随着连接数的增加而增加。后来打算替换为 gobwas/ws 解决。具体这里推荐一份演讲:

1m-go-websocket

CI/CD

CI 主要是通过 gitlab-ci ,自动构建环境、lint检查、跑单元测试等。

CD 的话麻烦一点,gitlab-ci 配合 ansible roles + supervisor 来部署。除了编写 ansible roles , gitlab-ci 里需要配置好相关的 ssh-agent ,因为我们的测试环境是需要 ssh-tunnel 来登录,会麻烦一些。

问题

  • 没有完成服务发现和配置中心相关的开发,后续服务治理、部署会带来很大不便 (可以使用 etcd3、Consul、Istio等)
  • 下单、资产变动的分布式事务问题
  • 行情处理的并发问题(数据竞争)
  • 百万级 ws 连接的优化
  • 自动上下币、对接区块链的流程未完成