新架构选型报告

V3.3

2017/02/04

技术架构设计

  • 组件间以同步请求应答模式为主。各个组件自行开放可调用的接口
  • 订阅推送模式为辅。一些实时消息需要被推送。必须有推送机制。
  • 各个组件(包括基础服务)地位是平等的。没有中心组件的概念。
  • 同步请求的幂等通过服务发现/负载均衡来实现。同一个请求同一时间只会发送给一个组件。同一组件对同一请求通过request_id来识别和控制。
  • 异步推送的幂等通过消息服务的“分组”功能实现。同一个组中有且只有一个组件会收到消息。

系统总体架构

新架构模型图.png

服务分层示例:

670070282994702997.png

系统设计目标

  • 开发友好。避免开发者关注过多细节。代码直观易理解。有良好的开发工具支持。
  • 单元测试友好。各个组件可以独立进行单元测试。不依赖其他组件
  • 安装部署友好。尽量避免安装部署时的手工操作。对目标环境的要求宽松
  • 运维友好。尽量自动运维。提供监控和管理工具
  • 暂不考虑云端部署和运行

系统性能目标

  • 前端页面的请求响应时间不应超过1秒
  • 对于本系统,应用单机部署的情况下,应该可以支持1000以内的并发请求
  • 公共行情服务应该能支持1000以内的长连接
  • 公共行情服务应该能在不超过2M带宽的互联网连接上正常工作(不会产生因带宽导致的数据堆积)

开发选型

主框架选型

目前考虑使用微服务架构体系作为新架构的基准体系。目前微服务架构是web系统比较流行的架构,有很多成熟的工具、框架和参考实现可以使用。使用微服务架构可以充分利用已有的资源,方便开发、部署和运维工作。

微服务架构里需要解决(但已有解决方案)的问题有:服务编排、服务发现、负载均衡、分布式调用跟踪、度量监控、日志聚合等。虽然每一个方面都有成熟产品予以支持,但手工去聚合这些服务,工作量还是不小的。选择整合上述服务的整合框架,是一个更好的选择。

目前对于微服务的各个方面有完整支持的框架大致有 Spring Boot (Apache)+ Spring Cloud(Apache) 和 Golang + Go-kit(MIT) 两类。

Golang是服务器开发领域的新兴语言,发展迅猛,与C语言集成方便,自带并发开发模式。在2016年底还第二次当选为TIOBE语言排行榜的年度语言。在微服务领域,大量的支持工具,如docker、kubernetes、etcd等,都是用Go开发的。这些工具对Go的支持也非常优秀。

Go-kit框架是Go语言中对微服务模式开发支持较为完整的一个框架。它提供了对服务发现、负载均衡、分布式调用跟踪、度量监控的支持,还支持多种请求应答模式的接口,如HTTP RESTful、gRPC、Thrift等。更难能可贵的是,Go-kit一开始就秉承着集成第三方优秀工具的理念,对目前主流的第三方工具都有良好的支持。

但是Go目前存在着几个问题。第一是还没有足够优秀的IDE支持,对于开发者不够友好。第二是由于语言较新,在部分领域还是没有比较好的第三方库支持。第三是go-kit这个框架,功能还不够完善,比如没有订阅发布模式的支持。

Spring Boot + Spring Cloud,主要使用Java语言开发,也可以使用JVM支持的其他语言,如Groovy、Scala等进行开发。它高度依赖Spring框架,相对较重,而且使用的功能,原生都是Spring框架自己的实现,但Spring框架本身功能完善,社区活跃,实际应用案例也多。对于第三方工具支持,可以通过第三方项目进行整合。

Java也是比较成熟的开发语言,相关的开发环境和资源也成熟得多。还可以使用Scala等语言来降低开发难度。

基于以上讨论,我们主要选择 Spring Boot + Spring Cloud,配合Java作为主开发框架和环境。

部分组件根据实际需要,选用其他的开发语言和框架进行开发。但要与现有工具与协议保持一致。

交互协议选型

交互协议的备选方案有:

  • HTTP RESTful + JSON
  • gRPC(3-clause BSD)
  • Thrift(Apache)

