交易所项目重构总结
前言
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 解决。具体这里推荐一份演讲:
CI/CD
CI 主要是通过 gitlab-ci ,自动构建环境、lint检查、跑单元测试等。
CD 的话麻烦一点,gitlab-ci 配合 ansible roles + supervisor 来部署。除了编写 ansible roles , gitlab-ci 里需要配置好相关的 ssh-agent ,因为我们的测试环境是需要 ssh-tunnel 来登录,会麻烦一些。
问题
- 没有完成服务发现和配置中心相关的开发,后续服务治理、部署会带来很大不便 (可以使用 etcd3、Consul、Istio等)
- 下单、资产变动的分布式事务问题
- 行情处理的并发问题(数据竞争)
- 百万级 ws 连接的优化
- 自动上下币、对接区块链的流程未完成