《Spring Cloud与Docker微服务架构实战》(周立著)摘抄笔录
如今越来越多基于SpringCloud、Dubbo等框架实现的微服务架构开始涌现,逐渐代替了传统的大型单体应用架构。这种变革发生的原因不仅是相关技术进步的推动作用,也很大程度上归因于传统大型单体应用不断涌现出的各种问题:
复杂性高:对于百万行级别的大型应用来说,项目包含的模块过多、模块的边界模糊、依赖关系不清晰、代码质量参差不齐、混乱地堆砌在一起,这整个项目非常复杂。每次修改代码都心惊胆战,甚至添加一个简单的功能,或者修改一个Bug都会带来隐含的缺陷。
技术债务:随着时间推移、需求变更和人员更迭,会逐渐形成应用程序的技术债务,并且越积越多。“不坏不修(Not broken, don’t fix)",这在软件开发中非常常见,在单体应用中这种思想更甚。已使用的系统设计或代码难以被修改,因为应用程序中的其他模块可能会以意料之外的方式使用它。
部署频率低:随着代码的增多,构建和部署的时间也会增加。而在单体应用中,每次功能的变更或缺陷的修复都会导致需要重新部署整个应用。全量部署的方式耗时长、影响范围大、风险高,这使得单体应用项目上线部署的频率较低。而部署频率低又导致两次发布之间会有大量的功能变更和缺陷修复,出错概率比较高。
可靠性差:某个应用Bug,例如死循环、OOM等,可能会导致整个应用的崩溃。
扩展能力受限:单体应用只能作为一个整体进行扩展,无法根据业务模块的需要进行伸缩。例如,应用中有的模块是计算密集型的,它需要强劲的CPU;有的模块则IO密集型的,需要更大的内存。由于这些模块部署在一起,不得不在硬件的选择上做出妥协。
阻碍技术创新:单体应用往往使用统一的技术平台或方案解决所有的问题,团队中的每个成员都必须使用相同的开发语言和框架,要想引人新框架或新技术平台会非常困难。例如,一个使用struts 2构建的、有100万行代码的单体应用,如果想要换用SpringMVC,毫无疑问切换的成本是非常高的。
以上问题在单体应用框架内是很难解决的,且随着项目体量的增大,问题的复杂程度也随之增加,因此微服务架构应运而生。微服务并没有一个权威统一的定义,但大致具有以下特性:
1、每个微服务可独立运行在自己的进程里,一系列独立运行的微服务共同构建起整个系统。
2、每个服务为独立的业务开发,一个微服务只关注某个特定的功能,例如订单管理、用户管理等。
3、微服务之间通过一些轻量的通信机制进行通信,例如通过RESTful API进行调用。
4、可以使用不同的语言与数据存储技术。
5、全自动的部署机制。
与单体应用框架相比,微服务架构具有以下优势:
易于开发和维护:一个微服务只会关注一个特定的业务功能,所以它业务清晰、代码量较少。开发和维护单个微服务相对简单。而整个应用是由若干个微服务构建而成的,所以整个应用也会被维持在一个可控状态,同时多个微服务应用可并行开发,迅速迭代,加快项目整体开发进度。
单个微服务启动较快:单个微服务代码量较少,所以启动会比较快。
局部修改容易部署:单体应用只要有修改,就得重新部署整个应用,微服务解决了这样的问题。一般来说,对某个微服务进行修改,只需要重新部署这个服务即可。
技术栈不受限:在微服务架构中,可以结合项目业务及团队的特点,合理地选择技术栈。例如某些服务可使用关系型数据库MySQL;某些微服务有图形计算的需求,可以使用Ne04j;甚至可根据需要,部分微服务使用Java开发,部分微服务使用Node.js 开发。
按需伸缩:可根据需求,实现细粒度的扩展。例如,系统中的某个微服务遇到了瓶颈,可以结合这个微服务的业务特点,增加内存、升级CPU或者是增加节点。
单体应用架构的缺点,恰恰是微服务的优点,而这些优点使得微服务看起来简直非常完美。然而完美的东西并不存在,就像银弹不存在一样。微服务虽然有很多吸引人的地方,但它并不是免费的午餐,使用它是有代价的:
运维要求较高:更多的服务意味着更多的运维投人。在单体架构中,只需要保证一个应用的正常运行。而在微服务中,需要保证几十甚至几百个服务的正常运行与协作,这给运维带来了很大的挑战。
分布式固有的复杂性:使用微服务构建的是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都会带来巨大的挑战。
接口调整成本高:微服务之间通过接口进行通信。如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。
重复劳动:很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。
为尽量减少上述负面影响,则需要在微服务的设计阶段合理划分各微服务体量和边界 。微服务之间应相对独立并保持松耦合。itmuch认为,领域驱动设计(Domain DrivenDesign,简称DDD)中的“界限上下文(Bounded Context)"可作为划分微服务边界、确定微服务粒度的重要依据。
尽管Spring Cloud带有"cloud"的字样,但它并不是云计算解决方案,而是在Spring Boot基础上构建的,用于快速构建分布式系统的通用模式的工具集。使用Spring Cloud开发的应用程序非常适合在Docker或者paas(例如Cloud Foundry)上部署,所以又叫作云原生应用(Cloud Native Application)。Spring Cloud具有以下特点:
1、约定优于配置。
2、适用于各种环境。开发、部署在PCServer或各种云环境(例如阿里云、AWS等)均可。
3、隐藏了组件的复杂性,并提供声明式、无xml的配置方式。
4、开箱即用,快速启动。
5、轻量级的组件。spring cloud整合的组件大多比较轻量。例如Eureka、zuul,等等,都是各自领域轻量级的实现。
6、组件丰富,功能齐全。Springcloud为微服务架构提供了非常完整的支持。例如,配置管理、服务发现、断路器、微服务网关等。
7、选型中立、丰富。例如,spring cloud支持使用Eureka、Zookeeper或consul实现服务发现。
8、灵活。spring Cloud的组成部分是解耦的,开发人员可按需灵活挑选技术选型。
以下介绍Spring Cloud部分重要组件。
Eureka是Netflix的服务发现组件,本身是一个基于REST的服务,它包括server和client两部分。Spring Cloud将其继承在子项目Spring Cloud Netflix中,从而实现微服务注册与发现。Eureka包含两个组件:Eureka server和Eureka Client,它们的作用如下:
1、Eureka server提供服务发现的能力,各个微服务启动时,会向Eureka server注册自己的信息(例如IP、端口、微服务名称等),Eureka server会存储这些信息。
2、Eureka Client是一个Java客户端,用于简化与Eureka server的交互。
3、微服务启动后,会周期性(默认30秒)地向Eureka server发送心跳以续约自己的“租期”。
4、如果Eureka server在一定时间内没有接收到某个微服务实例的心跳,Eureka server 将会注销该实例(默认90秒)。
5、默认情况下,Eureka server同时也是Eureka Cliento多个Eureka server实例,互相之间通过复制的方式,来实现服务注册表中数据的同步。
6、Eureka Client会缓存服务注册表中的信息。这种方式有一定的优势一一首先,微服务无须每次请求都杳询Eureka server,从而降低了Eureka server的压力;其次,即使 Eureka server所有节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者并完成调用。
综上,Eureka通过心跳检杳、客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。
Eureka高可用性
Eureka Client会定时连接Eureka server,获取服务注册表中的信息并缓存在本地。微服务在消费远程API时总是使用本地缓存中的数据。因此一般来说,即使Eureka server发生宕机,也不会影响到服务之间的调用。但如果Eureka server宕机时,某些微服务也出现了不可用的情况,Eureka client中的缓存若不被更新,就可能会影响到微服务的调用,甚至影响到整个应用系统的高可用性。因此,在生产环境中,通常会部署一个高可用的Eureka server集群。
Eureka server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka server 实例会彼此增量地同步信息,从而确保所有节点数据一致。
Eureka的自我保护模式
默认情况下,如果Eureka server在一定时间内没有接收到某个微服务实例的心跳,Eureka server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka server之间无法正常通信,以上行为可能变得非常危险了一一因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式"来解决这个问题一一当Eureka server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进人自我保护模式。 一旦进人该模式,Eureka server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka server节点会自动退出自我保护模式。
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。在Spring Cloud中,可以使用eureka.server.enable-self-preservation :false禁用自我保护模式。
Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为 Ribbon配置服务提供者地址列表后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。在spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从Eureka server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。
Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0 以及WebSocket0 。Feign可帮助我们更加便捷、优雅地调用HTTP API。在spring Cloud中,使用Feign非常简单一一一创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。spring Cloud对Feign进行了增强,使Feign支持了spring MVC注解,并整合了Ribbon 和Eureka,从而让Feign的使用更加方便。
我们常把“基础服务故障"导致“级联故障’'的现象称为雪崩效应。雪崩效应描述的是提供者不可用导致消费者不可用,并将不可用逐渐放大的过程。要想防止雪崩效应,必须有一个强大的容错机制。该容错机制必须实现两点:为网络请求设置超时,以及使用断路器模式。
Hystrix实现容错
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第。方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。
包裹请求:使用Hystrixcommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用到了设计模式中的“命令模式"。
跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。
监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时以及被拒绝的请求等。
退回机制:当请求失败、超时、被拒绝或断路器打开时,执行回退逻辑。回退逻辑可由开发人员自行提供,例如返回一个缺省值。
自我修复:断路器打开一段时间后,会自动进人“半开"状态。此时,断路器可允许一个请求访问依赖的服务。如果该请求能够调用成功,则关闭断路器;否则继续保持打
开状态。
当请求失败、被拒绝、超时或者断路器打开时,都会进人回退方法。但进入回退方法并不意味着断路器已经被打开。那么,如何才能明确了解断路器当前的状态呢?断路器的状态也会暴露在Actuator提供的/health端点中,这样就可以直观地了解断路器的状态。
Feign使用Hystrix
spring Cloud默认已为Feign整合了Hystrix,只要Hystrix在项目的classpath中, Feign默认就会用断路器包裹所有方法。这样虽然方便,但很多场景下并不需要该功能。如何为
Feign禁用Hystrix呢?为指定Feign客户端禁用Hystrix 借助Feign的自定义配置,可轻松为指定名称的Feign客户端禁用Hystrixo。也可为Feign全局禁用Hystrix,只需设置配置feign.hystrix.enable=false即可。
某些场景下,Feign或Ribbon整合Hystrix后,会出现首次调用失败的问题。其原因在于Hystrix默认的超时时间是1秒,如果在1秒内得不到响应,就会进人fallback逻辑。由于spring的懒加载机制,首次请求往往会比较慢,因此在某些机器(特别是配置低的机器)上,首次请求需要的时间可能就会大于1秒。解决该问题可用延长Hystrix超时时间,或者禁用Hystrix超时,或者为Feign禁用Hystrix。
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
1、客户端会多次请求不同的微服务,增加了客户端的复杂性。
2、存在跨域请求,在一定场景下处理相对复杂。
3、认证复杂,每个服务都需要独立认证。
4、难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
5、某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
Zuul 是 Netflix 开源的微服务网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
审查与监控:对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务。同时对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。
动态路由:动态地将请求路由到不同的后端集群。做一些逻辑处理,比如聚合多个服务的数据返回。
压力测试:压力测试是一项很重要的工作,像一些电商公司需要模拟更多真实的用户并发量来保证重大活动时系统的稳定。通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理。
负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
多区域弹性:跨越 AWS Region 进行请求路由,旨在实现 ELB(Elastic Load Balancing,弹性负载均衡)使用的多样化,以及让系统的边缘更贴近系统的使用者。