其中 HTTP RESTful + JSON是基于文本的协议。它的开发简便,只需要一个标准的web app框架即可进行开发。各大语言对此都有良好的支持。JSON作为报文定义格式,方便灵活,调试查看都很容易。其缺点是性能不高。HTTP是短连接模式,每次连接都会耗费一次握手连接的时间。而且JSON格式也需要解析,会进一步浪费时间。HTTP也不支持流传输,在连续信息的传输上没有好的解决方案。

gRPC是Google推出的一个RPC协议框架。它采用 http/2 作为传输协议,用 protobuf 3.0 作为报文定义协议。http/2在现有http的基础上增加了多路复用支持、流传输支持、加密和压缩等更多的功能。protobuf是一个基于二进制的协议,对于相同内容,protobuf定义的报文比JSON要小上很多,解析速度也要快得多。gRPC同样支持多种语言。

Thrift是Apache基金会下的一个RPC协议框架。它的功能大致与gRPC一致。但Thrift有以下问题:

  • Thrift有多个不同的实现。每一个实现支持的功能和性能各不相同
  • 缺乏完善的文档
  • 协议为Thrift私有格式
  • 与gRPC中的protobuf一样通过生成代码来处理报文,但Thrift生成的代码质量相对较差
  • 不支持流传输

基于以上讨论,我们选择gRPC作为系统内部交互协议,HTTP RESTful + JSON 作为API网关与前端之间的交互协议。

缓存系统选型

鉴于目前系统使用的redis已经相对成熟,且无明显问题。新系统中的缓存系统仍然采用redis(3-clause BSD)。

数据库系统的选型

目前的备选方案有:

  • Oracle(商业授权)
  • MySQL(GPL:社区版/商业授权:标准版/企业版)
  • PostgreSQL(PostgreSQL License,simliar to the BSD or MIT licenses)
  • MariaDB(GPL)

Oracle是商业软件,会增加客户的成本。且部署相对复杂。没有特殊情况不再选用。

MySQL基本上是世界上最流行的开源数据库服务。被大量在线系统及企业使用。拥有异常丰富的资源。

PostgreSQL拥有丰富的功能和大量企业级特性。但很多功能是我们不需要使用的。而且PostgreSQL的社区和资源也不如mysql的丰富和活跃。

MariaDB是mySQL被Oracle公司收购后,从早起MySQL版本独立出来的一个分支,目前与MySQL并行发展。MariaDB与指定版本的MySQL具有二进制兼容性。基本上可以认为它和MySQL是一样的,很多MySQL的经验可以直接使用在MariaDB上。MariaDB是完全开源的,其开源版本甚至拥有部分MySQL企业版才提供的闭源特性。另外MariaDB还拥有一些独有的特性,比如更好的内核引擎等。

基于以上讨论,我建议选择 MariaDB 作为我们的数据库系统。当然,MySQL是一个强有力的备选方案。

消息中间件选型

目前的消息中间件有两个发展方向:面向功能的和面向效率的。

面向功能的中间件提供了很多丰富的功能,例如消息时序、重演、跨组件事务保证等。但一般来说,这类中间件的消息处理效率不高,通常在每秒千笔的级别,这类消息中间件适合于做批处理、实时性不高的消息体系,如邮件系统等的实现。

面向效率的中间件主要是可以高效处理消息,通常可以达到每秒十万甚至百万级。但没有提供很丰富的功能。如果需要时序、重演等功能,需要自己去实现。

对于本系统的使用场景,对于消息的时延和消息量更重视。因此,我们选择面向效率的中间件作为订阅通知功能的基础。

此类中间件的备选有以下几种:

  • Kafka(Apache)
  • NATS(MIT)
  • NSQ(不明。看起来像Apache)
  • zeroMQ(LGPL v3)

这其中zeromq跟其他的都不一样,zeromq没有中心服务器,相当于是一个开发库。虽然其效率很高,但这种模式不适合我们的系统。第一它提供的功能相对比较核心,相对其它几个服务会大大增加开发量。其次内嵌的机制会导致应用组件带有状态,对管理、扩容都不利。

