本文介绍一个实际开发并上线的加密货币交易所项目的逻辑架构设计,其中 包括撮合服务、做市服务、用户服务、市场数据服务、钱包服务、报表服务等核心组件, 并采用了基于AKKA集群的微服务架构。
加密交易所的逻辑架构图如下所示:
订单服务(Order Service)包含了负责匹配委托单并生成市场数据的撮合引擎(matching engine)。 在加密货币交易所中,委托单按交易对进行撮合匹配,因此一个ETH/BTC的买单永远不会匹配到一个 ETH/USDT的卖单。通常认为每一个交易对的订单匹配是依次进行的,例如,撮合进程必须是 单线程的(每个交易对一个线程)。不过实际上还是有办法让这个流程中的某些环节实现 并行化以便提高系统的吞吐量。订单处理过程包括以下步骤:
假设从第1步到第5步需要5ms,第6步到第7步需要2ms,第8步需要2ms,第9步需要2ms。如果系统 几乎同时接收到9个委托订单,使用单线程来执行上述全部步骤需要 9 x (5 + 2 + 2 + 2) = 99ms。 然而,在上面的步骤中,其实只有第6步和第7步是真正需要单线程的。如果我们为第1步到第5步使用 3个Actor,第6步到第7步使用1个actor以确保这两个步骤单线程执行,第8步和第9步使用1个actor, 那么处理流程就变成了这样:
现在全部9个订单的总处理时间就缩短到了25ms,相对于前面的全串行处理所需的99ms,这是 不小的改善。
订单服务(Order Service)是一个使用Akka和Spring框架开发的独立应用。它作为一个节点加入Akka集群, 然后集群中的前台服务器(Front Server)节点就可以发现它。当启动订单服务实例时, 我们可以在命令行参数中指定需要处理哪些交易对。我们可以使用单一的订单服务节点 来处理所有的交易对,也可以使用多个订单服务节点分别处理不同的交易对集合。这意味着 订单服务支持按交易对进行分片(sharding)。由于Akka系统actor的位置透明特性,前台 服务节点并不需要关心订单服务是运行在一个节点还是多个节点上,这使得撮合引擎具备 了水平扩展(Scale Out)的能力。
如果在集群中针对一个交易对(Trading Pair)存在多个订单服务节点,前台服务节点将 始终访问最近加入集群的节点。我们可以启动一个订单服务节点A来处理BTC/USDT交易对, 然后启动第二个订单服务节点B也来处理BTC/USDT交易对。前台服务器会始终将消息发送 给最后加入集群的订单服务节点,因此集群中所有的前台服务节点都会发送BTC/USDT订单 消息给节点B。如果之后的某个时间,节点B从集群中被移除,那么所有的前台服务节点 将会得到Akka集群的通知,之后就会将BTC/USDT订单消息发送给节点A了。
我们使用这个用于前台服务节点和订单服务节点之间的简单的服务发现及路由协议,来实现订单服务的 热备份(Hot Standby)高可用性。这个简单协议使得订单服务的水平扩展和升级非常容易。 例如,当有一个新的订单服务需要部署时,我们可以先让老版本节点继续运行,然后启动 新版本的节点。一旦新版本上线,前台服务节点将自动使用最新加入的节点而非之前的老版本 节点,接下来我们只需要关掉老版本节点就可以了。
为了尽可能缩短订单匹配的时间,订单服务需要在内存中缓存(Cache)整个交易委托账本。 当系统中存在多个处理相同交易对的订单服务节点时,就会在不同的节点上缓存交易委托 账本的完全一致的多个拷贝。我们设计了一个简单但有效的方法来校验并同步多个节点上 的内存委托账本。
订单服务还负责生成以下市场数据:
订单服务将这些市场数据存入Redis服务中,并通过Redis的复制机制同步到前台集群 中的redis服务器上,然后由前台服务器载入并推送给订阅该数据的用户。
做市(Market making)对于加密货币交易所 —— 尤其是对于一个新创办的 加密交易所 —— 的成功至关重要。如果交易所没有足够的交易人,那么买卖价差(bid-ask spread) 通常要大于那些具有较好流动性的加密货币交易所。做市商(Market maker)为交易所增加了流动性, 缩小了买卖价差,同时也为交易委托账本增加了深度,这些因素同时也会更加吸引交易人的加入。
做市商可以来自交易所的外部,他们使用交易所提供的API向交易委托账本中挂买单(Bid)和卖单(Ask)。 对于一个刚创立的加密交易所而言,其本身也需要为某些交易对提供流动性,从而承担了做市商 的角色。
一个基本的做市策略包括同时挂买单和卖单,这样当两方的订单都被市场吃掉后就挣到了买卖价差。 由于加密货币的价格剧烈不稳定性,当市场价格向一个方向持续运动时,使用这种策略的做市商有可 能损失惨重。例如,做市商挂买单以300 USDT的价格买1 ETH,同时挂另一个卖单以301 USDT的价格 卖1 ETH。如果这两个订单都成交了,那么做市商就赚了1 USDT。如果卖单成交,价格继续上涨至 310USDT,那么做市商的买单就没有机会在短期内成交,这导致做市商面临一个潜在的9 USDT的亏损。
做市模块使用订单服务以及外部交易所的交易委托账本、做市商的有效资金等作为输入,生成一系列 不同价位的卖单和卖单。当任何输入发生变化,输出的做市委托单也会相应地变化,从而引起交易 委托账本的相应变化。出于性能方面的考虑,订单服务内置了做市支持能力。订单服务将区别对待来自 普通用户的委托单以及来自做市模块的委托单 —— 这些做市委托单只需要很少的资源,因此更加具有 动态性。它们可以作为一个整体加入交易委托账本,或者进行一些调整,乃至从账本中整体移除,而 不需要像普通用户的委托单那样必须逐一对待。同时,这些做市委托单也可以与用户单成交,如果任何 用户的委托能够匹配这些做市单,那么接下来的执行和普通委托单是一致的。
除了向交易委托账本中添加订单,做市模块还能够向外部交易所下单,并且监视外部交易所中 的订单执行状态,以实现自动化的对冲(Hedging)。
做市模块是使用Akka和Spring框架开发的一个独立应用。它连接到集群中的订单服务节点,看起来 像订单服务的扩展模块 —— 订单服务的运行不需要做市模块。
用户服务是一个用Akka和spring框架开发的独立应用,它为前台服务器提供用户相关的服务,例如 用户注册、用户身份验证、KYC提交、密码管理、API密钥管理、充值地址管理等等。
当用户服务实例启动时,它会加入Akka集群,前台服务节点将会自动发现用户服务。我们可以在集群中 启动多个用户服务节点。在这种情况下,前台服务器以轮询调度方式(round robin)将消息发送给所有 的存活用户服务节点,从而保证了用户服务模块的可扩展性及高可用性。
用户服务可以直接访问核心数据库以及redis集群。它使用钱包服务节点提供的服务来创建新的区块链 地址。
与逻辑架构图中的其他模块不同,钱包服务不是一个单一的独立应用,而是由多个应用组成。这主要 是基于安全方面的考虑。作为一个整理,钱包服务:
上述这些功能,有些是由用户服务模块通过REST接口调用,有些则需要接入区块链网络,而另一些 则不需要接入区块链网络或其他模块,甚至不需要网络接入。我们将钱包服务基于其连接性与安全需求 等拆分为多个应用。
在核心站点中包含一个Maria DB Galera集群。集群中的主(Master)数据库实例由应用直接使用,同时在集群中 创建了两级从(Slave)数据库:
一个从集群中直接复制的数据库是复制中转库(replication staging database)。它作为边缘站点中的主库 提供服务。当从核心数据库集群中复制时,它会过滤掉前台服务和web服务不使用的数据表,这可以避免泄漏 敏感数据,同时减少了需要复制到边缘站点的数据量。
另一个直接从集群中复制的数据库是报表库实例。在这个MariaDB从库实例上创建有两个库,一个用于从核心 集群中复制,另一个服务于管理服务器的报表功能。我们使用多维数据模型,并且在报表数据库中实现分析报表 的生成。新的数据每个几秒钟就从复制库中抽取、转换并载入,以便保证管理服务器的数据时效。
出于安全及报表目的,每个30分钟或1个小时,系统制作一个所有用户状态的快照,包括账户余额、订单、交易执行 等等。快照是活的,因为所有的历史快照都可以直接从报表数据库中查询并加以利用。当制作快照时,ETL 流程将临时停止复制过程,以确保复制库处于静止状态。
核心站点中的Redis用于保存订单服务生成的市场数据,并通过复制机制推送到前端站点。我们之前在Part 2 中已经详细介绍了redis的复制机制。
管理服务器是一个J2EE web应用,使用spring框架和spring mvc开发。Tomcat作为管理服务模块的应用服务器。 Nginx置于Tomcat之前以启用HTTPS以及内容缓存。
管理服务器为交易所内部员工提供后台管理系统。采用基于角色的权限分配机制,用户可以分配不同的角色, 而不同的角色则可以访问系统中的不同功能。管理服务器可以对每个用户的每个操作都生成审计日志。
我们使用ELK栈作为准实时分析子系统的基础。Logstash负责从各种来源(例如前台服务器和web服务器)采集 日志并存入Elasticsearch数据库。验证过身份的交易所员工可以使用Kibana来查看预定义的报表,或者 在ElasticSearch上执行任意的查询。我们也在管理服务器上构建了一些查询Elasticsearch数据的报表, 这样大部分员工就不用同时访问管理服务器和Kibana了。
原文链接: Cryptocurrency Exchange Architecture with Akka Microservices - Part 3
汇智网翻译整理,转载请标明出处