sjk000
作者sjk0002017-07-11 09:44
其它, 456

【交易技术前沿】微服务架构的研究与应用 / 陈林博,何支军

字数 16887阅读 5387评论 1赞 3

本文选自《交易技术前沿》第二十四期 (2016年9月)。
陈林博,何支军
中国证券登记结算有限责任公司上海分公司技术开发部,上海
Email: Lbchen@chinaclear.com.cn

摘要:软件架构经过多年的发展,为满足业务的需求,已从单一的单块架构逐渐向基于服务的架构发展。微服务架构作为最近几年在软件架构领域最新的名词,尽管诞生时间不长,但已在飞速发展的互联网企业中初露峥嵘,有效地支持了国内外知名的互联网公司的发展。本文试图从软件架构的发展历程出发,详细分析软件单块架构的优劣势,以此引出了微服务架构的概念,剖析了微服务架构的本质特点、发展趋势及其在国内外应用现状。同时就构建微服务架构的核心问题作出分析与阐述,围绕微服务如何划分、服务之间的通信机制、服务注册与发现机制、跨服务的数据访问以及服务容错机制等方面,分析比较了现有的技术方案,以期为读者全面揭示微服务架构。
关键词:单块架构、微服务架构、服务通信、服务注册与发现

1 单块架构

1.1 软件架构发展简述

软件的架构设计是决定应用系统是否能够被正确、有效实现的关键要素之一。架构设计描述了在应用系统的内部,如何根据业务、技术、组织,以及灵活性、可扩展性、可维护性等多种因素,将应用系统划分成不同的部分,并使这些部分彼此之间相互分工、相互协作,从而为用户提供某种特定价值的方式。

软件架构中以“层”来划分软件,软件应用的层级概念经历了单层架构、两层架构到三层架构的发展历程。单层架构是将所有的页面逻辑、业务逻辑以及数据库访问逻辑融合一起,因此代码之间的调用相互交错,错综复杂。 两层架构中,数据访问部分的代码逐渐有了清晰的结构,但表示逻辑和业务逻辑依然交织在一起。如Java语言的JDBC、IO流,或者.NET的 ADO.NET等。

在三层架构中,表示层、业务逻辑层以及数据访问层的概念逐渐清晰。其中表示层通常指当用户使用应用程序时,看见的、听见的、输入的或者交互的部分。业务逻辑层是指是根据用户输入的信息,进行逻辑计算或者业务处理的部分,聚焦应用程序对业务问题的逻辑处理,以及业务流程的操作。数据访问层则关注应用程序是如何有效地将数据存储到数据库、文件系统或者其他存储介质中。如图1所示。

QQ截图20170710162216.png

QQ截图20170710162216.png

三层架构一方面是为了解决应用程序中代码间调用复杂、代码职责不清的问题。通过在各层间定义接口,并将接口与实现分离,可以很容易地用不同的实现来替换原有层次的实现,从而有效降低层与层之间的依赖。另一方面,从某种程度上也解决了企业内部如何有效地根据技能调配人员,提高生产效率的问题。因此在很长的一段时间里,它一直是软件架构的经典模式之一。

1.2 单块架构

1.2.1 单块架构的定义

软件的三层架构是从逻辑层面上的分层,而物理层面上软件,即使经过三层的分拆,可由不同的开发团队予以实现,但经历编译、打包及部署后,在不考虑负载均衡以及水平扩展的情况下,最终还是运行在同一个机器的同一个进程中。对于这种功能集中、代码和数据中心化、一个发布包、部署后运行在同一进程的应用程序,通常称之为单块架构(Monolith Architecture)的应用。 典型的单块架构应用,莫过于传统的J2EE项目所构建的产品或者项目,它们存在的形态一般是WAR包或者EAR包。当部署这类应用时,通常是将整个一块都作为一个整体,部署在同一个WEB 容器,如Tomcat或者Jboss中。当这类应用运行起来后,所有的功能也都运行在同一个进程中,如图2所示。因此单块架构应用是基于分层软件架构设计的系统基础之上,从部署模式、运行模式角度去考虑的一种定义方式。
QQ截图20170710162501.png

QQ截图20170710162501.png

1.2.2 单块架构的优劣势分析

对单块架构的应用程序而言,现有的集成开发工具比较适合单块架构的应用程序,如NetBeans、Eclipse、IDEA等,都能够有效加载并配置整个应用程序的依赖,方便开发人员开发、运行、调试等,因此易于开发。由于所有的功能最终都会打成一个包,并运行在一个进程中,因此启动集成开发环境或者将发布包部署到某一环境,就可以立即开始系统测试或者功能测试。并且其部署时只需复制该软件包到服务器相应的位置即可,因此其水平伸缩更为简单,更容易建立集群服务。