Kafka是由Scala实现,使用zookeeper作为集群基础,文档比较不清晰。容易出问题。现有系统中在部署时也出现了不少因为kafka配置不当导致的问题。但Kafka的应用相对比较广泛,资料较多。现有系统中也有一定的使用经验。

NATS (stream server)是Kafka的有力竞争者。NATS由go语言实现,性能卓越,可以达到每秒50M字节的传输速率,对于100字节的消息,处理可以达到每秒百万级,系统需要用到的功能,如分组消息、保证送达等等,也都具备。但NATS主要是基于内存处理,如果使用文件持久化会降低性能,这方面需要做进一步的评估。另外,目前NATS的问题在于比较新,还没有足够的实际产品来证明其可靠性。

NSQ也是由GO实现的,但实现机制不同,性能弱于NATS。暂不考虑。

65936-b654433694f23389.png

这是一个第三方测试做出的性能报告。可以看到,NATS的吞吐量性能还是非常突出的。NATS的部署也比Kafka要简便得多。然而NATS缺乏一些集群特性,比如 Streaming Replication。这些是Kafka所具备的。

接口方面,NATS缺乏成熟的C++接口。Kafka有C++接口[librdkafka(2-clause BSD)]。考虑到部分报盘服务因为柜台提供的是动态库,用C++开发可能更合适。如果是这样的话Kafka会更有优势。

基于以上讨论,加之下面我们提到的Kafka Streams流式计算引擎,我们选择使用Kafka作为首选消息中间件。使用其0.10+版本(包含Kafka Streams)及docker镜像避免部署问题和早期版本引起的bug。

定时服务选型

为了保证组件是无状态的,组件的逻辑执行均需要外部触发(通过请求或推送消息)。但对于定时任务来说,缺少这样的触发源。如果将定时任务直接实现在组件内部,就会使得组件存在状态,增加整个系统的复杂性。

为了解决这个问题,决定引入定时服务,由定时服务通过订阅发布接口推送定时消息,触发业务组件的定时任务。

目前没有现成的定时服务可选。定时服务考虑自行开发。

定时服务发布两类推送:

  • 特定时间触发
  • 周期触发

定时服务提供以下接口:

  • 注册特定时间。需要提供服务名和时间。调用该接口后,定时服务会返回一个消息中间件的主题,同时启动计时。对于相同服务相同时间的注册请求,返回的主题是相同的,并且除了第一次调用之外的后续调用不会影响当前的记时。当到达指定时间时,定时服务会向之前返回的主题发送一个消息,通知订阅者时间到。
  • 注册周期。需要提供服务名和周期时间。调用该接口后,定时服务会返回一个消息中间件的主题,同时启动计时。对于相同服务相同时间的注册请求,返回的主题是相同的,并且除了第一次调用之外的后续调用不会影响当前的记时。当到达周期指定时间时,定时服务会向之前返回的主题发送一个消息,通知订阅者时间到,同时开始下一轮周期的计时。
  • 取消注册特定时间/取消注册周期。上述两个接口的反操作。

由于定时服务的消息是基于网络的,精度不宜过高。目前考虑精度不高于10毫秒。

认证授权选型

内部组件不采用认证授权模式。由网络隔离保证其网络安全性。

API网关采用OAuth2认证授权,防止未经认证的客户端随意访问。

公共行情服务有长连接和短连接两种接口。短链接接口也使用OAuth2认证授权。长连接接口在连接创建后使用登录认证API进行认证授权,若认证失败直接断开连接。认证成功可以继续后续操作。

数据分析服务选型

作为一个资管系统,除了对基础信息的管理之外,对数据的整理与分析也是非常重要的。数据的分析以指标的形式呈现。一个指标可以通过基础数据及其他指标值计算得来。指标主要被应用于风控、报表及数据分析展现界面。

指标分成基础指标和高层指标两类,基础指标会直接影响业务,而高层指标主要用于分析和风控,实时性相对较低,且相对独立与业务本身。但高层指标通常需要在多个维度上进行计算,而且每一个维度拥有多个指标。指标间的依赖关系较为复杂。

