作为金融企业,国投瑞银基金多年以来IT工作主要还是以运维为主,主要业务系统基本采用外购模式,但随着业务的不断发展,业务部门个性化需求越积越多,外购与外包已经不能很好满足业务员部门的需要了。2016年底公司着手开发团队的组建工作,同时对公司的业务开发平台进行架构选型与设计,以求统一开发平台,提升研发效率,从而加快业务部门的业务需求处理效率。
下面我们将就这两年在平台架构选型、平台架构设计、平台及相关子系统的逐步完善背后的一些经验进行分享。
该架构全部基于开源平台,经过三年多的生产上线实践,平台运行平稳,可扩展性强,可用性高,可以很好满足公司对于金融业务不断发展的需要,这对类似的中小型企业的业务架构选型也具有一定的参考意义。
注:UFOS:国投瑞银基金运营系统
在初期平台架构设计与选型时,我们根据现有业务系统的需求,梳理出了技术架构选型需要考量关键因素:
当公司规模不大,实力不足以自己实现部分或全部架构,选择现成的“轮子”来组装自己的架构就成了一种自然的选择。在选择上可能会更多考虑如何使用更“标准”的“轮子”来满足自己业务的需求,以便于今后业务的升级和扩展。
要实现上述平台的扩展性和高可用性,一般都离不开分布式架构,而分布式架构一般离不开服务来承载 。
基于服务的架构设计早已有之,比如基于RPC的服务调用,最早可追溯到CORBA,以及现在还有很多金融公司在交易系统中使用的BEA早期的框架Tuxedo(主要编程语言为C/C++)。后来者有Facebook的Thrift,Google Protocolbuf框架/grpc,阿里的Dubbo框架等等。这些框架支持消息的二进制编码(序列化与反序列化),效率高,因此成了对网络传输,并发处理要求高的应用如App应用,游戏,交易软件等的首选。
后来随着HTTP协议的广泛应用,发展衍生出面向服务的架构(SOA)的架构设计,该架构一般都应用在比较复杂,大型项目中,为了异构系统中的功能复用,或系统性能的考量将功能模块独立出来成为服务,服务可以分布式部署,服务之间通过标准的软件接口方式在网络中相互调用;为了统一服务调用标准,SOA往往还引入了数据总线概念,服务可以通过数据总线进行服务注册,服务的查找与调度。
SOA架构中的服务之间是松耦合的,服务的颗粒度相对比较粗,而近些年出现的微服务,则可以看作是对SOA服务的一种精简,细化,或者说是SOA服务的轻量版。
在谈及微服务的时候,大都会对应到单体应用,以示鲜明对比;单体应用其实就是一个服务中包含了太多种功能的应用,它跟面向对象的设计里的单体类(包含太多功能实现的类)的提法颇有些类似,英文单词中有一个专有名词monolithic来描述二者:
如果你再仔细对照微服务和类,你会发现两者有诸多相似之处,比如微服务和类在设计原则上也是一致的,也就是高内聚/封装与松耦合,高内聚也就是只负责一项任务,也就是单一职责原则,而松耦合则是指模块之间的接口尽量简单,减少耦合度,这样也使得开发,独立部署和升级微服务更加容易。
微服务同时也很好地匹配了敏捷开发团队,减少了开发团队的沟通成本,更小的代码库同时有效降低了开发团队之间的冲突,使得小团队开发更加有效。
2016年,当时的微服务还不像在现在的市场上一样炙手可热,当时微服务兴起不算太久,在互联网企业有早行者,但市场整体上参考资料与可参考的案例相对并不多,市场上对选型微服务架构也不是太明朗。但对照前面架构选型的各种考量因素,微服务还是非常匹配我们的选型标准的,而且能与我们小团队敏捷开发的组织架构相匹配,因而微服务架构基本成为了我们架构中的首选。
既然选择了微服务,接下来的工作就是微服务框架的选型,选型中我们主要考量的因素是:
我们列出了当时市面上比较流行的微服务框架候选者:
当时微服务兴起不久,市场框架选择并不是太明朗,Spring Cloud当时也在不断地演进与完善中,Dubbo已经停止更新,但相对来说,国内市面上这两者的选择比较多,微服务开发的主流语言也是Java。
通过对比和调研我们最终选择了基于容器的微服务方案,这主要是基于:
开发语言我们还是选定了主流的Java,即使我们有一些历史遗留的C#项目,Java毕竟是编程语言的排头兵,也是开源的主力军,有很多的开源“轮子”使用,可以大大加快开发进程,即使其编译后的执行包偏大(几十到近百兆,虽然自Java 9的模块化编程有所改善),但对我们的业务平台相对来说并没有多大影响。
Java框架我们选择了Spring Boot,它在Spring MVC基础上简化了配置管理,也有多种starter,简化了编程,可以快速搭建微服务应用。
架构前期,由于人力有限,我们并未对微服务前端框架进行选型,更多是依赖外包开发商的现成框架,比如第一个项目的几个微服务就是采用的是基于jQuery的框架;第二个项目的开发商采用的是vue.js,并基于vue.js进行了组件包装(尚未完全完工),不过该组件包装需要额外付费。由于我们对外包方包装的组件信心不是很足,因为包装的组件基本没有测试用例,这迫使我们下定决心进行前端选型,这也有利于后续业务开发的一致性与可维护性,保证应用研发的质量。
随后我们花了两个多星期对市面上的前端主流框架进行了初略快速的选型,并做了Demo,最终Google公司当年的前端新品Angular 2框架被采纳。一是AngularJS受众大,这次Google团队不兼容重新设计也从AngularJS 1.x吸取了大量的经验,并结合近年来新的Web的进化和前端开发尤其是移动方面的变革,运用全新的思路进行重新架构,精简了1.x的概念与指令,并利用单向数据流,服务端渲染机制等大大提升框架性能;二是Angular支持组件式开发,并支持TypeScript,TypeScript吸收了许多面向对象的编程语言优势,跟后端语言更接近,使得后端人员上手前端开发比较容易。我们的开发人员通过Demo的学习与服务的前后端一体化指导开发,很快就成为前后端一体开发的全栈开发工程师。
后续Angular的高速发展印证我们选择还是正确的,对于自身没有很强研发实力的,选择正确的框架还是非常重要的,尤其是大厂商的产品,因为其周边的生态也会日趋完善,产品生命周期更长久(慎用小开发商的产品,对开发中组件的引入最好也要做好审批)。
在Angular之上我们选择了开源的PrimyNG组件套件以简化前端开发,PrimeNG 是一个极为完善的开源 Angular UI 组件库,现在已经发展到 80 多个组件,基本可满足我们业务开发中的所有 UI 需求,虽然当时的版本还偶有Bug或功能未完善的组件。现在,我们的前端已经形成了一套比较成熟的开发模板,可以快速完成微服务前端模块的搭建。
当然,在Angular上还有其他一些成熟的开源组件平台可以选择,比如官方的Material和阿里的NG-ZERO组件等。
运行中的微服务实例其实就是一个个的进程,它们可以在网络中分布式部署,微服务调用涉及到网络之间数据的交换,其实也就是数据对象的序列化与反序列化。
RPC框架通常有自己的接口描述语言(Interface Definition Language,IDL),框架提供工具可以将IDL生成服务端和客户端的stub,stub可以实现消息序列化与反序列化,以便于数据消息对象通过网络在客户端与微服务之间传输。由于RPC框架支持二进制编码方式进行序列化,因此传输效率更高,可以获得更高的性能和更低的延迟;而且相对纯文本方式的数据传输,数据安全性更高。为增加安全性,一般这种编码还会设计相应的数据头,以便于对主体数据进行加密与签名。
RPC通信由于双边需要stub,更多用在有独立客户端的情况如Client/Server模式下,很多还受限于具体框架支持的语言,也与平台相关(比如很多本质上是基于二进制,有的对大端字节顺序的平台可能就不支持),因此选用和语言和平台无关的通信协议是一种更好的方式。
RESTful API是基于超文本传输协议HTTP之上一种架构设计风格,当时已经在Web应用开发中比较流行了,它通过URI来唯一定位一个需要操作的资源,而使用标准的HTTP方法来完成对资源的CURD操作,这种设计简洁,轻量,易用。URI中的数据传输简单的可以采用Key/Value的形式,复杂的可以采用JSON数据格式,后者在诸多编程语言中可以方便实现数据的序列化和反序列化。
选择开放标准的传输协议,其上的生态链更丰富。我们微服务平台架构选择了当时流行的B/S架构模式,RESTful API就成为我们一种自然的选择,结合SpringBoot框架,它给我们带来以下好处:
除了基于REST API方式的数据交换以外,我们在设计规范中也规定了其他的RPC通信模式,比如Thrift和Protocolbuf,当遇到以下场景可以考虑使用:
RESTful API微服务调用一般都采用请求/响应的同步模式(单向通知除外),如果需要进行异步的API调用,比如有些耗时的请求,客户端一直阻塞等待响应可能不是一种好的处理方式,采用异步处理方式有:
消息系统使用方式通常有以下几种方式:
采用消息机制的优点:
消息机制的缺点:
根据业务需求,我们对消息系统的选型更侧重在可靠性,其他方面如吞吐量等并无更多的特别要求,市面上有较多成熟的消息系统可以选择,在金融系统中,RabbitMQ就因其较强的扩展性,较高的可靠性和可用性被广泛使用,ActiveMQ也有不少使用案例。
ActiveMQ 是基于 JMS实现的消息系统,它主要提供了两种类型的消息:点对点以及发布/订阅。它是一个高可靠性、高性能和可扩展的消息系统,支持消息标准协议,例如AMQP(1.0标准)和STOMP,官方支持多种编程语言,包括经常使用的Java, C++(RabbitMQ官方并不提供C++编程语言包,而我们在平台中规划了C++应用比如传真微服务)和Python, ActiveMQ可以很好满足现有业务的需求,最终我们选择了ActiveMQ作为我们消息系统。
设想一下,微服务按照我们上面所述通信方式提供了REST API或者RPC接口调用,为了完成一次服务请求,调用方需要知道服务实例的网络位置(IP地址和端口)。传统应用都运行在物理硬件上,服务实例的网络位置都是相对固定的(DNS或者静态IP地址)。而对于一个现代的,基于云部署的微服务应用来说,这却是一个很麻烦的问题,服务实例的网络位置都是动态分配的,而且因为扩展、失效和升级等需求,服务实例会经常动态改变,因此,客户端代码需要使用一种更加复杂的服务发现机制。
在我们的架构设计中,我们选择了容器作为微服务的载体。其设计思路就是把一个微服务装入一个容器中,也就是一个容器中运行一个微服务,微服务通过容器对外提供服务接口调用;而容器作为一种标准构件,非常容易在网络中实现管理和监控;服务的发现和注册可以通过容器相关技术来实现,这会用到容器的编排与管理技术。
当时在容器市场上,Docker可谓一枝独秀,但容器编排还处在一片混战中,局势并不是太明朗,市面上流行的编排方案有Kubernetes,Mesos和Docker Swarm等,Docker Swarm是由Docker容器厂商创建的集群工具,它对外提供的是完全标准的 Docker API,可以与Docker引擎无缝集成,任何使用 Docker API 与 Docker 进行通讯的工具(Docker CLI, Docker Compose)都可以完全无缝地和 Docker Swarm协同工作,因此Docker的经验也可以继承过来,非常容易上手,学习曲线和二次开发成本相对Kubernetes都比较低。同时Docker Swarm本身专注于Docker集群管理(Kubernetes对容器管理进行了抽象,支持Docker,rtk等),非常轻量,占用资源也非常少,运行效率也高,也有支持几千个容器的集群案例。
虽然Kubernetes发展势头更猛,根据项目需求和公司研发实力,我们最终还是选择了能更快实施与部署的Docker Swarm方案(当然到了2017年中,容器编排之争基本落下帷幕,最新统计Kubernetes占据了75%以上市场,Docker Swarm从曾经的三成已经减少到了个位数)。
Docker Swarm在版本1.12之前是一个独立的项目叫Swarm Standalone,这也是我们使用的第一个版本,它的服务发现依赖于外部的 k/v存储,我们按官方指引选用了Consul集群;Swarm也是独立的进程,需要独立安装,我们采用了容器模式运行Swarm进程。
版本1.12后Docker Swarm升级为Swarm Mode,Swarm也合并到了Docker引擎之中,也就是安装了Docker引擎Swarm服务(内置安全认证,服务发现,负载均衡,集群放置在内置的datastore的meta data,调度,容器网络)就已经在那里了,不需要额外安装,你所需要的只是创建集群。
Swarm Mode内置的服务发现是通过每个节点Docker引擎内置的DNS Server来实现,在集群中创建的服务都会在相应的节点的DNS Server进行登记,各节点的DNS Server之间通过gossip协议进行信息交换;每个服务都有一个DNS解析器,它将DNS查询转发到节点Docker引擎,由DNS Server来进行解析, 如果不能解析则转发到配置的外部DNS Server,这样服务在集群中任意节点就可以相互访问了。
服务的负载均衡(LB)缺省是通过虚IP(VIP,使用微服务的服务名)或一组服务的IP(tasks.服务名)根据内置的IPVS来实现,也可以在创建服务的时候指定dnsrr模式通过round-robin轮询来实现。
就Swarm Mode集群本身来说,安装比Swarm Standalone简单了不少,升级和移植也比较简单。通过近3年多我们业务系统生产的营运实践表明,Docker Swarm可以说完美满足我们的现有业务平台的需要,包括其稳定性,集群的高可用性(包括容器自我检测与自动重启,错误节点中的容器自动转移)和可扩展性(包括集群节点的动态扩容,容器服务的实例动态扩充),也可以满足业务未来多年发展的需要。
Figure 1: Docker Swarm服务发现与负载均衡
微服务除了内部相互之间调用和通信之外,最终要以某种方式暴露出去,才能让外界系统(例如Web应用、移动应用等)访问到,这就涉及服务的前端路由,它是连接内部微服务和外部应用系统的一个通道。
HaProxy与Ngix等工具也可以实现HTTP反向代理,但基于以下特性,开源的HTTP反向代理与负载均衡工具Traefik成为我们的最终选择:
可以说Traefik非常适合容器化的微服务,采用Traefik可以带来以下好处:
Figure 2: HTTP 反向代理
由于采用分布式架构,并且使用容器来承载微服务,如果使用本地日志文件模式,日志就散落在各个容器内部或各个宿主机上了,这样不利于日志的统一管理和使用,因此,采用一个集中的日志系统中心,也就成了一个必然的选择。
Figure 3: 日志子系统
在我们的架构选型中,我们选择了流行的开源框架ELK栈;日志写入远端的Elasticsearch,通常可以采用两种方式,一种方式是通过日志代理,如Elasticsearch提供的高效的Beats工具,可以将Beats与业务服务部署在一起,这适合第三方服务(没有源码)或开发语言无标准日志组件的服务。而另外一种方式则是通过日志的SocketAppender,直接将日志通过网络写入远程的日志服务,如LogStash,很多标准的日志组件都支持这种方式,如Java标准日志输出如Log4j,Logback等。这种方式也比较适合在容器中部署的微服务,不需要额外再部署另外的日志工具。在我们微服务平台中,日志输出我们选用了性能较高的Logback,并选用了与之配套的LogStash输出插件,通过该插件(代理)Logback可以将日志通过Socket直接输出到Logstash服务,而这毋须对代码做任何改动,仅需要通过简单的配置文件配置即可方便实现,对调用日志的应用微服务完全透明。
为便于后续的日志查找和Kibana中的日志数据展示 我们需要对日志的格式进行规范化,以便将日志中的关键信息以键值对的方式存入ElasticSearch,规范化涉及到日志文本的编码与解码,分别在应用端和LogStash端,LogStash服务可以通过配置来对消息进行Mapping和过滤。
如果日志量比较大,则需要在日志输出与LogStash中间增加消息缓冲,Kafka是一个高吞吐量的消息系统,Log4j2有直接输出到Kafka的Appender。
监控系统是平台服务治理中的一个重要组成部分,没有监控的应用系统可以称作一个裸奔的系统;我们原有的业务平台已经有了一套传统的监控系统Netgain,但更多是对基础设施的监控,缺乏对应用系统内部状态的真正监控,比如对微服务和容器的支持,不能很好满足UFOS微服务平台的需求。
Prometheus作为从CNCF毕业的第二个开源项目(第一个是容器编排项目Kurbernetes,Prometheus本来也是源自Google对Kurbernetes的监控),它能很好地监控服务以及容器,除了能与Kurbernetes无缝集成以外,也可以与Swarm很好地集成,尤其是配合Docker Swarm中的label与global配置选项使用,可以非常方便实施远程应用监控代理(exporter)的部署。
由于Prometheus是一个开放的监控平台,因此有大量的官方及第三方的监控代理Exporter(监控代理可以协助不支持Prometheus数据采集接口的第三方服务公开自身的监控数据),在UFOS中主要使用了以下监控/代理:
其中BlackBox采用的是非代理模式,由于已有netgain做基础设施监控,所以并未使用SNMP Exporter。
Prometheus本身也提供了告警服务模块AlertManager,你可以通过基于Prometheus内置的强大的查询语言PromQL来设置告警规则,Prometheus会根据设定的时间间隔从配置好的服务收集度量指标,如果某个指标与定义好的规则匹配,则触发告警。AlertManager支持告警分组,告警抑制和静默,告警撤销,告警规则正则表达式匹配,告警模板等功能特点。AlertManager支持多种告警通知,如邮件,Scribe,Hipchat,Wechat(官方原生支持)等,还支持web接口调用,可以通过webhook与微服务集成,比如阿里钉钉就可以通过该种方式接入。
Figure 4: 监控子系统架构图
Prometheus提供多种客户端API接口调用库,如官方提供Java,Python,Go,第三方提供C++,PHP等库,通过这些库你可以很方便在你的微服务中植入监控的度量数据(通过微服务Web接口,如果是批处理任务,则可以将生成的监控度量数据发送到PushGateway服务进行托管),为Prometheus服务进程拉取到,这样我们可以方便实现对业务数据的监控。
监控界面展示使用Grafana,Grafana 是一个开源的图表可视化系统,支持多种时序数据库如InfluxDB,当然也支持Prometheus,Grafana有丰富的图形展示组件,官方网站也提供大量现成的模板,UFOS中对Swarm节点,微服务,数据库,告警等资源进行了监控展示。
为保障业务的稳定可用,平台应保证持续可用,不会无故宕机,即使出现故障也可以快速发现和定位,通过监控机制,能在系统用户发现之前尽快解决问题,抑或系统能通过设计自动发现故障并进行自动故障转移,比如通过主备或集群的冗余方式来避免单点的问题,这里我们将针对后者,从系统设计来提升系统高可用性进行简要介绍。
UFOS运行平台基于Linux系统,平台入口是HTTP反向代理Traefik,为实现入口的高可用,我们必须保证Traefik的冗余备份。
Traefik本身支持集群方式的HA方式,基于配置的K/V存储,官方推荐的是Consul。但是由于我们服务平台是基于Swarm集群,Traefik是以Swarm服务方式运行(限制在Swarm Manager节点),它可以通过Swarm Manager节点读取到足够的Swarm中运行的服务实例的相关信息。而Swarm Manger之间通过Raft算法实时交换信息,因此运行多个独立的Traefik实例它们获的服务实例信息是最新也是对等的,所以我们并不需要按官方指引的使用K/V存储来实现Traefik的高可用。
为实现Traefik的故障自动转移,我们对运行Traefik Replica实例的Swarm Manager节点设计了基于VIP的Linux集群方案,使用Pacemaker+Corosync,其中Corosync用于检测节点间通讯是否正常,而pacemaker则用于管理集群资源。当检测到Linux集群中的任何一台节点故障时VIP会自动切换到其他的正常节点,入口也自动切换到该节点上运行的Traefik上来,保证HTTP访问代理的可用。
所有的微服务都是以Swarm服务的方式运行在Swarm容器平台上,微服务的高可用性由Swarm提供。Swarm容器编排系统本身支持高可用,在UFOS Swarm集群中配置了三台Manager节点(最多可以承受一台Manager故障),Manager之间通过Raft进行Leader的选举,这种选举保证了单个节点的异常不影响整个Swarm集群的运行。
Swarm中运行的微服务容器也是高可用的,一是可以通过启动多个相同微服务实例来实现微服务的高可用,Swarm内部可以通过VIP的方式来实现微服务容器之间的负载均衡与故障的无缝切换(VIP只会转发到健康的服务)。即使是单个微服务容器实例,Swarm仍能保证微服务的高可用性,如因节点故障,导致节点中运行的微服务容器异常,Swarm Manager可以自动检测到节点异常,然后把异常节点中的微服务容器,转移到集群中其他健康的节点上去,并在其他节点重启微服务应用,这样仍然可以保证容器中运行的微服务可以被访问,从而实现微服务的高可用性(容器编排技术可以保证容器的动态发现,即使容器被转移到其他节点上重启,从而实现微服务的动态访问,当然这里可能有个延迟,要实现这点还有一个就是需要保证微服务被设计为无状态的)。
Oracle Database采用典范的RAC集群,MongoDB则是基于官方提供的容器镜像,以容器方式实现了三台MongoDB的Replica配置。
Redis采用主从复制模式,配置了一主二从三个节点,同时配置了相等数目的Redis Sentinel,这些Sentinel能共同合作完成故障发现与判断,以及故障转移,并通知应用方,从而实现真正的高可用。
ActiveMQ采用官方推荐方式,实现了基于RDBMS的主从模式,从消息队列定时从RDBMS共享表中检测主消息队列的刷新情况,如主消息队列异常,未能在指定时间内更新,从消息队列提升自己为主消息队列,从而实现主从的切换。这里需要注意的是必须保证主从服务节点的系统时间的同步。
文件系统的高可用是通过NFS文件系统与底层的存储来实现。
经过生产环境的实践,随着平台的不断完善和运维经验的不断积累,UFOS平台的可用性已从99.95%逐步提升到了99.99%。
以上各小节对微服务平台的各个子系统依次进行了描述,下图是各子系统集成到一起组成的一个完整的微服务平台整体架构图:
Figure 5: 微服务平台整体技术架构图
该基于容器的微服务架构平台给我们的研发带来了以下益处:
经过三年多微服务平台运营实践,总结起来该基于容器的微服务架构平台给我们的研发带来以下益处:
该平台架构可以作为中小企业对微服务平台架构选型的一种参考,当然你可以使用Kubernetes替换Docker Swarm, 毕竟后者成为了小众产品(如果从入手的简洁性,Swarm依然还是具有吸引力的,几天之类上手),其他子系统的选型也可以作为参考。
领取专属 10元无门槛券
私享最新 技术干货