然而随着公司或者组织业务的不断扩张,需求不断的增加以及用户量的不断增加,单块架构的优势已逐渐无法适应互联网时代的快速变化,面临着越来越多的挑战。一方面,随着业务的扩大,如何为用户提供可靠的服务,如何有效处理用户增多随之而来的并发请求数增多、响应慢等问题,以及如何有效解决用户增多后带来的大数据量的问题等。另外一方面,随着公司或者组织业务的不断扩张,需求不断的增加,越来越多的人加入开发团队,代码库也在急剧膨胀。在这种情况下,单块架构的可维护性、灵活性在降低,而测试成本、构建成本以及维护成本却在显著增加,如图3所示。

1.2.2 单块架构的优劣势分析

对单块架构的应用程序而言,现有的集成开发工具比较适合单块架构的应用程序,如NetBeans、Eclipse、IDEA等,都能够有效加载并配置整个应用程序的依赖,方便开发人员开发、运行、调试等,因此易于开发。由于所有的功能最终都会打成一个包,并运行在一个进程中,因此启动集成开发环境或者将发布包部署到某一环境,就可以立即开始系统测试或者功能测试。并且其部署时只需复制该软件包到服务器相应的位置即可,因此其水平伸缩更为简单,更容易建立集群服务。

然而随着公司或者组织业务的不断扩张,需求不断的增加以及用户量的不断增加,单块架构的优势已逐渐无法适应互联网时代的快速变化,面临着越来越多的挑战。一方面,随着业务的扩大,如何为用户提供可靠的服务,如何有效处理用户增多随之而来的并发请求数增多、响应慢等问题,以及如何有效解决用户增多后带来的大数据量的问题等。另外一方面,随着公司或者组织业务的不断扩张,需求不断的增加,越来越多的人加入开发团队,代码库也在急剧膨胀。在这种情况下,单块架构的可维护性、灵活性在降低,而测试成本、构建成本以及维护成本却在显著增加,如图3所示。

2微服务架构

传统的IT系统属于单块架构系统,单块架构在开发、部署、测试及水平扩展是行具有很强的优越性,尤其适用于代码量少且功能较为简单的软件应用。但对于复杂应用而言,单块架构则显得较为笨重。单一的服务变化而带来的系统更新,则需要重新部署整个系统以配合。为了满足业务需求而进行的水平扩展,则需要将整个系统重新部署到新的服务器上,而其他业务模块也被动部署,从而导致资源极大的浪费。 针对单块架构不能适应当前互联网时代的快速变化,同时,十年来敏捷、精益、持续交付等价值观、方法论的提出以及实践,虚拟化技术的成熟并成为主流,推动了微服务架构的理念与应用的开展。

2.1微服务架构的定义

2.1.1 微服务架构简述

微服务架构(Micro-service Architecture)在2011年威尼斯举办的软件架构学术研讨会上首次被提出,并在2012年时分别由James Lewis[1]和Fred George[2]在多个会议和论文中陆续阐述。一直以来,国内外多家知名互联网公司如Amazon[3]、Netflix[4]、Twitter[5]、eBay[6]、Uber[7]、阿里巴巴[8]等,都在实施并构建微服务架构以满足公司的业务需求。

微服务架构与单块架构的水平划分不同,它采用垂直划分模式,将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,并对外提供最终的功能。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通,如图4所示。

微服务架构中的每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。对于具体的每个服务,可根据业务需求和实际运行环境,选择合适的语言、工具对其进行构建。这也就意味着微服务架构上运行的服务,可采用不同的技术栈来实现其具体业务功能。
QQ截图20170710162701.png

QQ截图20170710162701.png

