我们 2.0 版本的交易系统整体架构就如上图所示,...
我们 2.0 版本的交易系统整体架构就如上图所示,划分为了行情服务、客户端服务、撮合服务、管理端服务。行情服务主要对外提供推送行情数据的 WebSocket API。撮合服务就是一个内存撮合引擎,其输入是一个定序的委托订单队列,而输出包含成交记录和其他各种事件,包括撤单成功、撤单失败、订单进入了 Orderbook 等。撮合服务如果重启,则会从 MySQL 数据库查询出所有未成交订单,重新组成 Orderbook。客户端服务的核心功能就是接收和处理客户端各种 HTTP 接口请求,管理端则是提供给系统管理人员对整个系统的用户、订单、资产、配置等进行统一查看和管理。
虽然拆解为了 4 个服务,但我觉得,这还不是微服务架构,只能说已经变成分布式架构了,但「分布式」和「微服务」是两个不同的概念。微服务是分布式的,但分布式并不一定用微服务。其实,在实际项目中,从单体应用到微服务应用也不是一蹴而就的,而是一个逐渐演变的过程。而 2.0 版本,只是整个演变过程中的第一个阶段。
现在,好多小团队小项目,一上来就微服务,很多只是为了微服务而微服务,这绝对不是合适的做法。从本质上来说,架构的目的是为了「降本增效」——这四字真言是我从玄姐(真名孙玄)那学来的。项目一开始就采用微服务,一般都达不到降本增效的目的,因为微服务架构应用相比单体应用,其实现成本、维护成本都比单体应用高得多,除非一开始就是构建一个大型应用。
当业务规模和开发人员规模都已经不小的时候,比较适合用微服务,这时候用微服务主要解决两个问题:快速迭代和高并发。当业务和人员规模比较小的时候,用一个或几个单体应用完成整个系统,一般迭代速度会更快。但到了某个临界点,就会开始出现一个或多个痛点,这之后,反而会拖慢迭代速度。
而遇到高并发时,其实,单体应用只要是无状态化的,通过部署多个应用实例,也可以承载一定的并发量。但如果单体应用变得庞大了,承载了比较多业务功能的时候,再对整个单体应用横向扩容,就会严重浪费资源。因为,并非所有业务都是需要扩容的,比如,下单容易产生高并发,需要扩容,但注册并不需要扩容。全部业务都绑定到同个单体中一起扩容,那消耗的资源就会比较庞大。另外,当某一块业务出现高并发,服务器承载不了的时候,影响的是该单体应用的所有业务。因此,拆分微服务就可以解决这些因为高并发而导致的问题。
那么,接下来,就来聊聊我们的交易系统,微服务化的架构是如何逐步演进的。
2.0 版本之后,就会进入集中迭代业务需求的阶段了,有大量业务需求有待完善和增加。大的业务板块包括:
相比这些交易,现有业务的交易板块一般就称为「币币交易」。另外,币币交易和杠杆交易都属于现货交易的范畴,合约交易属于金融衍生品的范畴。这些板块的业务现在基本成为每个交易平台的标配了。另外,如果业务继续扩展下去,还有持币生息、借贷、挖矿、 DEX(去中心化交易),以及各种 DeFi(去中心化金融) 。就说现在的三大所,每一个的业务线都是已经很多了。但我们现在先不去考虑这些业务。
除了这些大板块,还有些中小业务也需要补充的,包括:手机号注册、人机校验、邀请分佣、在线客服、系统公告、运营活动、语言国际化、资产归集、钱包冷热分离等等。另外,还要增加支持 Android 和 iOS 端。
然而,以上这么多大小业务的需求,肯定不是一蹴而就的,需要根据优先级,通过一个又一个迭代版本逐渐去完成。按需求的优先级来规划的话,应是先完成那些中小业务的需求,紧接着可以依次完成:币币交易开放 API、场外交易、杠杆交易、杠杆交易开放 API、交割合约、永续合约、期权合约、合约开放 API。
因为每个版本的迭代周期比较短,目标就是快速实现功能并上线,因此就直接在原有的服务里面添加各业务板块的功能了。开放的 HTTP API 也没有独立出来,就直接和内部 API 共用一套了,只从参数上区分是开放 API 还是内部 API。内部 API 会传 Token,走 JWT 鉴权;开放 API 会传 Sign 参数,走 API Key 的签名校验机制。
这些业务板块都上线之后,我们整个交易系统的架构图就大致如下了:
加班加点把这些业务板块的需求都上线之后,做个复盘总结,就会发现存在几个比较严重的问题:
服务拆分的时机,是由痛点驱动的。以上这些问题,就是已经出现的痛点,那要解决这些痛点,方法就是一个字:「拆」。那接下来的问题就是:如何拆分?
微服务拆分,本质就是对业务复杂度进行分解,将整套系统分解为多个独立的微服务,就可以让不同小团队负责不同的微服务,实现整个微服务从产品设计、开发测试,到部署上线,都能由一个团队独立完成。从而,多个小团队就能并行研发多条业务线,实现整套系统的快速迭代。
因此,进行服务拆分,考虑的第一个拆分维度就是相互独立的业务域。很明显,对于我们的交易系统来说,可以拆分的业务域就是:现货交易、场外交易、合约交易。现货交易包括了币币交易和杠杆交易,这两者不能拆分,因为两者是在同一套撮合机制里的,即是说币币交易的订单和杠杆交易的订单是在同一个订单池里撮合的,行情数据也是同一套。合约交易还有各种子域,虽然每种子域也基本是独立运行的,但很多业务规则是大同小异的,所以当前没必要再进一步拆分。
再考虑第二个拆分维度,分析业务流程,如果有异步操作,那就可以拆分。对于交易系统,就看看最核心的撮合交易流程是如何的,最通用的简化流程如下:
下单 ——> 定序队列 ——> 撮合 ——> 输出队列 ——> 清算
撮合的前后,分别有定序队列和输出队列,因此,下单与撮合之间是异步的,撮合与清算之间又是异步的。那就可以将下单、撮合、清算分离独立服务。下单(包括撤单等)部分可以独立为交易服务,撮合就是撮合服务了,清算逻辑则抽离成清算服务。
行情数据模块也是相对独立的,所以我们之前也抽离成了独立的行情服务。
另外,杠杆交易和各种合约交易都有保证金制度,需要实时监控用户的资产并计算风险率,如果达到风险阀值则自动执行对应策略,如强制平仓、自动清算等。实现这些功能的也最好单独服务,我们可以称为风控服务。这块也是需要全内存高速计算的,以后再讲具体如何设计。
除了场外交易不属于撮合交易,现货和合约都可以按上面的拆分维度进一步拆分:
现货 | 合约 | |
---|---|---|
交易服务 | 现货交易服务 | 合约交易服务 |
撮合服务 | 现货撮合服务 | 合约撮合服务 |
清算服务 | 现货清算服务 | 合约清算服务 |
行情服务 | 现货行情服务 | 合约行情服务 |
风控服务 | 现货风控服务 | 合约风控服务 |
其实,还有一些通用业务,如用户的注册、登录,以及公告内容、Banner 图、在线客服等等,这些可以归到一个公共服务,做统一管理。
还有,管理端后台服务只是一些 CRUD,也没出现痛点问题,可以暂不拆分。
最终,在业务层,我们将系统拆分为了这些业务服务:管理端后台服务、公共服务、场外交易服务、现货交易服务、现货撮合服务、现货清算服务、现货行情服务、现货风控服务、合约交易服务、合约撮合服务、合约清算服务、合约行情服务、合约风控服务。
业务服务都拆分之后,大一统的数据库就很容易成为性能瓶颈,且还有单点故障的风险。另外,只有一个数据库,所有服务都依赖它,数据库一旦进行调整,就会牵一发而动全身。所以,我们要将数据库也进行拆分。
微服务架构下,一套完整的微服务组件,其独立性不仅仅只是代码上对业务层需求上的研发和部署上线独立,还包括该业务组件对自身的数据层的独立自治和解耦。因此,理想的设计是每个微服务业务组件都有自己单独的数据库,其他服务不能直接调用你的数据库,只能通过服务调用的方式访问其他服务的数据。
所以,数据库如何拆分,基本也是跟随业务组件而定。对于我们的交易系统来说,撮合服务和风控服务是全内存计算的,没有自己独立的数据库,其他服务都有自己的独立数据库或缓存。如下图:
但拆分之后,数据库变成了分布式的,不可避免地就会引入一些新问题,主要有三大块:
单个数据库的时候,数据库事务的 ACID 是很容易达到的。但到了分布式环境下,ACID 就很难满足了,就需要在某些特性之间进行平衡取舍。我们应该知道,分布式环境下,有个 CAP 理论,即一致性、可用性、分区容忍性,三者在分布式系统中无法同时满足,最多只能满足两项。P 是必选项,所以一般就需要在 C(一致性)和 A(可用性)之间进行抉择。如果是选择了 C,则需要保证强一致性,保证强一致性的事务,也称为刚性事务。解决刚性事务的方案主要有 2PC、3PC,能够保证强一致性,但性能很差。大部分场景下的分布式事务其实对强一致性的要求不会太高,所以只要在一定时间内做到最终一致性就可以了。保证最终一致性的事务,称为柔性事务,其设计思想则是基于 BASE 理论。柔性事务的解决方案主要有 TCC补偿型、异步确保型、最大努力型。而具体选择哪种方案来解决分布式事务问题,就要根据具体的业务场景来分析和选型了。关于分布式事务更详细的内容,以后再单独细说。
数据统计分析问题,更多是管理后台的需求,管理后台需要为运营人员提供统计报表、数据分析等功能,这其实可以归到 OLAP 的范畴了,因此推荐的方法就是将各个库的数据整合到 NewSQL 数据库里进行处理。
跨库查询,其实最多的场景就是 A 业务组件需要查询 B 业务组件甚至更多其他业务组件的数据的时候,那要解决此问题,常用的有几种解决思路。第一种思路就是增加冗余字段,但冗余字段不宜太多,且还需要解决冗余字段数据同步的问题。第二种方案则是增加聚合服务,将不同服务的数据统一封装到一个新的服务里做聚合,对外提供统一的 API 查询接口。还有一种方案就是分次查询每个服务,再组装数据,可以直接在客户端做,也可以在服务端做。
微服务化的最后一步拆分则是采用水平方向的分层架构,可用最简单的三层架构,将所有微服务划分为网关层、业务逻辑层、数据访问层。
增加网关层是很好理解的,这是所有微服务系统的标配。网关层是整个系统的后端总入口,对内提供给官方的客户端和管理端访问,对外通过开放 API 的形式提供给第三方应用访问,负责对请求鉴权、限流、路由转发等,不会涉及具体的业务逻辑。
增加网关层是毋庸置疑的,这不用考虑,需要考虑的是配置多少个网关才合适?一个统一的网关无法解决我们前面提到的开放 API 和内部 API 强耦合的问题,所以是肯定需要多网关的。开放 API 和内部 API 是应该要分开的,两者的鉴权方式不同,限流的策略也不同,更重要的是考虑隔离性,互不影响。管理端和客户端的 API 也最好分开,两者用户和权限是不同的,管理端的管理员用户有着访问和操作更多数据的权限,如果不小心泄露给到了客户端,那就是严重的安全事故了。所以,网关层至少可以分为三个网关:Open API Gateway(开放API网关)、Client API Gateway(客户端API网关)、Admin API Gateway(管理端API网关)。与各端的访问关系如下图:
如果再细分,还可以把 WebSocket API 和 HTTP API 再拆分,不过目前阶段可以暂时先不做分离。
网关层就先聊这么多,接着聊聊业务逻辑层和数据访问层。据我了解,很多项目是没有再独立出数据访问层的微服务的,我以前做过的项目也是。我以前看过的文章和书籍,也从没有提到过这样拆分的思路。这思路还是我从玄姐那学到的,从他那里才了解到,那些大厂的项目,很多都是这么拆分的。
在不拆分的情况下,其实每个服务内部也有划分为业务逻辑层和数据访问层的。在这种情况下,A 业务的服务需要查询 B 业务的数据时,就直接访问 B 服务提供的接口。这种方式,最大缺点就是容易造成数据流紊乱,因为这是横向的服务调用,随着服务越来越多,服务间的调用关系越来越复杂,会变成混乱的网状关系,容易出现非预期的结果,还有可能会发生循环调用,且定位问题也会变得困难。
拆分之后,则 A 需要查询 B 的数据时,就是 A 的业务逻辑层服务调用 B 的数据访问层服务,变成了纵向的服务调用,数据流很清晰。
至此,我们整个系统的微服务化拆分就算基本完成了,最终的整体架构图大致如下:
其实,微服务化还剩下最后一块拼图,那就是注册中心,这也是微服务架构中的一个基础组件。
注册中心主要解决以下几个问题:
简单地说,注册中心主要实现服务的注册与发现。现在,注册中心的选型有好多种,包括 Zookeeper、Eureka、Consul、Etcd、CoreDNS、Nacos 等,还可以自研。这么多选择,应该选哪个比较好呢?其实,可以先从 CAP 理论去考虑,本质上,注册中心应该是 CP 模型还是 AP 模型的?
对于服务发现场景来说,针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不尽相同,也并不会造成灾难性的后果。但是对于服务消费者来说,如果因为注册中心的异常导致消费不能正常进行,对于系统来说则是灾难性的。因此,对于服务发现来讲,注册中心应该是 AP 模型。
再从服务注册的角度分析,如果注册中心发生了网络分区,CP 场景下新的节点无法注册,新部署的服务节点就不能提供服务,站在业务角度这是我们不想看到的,因为我们希望将新的服务节点通知到尽量多的服务消费方,不能因为注册中心要保证数据的一致性而让所有新节点都不生效。所以,服务注册场景,也是 AP 的效果好于 CP 的。
综上所述,注册中心应该优先选择 AP 模型,保证高可用,而非强一致性。那以上所罗列出来的注册中心,可选的就只剩下 Eureka、Nacos,或者自研了。在实际项目中,自研的相对比较少,现在越来越多项目选择了使用 Nacos,因为其功能特性最强大,而且 Nacos 不只是注册中心,还是配置中心。所以,如果不是自研的话,其实可以直接选择 Nacos。
微服务化的落地,远没有很多网上教程说的那么简单,只有自己去经历过,才知道最佳实践的落地方法。另外,微服务化之后,后续还有很多更复杂的问题需要一一去解决,包括服务治理,比如服务降级、熔断、负载均衡等,以及服务网格化,甚至无服务化,都是需要一步步去实施的。
往期文章: 交易系统架构演进之路(二):2.0版
扫描以下二维码即可关注公众号(公众号名称:Keegan小钢)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!