基于以上考虑,我们将基础指标的计算作为业务模块自身的一部分,而将高层指标独立出来进行异步处理。该工作由数据分析服务来完成。

高层指标间相互引用比较复杂,计算量大,但对于计算实时性要求相对较低,计算连续,没有特定的边界。根据这种特点,我们考虑使用大数据的流式计算工具进行高层指标的计算。

目前流行的流计算系统有Apache Storm、Apache Spark、Apache Samza和Apache Flink等。这些系统各有特点。然而他们都有一个比较大的问题:系统庞大,依赖复杂。这些系统主要考虑的是在多机集群下的工作模式,而很少思考如何在有限资源上完成工作。我们的系统需要考虑离线部署而非云部署。很多情况下资源有限。引入太重的服务是不太现实的

在这里,我考虑使用Kafka 0.10开始提供的Kafka Streams流式计算引擎。这个引擎最大的特点是轻量、依赖小。比较适合在小规模的系统中使用。它除了最新版的Kafka之外没有其它依赖。而Kafka恰恰是我们作为消息中间件的选择。

数据的输入分为实时数据和静态数据两类。实时数据主要是盘中实时变化的基础指标信息。在Kafka Stream中,实时信息可以直接通过Kafka进行传递。静态数据可以考虑在盘后清算结束后,数据分析服务去所需的服务抓取必要的数据留档在自己的数据库中作为历史数据使用。

测试选型

由于微服务的分布式特性,除了常规的系统集成测试外,我们还需要更多的测试来保证组件本身的功能、组件接口等符合设计需求。

我们拟在系统中引入以下测试:

单元测试

单元测试主要保证组件内部的函数功能实现符合标准。单元测试要求与外界隔离,可以完全独立的运行。这就要求我们使用Stub/Mock消除对外界的依赖。

对于Spring boot开发的组件,我们使用JUnit作为单元测试的主框架,考虑使用Mockito来创建stub消除对外界的依赖。

组件测试

组件测试主要保证自身对外提供的服务符合标准。但测试时其依赖的服务未必可用。这时需要对其依赖的服务做stub。

我们系统使用gRPC作为内部组件的交互协议,gprc的自动生成代码中带有测试的代码。我们可以利用这些代码搭建外部服务stub。

对于前端的测试,在实际系统中是通过HTTP(s)协议,通过API网关与内部组件进行交互,在组件实现之前,我们可以利用WireMock模拟http的交互结果。

契约测试

契约测试主要保证服务之间的接口调用符合标准。

Pact目前是契约测试的事实标准。我们也选择Pact做契约测试。

端到端测试

端到端测试是全系统的完整测试。可以沿用现有的测试方式。但是在测试环境的搭建上,我们可以考虑使用Vargrant在代码构建时自动生成完整的测试环境,避免测试环境部署的各种问题。

运维部署选型

应用容器选型

使用应用容器可以屏蔽运行环境依赖及冲突问题,使得应用的部署更加简便。更重要的是,应用容器通常都提供了API和自动化操作支持,可以对接各种管理工具使得对不同应用的管理统一化、自动化。

目前docker是应用容器当仁不让的选择,几乎所有容器化的管理工具都支持它,它也被无数实际系统所选用,拥有丰富的资源和实践经验。因此,我们也选择docker作为应用容器。

服务编排选型

服务编排主要是对各个服务做一个总体的管理与监控。它可以解决服务间依赖性、服务规模伸缩等问题。服务编排服务通常与服务发现、服务健康监控等整合使用,对系统整体的可用性做出一定的保证。

目前备选的有 Kubernetes(Apache) 和 Docker Compose (Apache),暂时未定。由部署方案确定

日志聚合服务选型

日志聚合服务可以将各个服务独立的日志文件信息聚合汇总,并可以进行分析、处理和搜索。方便在一个总体的维度上对日志进行跟踪和观察。

目前知名的日志聚合服务主要有ELK,且没有其他备选。而且日志聚合相对独立于开发。未来更换代价也不会太大。因此直接选择ELK(Apache)。

度量监控选型

