# springcloud 学习笔记 **Repository Path**: 2452860/springcloud-demo ## Basic Information - **Project Name**: springcloud 学习笔记 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-08-04 - **Last Updated**: 2022-08-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringCloud 微服务 ## 微服务架构 - 单体应用架构 所有的功能模块都放到一个工程中进行编码、编译、打包,并部署在一个tomcat服务中,这种架构简单实用便于维护,开发成本低,但是随着系统功能的不断迭代,使得项目代码杂乱,耦合严重,新增业务也变的困难,核心业务与边缘业务耦合严重,会相互影响,边缘业务会影响核心业务功能的正常使用。 - 垂直应用架构 为了避免单体架构的问题,可以把项目垂直切分成多个项目,可以按照模块垂直进行划分,将核心功能和边缘功能相互独立开来,避免业务之间互相影响。拆分出子系统,各个系统之间可以实现流量分担,各个系统之间相互独立互不影响。但是各个系统之间相互调用,如果某个服务的ip或者端口发生了改变,需要手动修改代码,维护成本增加,随着子系统的的增多,或者机器的增加,负载均衡的配置维护也会变得复杂,同时也不利于对系统的监控。 ![](img/20210727103750.png) - SOA应用架构 在做了垂直划分之后,随着模块的增多,维护成本也会变高,为了解决在垂直应用系统中接口协议不统一,服务无法监控,服务负载均衡困难等问题,我们可以引入阿里巴巴开源的Dubbo框架,来实现服务治理功能。 SOA (Service-Oriented Architecture),即面向服务的架构。根据实际业务,把系统拆分成合适的、独立部署的模块,模块之间相互通信(通过Webservice/Dubbo等技术进行通信)。 - 优点 - 服务以接口为粒度 ,为开发者屏蔽远程调用底层细节 - 业务分层以后架构更加清晰,每个业务模块职责单一,扩展性更强 - 数据隔离,权限回收,数据访问都通过接口,让系统更加稳定安全 - 服务应用本身无状态化 (应用本身不做内存级缓存而是把数据存入db ) - 服务责任易确定 每个服务可以确定责任人 这样更容易保证服务质量和稳定 - 缺点 - 粒度控制复杂 ,如果没有控制好可能导致服务模块越来越多,从而引起超时,分布式事务等问题 - 服务接口数量不宜控制 容易引发接口爆炸 ,服务接口建议以业务场景进行单位划分,并对相近的业务做抽象,防止接口爆炸 - 版本升级兼容困难 ,尽量不要删除方法、字段、枚举类型,新增字段也可能不兼容 - 调用链路长服务质量不可监控 ![](img/20210720152915.png) - 微服务架构 微服务架构是对SOA架构的一种扩展,其粒度分的更细,把应用拆成一个一个的微笑服务,并且不同的服务可以使用不同的语言和存储,不同服务之间通过Restful等轻量级机制进行通信,这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署 ![](img/1.jpg) ## 微服务架构中的一些概念 ### 服务注册与发现 服务注册:服务提供者将所提供服务的信息(服务器IP和端口、服务访问协议等)注册/登记到注册中心 服务发现:服务消费者能够从注册中心获取到较为实时的服务列表,然后根究一定的策略选择一个服务访问 ![](img/20210727153413.png) ### 负载均衡 负载均衡即将请求压力分配到多个服务器(应用服务器、数据库服务器等),以此来提高服务的性能、可靠性 ![](img/20210727153627.png) ### 熔断 熔断即断路保护。微服务架构中,如果下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。 ![](img/20210727153959.png) ### 链路追踪 微服务架构,一个项目往往会拆分出多个服务,不同的服务可能由不同的团队开发维护,而这些服务可能部署到不同的机器上,当服务集群扩大后,一个服务可能部署到上百台服务器上,甚至会跨中心部署,此时一个请求过来,会将请求负载到不同的服务器上,如果此时发生了异常,在排查问题时将会非常麻烦,此时可以引入链路追踪技术。所谓的链路追踪技术,就是对一次请求涉及的很多个服务链路进行日志记录、性能监控。 ![](img/20210727155123.png) ### API 网关 微服务架构上,不同的服务往往会有不同的访问地址,客户端可能会调用多个服务的接口,才能完成一个业务需求,此时如果让客户端直接调用服务,客户端需要配置不同的URL地址,增加了维护难度,并且每一个服务都需要单独进行身份认证功能,为了解决上述问题,我们可以引入API网关。 API可以统一调用API网关层,由API网关层将请求转发到不同的服务上,API网关可以提供 1. 统一接入(路由) 2. 安全防护(统⼀鉴权,负责网关访问身份认证验证,与“访问认证中心”通信,实际认证业务逻辑交移“访问认证中心”处理) 3. 黑白名单(实现通过IP地址控制禁止访问网关功能,控制访问) 4. 协议适配(实现通信协议校验、适配转换的功能) 5. 流量管控(限流) 6. 长短链接支持 7. 容错能力(负载均衡) ## SpringCloud > Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 ![](img/20210727161205.png) Spring Cloud其实是⼀套规范,是⼀套用于构建微服务架构的规范,而不是⼀个可以拿来即用的框架(所谓规范就是应该有哪些功能组件,然后组件之间怎么配合,共同完成什么事情) 。在这个规范之下第三方的Netflix公司开发了一些组件、 Spring官方开发了一些框架/组件,包括第三方的阿里巴巴开发了⼀套框架/组件集合Spring Cloud Alibaba,这些才是Spring Cloud规范的实现。 ### SpringCloud 架构 Spring Cloud 生态圈中的组件,按照发展可以分为第⼀代 Spring Cloud组件和第⼆代 Spring Cloud组件 | | 第一代 Spring Cloud(Netflix, SCN) | 第二代 Spring Cloud(主要就是 Spring Cloud Alibaba, SCA | | -------------- | ----------------------------------------------------- | -------------------------------------------------------- | | 注册中心 | Netflix Eureka | 阿里巴巴 Nacos | | 客户端负载均衡 | Netflix Ribbon | 阿里巴巴 Dubbo LB、 Spring Cloud Loadbalancer | | 熔断器 | Netflix Hystrix | 阿里巴巴 Sentinel | | 网关 | Netflix Zuul:性能⼀般,未来将退出Spring Cloud 生态圈 | 官方 Spring Cloud Gateway | | 配置中心 | 官方 Spring Cloud Config | 阿里巴巴 Nacos、携程 Apollo | | 服务调用 | Netflix Feign | 阿里巴巴 Dubbo RPC | | 消息驱动 | 官方 Spring Cloud Stream | | | 链路追踪 | 官方 Spring Cloud Stream | | ### SpringCloud 体系结构 ![](img/20210727162429.png) - 注册中心负责服务的注册与发现,将各服务连接起来 - API网关负责转发所有外来的请求 - 断路器负责监控服务之间的调用情况,连续多次失败进行熔断保护 - 配置中心提供了统⼀的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息 ### 第一代SpringCloud 核心组件 #### Eureka 服务注册中心 Eureka是Netflix中的一个开源框架。它和 zookeeper、Consul一样,都是用于服务注册管理的。 ![](img/20210727163352.png) 分布式微服务架构中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息 。 ##### 注册中心对比 - Zookeeper Zookeeper它是⼀个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的⼀些数据管理问题,如:统⼀命名服务、状态同步服务、集群管理、分布式应用配置项的管理等 - Eureka 由Netflix开源,并被Pivatal集成到SpringCloud体系中,它是基于 Restful API 风格开发的服务注册与发现组件 - Consul Consul是由HashiCorp基于Go语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件, 采用Raft算法保证服务的⼀致性,且支持健康检查 - Nacos Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说 Nacos 就是 注册中心 + 配置中心的组合,帮助我们解决微服务开发必会涉及到的服务注册与发现,服务配置,服务管理等问题。 Nacos 是Spring Cloud Alibaba 核心组件之⼀,负责服务注册与发现,还有配置 。 | 组件名 | 语言 | CAP | 对外暴露接口 | | --------- | ---- | ---------------------------- | ------------ | | Eureka | Java | AP(自我保护机制,保证可用) | HTTP | | Consul | Go | CP | HTTP/DNS | | Zookeeper | Java | CP | 客户端 | | Nacos | Java | 支持AP/CP切换 | HTTP | ##### Eureka 基础架构 ![](img/20210727164817.png) Eureka 交互流程及原理 ![](img/20210727165027.png) Eureka 包含两个组件: Eureka Server 和 Eureka Client, Eureka Client是一个Java客户端,用于简化与Eureka Server的交互; Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server 进行注册自己的信息(例如网络信息), Eureka Server会存储该服务的信息。 1)图中us-east-1c、 us-east-1d, us-east-1e代表不同的区也就是不同的机房 2)图中每⼀个Eureka Server都是一个集群 3)图中Application Service 作为服务提供者向Eureka Server中注册服务,Eureka Server接受到注册事件会在集群和分区中进行数据同步, ApplicationClient作为消费端(服务消费者)可以从Eureka Server中获取到服务注册信息,进行服务调用 4)微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息 5)Eureka Server在一定时间内没有接收到某个微服务节点的心跳, Eureka Server将会注销该微服务节点(默认90秒) 6)每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的方式完成服务注册列表的同步 7)Eureka Client 会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者 ##### Eureka 应用 **搭建Eureka Server服务注册中心** pom文件 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.5.3 com.h52mm eureka-server-demo 0.0.1-SNAPSHOT eureka-server-demo Demo project for Spring Boot 1.8 2020.0.3 org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import org.springframework.boot spring-boot-maven-plugin ``` 配置文件 ```properties server.port=8070 spring.application.name=demo-eureka-server eureka.instance.hostname=localhost eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/ #自己就是服务不需要注册自己 eureka.client.register-with-eureka=false #自己就是服务不需要从Eureka Server获取服务信息,默认为true,置为false eureka.client.fetch-registry=false ``` SpringBoot启动类,使用@EnableEurekaServer声明当前项目为EurekaServer服务 ```java package com.h52mm.eurekaserverdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaServerDemoApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerDemoApplication.class, args); } } ``` 启动工程 ![](img/20210727180844.png) ![](img/20210728091343.png) ![](img/20210728091706.png) **Eureka 高可用集群** 在生产环境中我们通常我们会实现EurekaServer集群的高可用,Eureka Server集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。 ![](img/20210728092134.png) **配置三个Eureka Server** 1. EurekaServer1 配置 ```properties server.port=8070 spring.application.name=demo-eureka-server eureka.instance.hostname=eurekaServer1 eureka.client.service-url.defaultZone=http://eurekaServer2:8071/eureka/,http://eurekaServer3:8072/eureka/ eureka.client.register-with-eureka=true eureka.client.fetch-registry=true ``` 2. EurekaServer2 配置 ```properties server.port=8071 spring.application.name=demo-eureka-server eureka.instance.hostname=eurekaServer2 eureka.client.service-url.defaultZone=http://eurekaServer1:8070/eureka/,http://eurekaServer3:8072/eureka/ eureka.client.register-with-eureka=true eureka.client.fetch-registry=true ``` 3. EurekaServer3 配置 ```properties server.port=8072 spring.application.name=demo-eureka-server eureka.instance.hostname=eurekaServer3 eureka.client.service-url.defaultZone=http://eurekaServer1:8070/eureka/,http://eurekaServer2:8071/eureka/ eureka.client.register-with-eureka=true eureka.client.fetch-registry=true ``` **说明** 1. 在一个服务实例中,把另外的实例作为了集群中的镜像节点,在配置url 的时候需要和其他实例的eureka.instance.hostname和server.port保持一致 2. register-with-eureka 和 fetch-registry 在单节点时设置为了 false, 因为只有⼀台 Eureka Server,并不需要自己注册自己,而现在有了集群,可以在集群的其他节点中注册本服务 3. 在一台机器上配置运行不通实例,配置了eureka.instance.hostname,需要修改host地址,windows hosts 所在路径(‪C:\Windows\System32\drivers\etc\hosts) ```tex 127.0.0.1 eurekaServer1 127.0.0.1 eurekaServer2 127.0.0.1 eurekaServer3 ``` ![](img/20210728093658.png) ![](img/20210728093808.png) ![](img/20210728093906.png) **微服务提供者 ** ```yaml server: port: 8090 spring: application: name: eureka-provider-demo eureka: client: service-url: # eureka server 的路径 defaultZone: http://localhost:8070/eureka/ # 把eureka server 集群的所有地址都写上,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: prefer-ip-address: true # 使用ip注册 # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ipaddress}:${spring.application.name}:${server.port}:@project.version@ ``` 测试类 ```java package com.h52mm.eureka.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("test") public class TestController { @Value("${server.port}") private String port; @GetMapping("sayHello/{name}") public String sayHello(@PathVariable(value = "name") String name){ return "hello "+name+" :"+port; } } ``` 启动类 ```java package com.h52mm.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class EurekaProviderDemoApplication { public static void main(String[] args) { SpringApplication.run(EurekaProviderDemoApplication.class,args); } } ``` **微服务消费者** ```yaml server: port: 8060 spring: application: name: eureka-consumer-demo eureka: client: service-url: # eureka server 的路径 defaultZone: http://localhost:8070/eureka/ # 把eureka server 集群的所有地址都写上,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: prefer-ip-address: true # 使用ip注册 # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ipaddress}:${spring.application.name}:${server.port}:@project.version@ ``` 测试类 ```java package com.h52mm.eureka.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController public class HelloController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @GetMapping("sayHello/{name}") public String sayHello(@PathVariable(value = "name") String name){ //从eureka server 中获取 服务列表 List instances = discoveryClient.getInstances("eureka-provider-demo"); //获取实例,不考虑负载均衡,默认取第一个 ServiceInstance serviceInstance = instances.get(0); //取主机地址 String host = serviceInstance.getHost(); //端口号 int port = serviceInstance.getPort(); //拼接请求的url String url="http://"+host+":"+port+"/test/sayHello/"+name; String result = restTemplate.getForObject(url, String.class); return result; } } ``` 启动类 ```java package com.h52mm.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient public class EurekaConsumerApplication { public static void main(String[] args) { SpringApplication.run(EurekaConsumerApplication.class,args); } @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` 启动注册中心,启动服务提供者,启动服务消费者 ![](img/20210728163309.png) 访问消费者接口 ![](img/20210728163850.png) ##### Eureka 详解 **Eureka 元数据** Eureka的元数据有两种:标准元数据和自定义元数据 标准元数据: 主机名、 IP地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。 自定义元数据: 可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。 类似于 ```yaml instance: prefer-ip-address: true metadata-map: #⾃定义元数据(kv⾃定义) cluster: cluster1 region: region1 ``` 可以通过DiscoveryClient 获取所有的元数据信息,如 ```java List instances = discoveryClient.getInstances("eureka-provider-demo"); ``` ![](img/20210728165904.png) **Eureka 客户端** 服务注册 1)导入 eureka-client 依赖坐标,配置 Eureka 服务注册中心地址 2)服务启动时会向注册中心发起注册请求,携带服务元数据信息 3)Eureka 注册中心会把服务的信息保存在Map中 服务续约 服务每隔30秒会向注册中心续约(心跳)⼀次(也称为报活),如果没有续约,租约在90秒后到期,然后服务会被失效。每隔30秒的续约操作我们称之为心跳检测。 获取服务列表 每隔30秒服务会从注册中心中拉取⼀份服务列表 1)服务消费者启动时,从 EurekaServer服务列表获取只读备份,缓存到本地 、 2)每隔30秒,会重新获取并更新数据 3)每隔30秒的时间可以通过配置eureka.client.registry-fetch-interval-seconds修改(往往不需要修改) **Eureka 服务端** 服务下线 1)当服务正常关闭操作时,会发送服务下线的REST请求给EurekaServer 2)服务中心接受到请求后,将该服务置为下线状态 服务剔除 Eureka Server 会定时(间隔值是eureka.server.eviction-interval-timer-in-ms,默认60s)进行检查,如果发现实例在一定时间(此值由客户端设置的eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到心跳,则会注销此实例 自我保护 如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障, Eureka Server 自动进入自我保护机制 ![](img/20210728173028.png) 默认情况下,如果 Eureka Server 在⼀定时间内(默认90秒)没有接收到某个微服务实例的心跳, Eureka Server 将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。 当 Eureka Server 处于自我保护机制时 1)不会剔除任何服务实例(可能是服务提供者和EurekaServer之间网络问题),保证了大多数服务依然可用 2)Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用,当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中 3)在Eureka Server 工程中通过eureka.server.enable-self-preservation配置可用关停自我保护,默认值是打开 ```properties eureka: server: enable-self-preservation: false # 关闭⾃我保护模式(缺省为打开 ``` #### Ribbon 负载均衡 负载均衡一般分为服务器端负载均衡和客户端负载均衡,服务器端负载均衡可以用Nginx、F5等技术实现,客户端负载均衡可以使用Ribbon 实现,服务消费者客户端会有一个服务器地址列表,调用方在请求前通过一定的负载均衡算法选择⼀个服务器进行访问,负载均衡算法的执行在请求客户端进行。 Ribbon是Netflix发布的负载均衡器 ,一般配合Eureka 进行使用,Ribbon从Eureka 服务上获取服务列表信息,然后从这些服务列表信息中根据负载均衡算法选择一个进行远端调用。 ![](img/20210728175301.png) ##### Ribbon 应用 注意:在引入了eureka-client,它会引入 Ribbon 相关Jar,因此不需要重新引入jar包 在客户端上RestTemplate 上加上 @LoadBalanced 注解 ```java @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } ``` 改造客户端程序 ```java package com.h52mm.eureka.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController public class HelloController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @GetMapping("sayHello/{name}") public String sayHello(@PathVariable(value = "name") String name) { // //从eureka server 中获取 服务列表 // List instances = discoveryClient.getInstances("eureka-provider-demo"); // // //获取实例,不考虑负载均衡,默认取第一个 // ServiceInstance serviceInstance = instances.get(0); // //取主机地址 // String host = serviceInstance.getHost(); // // //端口号 // int port = serviceInstance.getPort(); // // //拼接请求的url // String url = "http://" + host + ":" + port + "/test/sayHello/" + name; //ribbon 负载均衡直接配置服务端名称即可 String ribbonUrl="http://eureka-provider-demo//test/sayHello/"+name; String result = restTemplate.getForObject(ribbonUrl, String.class); return result; } } ``` provider 启动两个服务 ,端口号分别为8090,8091 ![](img/20210729103941.png) 连续访问客户端程序可以看到轮询的效果 ![](img/20210729104626.png) ![](img/20210729104641.png) ##### Ribbon 负载均衡策略 Ribbon内置了多种负载均衡策略,内部负责负载均衡的顶级接口为com.netflix.loadbalancer.IRule ,其类树如下 ![](img/20210729111250.png) | 负载均衡策略 | 描述 | | -------------------------------------------- | ------------------------------------------------------------ | | RoundRobinRule:轮询策略 | 默认超过10次获取到的server都不可用,会返回⼀个空的server | | RandomRule:随机策略 | 如果随机到的server为null或者不可用的话,会 while不停的循环选取 | | RetryRule:重试策略 | 一定时限内循环重试。默认继承 RoundRobinRule,也支持自定义注入, RetryRule会在每次选取之后,对选举的server进行判断,是否为null,是否alive,并且500ms 内会不停的选取判断。而RoundRobinRule失效的策略是超过10次, RandomRule是没有失效时间的概念,只要serverList没有则都挂 | | BestAvailableRule:最小连接数策略 | 遍历serverList,选取出可用的且连接数最小的⼀个server。该算法里面有⼀个LoadBalancerStats 的成员变量,会存储所有server的运行状况和连接数。如果选取到的server为null,那么会调用 RoundRobinRule重新选取。 | | AvailabilityFilteringRule: 可用过滤策略 | 扩展了轮询策略,会先通过默认的轮询选取⼀个 server,再去判断该server是否超时可用,当前连接数是否超限,都成功再返回。 | | ZoneAvoidanceRule:区 域权衡策略(默认策略) | 扩展了轮询策略,继承了2个过滤器: ZoneAvoidancePredicate和 AvailabilityPredicate,除了过滤超时和链接数过多的server,还会过滤掉不符合要求的zone区域里面的所有节点, AWS --ZONE 在⼀个区域/机房内的服务实例中轮询 | **修改负载均衡策略** ```yaml #针对的被调用方微服务名称,不加就是全局⽣效 lagou-service-resume: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载策略调整 ``` ##### Ribbon 工作原理 ![](img/20210729135702.png) 负载均衡管理器LoadBalancer是Ribbon的核心,它相当于人的大脑,围绕在它周围的有IRule、 IPing等 - IRule:是在选择实例时候的负载均衡策略对象 - IPing:是用来向服务发起心跳检测的,通过心跳检测来判断该服务是否可用 - ServerListFilter:根据一些规则过滤传入的服务实例列表 - ServerListUpdater:定义了一系列的对服务列表的更新操作 #### Hystrix 熔断器 ##### 微服务雪崩效应 在微服务中一个请求可能会在多个微服务之间相互调用,这其中调用链路可能会很长,如果此时链路中一个服务由于某些原因导致不能提供服务,那么在这条调用路上会形成积压,从而导致其他服务跟着一起宕机,这样就形成了雪崩效应。 **解决方案** 1. 服务熔断 熔断机制是应对雪崩效应的⼀种微服务链路保护机制 ,当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,进行服务的降级,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。 注意: 1)服务熔断重点在“断”,切断对下游服务的调用 2)服务熔断和服务降级往往是⼀起使用的, Hystrix就是这样 2. 服务降级 当整体资源不够用了,先将一些不关紧的服务停掉(调用的时候,返回一个预留的值,也叫做兜底数据 ),从而保障核心业务的正常进行,待度过难关高峰后,在恢复服务的调用。 服务降级一般是从整体考虑,就是当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备⼀个本地的fallback回调,返回⼀个缺省值 。 3. 服务限流 服务降级是当服务出问题或者影响到核心业务流程的性能时,暂时将服务屏蔽掉,待高峰或者问题解决后再打开,但是有些场景中,虽然该业务不是核心业务,但是也很重要,比如电商中的秒杀活动,此时如果将其熔断降级就不太合适,此时我们可以使用服务限流来解决。常见的限流措施。 - 限制总并发数(比如数据库连接池、线程池) - 限制瞬时并发数(如nginx限制瞬时并发连接数) - 限制时间窗口内的平均速率(如Guava的RateLimiter、 nginx的limit_req模块,限制每秒的平均速率) - 限制远程接口调用速率、限制MQ的消费速率等 ##### Hystrix 简介 Hystrix 是Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。 Hystrix主要通过以下几点实现延迟和容错 - 包裹请求 使用HystrixCommand包裹对依赖的调用逻辑 - 跳闸机制 当某服务的错误率超过⼀定的阈值时, Hystrix可以跳闸,停止请求该服务⼀段时间 - 资源隔离 Hystrix为每个依赖都维护了⼀个小型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定 - 监控: Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等 - 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值 - 自我修复:断路器打开⼀段时间后,会自动进入“半开”状态 ##### Hystrix 应用 服务消费者工程引入Hystrix 依赖坐标 ```XML org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` 服务消费者工程启动类加入@EnableCircuitBreaker 注解开启熔断功能 ![](img/20210729160816.png) 定义服务降级处理方法,在方法上使用@HystrixCommand的fallbackMethod属性关联到服务降级处理方法 ```java package com.h52mm.eureka.controller; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class HelloController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @GetMapping("sayHello/{name}") @HystrixCommand( //线程池标识,要保持唯一,不唯一的话就共用了 threadPoolKey = "helloTestTimeOut", //线程池配置细节 threadPoolProperties ={ @HystrixProperty(name="coreSize",value = "1") ,//线程数 @HystrixProperty(name = "maxQueueSize",value = "20") // 等待队列长度 }, //熔断得细节属性配置 commandProperties = { //服务降级超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000"), // 统计时间窗口定义 @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"), //统计时间窗口内的最⼩请求数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"), //统计时间窗口内的错误数量百分比阈值 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"), //自我修复时的活动窗口长度 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000") }, fallbackMethod="myFallBack" //回退方法 ) public String sayHello(@PathVariable(value = "name") String name) { // //从eureka server 中获取 服务列表 // List instances = discoveryClient.getInstances("eureka-provider-demo"); // // //获取实例,不考虑负载均衡,默认取第一个 // ServiceInstance serviceInstance = instances.get(0); // //取主机地址 // String host = serviceInstance.getHost(); // // //端口号 // int port = serviceInstance.getPort(); // // //拼接请求的url // String url = "http://" + host + ":" + port + "/test/sayHello/" + name; //ribbon 负载均衡直接配置服务端名称即可 String ribbonUrl="http://eureka-provider-demo//test/sayHello/"+name; String result = restTemplate.getForObject(ribbonUrl, String.class); return result; } //降级回退方法 public String myFallBack(String name){ return "服务被降级"; } } ``` 服务提供者接口增加时间休眠,模拟接口超时现象 ```java package com.h52mm.eureka.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("test") public class TestController { @Value("${server.port}") private String port; @GetMapping("sayHello/{name}") public String sayHello(@PathVariable(value = "name") String name){ try { Thread.sleep(3000); }catch (Exception e){ e.printStackTrace(); } return "hello "+name+" :"+port; } } ``` 启动工程,访问客户端接口,返现接口进入降级方法中 ![](img/20210729164430.png) **注意** 1. 降级(兜底)方法必须和被降级方法相同的方法签名(相同参数列表、相同返回值) 2. 可以在类上使用@DefaultProperties注解统一指定整个类中共用的降级(兜底)方法 ##### Hystrix 舱壁模式 (线程池隔离策略) ![](img/20210729165017.png) **如果不进行任何设置,所有熔断方法使用⼀个Hystrix线程池(10个线程),当线程被占满的时候,其他请求会等待或者被拒绝连接,这时可能并不是下层服务不可用导致的,而是线程机制导致的 ** 为了避免上述现象,Hystrix 不是采用增加线程数来解决,而是单独的为每⼀个控制方法创建一个线程池(舱壁模式 ) ![](img/20210729170055.png) ##### Hystrix 的工作流程与高级应用 **工作流程描述** ![](img/20210729170446.png) 1. 当调用出现问题时,开启一个时间窗(10s) 2. 在这个时间窗内,统计调用次数是否达到最小请求数?如果没有达到,则重置统计信息,回到第1步 ,如果达到了,则统计失败的请求数占所有请求数的百分比,是否达到阈值? 如果达到,则跳闸(不再请求对应服务) 如果没有达到,则重置统计信息,回到第1步 3. 如果跳闸,则会开启一个活动窗口(默认5s),每隔5s, Hystrix会让一个请求通过,到达那个问题服务,看是否调用成功,如果成功,重置断路器回到第1步,如果失败,回到第3步 ```java commandProperties = { //服务降级超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000"), // 统计时间窗口定义 @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"), //统计时间窗口内的最⼩请求数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"), //统计时间窗口内的错误数量百分比阈值 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"), //自我修复时的活动窗口长度 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000") } ``` 上述配置也可以统一配置到配置文件中 ```yaml # 配置熔断策略: hystrix: command: default: circuitBreaker: # 强制打开熔断器,如果该属性设置为true,强制断路器进⼊打开状态,将会拒绝所有的请求。 默认false关闭的 forceOpen: false # 触发熔断错误⽐例阈值,默认值50% errorThresholdPercentage: 50 # 熔断后休眠时⻓,默认值5秒 sleepWindowInMilliseconds: 3000 # 熔断触发最⼩请求次数,默认值是20 requestVolumeThreshold: 2 execution: isolation: thread: # 熔断超时设置,默认为1秒 timeoutInMilliseconds: 2000 ``` ##### Hystrix Dashboard 断路监控仪表盘 正常状态是UP,跳闸是⼀种状态CIRCUIT_OPEN,可以通过/health查看,前提是工程中需要引入SpringBoot的actuator(健康监控),它提供了很多监控所需的接口,可以对应用系统进行配置查看、相关功能统计等。 在工程中引入 ```xml org.springframework.boot spring-boot-starter-actuator ``` 新建一个监控服务工程,导入依赖 ```xml springcloud-demo com.h52mm 0.0.1-SNAPSHOT 4.0.0 hystrix-dashboard-demo org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 启动类 ```java package com.h52mm.hystrix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; @SpringBootApplication @EnableHystrixDashboard //开启hystrix dashboard public class HystrixDashboardApplication { public static void main(String[] args) { SpringApplication.run(HystrixDashboardApplication.class,args); } } ``` 配置文件 ```yaml server: port: 9000 spring: application: name: hystrix-dashboard-demo eureka: client: service-url: # eureka server 的路径 defaultZone: http://localhost:8070/eureka/ # 把eureka server 集群的所有地址都写上,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: prefer-ip-address: true # 使用ip注册 # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ipaddress}:${spring.application.name}:${server.port}:@project.version@ ``` 在被监测的微服务中注册监控servlet(服务) ```java /** * 在被监控的微服务中注册一个serlvet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的 * 前提:被监控的微服务需要引入springboot的actuator功能 * @return */ @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } ``` 对proviter 的调用方法开启熔断服务 ```java package com.h52mm.eureka.controller; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("test") public class TestController { @Value("${server.port}") private String port; @GetMapping("sayHello/{name}") @HystrixCommand( //线程池标识,要保持唯一,不唯一的话就共用了 threadPoolKey = "helloTestTimeOut", //线程池配置细节 threadPoolProperties ={ @HystrixProperty(name="coreSize",value = "1") ,//线程数 @HystrixProperty(name = "maxQueueSize",value = "20") // 等待队列长度 }, //熔断得细节属性配置 commandProperties = { //服务降级超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000"), // 统计时间窗口定义 @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"), //统计时间窗口内的最⼩请求数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"), //统计时间窗口内的错误数量百分比阈值 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"), //自我修复时的活动窗口长度 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000") }, fallbackMethod="myFallBack" //回退方法 ) public String sayHello(@PathVariable(value = "name") String name){ // try { // Thread.sleep(3000); // }catch (Exception e){ // e.printStackTrace(); // } return "hello "+name+" :"+port; } //降级回退方法 public String myFallBack(String name){ return "服务被降级"; } } ``` proviter 服务暴露监控接口 ```yaml # springboot中暴露健康检查等断点接口 management: endpoints: web: exposure: include: "*" # 暴露健康接口的细节 endpoint: health: show-details: always ``` proviter 开启熔断服务 ```java package com.h52mm.eureka; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; @SpringBootApplication @EnableDiscoveryClient //开启熔断服务 @EnableCircuitBreaker public class EurekaProviderDemoApplication { public static void main(String[] args) { SpringApplication.run(EurekaProviderDemoApplication.class,args); } /** * 在被监控的微服务中注册一个serlvet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的 * 前提:被监控的微服务需要引入springboot的actuator功能 * @return */ @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } ``` 访问 http://localhost:8090/actuator/hystrix.stream,在访问 http://localhost:8090/test/sayHello/张三 ![](img/20210730095048.png) 但是这种方式不直观,我们可以结合仪表盘更友好的展示这些信息,访问 http://localhost:9000/hystrix ![](img/20210730095416.png) ![](img/20210730095707.png) 上图中的实心圆 - 大小:代表请求流量的大小,流量越大球越大 - 颜色:代表请求处理的健康状态,从绿色到红色递减,绿色代表健康,红色代表很不健康 曲线波动图 记录了2分钟内该方法上流量的变化波动图,判断流量上升或者下降的趋势 ##### Hystrix Turbine 聚合监控 上面我们针对的是一个微服务实例的Hystrix数据查询分析,但是我们的服务实例是多个的,如果每次只能查看单个实例的监控,就需要经常切换很不方便,在这样的场景下,我们可以使用 HystrixTurbine 进行聚合监控,它可以把相关微服务的监控数据聚合在⼀起,便于查看。 ![](img/20210730100451.png) Turbine服务搭建 新建项目,引入依赖坐标 ```xml org.springframework.cloud spring-cloud-starter-netflix-turbine ``` 配置文件 ```yaml server: port: 9010 spring: application: name: hystrix-turbine-demo eureka: client: service-url: # eureka server 的路径 defaultZone: http://localhost:8070/eureka/ # 把eureka server 集群的所有地址都写上,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: prefer-ip-address: true # 使用ip注册 # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ipaddress}:${spring.application.name}:${server.port}:@project.version@ #turbine配置 turbine: # appCofing配置需要聚合的服务名称,如果需要配置多个,使用英文逗号拼接 app-config: eureka-provider-demo clusterNameExpression: "'default'" #集群默认名称 ``` 启动类 ```java package com.h52mm.turbine; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.turbine.EnableTurbine; @SpringBootApplication @EnableDiscoveryClient @EnableTurbine // 开启聚合功能 public class HystrixTurbineDemoApplication { public static void main(String[] args) { SpringApplication.run(HystrixTurbineDemoApplication.class,args); } } ``` 浏览器访问Turbine项⽬, http://localhost:9010/turbine.stream,就可以看到监控数据了 ![](img/20210730103213.png) 通过dashboard的页面查看数据更直观,把刚才的地址输入dashboard地址栏 ![](img/20210730103900.png) #### Feign 远程调用组件 当服务消费者使用RestTemplate 调用服务提供者存在很多不便之处 1. 需要拼接URL 2. restTmplate.getForObJect ![](img/20210730104332.png) 如上图,这两行代码都比较模板化,有没有什么框架可以帮我们实现这个功能 ##### Feign 简介 Feign是Netflix开发的一个轻量级RESTful的HTTP服务客户端(用它来发起请求,远程调用的) ,是以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用, Feign被广泛应用 Spring Cloud 的解决方案中 类似于Dubbo,服务消费者拿到服务提供者的接口,然后像调用本地接口方法⼀样去调用,实际发出的是远程的请求 优点 - Feign可帮助我们更加便捷,优雅的调用HTTP API:不需要我们去拼接url然后在调用restTemplate的api - SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解 (OpenFeign ) ##### Feign 配置应用 服务消费者工程,引入Feign依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` 服务消费者工程启动类添加@EnableFeignClients 注解,支持Feign ```java package com.h52mm.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient //开启熔断服务 @EnableCircuitBreaker @EnableFeignClients //开启Feign public class EurekaConsumerApplication { public static void main(String[] args) { SpringApplication.run(EurekaConsumerApplication.class,args); } @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` **注意:**此时去掉Hystrix熔断的支持注解@EnableCircuitBreaker即可包括引入的依赖,因为Feign会自动引入 创建Feign接口 ```java package com.h52mm.eureka.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; // name:调用的服务名称,和服务提供者yml文件中spring.application.name保持一致 @FeignClient(name = "eureka-provider-demo") public interface HelloFeignService { @RequestMapping(value="test/sayHello/{name}",method = RequestMethod.GET) public String sayHello(@PathVariable(value = "name") String name); } ``` **注意:** 1)@FeignClient注解的name属性用于指定要调用的服务提供者名称,和服务提供者yml文件中spring.application.name保持一致 2)接口中的接口方法,就好比是远程服务提供者Controller中的Hander方法(只不过如同本地调用了),那么在进行参数绑定的时,可以使用@PathVariable、 @RequestParam、 @RequestHeader等,这也OpenFeign对SpringMVC注解的支持,但是需要注意value必须设置,否则会抛出异常 使用接口完成远程调用 ```java package com.h52mm.eureka.controller; import com.h52mm.eureka.service.HelloFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class HelloController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @Autowired private HelloFeignService helloFeignService; @GetMapping("sayHello/{name}") // @HystrixCommand( // //线程池标识,要保持唯一,不唯一的话就共用了 // threadPoolKey = "helloTestTimeOut", // //线程池配置细节 // threadPoolProperties ={ // @HystrixProperty(name="coreSize",value = "1") ,//线程数 // @HystrixProperty(name = "maxQueueSize",value = "20") // 等待队列长度 // }, // //熔断得细节属性配置 // commandProperties = { // //服务降级超时时间 // @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000"), // // 统计时间窗口定义 // @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "8000"), // //统计时间窗口内的最⼩请求数 // @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"), // //统计时间窗口内的错误数量百分比阈值 // @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"), // //自我修复时的活动窗口长度 // @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "3000") // }, // fallbackMethod="myFallBack" //回退方法 // ) public String sayHello(@PathVariable(value = "name") String name) { // //从eureka server 中获取 服务列表 // List instances = discoveryClient.getInstances("eureka-provider-demo"); // // //获取实例,不考虑负载均衡,默认取第一个 // ServiceInstance serviceInstance = instances.get(0); // //取主机地址 // String host = serviceInstance.getHost(); // // //端口号 // int port = serviceInstance.getPort(); // // //拼接请求的url // String url = "http://" + host + ":" + port + "/test/sayHello/" + name; //ribbon 负载均衡直接配置服务端名称即可 // String ribbonUrl="http://eureka-provider-demo//test/sayHello/"+name; // // String result = restTemplate.getForObject(ribbonUrl, String.class); String result = helloFeignService.sayHello(name); return result; } //降级回退方法 public String myFallBack(String name){ return "服务被降级"; } } ``` 负载均衡配置 Feign 本身已经集成了Ribbon依赖和自动配置,因此我们不需要额外引入依赖,可以通过 ribbon.xx 来进行全局配置,也可以通过服务名.ribbon.xx 来对指定服务进行细节配置配置 ```yaml #ribbon 配置 eureka-provider-demo: ribbon: #请求连接超时时间 #ConnectTimeout: 2000 #请求处理超时时间 #ReadTimeout: 5000 #对所有操作都进⾏重试 OkToRetryOnAllOperations: true ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用 MaxAutoRetriesNextServer: 0 #切换实例的重试次数 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整 ``` 配置熔断 ```yaml # feign 开启熔断 feign: hystrix: enabled: true ``` Feign的超时时长设置那其实就上面Ribbon的超时时长设置 Hystrix超时设置 ```yaml hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 ``` **注意** 1)开启Hystrix之后, Feign中的方法都会被进行⼀个管理,一旦出现问题就进入对应的回退逻辑处理 2)针对超时这一点,当前有两个超时时间设置(Feign/hystrix),熔断的时候是根据这两个时间的最小值来进行的,即处理时长超过最短的那个超时时间就熔断进入回退降级逻辑 自定义FallBack处理类,需要实现FeignClient 接口 ```java package com.h52mm.eureka.service.impl; import com.h52mm.eureka.service.HelloFeignService; import org.springframework.stereotype.Component; /** * 降级回退处理类 */ @Component public class HelloFeignFallBackServiceImpl implements HelloFeignService { @Override public String sayHello(String name) { return "服务被降级"; } } ``` 在@FeignClient注解中关联2)中自定义的处理类 ```java package com.h52mm.eureka.service; import com.h52mm.eureka.service.impl.HelloFeignFallBackServiceImpl; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; // name:调用的服务名称,和服务提供者yml文件中spring.application.name保持一致 @FeignClient(name = "eureka-provider-demo",fallback = HelloFeignFallBackServiceImpl.class,path = "/test") public interface HelloFeignService { @RequestMapping(value="/sayHello/{name}",method = RequestMethod.GET) public String sayHello(@PathVariable(value = "name") String name); } ``` Feign对请求压缩和响应压缩的支持 Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗 ```yaml feign: compression: request: enabled: true mime-types: text/html,application/xml,application/json # 此处是默认值 min-request-size: 2048 #设置触发压缩的大小下限,此处是默认值 response: enabled: true # 开启响应压缩 ``` Feign的日志级别配置 Feign是http请求客户端,类似于咱们的浏览器,它在请求和接收响应的时候,可以打印出比较详细的⼀些日志信息(响应头,状态码等等) 开启Feign日志功能及级别 ```java package com.h52mm.eureka.config; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfig { @Bean public Logger.Level feignLevel() { return Logger.Level.FULL; } } ``` 配置log日志级别为debug ```yaml logging: level: # Feign⽇志只会对⽇志级别为debug的做出响应 com.lagou.edu.controller.service.ResumeServiceFeignClient: debug ``` #### GateWay 网关组件 ##### GateWay 简介 Spring Cloud GateWay是Spring Cloud的一个全新项目,目标是取代Netflix Zuul,它基于Spring5.0+SpringBoot2.0+WebFlux(基于高性能的Reactor模式响应式通信框架Netty开发,使用异步非阻塞模型)等技术开发,性能高于Zuul,官方测试, GateWay是Zuul的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理方式 Spring Cloud GateWay不仅提供统⼀的路由方式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成⼀些功能) 链的方式提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等 - 路由 (route): 网关最基础的部分,也是网关比较基础的工作单元。路由由⼀个ID、⼀个目标URL(最终路由到的地址)、⼀系列的断言(匹配条件判断)和Filter过滤器(精细化控制)组成。如果断言为true,则匹配该路由 - 断言(predicates):参考了Java8中的断言java.util.function.Predicate,开发人员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于nginx中的location匹配⼀样),如果断言与请求相匹配则路由 - 过滤器(filter):⼀个标准的Spring webFilter,使用过滤器,可以在请求之前或者之后执行业务逻辑 ![](img/20210730145341.png) ![](img/20210730145521.png) 客户端向Spring Cloud GateWay发出请求,然后在GateWay Handler Mapping中找到与请求相匹配的路由,将其发送到GateWay Web Handler; Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑 ##### GateWay 应用 引入jar包 ```xml org.springframework.cloud spring-cloud-commons org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import ``` **注意** GateWay不需要使用web模块,它需要引入的是WebFlux application.yml 配置文件 ```yaml server: port: 9002 spring: application: name: service-gateway cloud: gateway: routes: # 路由可以有多个 - id: service-user-router # ⾃定义的路由ID,保持唯一 uri: lb://service-user # 动态路由配置 predicates: # 断言 - Path=/api/user/** filters: # 过滤器 - StripPrefix=2 # 会去掉 /api/user 后转发请求 - id: service-email-router uri: lb://service-email predicates: - Path=/api/email/** filters: - StripPrefix=2 - id: service-code-router uri: lb://service-code predicates: - Path=/api/code/** filters: - StripPrefix=2 # 注册中心客户端配置 eureka: client: service-url: defaultZone: http://EurekaServerA:8761/eureka,http://EurekaServerB:8762/eureka instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ ``` ###### Predicates 断言 Spring Cloud GateWay 帮我们内置了很多 Predicates 功能,实现了各种路由匹配规则(通过 Header、请求参数等作为条件)匹配到对应的路由 ![](img/20210803144129.png) **时间点后匹配 ** ```yaml spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] ``` **时间点前匹配 ** ```yaml spring: cloud: gateway: routes: - id: before_route uri: https://example.org predicates: - Before=2017-01-20T17:42:47.789-07:00[America/Denver] ``` **时间区间匹配 ** ```yaml spring: cloud: gateway: routes: - id: between_route uri: https://example.org predicates: - Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] ``` **Cookie正则匹配 ** ```yaml spring: cloud: gateway: routes: - id: cookie_route uri: https://example.org predicates: - Cookie=chocolate, ch.p ``` **Header正则匹配 ** ```yaml spring: cloud: gateway: routes: - id: header_route uri: https://example.org predicates: - Header=X-Request-Id, \d+ ``` **Host匹配 ** ```yaml spring: cloud: gateway: routes: - id: host_route uri: https://example.org predicates: - Host=**.somehost.org,**.anotherhost.org ``` **请求Method匹配 ** ```yaml spring: cloud: gateway: routes: - id: method_route uri: https://example.org predicates: - Method=GET,POST ``` **请求路径正则匹配 ** ```yaml spring: cloud: gateway: routes: - id: path_route uri: https://example.org predicates: - Path=/red/{segment},/blue/{segment} ``` **请求包含某参数匹配** ```yaml spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=green ``` **请求包含某参数并且参数值匹配正则表达式 ** ```yaml spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=red, gree. ``` **远程地址匹配 ** ```yaml spring: cloud: gateway: routes: - id: remoteaddr_route uri: https://example.org predicates: - RemoteAddr=192.168.1.1/24 ``` ###### 动态路由 GateWay 支持自动从注册中心中获取服务列表并访问 1)pom.xml中添加注册中心客户端依赖(从注册中心获取服务列表) 2)动态路由配置 ,动态路由设置时, uri以 lb: //开头(lb代表从注册中心获取服务),后面是需要转发的服务名称 ![](img/20210803150056.png) ​ ###### 过滤器 从过滤器生命周期(影响时机点)的角度来说,主要有两个pre和post | 类型 | 作用 | | ---- | ------------------------------------------------------------ | | pre | 过滤器在请求被路由之前调用,我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等 | | post | 过滤器在路由到微服务以后执行,这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等 | 从过滤器类型的角度, Spring Cloud GateWay的过滤器分为 GateWayFilter 和 GlobalFilter 两种 | 类型 | 影响范围 | | ------------- | ------------------ | | GateWayFilter | 应用到单个路由上 | | GlobalFilter | 应用到所有的路由上 | **GlobalFilter 全局过滤器是使用比较多的过滤器 ** 使用过滤器实现对 token 的校验 ```java package com.h52mm.gateway.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.h52mm.gateway.feign.TokenFeignClient; import com.h52mm.model.ResultDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.List; @Component @RefreshScope public class LoginFilter implements GlobalFilter, Ordered { private Logger logger= LoggerFactory.getLogger(LoginFilter.class); @Autowired private TokenFeignClient tokenFeignClient; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { logger.info("----------登录拦击校验-------------"); logger.info("appname={}",appname); ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String path = request.getURI().getPath(); logger.info("path={}",path); //登录和注册方法不校验token if(path.contains("login")||path.contains("addUser") ||path.contains("getCode")||path.contains("getAppName")){ return chain.filter(exchange); } //获取header HttpHeaders headers = request.getHeaders(); List token = headers.get("token"); //token 为空禁止访问 if(token==null||token.size()==0){ JSONObject message =new JSONObject(); message.put("code","500"); message.put("msg","你没有权限访问该接口"); byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.OK); //指定编码,否则在浏览器中会中文乱码 response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } //校验token 的正确性 ResultDTO resultDTO = tokenFeignClient.checkToken(token.get(0)); if(!"200".equals(resultDTO.getCode())){ //校验失败 byte[] bits = JSON.toJSONString(resultDTO).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.OK); //指定编码,否则在浏览器中会中文乱码 response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } logger.info("token={}",token); logger.info("----------登录拦击校验-------------"); // 合法请求,放行,执⾏后续的过滤器 return chain.filter(exchange); } @Override public int getOrder() { //返回值表示当前过滤器的顺序(优先级),数值越⼩,优先级越⾼ return 0; } } ``` #### Spring Cloud Config 分布式配置中心 在程序开发中我们往往会遇到很多配置文件,在单体架构中这些配置文件改起来方便,但是如果在微服务架构下,会有很多工程,很多配置文件,我们改动配置文件需要重启所有的服务,这会变的非常繁琐,有时我们需要在运行期间动态调整配置信息,此时我们就需要对配置文件进行集中式管理。 Spring Cloud Config是一个分布式配置管理方案,包含了 Server端和 Client端两个部分 ![](img/20210803151953.png) - Server 端:提供配置文件的存储、以接口的形式将配置文件的内容提供出去,使用@EnableConfigServer注解 即可在 Spring boot 应用中嵌入 - Client 端 :通过接口获取配置数据并初始化应用 **Spring Cloud Config 默认使用Git 存储配置文件内容,也可以使用SVN** ##### Config 应用 1. 在git(gitlab,github,gitee等) 中新建仓库,用于存储配置文件 2. 将配置文件上传到git仓库中,命名规则一般如下 {application}-{profile}.yml 或者 {application}-{profile}.properties application为应用名称, profile指的是环境 (开发环境,测试环境,生产环境等) 3. pom.xml 中引入依赖坐标 ```yaml org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-config-server ``` 4. 配置启动类 ```java package com.h52mm.config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableDiscoveryClient @EnableConfigServer public class ServiceConfigApplication { public static void main(String[] args) { SpringApplication.run(ServiceConfigApplication.class,args); } } ``` 5. application.yml 文件配置 ```yaml server: port: 9006 spring: application: name: service-config cloud: config: server: git: uri: git仓库地址 username: 用户名 password: 密码 search-paths: - config-test # 配置文件的名称 label: master #读取的分支 # 注册中心客户端配置 eureka: client: service-url: defaultZone: http://EurekaServerA:8761/eureka,http://EurekaServerB:8762/eureka instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # springboot中暴露健康检查等断点接⼝ management: endpoints: web: exposure: include: "*" # 暴露健康接⼝的细节 endpoint: health: show-details: always ``` 6. 测试访问: http://localhost:9006/master/config-test.yml,查看到配置文件内容 7. 客户端引入jar包依赖 ```xml org.springframework.cloud spring-cloud-config-client ``` 8. 在bootstrap.yml 配置文件中配置 ```yaml spring: cloud: config: name: config profile: test label: master uri: http://localhost:9006 ``` **bootstrap.yml是系统级别的,优先级比application.yml高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启用 **