一.简介
Spring Cloud从技术架构上降低了对大型系统构建的要求,使我们以非常低的成本(技术或者硬件)搭建一套高效、分布式、容错的平台,但Spring Cloud也不是没有缺点,小型独立的项目不适合使用。
什么是SpringCloud
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
和Spring Boot 是什么关系
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具;Spring Boot专注于快速、方便集成的单个个体,Spring Cloud是关注全局的服务治理框架;Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,可以不基于Spring Boot吗?不可以。
Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。
spring > spring boot > Spring Cloud 这样的关系。
SpringCloud的版本
Spring Boot | Spring Cloud |
---|---|
1.2.x | Angel版本 |
1.3.x | Brixton版本 |
1.4.x | Camden版本 |
1.5.x | Dalston版本、Edgware版本 |
2.0.x | Finchley版本 |
Spring Cloud的优势
微服务的框架那么多比如:dubbo、Kubernetes,为什么就要使用Spring Cloud的呢?
- 产出于spring大家族,spring在企业级开发框架中无人能敌,来头很大,可以保证后续的更新、完善。
- 有Spring Boot 这个独立干将可以省很多事,大大小小的活Spring Boot都搞的挺不错。
- 作为一个微服务治理的大家伙,考虑的很全面,几乎服务治理的方方面面都考虑到了,方便开发开箱即用。
- Spring Cloud 活跃度很高,教程很丰富,遇到问题很容易找到解决方案
- 轻轻松松几行代码就完成了熔断、均衡负载、服务中心的各种平台功能
Spring Cloud对于中小型互联网公司来说是一种福音,因为这类公司往往没有实力或者没有足够的资金投入去开发自己的分布式系统基础设施,使用Spring Cloud一站式解决方案能在从容应对业务发展的同时大大减少开发成本。同时,随着近几年微服务架构和Docker容器概念的火爆,也会让Spring Cloud在未来越来越“云”化的软件开发风格中立有一席之地,尤其是在目前五花八门的分布式解决方案中提供了标准化的、全站式的技术方案,意义可能会堪比当前Servlet规范的诞生,有效推进服务端软件系统技术水平的进步。
二.服务发现组件 Eureka
Spring Cloud针对服务注册与发现,进行了一层抽象,并提供了三种实现:
Eureka(支持得最好)、Consul、Zookeeper。
Eureka 是 Netflix 开源的服务注册发现组件,服务端通过 REST 协议暴露服务,提供应用服务的注册和发现的功能。
所有的Eureka服务都被称为实例(instance)。Eureka服务又分为两类角色:Eureka Server和Eureka Client
Eureka-Client又分为Application Provider 和Application Consumer
Application Provider :服务提供者,内嵌 Eureka-Client ,它向 Eureka-Server 注册自身服务、续约、下线等操作
Application Consumer :服务消费者,内嵌 Eureka-Client ,它从 Eureka-Server 获取服务列表,分为全量获取和增量。
注意:Application Provider 和 Application Consumer只是角色,同一个服务即可以是服务的提供者,又可以是服务的消费者。甚至,在注册中心的集群环境下,Eureka Server,即可以EurekaServer,又是Eureka Client。 Eureka Server比较好理解,Eureka Client是他向其它EureakServer同步消息,它又变成了Eureker Client
Eureka的配置主要分为三类:
- eureka.instance.*: Eureka实例注册配置项,这些是无论Eureka是做Server,Client都需要的公共配置项。对应的配置类为org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean。
- eureka.server.*: Eureka做为Eureka Server才需要配置的选项,即使用eureka做注册中心时才需要配置这个选项。对应的配置类为org.springframework.cloud.netflix.eureka.EurekaClientConfigBean。
- eureka.client.*: Eureka做为Eureka Client才需要配置的选项,即服务需要向注册中心注册或使用注册中心中的服务时才需要配置这些选项。对应的配置类为org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean
Eureka架构图
引入依赖
引入依赖 父工程pom.xml定义SpringCloud版本
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
eureka模块pom.xml引入eureka-server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
首次注册
- 服务提供者在启动会向Eureka Server注册信息(包括服务名(Eureka Service Id)、service
id、ip、port、心跳周期等),EurekaServer获取注册请求,将服务实例信息存入到读写缓存中 - 服务消费者根据Eureka service id向Eureka
Server获取要访问服务的注册信息,第一次请求会一次性拉取对应Eureka service id的全部服务实例信息 - Eureka
Server收到请求后,会首先在只读缓存查找,如果找到则直接返回,否则再查找读写缓存,如果找到则将再存入到只读缓存中,然后返回查找结果 - 服务提供者获取信息的,将服务实例信息缓存到本地,在使用时根据算法从N个服务中中选取一个服务并向这个服务发送请求
相关的配置
# 本服务注册到注册到服务器的名称, 这个名称就是后面调用服务时的服务标识符,即Eureka Service Id
spring.application.name: icc-transfer
# EurekaClientConfig
# 配置 Eureka-Server 地址,集群配置在多个地址之间加,即可
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:${server.port}/eureka/
# 该实例是否向 Eureka Server注册自己
eureka.client.register-with-eureka: true
# 该实例是否向 Eureka 服务器获取所有的注册信息表
eureka.client.fetch-registry: true
续约/心跳/下线:
- 服务提供者会定时向Eureka Server发送心跳,默认30s
- Eureka Server收到心跳后,会更新对应的服务实例信息,如果服务的状态有变化则将实例的变化加入到”最近租约变更记录队列”中
- Eureka Server有个实例过期清理定时器,如果在指定时间内没有收到心跳(默认90s),则认为服务已经下线,会从读写缓存中移除此实例,将并此变化更新“最近租约变更记录队列”。通常建议将存活的最长时间设置为3个心跳
- 服务消费者也有个定时任务会定时去更新服务实例信息(默认30s),第一次全量拉取服务实例,会读取所有的实例信息; 之后使用增量拉取服务实例信息,Eureka Server根据”最近租约变更记录队列”,告诉请求方哪些服务实例有变化,只返回变化的实例信息。客户端根据返回的增量信息更新本地缓存。我们也可以禁用增量获取服务实例实例,每次使用全量获取服务实例信息
服务实例下线,又分为两种:
- 主动下线,在服务实例结束前,主动通知Eureka Server,在默认配置下,此时服务消费者最长在30s左右知道此服务已经下线
- 被动下线,服务进度崩溃/网络异常,此时服务消费者最长在(3次心跳+一次刷新频率30s)(共约120s左右)内知道此服务已经下线
相关的配置
# EurekaClientConfig,重在 Eureka-Client,例如, 连接的 Eureka-Server 的地址、获取服务提供者列表的频率、注册自身为服务提供者的频率等等
## 服务发现相关的配置
# 是否从 Eureka-Server 拉取服务实例注册信息,默认值为true
eureka.client.fetch-registry: true
# 从 Eureka-Server 拉取注册服务实例信息频率,默认:30 秒
eureka.client.registry-fetch-interval-seconds: 30
# 是否禁用增量获取服务实例注册信息
eureka.disableDelta: false
# EurekaInstanceConfig,重在服务实例,例如,等等。此处应用指的是,Application Consumer 和 Application Provider。
# 服务实例配置
# 心跳,租约续约频率,单位:秒
eureka.instance.lease-renewal-interval-in-seconds: 30
# eureka server多久没有收到心跳,则表示对应的实例过期,单位:秒。
eureka.instance.lease-expiration-duration-in-seconds: : 90s
# Eureka Server端服务实例租约过期定时任务执行频率
eureka.server.eviction-interval-timer-in-ms: 60s
缓存
在上图中Eureka Server有两个缓存,一个只读缓存,一个是读写缓存。
读写缓存:每次有服务实例状态变化(如注册服务、下线等)只会更新读写缓存
只读缓存永不过期,但是可能存在过期数据。此时为了保证只读数据的准确性,会有个定时器定时同步两个缓存,然后将状态变化的服务实例添加到”最近租约变更记录队列”。执行频率默认30s
为了提高实时性,可以关闭只读缓存。
“最近租约变更记录队列”里的数据也有有效期,默认为180s。
当有注册、状态变更、下线时,会将创建最近租约变更记录加入此队列中
用于注册信息增量获取
后台任务定时顺序扫描队列,当数据最后更新时间超过一定时长后进行移除
相关的配置
# 是否开启只读请求响应缓存。响应缓存 ( ResponseCache ) 机制目前使用两层缓存策略。优先读取只读缓存,读取不到后读取固定过期的读写缓存。
eureka.server.use-read-only-response-cache: true
# 只读缓存更新频率,单位:毫秒。只读缓存定时更新任务只更新读取过请求 ,因此虽然永不过期,也会存在读取不到的情况。
eureka.server.response-cache-update-interval-ms: 30s
#getResponseCacheAutoExpirationInSeconds() :读写缓存写入后过期时间,单位:秒。
eureka.server.response-cache-auto-expiration-in-seconds: 180s
# 移除队列里过期的租约变更记录的定时任务执行频率,单位:毫秒。默认值 :30 * 1000 毫秒。
eureka.server.delta-retention-timer-interval-in-ms: 30s
# 租约变更记录过期时长,单位:毫秒。默认值 : 3 * 60 * 1000 毫秒。
eureka.server.retention-time-in-m-s-in-delta-queue: 180s
Eureka Server – 自我保护机制
概念:
- Renews threshold: Eureka Server 期望每分钟最少收到服务实例心跳的总数
- Renews threshold = 当前注册的服务实例数 x 2 * 自我保护系数( eureka.renewalPercentThreshold )
- 之所以 x 2,是因为Eureka 硬编码认为每个实例每 30 秒 1 个心跳,即1分钟2个心跳
- Renews (last min): Eureka Server 最近 1 分钟收到服务实例心跳的总数
当启动自我保护时,如果Eureka Server最近1分钟收到服务实心跳的次数小于阈值(即预期的最小值),则会触发自我保护模式,此时Eureka Server此时会认为这是网络问题,它不会注销任何过期的实例。等到最近收到心跳的次数大于阈值后,则Eureka Server退出自我保护模式
刚开始接触spring cloud时,碰到这样的情况:服务提供者明明已经挂掉了,但是服务消息方要过好久(如30s)才知道。更糟糕的是服务提供者进度奔溃,没来的及发送下线请求到注册中心,则服务消费者可能需要120s时间才知道。面对这样的情况,我们总是尝试将心跳频率、刷新频率、注册信息在注册中心的生存时间等配置改到最小,以达到服务提供方一挂掉,服务调用方书上马上知道。但是这个方案有缺陷,中间的时差总时存在,无法完全消除。而且频繁的心跳会增加网络和服务的不必要的性能消耗,如果网络有抖动,则有可能出现误报。
不推荐这么修改的一个重要原因是Eureka 是AP系统,不是CP系统,为了可用性,它不保证实时一致性,它只保证最终一致性。还有一个原因是Eureka中有一部分硬编码,默认认为每分钟每个服务实例的心跳是2次,此值用于计算Eureka每分钟理论上应该收到的心跳的个数,此值会用于计算Eureka开启自我保护
在生产环境,推荐开启,避免网络抖动对服务的影响。有这样的极端场景,比如注册中心单独部署在一个集群中,所有服务向其注册服务完毕后,突然注册中心所在的集群网络和其它服务网络断开,但是服务实例之间是可达的,整个服务可以正常工作。此时如果没有开启自我保护机制,经过Ns后,注册中心清理所有的注册服务,然后网络恢复了,那么其它服务进行更新时,会认为其它服务已经掉线,则从本地移除所有的服务请求,则整个服务变的不可用
关于自我保护阈值,需要设置合理值。比如我们将全部服务均匀地部署在2台服务器,则需要设置阈值小于50%,否则如果出来一台服务器坏掉了,则此时就要求注册中心移除一般的服务注册中心是合理的,不应该启动自我保护
相关的配置
# 配置在Eureka Server端的配置文件
#是否开启自我保护模式。
eureka.server.enable-self-preservation: false
#开启自我保护模式比例,超过该比例后开启自我保护模式。
eureka.server.renewal-percent-threshold: 0.85
#自我保护模式比例更新定时任务执行频率。
eureka.server.renewal-threshold-update-interval-ms: 900s
Eureka集群
为了提高高可用,Eureka是提供集群功能。不仅Eureka Client端可以集群,做为注册中心的Eureka Server也可以集群 。
多个Eureka Client进程向注册中心注册的Eureka Service Id相同,则被认为是一个集群。
多个Eureka Server相互注册,组成一个集群
每个Eureka Server注册的消息发生变化时,会各个服务之间定时同步,中间过程每个服务的数据可能不一致,但是最终会变得一致
Eureka Server集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
新的Eureka Server节点加入集群后的影响
当有新的节点加入到集群中,会对现在Eureka Server和Eureka Client有什么影响以及他们如何发现新增的Eureka Server节点:
- 新增的Eureka Server:在Eureka Server重启或新的Eureka Server节点加进来的,它会从集群里其它节点获取所有的实例注册信息。如果获取同步失败,它会在一定时间(此值由决定决定)内拒绝服务。
- 已有Eureka Server如何发现新的Eureka Server:
- 已有的Eureka Server:在运行过程中,Eureka Server之间会定时同步实例的注册信息。这样即使新的Application Service只向集群中一台注册服务,则经过一段时间会集群中所有的Eureka Server都会有这个实例的信息。那么Eureka Server节点之间如何相互发现,各个节点之间定时(时间由eureka.server.peer-eureka-nodes-update-interval-ms决定)更新节点信息,进行相互发现。
- Service Consumer:Service Consumer刚启动时,它会从配置文件读取Eureka Server的地址信息。当集群中新增一个Eureka Server中时,那么Service Provider如何发现这个Eureka Server?Service Consumer会定时(此值由eureka.client.eureka-service-url-poll-interval-seconds决定)调用Eureka Server集群接口,获取所有的Eureka Server信息的并更新本地配置。
相关的配置
# Eureka-Server 启动时,从远程 Eureka-Server 读取不到注册信息时,多长时间不允许 Eureka-Client 访问。
eureka.server.wait-time-in-ms-when-sync-empty:5分钟
#Eureka-Server 集群节点更新频率,单位:毫秒。
eureka.server.peer-eureka-nodes-update-interval-ms:
# 初始化实例信息到Eureka服务端的间隔时间,单位为秒
eureka.client.initial-instance-info-replication-interval-seconds: 40
# 更新实例信息的变化到Eureka服务端的间隔时间,单位为秒
eureka.client.instance-info-replication-interval-seconds: 30
服务注册
我们现在就将所有的微服务都注册到Eureka中,这样所有的微服务之间都可以互相调用了。
(1)将其他微服务模块添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)修改每个微服务的application.yml,添加注册eureka服务的配置
eureka:
client:
service-url:
defaultZone: http://localhost:6868/eureka
instance:
prefer-ip-address: true
(3)修改每个服务类的启动类,添加注解
@EnableEurekaClient
(4)启动测试:将每个微服务启动起来,会发现eureka的注册列表中可以看到这些微服务了
三.Feign实现服务间的调用
Feign简介
Feign是简化Java HTTP客户端开发的工具(java-to-httpclient-binder),它的灵感来自于Retrofit、JAXRS-2.0和WebSocket。Feign的初衷是降低统一绑定Denominator到HTTP API的复杂度,不区分是否为restful。
Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign还整合了Ribbon和Hystrix(关于Hystrix我们后面再讲)。
Feign具有如下特性:
- 可插拔的注解支持,包括Feign注解和JAX-RS注解;
- 支持可插拔的HTTP编码器和解码器;
- 支持Hystrix和它的Fallback;
- 支持Ribbon的负载均衡;
- 支持HTTP请求和响应的压缩。
快速体验
实现步骤:
- 在调用方pom中引入feign组件依赖
- 在调用方启动类上添加2个注解@EnableDiscoveryClient,@EnableFeignClients
- 在调用方新建一个接口
- 接口上通过@FeignCleint注解指定要调用的微服名称(不能有下划线)
- 接口中写具体的被调用的方法(参考被调用的微服controller类中的方法) 注:记得添加controller类上的路径
- 如果有@PathVariable参数,一定要取别名
- 调用方通过@Autowired注入接口即可调用
注意启动顺序: 注册中心->被调用的微服->调用方微服
(1)在调用方模块添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)修改调用方模块的启动类,添加注解
@EnableDiscoveryClient
@EnableFeignClients
(3)在调用方模块创建包,包下创建接口
@FeignClient("base")//调用的服务名
public interface LabelClient {
@RequestMapping(value="/label/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable("id") String id);
}
@FeignClient注解用于指定从哪个服务中调用功能 ,注意 里面的名称与被调用的服务名保持一致,并且不能包含下划线。
@RequestMapping注解用于对被调用的微服务进行地址映射。注意 @PathVariable注解一定要指定参数名称,否则出错
(5)修改调用方模块的 ProblemController
@Autowired
private LabelClient labelClient;
@RequestMapping(value = "/mylabel/{labelid}")
public Result findLabelById(@PathVariable String labelid){
Result result = labelClient.findById(labelid);
return result;
}
四.熔断器Hystrix
Hystrix要解决的问题和产生的原因分析
Hystrix要解决的问题和必要性
避免因为单点故障导致服务级联失败,从而使得整个系统崩溃。
官方中提供一个例子,如果一个应用依赖30个服务,每个服务99.99%的时间处于正常服务状态
正常工作时间:99.9930 = 99.7% uptime
失败次数:0.3% of 1亿个请求 = 3,000,000 失败
每个月至少有2个小时处于异常状态
即使只有0.01%的失败率,每个月仍然有几个小时服务不可用
Hystrix问题产生的原因分析
正常请求处理流程如下
如果后端的一个服务出现延迟,则会阻塞整个请求
对于高并发的系统,即使只有一个后端的服务出现延迟,它也会导致整个系统的资源在几秒内被全部消耗掉。更糟糕的是,这些服务有可能被其他服务依赖,由于每个服务都有队列、线程池、其他系统资源等,一旦某个服务失效或者延迟增高,会导致整个系统发生更多的级联故障,从而导致这个分布式系统都不可用
Hystrix设计原则
- 防止单个服务的故障,耗尽整个系统服务的容器(比如tomcat)的线程资源,避免分布式环境里大量级联失败。通过第三方客户端访问(通常是通过网络)依赖服务出现失败、拒绝、超时或短路时执行回退逻辑
- 用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复
- 提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求
- 将所有请求外部系统(或请求依赖服务)封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待
工作流程
下图显示HystrixCommand或HystrixObservableCommand如何与HystrixCircuitBreaker及其逻辑和决策流程交互,包括计数器在断路器中的行为。
熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。 熔断器开关相互转换的逻辑如下图:
熔断器就是保护服务高可用的最后一道防线。
Hystrix特性
1.断路器机制
断路器很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
2.Fallback
Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.
3.资源隔离
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.
快速体验
Feign 本身支持Hystrix,不需要额外引入依赖。
(1)修改调用方模块的application.yml ,开启hystrix
feign:
hystrix:
enabled: true
(2)在调用方下创建impl包,包下创建熔断实现类,实现自接口LabelClient
@Component
public class LabelClientImpl implements LabelClient {
@Override
public Result findById(String id) {
return new Result(false, StatusCode.ERROR,"熔断器启动了");
}
}
(3)修改LabelClient的注解 ,添加fallback
@FeignClient(value="base",fallback = LabelClientImpl.class)
五.微服务网关Zuul
为什么需要微服务网关
不同的微服务一般有不同的网络地址,而外部的客户端可能需要调用多个服务的接口才能完成一个业务需求。比如一个电影购票的收集APP,可能回调用电影分类微服务,用户微服务,支付微服务等。如果客户端直接和微服务进行通信,会存在一下问题:
-
客户端会多次请求不同微服务,增加客户端的复杂性
-
存在跨域请求,在一定场景下处理相对复杂
-
认证复杂,每一个服务都需要独立认证
-
难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接和微服务通信,那么重构会难以实施
-
某些微服务可能使用了其他协议,直接访问有一定困难
上述问题,都可以借助微服务网关解决。微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。
什么是Zuul
Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
-
验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
-
审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
-
动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
-
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
-
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
-
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
-
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
编写一个Zuul网关
在原来的工程下,新建一个Zuul模块,引入依赖,代码如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
创建application.yml
server:
port: 5000
spring:
application:
name: api-geteway
zuul:
routes:
#标识你服务的名字,这里可以自己定义,一般方便和规范来讲还是跟自己服务的名字一样
hello-service:
#服务映射的路径,通过这路径就可以从外部访问你的服务了,目的是为了不爆露你机器的IP,面向服务的路由了,给你选一个可用的出来,
#这里zuul是自动依赖hystrix,ribbon的,不是面向单机
path: /hello-service/**
#这里一定要是你Eureka注册中心的服务的名称,是所以这里配置serviceId因为跟eureka结合了,如果单独使用zuul,那么就必须写自己机器的IP了,
#如url:http://localhost:8080/ 这样的不好就是写死IP了,万一这IP挂了,这高可用性,服务注册那套东西就用不起来了
serviceId: hello-service
eureka:
#客户端
client:
#注册中心地址
service-url:
defaultZone: http://localhost:8888/eureka/,http://localhost:8889/eureka/
修改入口类,增加EnableZuulProxy注解
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayZuulDemoApplication.class, args);
}
}
Filter工作原理
Zuul中的Filter
Zuul是围绕一系列Filter展开的,这些Filter在整个HTTP请求过程中执行一连串的操作。
Zuul Filter有以下几个特征:
- Type:用以表示路由过程中的阶段(内置包含PRE、ROUTING、POST和ERROR)
- Execution Order:表示相同Type的Filter的执行顺序
- Criteria:执行条件
- Action:执行体
Zuul网关的核心是一系列的过滤器,这些过滤器可以对请求或者响应结果做一系列过滤,Zuul 提供了一个框架可以支持动态加载,编译,运行这些过滤器,这些过滤器是使用责任链方式顺序对请求或者响应结果进行处理的,这些过滤器直接不会直接进行通信,但是通过责任链传递的RequestContext参数可以共享一些东西。
虽然Zuul 支持任何可以在jvm上跑的语言,但是目前zuul的过滤器只能使用Groovy脚本来编写。编写好的过滤器脚本一般放在zuul服务器的固定目录,zuul服务器会开启一个线程定时去轮询被修改或者新增的过滤器,然后动态进行编译,加载到内存,然后等后续有请求进来,新增或者修改后的过滤器就会生效了。
Filter Types
以下提供四种标准的Filter类型及其在请求生命周期中所处的位置:
- PRE Filters(前置过滤器) :当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤器,日志过滤器,还有路由选择过滤器
- ROUTING Filters (路由过滤器):该过滤器作用是把请求具体转发到后端服务器上,一般是通过Apache HttpClient 或者 Netflix Ribbon把请求发送到具体的后端服务器上
- POST Filters(后置过滤器):当把请求路由到具体后端服务器后执行的过滤器;场景有添加标准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等。
- ERROR Filters(错误过滤器):当上面任何一个类型过滤器执行出错时候执行该过滤器
除了上述默认的四种Filter类型外,Zuul还允许自定义Filter类型并显示执行。例如,我们定义一个STATIC类型的Filter,它直接在Zuul中生成一个响应,而非将请求在转发到目标。
Zuul请求生命周期
如下图,当zuul接受到请求后,首先会由前置过滤器进行处理,然后在由路由过滤器具体把请求转发到后端应用,然后在执行后置过滤器把执行结果写会到请求方,当上面任何一个类型过滤器执行出错时候执行该过滤器。
自定义Filter
实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。
public class MyFilter extends ZuulFilter {
@Override
String filterType() {
return "pre"; //定义filter的类型,有pre、route、post、error四种
}
@Override
int filterOrder() {
return 10; //定义filter的顺序,数字越小表示顺序越高,越先执行
}
@Override
boolean shouldFilter() {
return true; //表示是否需要执行该filter,true表示执行,false表示不执行
}
@Override
Object run() {
return null; //filter需要执行的具体操作
}
}
filterType
:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
pre
: 可以在请求被路由之前调用route
:在路由请求时候被调用post
:在route和error过滤器之后被调用error
:处理请求时发生错误时被调用
filterOrder
:通过int值来定义过滤器的执行顺序,值越小越优先
shouldFilter
:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效
run
:过滤器的具体逻辑。
将MyFilter加入到请求拦截队列,在类上添加Component注解或在启动类中添加以下代码:
@Bean
public MyFilter myFilter() {
return new MyFilter();
}
Zuul实现header的转发
Zuul实现路由转发时,会丢失请求头中的信息,可通过以下代码实现转发
@Override
public Object run() throws ZuulException {
System.out.println("zuul过滤器...");
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String authorization = request.getHeader("Authorization");
if(authorization!=null){
requestContext.addZuulRequestHeader("Authorization",authorization);
}
return null;
}
六.集中配置组件SpringCloudConfig
Spring Cloud Config简介
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config 组件中,分两个角色,一是config server,二是config client。
Config Server是一个可横向扩展、集中式的配置服务器,它用于集中管理应用程序各个环境下的配置,默认使用Git存储配置文件内容,也可以使用SVN存储,或者是本地文件存储。
Config Client是Config Server的客户端,用于操作存储在Config Server中的配置内容。微服务在启动时会请求Config Server获取配置文件的内容,请求到后再启动容器。
详细内容看在线文档: https://springcloud.cc/spring-cloud-config.html
配置服务端
将配置文件提交到码云
文件命名规则:
{application}-{profile}.yml或{application}-{profile}.properties
application为应用名称 profile指的开发环境(用于区分开发环境,测试环境、生产环境等)
将application.yml改名为base-dev.yml后上传
复制git地址 ,备用
配置中心微服务
(1)创建工程模块,pom.xml引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
(2)创建启动类ConfigServerApplication
@EnableConfigServer //开启配置服务
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
(3)编写配置文件application.yml
spring:
application:
name: test-config
cloud:
config:
server:
git:
uri: #此处填git地址
server:
port: 12000
(4)浏览器测试:http://localhost:12000/base-dev.yml 可以看到配置内容
配置客户端
(1)在工程添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
(2)添加bootstrap.yml ,删除application.yml
spring:
cloud:
config:
name: base
profile: dev
label: master
uri: http://127.0.0.1:12000
七.消息总线组件SpringCloudBus
SpringCloudBus简介
如果我们更新码云中的配置文件,那客户端工程只有重新启动程序才会读取配置。 那我们如果想在不重启微服务的情况下更新配置如何来实现呢? 我们使用SpringCloudBus来实现配置的自动更新。
代码实现
配置服务端
(1)修改配置中心微服务工程的pom.xml,引用依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
(2)修改application.yml ,添加配置
rabbitmq:
host: 192.168.137.188
management: #暴露触发消息总线的地址 发送一个post http://127.0.0.1:12000/actuator/bus-refresh
endpoints:
web:
exposure:
include: bus-refresh #bus-refresh 自定义名字 可以任意修改
配置客户端
(1)修改客户端工程 ,引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
(2)在【码云】的配置文件中配置rabbitMQ的地址:
rabbitmq:
host: 192.168.137.188
(2)启动,看是否正常运行
(3)修改码云上的配置文件 ,将数据库连接IP 改为127.0.0.1 ,在本地部署一份数据库。
(4)postman测试 Url: http://127.0.0.1:12000/actuator/bus-refresh Method: post
(5)再次观察输出的数据是否是读取了本地的mysql数据。
自定义配置的读取
(1)修改码云上的配置文件,增加自定义配置
sms:
ip: 127.0.0.1
(2)在客户端工程中新建controller,添加注解@RefreshScope,此注解用于刷新配置
@RefreshScope
@RestController
public class TestController {
@Value("${sms.ip}")
private String ip;
@RequestMapping(value = "/ip", method = RequestMethod.GET)
public String ip() {
return ip;
}
}
(3)运行测试看是否能够读取配置信息 ,OK.
(4)修改码云上的配置文件中的自定义配置
sms:
ip: 192.168.184.134
(5)通过postman测试 Url: http://127.0.0.1:12000/actuator/bus-refresh Method: post