杨思誉,刘海霞,童基均,冉宇瑶
(1.浙江理工大学 信息学院,浙江 杭州310016 ;2.浙江理工大学 科技与艺术学院,浙江 绍兴 312369)
随着计算机硬件性能的不断提升,以及虚拟化技术和计算机网络技术的发展与逐步完善,云计算(Cloud Computing)应运而生。企业将服务迁移到云平台上,可以节省运行维护硬件的人力资源成本、计算资源空闲时的闲置成本,在需要大量计算资源时可直接向云平台申请资源,从而降低了企业运行成本等。但是,许多在云计算平台上运行的应用仍然是被设计在传统硬件架构上,若出现热点事件或用户量极速增长的情况,手动扩容集群规模往往不能及时应对突发压力,甚至在扩容过程中还可能由于节点拓扑无法在线修改引发应用闪断,降低服务的可用性[1]。
云原生应用完全是从云服务中诞生的应用,即一切围绕着云计算而设计架构。相对于传统业务,更加充分且频繁地应用例如微服务、弹性计算和服务编排等云平台提供的先进技术,帮助业务更稳定、高效地运行。以Netflix 为例,其为世界上最大的点播流媒体平台之一,通过将所有服务“上云”的方式,使其运维成本下降87%。云原生技术给用户提供了一个抽象、弹性、高可用的基础设施(如数据库、网络、存储等),因此是未来的大势所趋。
本文针对当前业界应用架构的发展,采用云原生的设计理念,在使用Spring Cloud、Docker 和云计算平台作为工具的基础上,快速搭建一个可扩展、高可用、弹性的云服务应用程序。
云晴[2]对云原生发展历程进行了梳理:2013 年,Pivotal(美国云软件开发工具与服务公司)的Matt Stine 根据其多年的架构和咨询经验总结出来一个思想集合,并对其不断发展与完善;同年,Docker 项目正式发布;2014 年,Google 和Redhat 联合发布Kubernetes,用于更方便、快速地对容器进行管理;2015 年,由Google、Redhat 与微软等大型云计算厂商以及一些开源公司共同牵头成立了云原生基金会(CNCF)。CNCF 这个非盈利组织的初衷为推广孵化和标准化云原生相关技术,包括推动云原生计算的可持续发展与帮助云原生技术开发人员快速构建出色的产品。此后,CNCF 得到了快速发展,并逐渐构建出一整套云原生技术。
云计算是以虚拟化技术为基础、以网络为载体提供基础架构,整合大规模可扩展的计算、存储、数据、应用等分布式计算资源进行协同工作的超级计算模式[3]。如图1 所示,云平台提供的云计算服务自下而上可分为基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服务(SaaS),面对不同用户,可提供不同等级的抽象资源。
Fig.1 Cloud service system differentiation图1 云服务体系区分
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建与运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API[4],这些技术能够构建容错性好、易于管理及便于观察的松耦合系统。云原生应用的特点包括分布式、高可用、高性能、弹性、无状态、本地轻依赖等。如图2所示,云原生主要包括容器化、微服务、DevOps、持续交付等几方面内容,其中容器化是一种集装箱技术,通过将应用“装”起来以实现应用之间的相互隔离[5];微服务架构是随着分布式技术发展而出现的一种新型软件设计模式,其理念是分而治之,微服务也体现了系统设计中的内聚、低耦合理念;DevOps(Development 与Operations 的组合)旨在促进软件交付及基础设施开发人员(Dev)与IT 运维技术人员(Ops)之间的沟通合作;持续交付(Continuous Delivery,缩写为CD)是一种软件工程手法,其目标在于让软件的构建、测试与发布变得更快、更频繁,从而减少软件开发时间及成本,降低风险。
Fig.2 Main content of cloud native图2 云原生主要内容
Spring Cloud 是一系列框架、组件的有序集合,拥有功能完善、轻量级的微服务实现组件,例如在服务发现治理、服务容错、服务网关、服务配置、负载均衡、消息总线、服务跟踪等方面均有经过实践检验的成熟组件[6]。基于Spring Cloud 组件的云原生服务部署架构如图3 所示。
Fig.3 Cloud native server architecture图3 云原生服务部署架构
2.1.1 服务消费
目前,在Spring Cloud 中服务之间通过Restful 协议进行远程调用有两种方式:RestTemplate+Ribbon 和Feign。Spring Cloud Netflix 的微服务都是以HTTP 接口形式暴露的,可用Apache的HttpClient或Spring的RestTemplate调用。
Ribbon 是一个基于HTTP 与TCP 客户端的负载均衡器,可在客户端配置RibbonServerList(服务端列表),然后轮询请求以实现均衡负载。在联合使用Eureka 时,Ribbon-ServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka 注册中心获取服务端列表,同时其也会用NIWSDiscoveryPing 取代IPing,并将职责委托给Eureka 确定服务端是否已启动。
Feign 是一个用起来比Ribbon+RestTemplet 更方便的HTTP 客户端,使用Feign 就如同调用本地方法一样,无需关注目标服务所在环境。本架构设计采用Feign,Feign 相当于在Ribbon 之上再作了一次封装,隐藏了底层更多细节,更易于使用。
2.1.2 负载均衡
Ribbon 负责服务间的负载均衡,默认策略有轮询、随机、加权、哈希、一致性哈希等负载均衡算法。用户可根据自己的需求选择对应的负载均衡策略,也可通过继承抽象类方式实现自己的负载均衡策略。
在经典算法中,加权最少连接调度(Weighted Least-Connection,WLC)是LVS 集群中一种经典的动态负载均衡算法[7],该算法使用服务器节点的连接数作为负载评价标准。随着集群环境的规模化及资源的异构化,以负载评估为目标的负载均衡算法已不能满足集群调度的需要,因此布谷鸟搜索(Cuckoo Serch,CS)等元启发式算法被引入到负载均衡机制中。张娜等[8]在布谷鸟算法基础上引入了混沌变异和反向学习,增加了布谷鸟算法求最优解的准确性。
在分布式系统中,当单体应用程序被拆分成微服务时,每一个微服务都是一个单体应用程序,故每一个微服务都需要维护自身的配置。相同代码可能会在不同环境下运行:开发、测试、灰度测试、上线运行、并行容器等。本文希望微服务在修改配置后可实时生效,以便于集群统一管理,且拥有完善的权限和审核机制等。在采用分布式开发模式后,项目之间的相互引用随着服务的不断增多,相互之间的调用复杂度呈指数级增加,因此需要引用分布式配置中心。
Spring Cloud Config 为分布式系统中的外部配置提供服务器和客户端支持,以方便部署与运维。其中服务端也称为分布式配置中心,是一个独立的微服务应用,用来连接配置服务器并为客户端提供配置信息获取、信息加密/解密等访问接口。客户端则是通过指定的配置中心管理应用资源以及与业务相关的配置内容,并在启动时从配置中心获取与加载配置信息。默认采用git,并且可通过git 客户端工具方便地管理与访问配置内容。
在微服务架构中,服务对外暴露的是API 接口,并且对于同一个微服务会有多个服务提供者,因此要提供网关负责对外部请求的导航。在前端,展示页面时需要从多个微服务中聚合数据,而且服务的划分位置结构可能会有所改变。为统一管理API 的访问路径与前线,本文引入API 网关组件。网关可对外暴露聚合API,屏蔽内部微服务的微小变动,保持整个系统的稳定性。另外,网关还具有外部请求负载均衡、统一鉴权、协议转换、监控监测等一系列功能。
Zuul 是Netflix 开源的一个API Gateway 服务器,本质上是一个Web Servlet 应用。Zuul 在云平台上提供动态路由、监控、弹性、安全等边缘服务框架,相当于设备与云原生应用后端所有请求的导航。Zuul 的样例可参考Netflix在Github上的Simple Webapp例子,按照Netflix 在Github Wiki 上的文档说明进行使用。
在分布式环境中,许多服务依赖项中的一些请求必然会失败。Hystrix 是一个库,通过添加延迟容忍和容错逻辑,帮助人们控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点、停止级联失败和提供回退选项实现该功能。熔断器是微服务弹性化过程中的一部分,能很好地保护应用程序。
2.4.1 服务雪崩
在分布式系统中存在雪崩效应问题。当微服务A 调用微服务B,微服务B 调用微服务C 或其他微服务时,服务因调用链路上的某个微服务而下线导致整体不可用,或该服务响应时间过长,调用链上的前置服务会处于等待状态而耗尽资源(如线程池资源耗尽),导致服务集体下线,系统崩溃。
2.4.2 熔断与降级
为避免系统中出现雪崩效应,本文引入“服务熔断”和“服务降级”机制。熔断机制是应对雪崩效应的一种服务链路保护机制,当发现服务调用链路上的某一个服务下线或响应时间过长时,会进行服务降级,进而熔断该节点服务的调用,快速返回错误信息。当检测到该节点微服务的调用响应正常后,则恢复调用该链路。
服务降级的理念是在流量洪峰到来时,牺牲一些边缘服务以保护系统核心服务的可用性,其在客户端进行处理,与服务端无关。当察觉某个时间段内,某一个服务即将承受大流量的冲击,而另一些服务所占用的资源却大部分闲置时,可通过服务降级先将某些服务单元忍痛关闭掉,留下一些可返回的备选方法,等待整体系统承受住流量峰值,再重启被暂时关闭的服务。
2.4.3 服务监控
在云原生的理念中,一切部署、运维都由一个自动化平台进行维护,因此需要一个平台能实时监控所有微服务状态。常用的服务监控方式有:网关+Hystrix 组合、Eureka注册中心服务集成Spring Boot Admin Dashboard 服务。
在云原生应用中,单体系统被拆分成大量微服务,而一个请求的处理将会涉及大量的服务间调用。离散的微服务在让应用内部解耦的同时,也增加了调试方面的困难,导致难以定位问题出现的具体位置。一个请求的处理可能会呈树状以调用不同的微服务,为了追踪一个请求调用链路上的具体信息,本文引入分布式链路追踪的概念。
本次部署采用腾讯云的云服务器(Cloud Virtual Machine,CVM,1 核2G,50G 硬盘,CentOS 7.0)模拟远程云数据库(关系型数据库MySQL 7.0,非关系型数据库Redis 3.2),在两台远程服务器(4 核8G)上使用Docker 容器搭建服务集群,以模拟云原生应用的微服务(见表1)。
Table 1 Server configuration表1 服务器配置
3.2.1 基础设施
EUREKA-SERVER-1、EUREKA-SERVER-2、EUREKA-SERVER-3 是3 个分别运行在端口8001、8002、8003 的服务注册与发现集群,三者之间互相注册,组成了高可用集群。
HYSTRIX-DASHBOARD-TURBINE 是系统的自动化监控平台,负责监控所有微服务运行状态。其会分析所有流经微服务的请求,统计每个请求的响应状态(如成功、失败、挂起等)及响应时间,并根据一个时间片内的情况判断该微服务是否出现了问题,然后将其转化为UI 界面以警示运维人员,便于定位与排查问题。
ZUUL-APPLICIATION是一个APIGetaway网关,其是整个应用的入口点,也是唯一对外暴露的端口。所有请求都会流经该微服务,其会对流量进行筛选与鉴权,错误请求将会返回对应错误信息。在鉴权通过后,请求将被导航至系统内部的各个微服务上,系统内部被隐藏的微服务将处理这些请求,并返回相应数据和状态码。在该微服务上集成了Spring Security Oauth2,以对请求进行权限校验,只有携带了正确的access_token 请求才能通过,而鉴权失败的请求将被驳回。用户登录信息会被存储在云Redis 中,并会定时过期,过期后需要使用refresh_token 刷新access_token。
3.2.2 服务提供与服务消费
EUREKA-CLIENT 模拟的是服务提供者,并对外暴露RESTful 风格的HTTP 接口。在Eureka 服务注册表中,有两个与之对应的IP 地址,意味着有两个相同的微服务注册为同一个名字,这就是服务的横向扩展:通过增加机器的方式搭建集群以提高服务的整体性能和可用性。当服务消费者访问该微服务时,会根据其配置的负载均衡策略决定访问哪一个微服务。应用的弹性体现在可通过扩展任意一台机器的方式提升该服务总体性能上限,只需在启动时在注册中心使用相同的服务名注册它自己,其在整个服务网络上就是可被感知的,会自动分配流量给该服务。故运维人员在处理巨大的流量洪峰时,可通过申请短暂的计算资源进行应对,并在处理结束后马上将其释放。高可用体现在对该服务而言,任意一台机器下线只会导致处理流量峰值的能力下降,而不会导致整个服务下线。
CONSUMER1 模拟的是服务消费者,其通过HTTP 请求访问应用内的服务提供者。在该服务上集成了轮询规则的负载均衡器,即会按顺序一个个访问服务的不同提供者。
USER-INFORMATION 是一个完整的微服务,负责系统中用户信息的增删改查,模拟应用与云数据库之间的互动。
SIMHASH 是另一个微服务,其职责是提供文字查重服务。对外暴露一个接口的作用是计算两段文字的SimHash值,根据两组SimHash 判断两篇文章的海明距离。在该系统中,海明距离为0~10,数值越小,文章相似程度越高,这是一个CPU 密集型微服务。
在应用中每一个服务提供者与服务消费者上配置了服务熔断器,当某个服务因某些原因不可用时,熔断器会自动熔断服务,快速返回错误信息,避免因大量请求堆积而造成服务雪崩,导致整个应用不可用。
在部署应用程序前,应搭建自己的云计算集群。首先部署模拟云数据库的服务器DataServer。在DataServer 上,防火墙开放3306 端口部署MySQL,开放6379 端口部署Redis。其次,部署模拟应用生产环境的部分:AppServer1、AppServer2。在这两台机器上配置了Nginx 反向代理,监听并转发80 端口收到的请求,同时安装并配置Docker。如果使用传统的打包成镜像再上传的方式,需要进行大量数据传输,因此要花费大量时间等待。为提高服务部署效率,也方便微服务的横向扩展,本文选择对每个微服务编写Dockerfile,将代码上传到GitHub 远程仓库中。在服务端从远程仓库拉去代码文件并进行编译、Docker 镜像打包、运行镜像等操作。在AppServer1 上部署了Eureka Server、Zuul、HYSTRIX-DASHBOARD-TURBINE、UserInformation、Sim-Hash服务,在AppServer2上部署了Eureka Server、Zuul、ConfigServer、UserInformation、SimHash 服务,同时配置了防火墙,对外暴露80 端口如图4 所示。
由于不同微服务对计算机资源的需求不同,若简单、粗暴地令所有服务都在一台单独机器上运行,会造成大量计算资源浪费。使用Docker+微服务的思想可提高计算资源利用率,例如将CPU 密集型服务和IO 密集型服务部署在同一台计算机上,其之间出现冲突的可能性很小,能很好地共存,从而最大化地利用计算资源。本文将用户信息管理(IO 密集型服务)与相似文本查重算法(CPU 密集型服务)部署在同一台服务器上,尽可能实现计算资源的最大化利用。
Fig.4 Application architecture图4 应用架构
本文采用云原生的理念和开发方式构建一个云原生应用,应用包括集中配置中心、服务注册与发现、API Getaway 网关、服务消费和负载均衡器、服务熔断与降级、分布式链路追踪等模块,并且介绍了数个需要不同计算机资源的微服务:单点登录与请求鉴权、用户信息管理、文本查重算法设计与实现。在开发流程中使用GitHub 进行版本控制与持续交付,使用服务端打包Docker 的方式降低部署成本,使得开发更优雅、便捷。在部署应用时,选择将需要不同资源的服务部署于同一服务器中,以实现计算资源利用率的最大化。相比于使用传统架构,云原生应用是注定要在云平台上运行的应用程序,其经过设计、优化,易于迭代,且可以快速反馈。云原生应用给用户提供了一个抽象、弹性、高可用的基础设施(如数据库、网络、存储等),是未来发展的大势所趋,越来越多互联网公司、传统企业都选择将自己的应用“上云”。因此,未来大量根植于云平台的应用会不断出现并得到普及。