度量监控服务主要是在各业务服务内部插入度量点,通过收集度量点数据可以对各个服务的性能与健康状况进行监控与分析。

Spring Boot自带metrics特性,创建的服务自己已经带有一些度量信息,业务系统也可以利用Spring的度量接口,插入自己的度量点。这些度量信息可以导出到statsd(MIT)。然后可以使用Graphite(Apache)通过statsd提取度量监控指标并处理和展现。

度量监控效果类似下图:

graphite-nuxeo2.png

分布式跟踪选型

分布式跟踪主要通过在代码中插入一些跟踪点,并对其进行收集处理,可以展现出各个服务之间的调用链。还可以看出调用耗时以及健康状况等信息。

本系统的分布式跟踪主要通过 Spring Cloud Sleuth + Spring Cloud Zipkin 配合 Zipkin (Apache) 进行处理。

Zipkin的分布跟踪展示效果类似下图:

web-screenshot.png

服务发现选型

服务发现主要是保证在服务伸缩的情况下服务自身的可用性不受影响。系统的各个服务通过向服务发现服务注册,其他服务就可以通过服务发现服务来获取一个可用的服务地址。通常服务发现与负载均衡会同时实现。

目前的备选有 etcd(Apache) 和 consul(Mozilla Public License),暂时未定。由部署方案确定

etcd需要与其他服务配合使用才能实现服务发现功能。Consul自带服务发现功能,并提供基于DNS的查询,以及节点健康检查、WebUI等。目前暂时倾向于Consul。

版权问题

考虑到版权协议原因,所有上述第三方工具均不应作为我们产品的一部分进行发布。应该要独立安装。

开发中使用的第三方部件,均需要明确其版权声明。禁止使用GPL协议的开发部件。尽量避免使用LGPL协议的开发部件。其它版权声明的部件,请务必遵照其声明进行使用。

方案概述

没有特别说明的服务组件均基于上述选型结果直接处理。对一些需要特别说明的服务组件,在下面进行说明。

交易服务、事务服务、资金持仓服务方案

在需求中,各服务管理是一个个独立的个体。但在实际使用中,经常要对同一类服务做轮询操作(例如,在交易服务中找出最多持仓和最多资金的那一个)。有鉴于此,我们引入了服务管理的概念。服务管理是对同一类服务的统一管理。它负责处理对多个服务的广播式操作及负责结果的返回。对于特定的服务,服务管理直接做请求转发。

这样的设计中,服务管理和特定服务之间的交互将会非常频繁,而且服务不会绕过服务管理被其它组件直接调用(具体的服务有可能会调用其他服务的接口)。鉴于此,我们仅将服务管理作为系统组件,而将具体的服务作为服务管理组件的一个个子模块。

我们对于系统组件的开发选型是基于Java的Spring Boot,在这里,子模块的实现我们考虑使用Java的动态jar包加载技术。

风控服务方案

风控服务原则上是需要由客户自己来编写风控规则的。在现有系统里使用了Lua作为编写风控规则的脚本语言。出于兼容性考虑,在新系统中我们仍然使用Lua作为风控规则脚本。通过使用Luaj这个第三方库来实现。但Luaj和Lua之间的区别和兼容性需要进一步的评估

需要支持脚本的调试。LuaJ有调试接口,需要进一步研究

报盘服务

报盘服务针对不同的柜台系统有不同的实现。每一个柜台系统对应一个独立的报盘服务。对于特定的报盘服务,有可能需要调用C++的动态库。Java的JNI对C接口的动态库可以直接处理。但是对于C++接口不能很好处理。这时我们需要使用SWIG技术对C++做处理,生成Java的wrapper供Java使用。

另外,报盘服务可能需要与柜台系统长连接,因此报盘服务无法像其它服务一样做到无状态。因此对于报盘服务我们需要特殊对待。对它需要采用主备模式进行部署。

行情服务处理

目前的行情源来自东财行情,东财行情是受控网络中发布的,目前需要通过堡垒机转发到外网。但是堡垒机的接入IP也是受东财控制的,这不利于我们自身的业务推广。我们需要有一台完全独立掌控的服务器,将东财行情数据从堡垒机导出到这台机器。然后在自主控制的服务器上处理客户接入的问题。