(图来源http://martinfowler.com/articles/microservices.html#HowBigIsAMicroservice

因此微服务架构存在下述几个特点:

  • 精悍短小,其服务只关注某一具体事务;
  • 独立进程,每一个服务都应该独立运行在不同的进程之中,享有各自的虚拟内存空间;
  • 轻量级通信,采用基于语言无关、平台无关的协议(如XML、JSON),在不同服务间通信;
  • 松耦合且独立部署,服务的变化不会对有调用关系的其他服务造成影响,变更仅涉及该服务本地内容,因此可独立部署;
  • 持久层分散,即服务之间不共享持久层,服务之间的数据访问属于跨服务的数据访问,可通过发起服务调用或者保存数据副本的形式来实现。

2.1.2 微服务架构的优劣势分析

由于微服务架构的特点,其优势主要体现在以下多个方面:

  • 技术多样性,技术选型成本低:首先微服务架构中的各个服务可根据业务的需求,采用不同的技术栈进行实现,消除了对某个技术栈的长期依赖,因此技术选型上灵活度高,成本低。
  • 容错性高:在微服务架构中,单个服务的失效(如内存泄露)并不会导致其他服务也发生失效,而其他服务则可继续运行。若采用了服务多活的机制,单个服务的失效并不会造成该类服务的停止,相应的业务处理可由冗余服务代替完成。
  • 扩展性强,易部署:每个服务可以部署到最适合其资源需求的硬件上,其扩展性强。且单个服务部署仅涉及本地内容,不会影响其他服务。 - 可置换性高:由于每个微服务相对较小,对于开发人员来说,代码更易于理解。使用更好的语言或技术重写服务将变得更为快捷、方便和可行。

技术是把双刃剑,没有什么技术是完美并唾手可得的,微服务架构也在实现过程中带来更高的复杂度和挑战性。微服务架构作为分布式系统的一种形式,在实现过程中承受创建分布式系统所带来的额外的复杂性,同样也引入了明显的运维复杂性,面临跨服务部署的复杂性。

QQ截图20170710162826.png

QQ截图20170710162826.png

2.2微服务架构与SOA

面向服务架构(Service-Oriented Architecture,SOA)是一种设计方法,它将一系列服务聚合并对外提供一些最终的功能集。SOA按照不同的、可重用的粒度对复杂的企业IT系统进行划分,将功能相关的一组功能提供者组织在一起为消费者提供服务。

早期的SOA在实现时无一例外的都选择了企业服务总线(Enterprise Service Bus,ESB)来整合集成大量的、单一的应用,以保护企业前期投入的成本,提高软件的可重用性。然而由于过重的通讯协议、缺乏统一的中间件厂商服务标准、缺乏服务粒度的划分实践等核心问题上,并没有较好的解决之路,从而导致SOA的发展停滞不前。

2.2.1 微服务架构与SOA的区别

从面向服务角度来看,微服务架构和SOA本质上都是相似的,都为解决单块架构的缺陷。相比于十多年前提出的SOA,经过近几年互联网行业的高速发展,以及敏捷、持续集成、持续交付、DevOps,云技术等的深入人心,微服务架构已与SOA有很大区别,其详情如下表所示:
QQ截图20170710162910.png

QQ截图20170710162910.png

在实现层面上,微服务架构抛弃了ESB复杂的业务规则编排、消息路由等功能。微服务架构中服务是高内聚的,每个服务都会处理相应的业务,所有的业务逻辑应该尽量在服务内部处理。

微服务架构采用了多样化的数据持久层,持久层可以使用传统关系数据库和NoSQL。不同于传统的应用,微服务架构中可为每个服务选择一个新的适合业务逻辑的数据库系统,比如MongoDB[11]、PostgreSQL[12]。

2.3微服务架构在国内外的发展应用

目前,国内外多家知名互联网公司如Amazon、Netflix、Twitter、Uber等,都在实施并构建微服务架构以满足公司的业务需求。本节将介绍上述公司在微服务架构上的工作。

2.3.1 Twitter

早期的Twitter系统是全球最大的Ruby on Rails应用,随着用户规模和服务数量的快速增长,系统所有的数据库管理、Memcache链接以及公共API的代码属于同一个代码库,对开发运维带来了巨大的困难。同时采用的MySQL存储系统已经遇到性能瓶颈,单纯的添加硬件设施也无法解决根本问题,即前端Ruby服务器每秒处理交易的数量大大低于预期,与其硬件性能不成比例。在2010年世界杯上,一个点球和一张红牌产生的“推文风暴”都可能导致Twitter服务暂时失去响应。

为了解决性能、效率和可靠性上的问题, Twitter公司采用了微服务架构,将整块单一应用分解成不同的“服务”,建立的松耦合的服务关系。同时采用全新的分区策略,用Gizzard[13]框架创建容错的分片分布式数据库,用以存储推文,并采用Snowflake[14]解决MySQL的唯一ID生成问题。

由此带来的好处是,在2015年8月3日《天空之城》在日本的热播创下每秒新增143119条推文的Twitter峰值记录,是Twitter平均每秒发推数(TPS)5700条的25倍。而这次毫无征兆的“洪峰”并没有给Twitter全新的系统平台带来任何的宕机甚至延迟。

2.3.2 Amazon

2001年零售网站Amazon.com由大型的单体、多层的软件架构组成,由于每层含有多个组件,耦合度很高。随着项目的成熟和开发者的增多,代码库变得越来越庞大,架构越来越复杂,单块架构带来了非常高的额外开销,软件开发周期因此而变慢。Amazon公司大量的开发人员尽管只负责应用的中某一部分,但在添加新功能和修改bug时,不得不在相关的其他模块的协调中花费大量精力。

为了解决软件开发周期变慢而导致创新能力下降的问题,Amazon将单块架构迁移到面向服务的架构,将所有代码按照功能模块隔离,然后将每个模块用网络服务接口封装,各个模块间的通信采用web service API。微服务可以相互独立地迭代升级。

经过架构和组织的重大变革后,Amazon极大地提高了前段开发的效率。产品团队可以迅速作出决定并转化为微服务中的新功能。得益于微服务架构和持续交付流程,现在Amazon每年可进行5000万次部署。

2.3.3 Netflix

Netflix是个非常受欢迎的视频流服务提供商,占有多达30%的互联网流量,它有着大规模、基于微服务的架构,每天处理800+不同类型设备超过10亿次视频流API的请求,每个API可以展开成平均6次对后端服务的调用。

不仅如此,Netflix还在开源社区贡献了整个微服务框架,包括服务注册发现框架Eureka、服务网关Zuul、服务端框架Karyon、客户端框架Ribbon、服务容错组件Hystrix、服务配置组件Archaius、日志组件Blitz4j等等。

2.3.4 Uber

Uber在成立之初采用单体架构构建了一款仅服务于一座城市的产品,但随着Uber的迅速发展、核心领域模型的扩大,组件成了紧耦合的,持续集成成了很大的负担。新增特性、修复Bug、技术债务解决,全都在单个库中进行,使得业务拓展受到技术平台的限制。Uber开始效仿Amazon、Netflix将单个代码库拆分成多个代码库,由单体架构迁移到微服务架构。

在迁移过程中,为保证500多个服务能被发现,并且保证服务调用之间具有很高的容错性和低延迟,Uber采用了Apache Thrift[15]跨语言服务开发框架,通过将服务绑定到严格的契约来确保安全性。而作为Uber的核心系统,即调度系统,被分割成两大类服务包括供给服务和需求服务。供给类服务用于跟踪所有供给的性能和状态机,需求服务用于跟踪需求、订单和乘客需求的其他方方面面。供给类服务和需求类服务通过DISCO(调度优化)服务来串联。

通过向微服务架构的转变,Uber系统的可用性和可靠性得到极大的提升,同时其部署能力也得到较大极高。帮助Uber在过去4年的时间里,实现了业务量38倍的增长。

3 构建微服务架构的重点问题研究

根据不同的业务需求和环境,以及系统发展的前景,可采用不同的构建微服务架构的实施方案。但所有的实施方案都离不开下面几个核心问题:服务划分原则、服务通信机制、服务发现机制、跨服务数据访问及容错机制等。本章将重点介绍这些核心问题的研究及部分成果。

3.1 服务的划分

采用微服务架构面临的首要问题就是如何将一个单一应用拆分为多个服务。由于划分后的服务需要具有高内聚的特点,服务之间低耦合,单一服务提供的功能是可以独立被替换和升级。假设有A和B两个功能,如果A功能发生变化时同时B功能也需要变化,那么A和B这两个功能应该被划在一个服务中。

拆分服务可以按照动词或者是用例来进行拆分,也可以将系统按照名词或者资源进行拆分,拆分后的服务负责给定类型实体/资源的所有操作。以一个在线商店应用为例,如图6所示,该应用属于典型的三层架构组成的单块应用。表示层中含有前端显示UI,用于实现前端显示界面。业务逻辑层中包含分别用于处理目录、订单处理及用户处理等服务。数据访问层则包含产品、订单及客户相关的数据操作。该应用以WAR包的形式部署在Tomcat服务器上,并在一个进程中运行。

QQ截图20170710163140.png

QQ截图20170710163140.png

对该应用按照资源进行划分后,可拆分为下面两类共9个服务。第一类是前台服务,包括目录UI、结账UI、订单管理UI、账户管理UI 。第二类为后台服务,包括目录服务、历史浏览服务、推荐服务、订单服务、用户服务,如图7所示。
QQ截图20170710163204.png

QQ截图20170710163204.png

3.2 服务通信机制

微服务架构中的服务不仅要暴露给外界系统访问,并且由于单个服务运行在进程中,各个服务之间需要通过IPC实现通信。本节将分析常用的对外通信机制API网关,并阐述了针对内部通信的不同模式而采用的多种通信机制。

3.2.1 对外通信

微服务架构上的服务除了内部相互之间调用和通信之外,最终是要暴露给外部用户。直接的方案就是将客户端与微服务直连,每个微服务对外可见,即每一个微服务都会有一个对外服务端(URL地址)。
假设前述在线商店应用可通过移动客户端直接访问,为了实现产品查询功能,需要对图7中至少5个微服务逐个访问。而在实际生产中,展示一个页面有时需要调用多个的服务,这样会导致客户端代码非常复杂。同时由于微服务可采用不同的技术栈,每个服务对外通信的协议并不需要统一,如采用Thrift的RPC协议或者AMQP消息协议等,则对浏览器或防火墙并非友好。由于客户端与微服务直连,则对微服务的重构都将会造成客户端的被动更新,随着微服务数量增多,客户端被动更新频率将会难以接受。综上所述,客户端与微服务直连方式在实际使用中基本不会采用。

微服务架构中可采用API网关实现对外通信,如图8所示。API网关是一种位于应用的客户端和微服务之间的服务器,是外部进入系统的唯一节点,它提供了针对客户端定制的API。API网关负责请求转发、合成和协议转换,所有来自客户端的请求都要先经过API网关,然后路由这些请求到对应的微服务。API网关将经常通过调用多个微服务来处理一个请求以及聚合多个服务的结果。它可以在web协议与内部使用的非Web友好型协议间进行转换,如HTTP协议、WebSocket协议。
QQ截图20170710163352.png

QQ截图20170710163352.png

由于外部网络延迟等因素,API网关可为低性能网络的移动客户端提供粗粒度的API,为高性能网络的客户端提供细粒度的 API。比如,桌面客户端可以发起多次请求来检索关于某个产品的信息,而在移动客户端只需发送一次请求。因此可以多个节点的形式在网关服务器上部署,如图9所示:
QQ截图20170710163415.png
QQ截图20170710163415.png

每个节点不仅负责客户端与应用的通信,封装了微服务的细节,对外屏蔽后台服务的复杂性,还对外屏蔽了后台服务的升级和演化。除作为服务代理的功能外,节点还负责安全负载均衡、认证和防爬虫、限流和容错、监控和日志等功能。目前开源的网关组件有Netflix的Zuul[16],其特点是动态可热部署的过滤器(filter)机制,其它如HAproxy[17],Nginx[18]等都可以扩展作为网关使用。

3.2.2 内部通信

微服务架构与单块架构最大的不同之处就是组件之间的交互机制,在单块架构的应用之中,组件调用其他的组件会通过编程语言级别的方法或者函数来实现的。但是在微服务架构中,不同的服务运行在不同的进程之中,由此导致的结果就是服务必须使用进程间通信机制(inter-process communication,IPC)来实现通信功能。
微服务架构中存在以下几种交互模式,如下表所示:
QQ截图20170710163552.png

QQ截图20170710163552.png

其中一对一指的是每个请求有一个服务实例来响应,一对多是指每个请求有多个服务实例来响应。同步模式是指客户端请求需要服务端即时响应,甚至可能由于等待而阻塞。异步模式是指客户端请求不会阻塞进程,服务端的响应可以是非即时的。每个服务架构的内部通信都是以上这些模式的组合,而实现上述模式需要一个或多个IPC机制进行组合。

3.2.2.1 同步通信

分布式系统的进程间通信一般采用远程服务调用的方式来实现,远程服务调用根据实现方法可分为三类,分别是分布式对象技术、基于过程的服务调用和基于Web Service的服务调用。

分布式对象技术是在面向对象技术的基础上发展起来,主要解决不同进程中对象之间的调用问题,解决访问异地对象、访问异构对象,如RMI(Remote Method Invocation,远程方法调用)技术。基于过程的服务调用最新技术有开源的Apache Thrift,Protocol Buffers。基于Web Service的服务调用中最常见的有JSON-RPC[20]、XML-RPC[21]以及SOAP(Simple Object Access Protocol)[22]等三种消息交互方式。

由于Web Service在传输过程中存在过多的冗余信息,且在编码和解码xml文件上效率较低,因此基于Web Service的服务调用性能开销较差。分布式对象技术如RMI,其应用十分广泛,但要求在对等体系结构间才能实现通信。而当前大型的分布式系统中,不同的服务可能采用不同的编程语言来实现,因此实现不同类型的组件之间进行互操作的难度较大。另外在等待远程访问的响应时,经常会有同步调用被“阻塞”的情况,因而其可扩展性存在固有的缺陷。

Apache Thrift、Protocol Buffers在传输性能和跨语言的通信支持上都有比较大的优势,既能解决分布式对象技术的互不兼容问题,又能提供高性能的序列化和反序列化的解决方案,适用于高并发、大数据量、多语言的分布式环境中。

3.2.2.2 异步通信

异步通信的解决方案可采用基于消息的异步机制,如基于AMQP的消息代理。这种方式可解耦客户端和服务端,客户端只需要将消息发送到正确的通道(如消息代理),并且消息代理会一直缓存消息,直到消费者能够处理。客户端完全不需要了解具体的服务实例,更不需要一个发现机制来确定服务实例的位置。基于消息的通信机制同时支持多种通信模式,包括单向请求以及发布-订阅。

采用异步通信方式时,客户端向服务端发送消息提交请求后,不会继续等待服务端的响应,不会因为等待而阻塞。服务端若需要回复,则会发送另外一个独立的消息给客户端。目前开源的MQ框架非常多,比较流行的有RabbitMq[23]、ZeroMq[24]、Apache Kafka[25]。

目前已有实验研究[26]表明,由于ZeroMq实现了网络通讯库接口,因此在系统吞吐量上,ZeroMq要优于Apache Kafka和RabbitMQ。由于相比较于RabbitMQ,Kafka有更高效的存储格式,在消息传输过程中不需要等待代理的通知,并且在I/O处理上开销更小,因此Apache Kafka的性能由于RabbitMQ。

而ZeroMq并没有传统意义上的服务器,因此持久化消息机制上,ZeroMq并不能在其所在服务器宕机后保证消息不丢失。Apache Kafka和RabbitMQ则可实现消息持久化。

3.3服务注册与发现机制

传统的单块应用运行在物理硬件上,服务实例的网络位置都是相对固定,并可从配置文件中读取网络位置。而微服务架构上的应用是分布式的部署在不同的服务器上,其服务实例的网络位置因为扩展、失效和升级等需求,呈现动态改变的状态。因此,微服务架构中需要实现服务注册与发现机制,使得服务提供方注册并通告服务地址,服务的调用方要能发现目标服务。

3.3.1 服务注册

服务注册表是服务发现机制的前提,它用于包含服务实例的网络地址。服务注册表需要高可用且随时更新,消费者可以缓存从服务注册表获取的网络地址。为了保证高可用性,服务注册表以集群方式配置。如Netflix通过在每个AWS的EC2域运行一个或者多个Eureka[27]服务,以实现高可用性。每个Eureka服务器都运行在拥有弹性IP地址的EC2实例上,以此保证服务注册表的高可用性。服务消费方可以缓存从服务注册表获得的网络地址,同时需要保证注册表中的信息不会过时。

微服务在注册表上实现注册和卸载,当前有两种方式注册的方式。一种是自注册模式,由微服务自己主动注册。另一种是第三方注册模式,由其他系统提供服务实例的管理。

3.3.1.1 自注册模式

当使用自注册模式时,微服务实例本身需要负责在服务注册表中注册和卸载,并且需要发送心跳来保证注册信息不会过时。其架构如图10所示:
QQ截图20170710172712.png

QQ截图20170710172712.png

该模式实现相对简单,不需要其他系统协助完成。而主要缺点是需要把服务实例跟服务注册表紧密联系起来,这就意味着必须在每种编程语言和框架内部实现注册代码,以供微服务进行调用。采用此种注册模式的典型案例是Netflix的 Eureka,Eureka客户端负责处理服务实例的注册和卸载。

3.3.1.2 第三方注册模式

采用第三方注册模式时,服务实例并不负责向服务注册表注册,而是由另外一个系统模块即服务管理器(另称服务注册中心)负责注册。服务管理器通过查询部署环境或订阅事件来跟踪运行服务的改变。当管理器发现一个新可用服务,会向注册表注册此服务。同时,服务管理器也负责卸载终止的服务实例,图11为该模式的架构图。
QQ截图20170710172745.png

QQ截图20170710172745.png

该模式的主要优点是服务跟服务注册表是分离的,不需要为每种编程语言和架构完成服务注册逻辑。但由于在分布式系统中存在的不可靠,服务管理器本身的可用性需要额外的方式得到保障。

目前在开源项目Registrator[28]中采用了该模式,用于自动注册和卸载被部署在Docker容器中的服务。

3.3.2 服务发现

3.3.2.1 服务端发现模式

服务端发现模式(server-side discovery pattern)是在服务消费者和服务提供者之间建立独立的负载均衡器,该负载均衡器通常是专门的硬件设备如F5,或者基于软件如LVS,HAproxy等实现。服务消费方一般通过DNS发现负载均衡器,即运维人员为服务配置一个DNS域名,这个域名指向负载均衡器。负载均衡器上包含了服务注册表,记载所有服务的地址映射关系。当服务消费方调用某个目标服务时,它向负载均衡器发起请求,由负载均衡器以某种策略(比如Round-Robin)做负载均衡后将请求转发到目标服务。负载均衡器一般具备健康检查能力,能自动摘除不健康的服务实例。图12展示了这种方案的工作原理。
QQ截图20170710172822.png

QQ截图20170710172822.png

服务端发现模式的方案实现简单,在负载均衡器上也容易做集中式的访问控制,这一方案目前还是业界主流。这种方案的主要问题是单点问题,所有服务调用流量都经过负载均衡器,当服务数量和调用量大的时候,负载均衡器容易成为瓶颈,且一旦负载均衡器发生故障对整个系统的影响是灾难性的。另外,负载均衡器在服务消费方和服务提供方之间增加了一跳,也有一定的性能开销。

3.3.2.2 客户端发现模式

由于服务端发现模式的缺陷,客户端发现模式(client-side discovery pattern)则将负载均衡器的功能以库的形式集成到服务消费方进程里。该方案也被称为软负载或者客户端负载方案,图13展示了这种方案的工作原理。
QQ截图20170710173007.png

QQ截图20170710173007.png

该方案需要服务注册表配合支持服务自注册和自发现,服务提供方启动时,首先将服务地址注册到服务注册表(同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查),服务消费方要访问某个服务时,它通过内置的负载均衡器组件向服务注册表查询(同时缓存并定期刷新)目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。这一方案对服务注册表的可用性要求很高,一般采用能满足高可用分布式一致的组件(例如Zookeeper[29], Consul[30], Etcd[31]等)来实现服务注册表。

而在进程内实现的负载均衡则是一种分布式负载均衡方案,负载均衡和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能较好。但是该方案以客户库(Client Library)的方式集成到服务调用方进程里,多语言栈的支持力度比较弱,并且维护难度较高。

客户端服务发现模式的典型案例是Netflix的开源服务框架,对应的组件分别是:Eureka服务注册表,Karyonup>[32]</sup>服务端框架支持服务自注册和健康检查,Ribbonup>[33]</sup>客户端框架支持服务自发现和软路由。另外,阿里开源的服务框架Dubbo也是采用类似机制。

3.3.2.3 进化的客户端发现模式

针对客户端发现模式的不足之处,业界提出了主机独立负载均衡进程方案。其原理与客户端发现模式在进程内实现服务发现和负载均衡相同,是一种分布式的方案。不同之处在于,该方案将负载均衡和服务发现从消费者进程中迁移出来,单独演变为主机上一个独立的进程。主机上的一个或者多个服务要访问目标服务时,都通过同一主机上的独立负载均衡进程做服务发现和负载均衡,如图14所示。
QQ截图20170710173047.png

QQ截图20170710173047.png

由于该方案是一种分布式方案,因此不存在单点问题。负载均衡进程的失效只影响该主机上的服务消费者,消费者和负载均衡进程之间属于进程内调用,因此性能较好。同时,该方案还简化了服务调用方,不需要为不同语言开发客户库,负载均衡组件的升级不需要服务调用方改代码。该方案的不足是部署较复杂,环节多,出错调试排查问题不方便。

该方案的典型案例是Google最新推出的基于容器的PaaS平台Kubernetesup>[34]</sup>,其内部服务发现采用类似的机制。另一典型案例是Airbnb的SmartStackup>[35]</sup>服务发现框架,对应组件分别是:Zookeeper作为服务注册表,Nerve独立进程负责服务注册和健康检查,Synapse/HAproxy独立进程负责服务发现和负载均衡。

3.4 跨服务数据访问

在分布式的微服务架构中,为了确保服务之间的低耦合,每个服务有自己的数据库,并且不同的服务可能会使用不同类型的数据库。数据管理去中心化,其实质就是不设置统一的地方用于存放持久化的数据,数据采用分散存储的形式。例如,需要ACID事务的某个服务可能会使用关系型数据库,而管理社交网络的服务可能会使用图数据库。在实际生产环境中,部分请求可能需要访问多个服务的数据。

由于数据分散存储,因此跨服务的数据访问涉及数据的读取和更新两类问题,本节将介绍如何处理这两类问题。

3.4.1 数据读取

在微服务的架构中,跨服务的数据访问是指服务Svc1需要使用服务Svc2所维护的数据。如在第3.1节中描述的在线商店的微服务中,若每个顾客都有一个信用额度,并由“用户服务”来维护,而订单通过“订单服务”来管理。当顾客试图下订单时,系统必须检查所有订单的总额不超过他们的信用额度,即“订单服务”需要访问“用户服务”所维护的信用额度。当前可以采取两种方案来解决这类数据读取问题。

其一是由服务发起RPC调用以获取另一服务维护的数据,该方式实现简单,能保证获取最新的数据。但需要被调用的服务必须健康,这降低了可用性,同时额外的RPC调用增加了性能开销。

其二是由RPC发起方,如由“订单服务”,保存所需要访问的数据的副本。此种方法能增加可用性并降低响应时间,但是必须要实现数据同步机制,以保证副本的数据能得到及时更新。

3.4.2 数据更新

为确保主体及副本中的数据能得到及时更新,可采用两种解决方案,一种是分布式事务,另一种是事件驱动的异步更新。采用分布式事务能确保数据使用是一致,当对微服务架构中涉及到的数据进行更新时,必须要确保所有的相关服务处于可正常运行的状态。采用时间驱动的异步复制时,主体服务发布事件声明变化数据的详细信息,其他的服务订阅此类事件,并更新其维护的副本中的数据。该种方式在事件的生产者和消费者中实现了解耦,简化了开发并提升了可用性,但缺点是牺牲了部分一致性。

3.5 服务容错机制

当企业微服务化以后,服务之间会有错综复杂的依赖关系,例如,一个前端请求一般会依赖于多个后端服务,技术上称之为1到N的扇出。在实际生产环境中,分布式环境中部署的服务往往不是绝对可靠,服务可能会出错或者产生延迟。如果一个应用不能对其依赖的故障进行容错和隔离,那么该应用本身就处在被拖垮的风险中。特别在一个高流量的网站中,某个单一后端应用一旦发生延迟,可能在数秒内导致所有应用资源(线程,队列等)被耗尽,造成连锁失效(Cascading Failure),严重时可致整个系统瘫痪。

目前,业界在分布式服务容错中已形成了一套有效的容错模式,主要包括:电路熔断器模式(Circuit Breaker Patten)、舱壁隔离模式(Bulkhead Isolation Pattern)、限流(Rate Limiting/Load Shedder)、回退(fallback)。

3.5.1 电路熔断器模式

电路熔断器模式up>[36]</sup>的原理类似于实际电路中的熔断器,如果电路发生短路,熔断器能够主动熔断电路,以避免灾难性损失。在分布式系统中采用电路熔断器模式后,当目标服务慢或者大量超时,调用方能够主动熔断,以防止服务被进一步拖垮。如果情况又好转了,电路又能自动恢复,这就是所谓的弹性容错,即系统有自恢复能力。熔断器模式可以防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器模式也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

熔断器模式一般用于防止应用程序直接调用那些很可能会调用失败的远程服务或共享资源,而对于应用程序中的直接访问本地私有资源,比如内存中的数据结构,如果使用熔断器模式只会增加系统额外开销。同时,熔断器模式也不适合作为应用程序中业务逻辑的异常处理替代品\\。

3.5.2 舱壁隔离模式

舱壁隔离模式(Bulkhead Isolation Pattern),顾名思义,该模式像舱壁一样对资源或失败单元进行隔离,如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响。舱壁隔离模式实施的对象是多个服务共享的资源,如线程、连接等。

线程隔离(Thread Isolation)就是舱壁隔离模式的一个例子,假定一个应用程序A调用三个服务,如Svc1、Svc2和Svc3。且部署A的容器一共有120个工作线程,采用线程隔离机制,可以给对Svc1、Svc2和Svc3的调用各分配40个线程。当Svc2慢了,调用Svc2所分配的40个线程因慢而阻塞并最终耗尽。线程隔离可以保证调用Svc1和Svc3所分配的80个线程可以不受影响。如果没有这种隔离机制,当Svc2慢的时候,120个工作线程会很快在调用Svc2时被全部消耗,整个应用程序会运行缓慢甚至无反应。

3.5.3 限流

由于服务是有容量限制,没有限流机制的服务很容易在突发流量时被冲垮。限流(Rate Limiting/Load Shedder)是指对服务限定并发访问量,比如单位时间只允许规定数量的并发调用,对超过这个限制的请求可拒绝并回退。

在一个对外提供Web服务的应用中,首先可以从最前端的Web应用进行排队,控制流入应用的流量,也就是通过Web服务器的定制模块来实现QPS限流功能。根据被保护的Web服务器所能承受的最大压力做强制的QPS流量控制,超出QPS上限的用户进入排队等待页面。另外,为了避免前端某个Web应用出现大规模流量激增时造成后端服务无法承载的雪崩效应,后端的服务会针对低优先级的业务进行限流,以确保不同来源的业务压力不会压垮后端服务,保障核心业务的访问。

恰当的限流策略需要区分正常访问和恶意请求,不能将正常的用户请求抹杀掉。如果无法区分是否是恶意请求,需要将应用分级,确保优先级最高的应用能被访问到,优先级较低的应用则采用限流措施。

3.5.4 回退

在熔断或者限流发生的时候,应用程序的后续处理逻辑将采用回退(fallback)。回退是系统的弹性恢复能力,常见的处理策略可采用直接抛出异常,即快速失败(Fail Fast)。也可以返回空值或缺省值,还可以返回备份数据。如果主服务熔断了,可以从备份服务获取数据。

4 总结

本文从软件架构的发展历程出发,在比较分析单块架构的优缺点后,介绍了从SOA服务发展而来的微服务架构,剖析了微服务架构的本质特点、发展趋势及其在国内外应用现状。对构建微服务架构的核心问题作出分析与阐述。

参考文献

[1]James Lewis. Micro services – Java, the Unix way. 33rd Degree Conference for Java Masters, Krakow, Poland, 19-21 March,2012.
[2]http://www.slideshare.net/fredgeorge/micro-service-architecture
[3]https://www.amazon.com
[4]https://www.netflix.com
[5]https://twitter.com
[6]https://www.ebay.com
[7]https://www.uber.com
[8]http://www.dubbo.io
[9]https://github.com/Netflix/asgard
[10]https://pivotal.io/cn/platform
[11]https://www.mongodb.org
[12]http://www.postgresql.org
[13]https://github.com/twitter/gizzard
[14]https://github.com/twitter/snowflake
[15]https://thrift.apache.org
[16]https://github.com/Netflix/zuul
[17]http://www.haproxy.org
[18]http://nginx.org/en/
[19]https://github.com/google/protobuf
[20]http://json-rpc.org
[21]http://xmlrpc.scripting.com
[22]https://en.wikepedia.org/wiki/SOAP
[23]https://www.rabbitmq.com
[24]https://zeromq.org
[25]http://kafka.apache.org
[26]http://mikehadlow.blogspot.com/2011/04/message-queue-shootout.html
[27]https://github.com/Netflix/eureka
[28]https://github.com/gliderlabs/registrator
[29]https://zookeeper.apache.org
[30]https://consul.io
[31]https://coreos.com/etcd
[32]https://github.com/Netflix/karyon
[33]https://github.com/Netflix/ribbon
[34]http://kubernetes.io
[35]https://github.com/airbnb/smartstack-cookbook
[36]http://martinfowler.com/bliki/CircuitBreaker.html

作者简介

陈林博,工学博士,2013年毕业于同济大学计算机系统结构专业。目前从事基础技术框架研究、云计算研究与应用、在线架构设计等。主要研究领域包括:容错计算、软件可靠性评估、软件安全性设计等方面。

何支军,工程硕士,1971年2月出生,1993年毕业于复旦大学微电子专业。长期从事技术系统建设和运维工作。

转自微信公众号:上交所技术服务
文章内容仅供参考!

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

3

添加新评论1 条评论

wuwenpinwuwenpin软件开发工程师, 南京
2018-09-16 15:58
感谢分享!!
Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广