在堡垒机上部署的行情代理应用只做东财数据的简单转发,将东财的数据格式转换为我们自己的格式。然后公共行情服务提供订阅服务,系统本地行情服务发送订阅至公共行情服务,公共行情服务将所订阅的行情实时推送至本地行情服务。本地行情服务缓存后供系统其他组件查询之用。公共行情服务同时提供查询服务,查询请求由系统内其他组件向本地行情服务发起,本地行情服务转发给公共行情服务以获得结果。

本地行情服务和公共行情服务之间带宽有限,且需要主动推送。目前有两个方案可供选择。

一个方案是在现有的行情系统上进行改造,拆分出行情代理,并在微兆端行情服务(行情公共服务)上增加交互接口,紫山端的本地行情服务则参考上面提到的报盘服务架构进行实现。此方案可以充分利用现有代码,风险较小,成效快,且现有飞马架构的容量、速度都得到过证明,容易估算系统容量。缺点是目前的行情系统使用的交互协议是私有协议,难以优化。容易形成性能瓶颈。

第二个方案是完全重新实现。公共行情服务和本地行情服务之间的接口使用 gRPC 的流传输特性进行实现。这样做的好处是跟其他模块一致,降低维护难度。且gRPC使用的是标准技术,方便未来进行传输优化。缺点是完全推翻重写,代价较高。且在性能容量等问题上需要进行进一步的认证。

根据讨论结果,倾向于重新实现的方案

因为涉及长连接,和报盘服务一样,本地行情服务也是带状态的,必须采用主备模式进行部署。

接口调用示例

接口调用V0.2.png

投资决策的提交服务是相对比较复杂的交互,组件间会有十几次调用交互。组件间调用的耗时大约在1毫秒以下,2整个逻辑完成应该在10毫秒的级别。对于本系统来说,是一个可接受的延时。

旧系统兼容性

考虑到原有架构相对封闭,功能拆分困难,建议对原系统进行独立维护,新系统并行开发。待开发完包含原系统的功能后进行整体替换。

开发约束

  • 各组件应该是无状态的。方便编排工具随时启停时状态一致,保证服务的可用性。组件的缓存由独立的缓存服务(如redis)负责。
  • 数据库、缓存服务均仅做数据存储,不使用存储过程、触发器等进行逻辑处理。逻辑均由各组件负责完成。
  • 各组件可以根据自身需要自由使用缓存、存储等服务。但只能访问自己的数据,不能访问其他组件的数据。数据的交互均通过接口完成。
  • 实现接口功能时需要考虑同一个请求的幂等调用。对于同一个请求,无论调用几次其返回结果应该是相同的。多实例的幂等由运维来保证。

开发中可能遇到的问题

跨进程的临界资源

因为缓存独立于进程,当同一服务同时运行多个进程时,缓存就成了临界资源。对缓存的不加锁访问可能会导致一份数据在更改时被多次使用。但如果对所有操作加锁,则会对性能造成很大的影响。


目前设想的解决方案是采用乐观锁策略:读及操作数据不加锁。写前再读一次相关数据检查是否发生变化,如果变化则直接出错。没有变化则写入缓存。写时对相关数据的读和写都加锁。


7x24

为了减少客户运维工作量,降低日常运维难度,我们考虑整个系统是 7x24 运行的。
开发部分需要考虑在 7x24 情况下,如何正确处理日期切换相关的功能。例如,行情、初始化信息、报单编号、状态等。


运维需要考虑 7x24 情况下如何实现备份、清理等功能

分布式事务

目前在交易服务处理持仓和资金时需要以事务处理。

为了简化分布式事务的复杂性,目前考虑将持仓和资金合并为一个进程。

被调用方(实际上考虑所有组件)的所有关键操作(主要是IO操作)日志留痕。此日志可以直接利用分布式调用跟踪的跟踪点实现,经过分布式调用跟踪系统或日志聚合系统汇总后形成全系统的操作轨迹。对于最终失败的操作,根据操作日志,进行人工或自动形式的纠错处理。