简单的 微服务netflix 学习
一.Eureka 学习
1. 服务简单搭建
1.1 首先确定pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>eureka-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--为服务注册中心引入 Eureka Server 的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!--devtools 和 lombok 均为开发辅助模块,根据需求适当选择--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
1.2 设置服务对应的application.yml文件
server: port: 7001 #该 Module 的端口号 eureka: instance: #eureka服务端的实例名称 hostname: 3.2.26.244 client: # false表示不向注册中心注册自己。 register-with-eureka: false # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 fetch-registry: false service-url: # 集群版本 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/ #将这个 Eureka Server 注册到 7001 和 7003 上 # 单机版服务注册中心 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
1.3 编写服务启动类
package cn.git.netflix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @description: eurekaServer服务注册中心 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-13 11:15:51 */ @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
1.4 启动服务,登录首页观察
服务启动一段时间后发现有红字产生
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
中文含义为: 紧急!Eureka已经不能确认这些已经启动的实例是否可用,由于最近的续订次数小于续订阈值(续订期望值),为了安全起见(实例可用),当前这些实例不会删除。
出现上面这种情况,其实是Eureka进入自我保护模式(
SELF PRESERVATION MODE
)。自我保护机制: 我们知道,当微服务客户端启动后,会把自身信息注册到Eureka注册中心,以供其他微服务进行调用。一般情况下,当某个服务不可用时(一段时间内没有检测到心跳或者连接超时等),那么Eureka注册中心就会将该服务从可用服务列表中剔除,但是在微服务架构中,因为服务数量众多,可能存在跨机房或者跨区域的情况,因此当某个服务心跳探测失败并不能完全说明其无法正常提供服务而将其剔除,并且服务一旦剔除后,再重新注册将会重新进行负载均衡等等一系列的操作,考虑到性能问题,eureka会将不可用的服务暂时断开,并期望能够在接下来一段时间内接收到心跳信号,而不是直接剔除,同时,新来的请求将不会分发给暂停服务的实例,这就是eureka的保护机制,它保护了因网络等问题造成的短暂的服务不可用的实例,避免频繁注册服务对整个系统造成影响。
进入条件:如果最近一分钟实际接收到的心跳值Renews除以期望的心跳阈值 Renews threshold小于等于0.85,即 Renews/Renews threshold≤0.85
Renews threshold:Eureka注册中心期望接收到的心跳值,其中0.85是默认值eureka.server.renewalPercentThreshold=0.85
计算方法:
- 服务注册中心不注册自己: (1+2*n)
- 服务注册中心注册自己: (
1+2*(n+1)
)参数含义:数字1 这个是Eureka框架代码里面写时的最低阈值1,意味着没有任何客户端注册,阈值也有1。
数字2 这个是最近一分钟接收到的心跳次数,Eureka客户端和注册中心心跳检测默认是30s一次,那么一分钟就是2次,通过下面的参数进行调整
定义每分钟发送到服务器的更新数 default is 30 也就是每30秒续订一次
eureka instance.leaseRenewalIntervalInSeconds=30
解决方式:
调低0.85默认值 :eureka.server.renewalPercentThreshold=0.45
关闭保护模式 :eureka.server.enable-self-preservation=false,关闭会出现以下信息,默认是开启的 不建议修改。
THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.
翻译:自我保护模式已关闭。 在网络/其他问题的情况下,这可能无法保护实例到期,意思就是可能提供不可用的服务给客户端调用
二.Ribbon spring cloud 负载均衡组件
1.ribbon简介
Netflix Ribbon 是 Netflix 公司发布的开源组件,其主要功能是提供客户端的负载均衡算法和服务调用。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Feign 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Ribbon。
Ribbon 是 Spring Cloud Netflix 模块的子模块,它是 Spring Cloud 对 Netflix Ribbon 的二次封装。通过它,我们可以将面向服务的 REST 模板(RestTemplate)请求转换为客户端负载均衡的服务调用。
Ribbon 是 Spring Cloud 体系中最核心、最重要的组件之一。它虽然只是一个工具类型的框架,并不像 Eureka Server(服务注册中心)那样需要独立部署,但它几乎存在于每一个使用 Spring Cloud 构建的微服务中。
Spring Cloud 微服务之间的调用,API 网关的请求转发等内容,实际上都是通过 Spring Cloud Ribbon 来实现的,包括后续我们要介绍的 OpenFeign 也是基于它实现的。
2.负载均衡
在任何一个系统中,负载均衡都是一个十分重要且不得不去实施的内容,它是系统处理高并发、缓解网络压力和服务端扩容的重要手段之一。
负载均衡(Load Balance) ,简单点说就是将用户的请求平摊分配到多个服务器上运行,以达到扩展服务器带宽、增强数据处理能力、增加吞吐量、提高网络的可用性和灵活性的目的。
负载均衡分为 客户端以及服务端 如下图:
当客户端发送请求时,该请求不会直接发送到服务端进行处理,而是全部交给负载均衡服务器,由负载均衡服务器按照某种算法(例如轮询、随机等),从其维护的可用服务清单中选择一个服务端,然后进行转发。
服务端负载均衡具有以下特点:
- 需要建立一个独立的负载均衡服务器。
- 负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。
- 可用服务端清单存储在负载均衡服务器上。
客户端负载均衡的工作原理如下图。
客户端负载均衡是将负载均衡逻辑以代码的形式封装到客户端上,即负载均衡器位于客户端。客户端通过服务注册中心(例如 Eureka Server)获取到一份服务端提供的可用服务清单。有了服务清单后,负载均衡器会在客户端发送请求前通过负载均衡算法选择一个服务端实例再进行访问,以达到负载均衡的目的;
客户端负载均衡也需要心跳机制去维护服务端清单的有效性,这个过程需要配合服务注册中心一起完成。
客户端负载均衡具有以下特点:
- 负载均衡器位于客户端,不需要单独搭建一个负载均衡服务器。
- 负载均衡是在客户端发送请求前进行的,因此客户端清楚地知道是哪个服务端提供的服务。
- 客户端都维护了一份可用服务清单,而这份清单都是从服务注册中心获取的。
Ribbon 就是一个基于 HTTP 和 TCP 的客户端负载均衡器,当我们将 Ribbon 和 Eureka 一起使用时,Ribbon 会从 Eureka Server(服务注册中心)中获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
服务端负载均衡 VS 客户端负载均衡
不同点 服务端负载均衡 客户端负载均衡 是否需要建立负载均衡服务器 需要在客户端和服务端之间建立一个独立的负载均衡服务器。 将负载均衡的逻辑以代码的形式封装到客户端上,因此不需要单独建立负载均衡服务器。 是否需要服务注册中心 不需要服务注册中心。 需要服务注册中心。
在客户端负载均衡中,所有的客户端和服务端都需要将其提供的服务注册到服务注册中心上。可用服务清单存储的位置 可用服务清单存储在位于客户端与服务器之间的负载均衡服务器上。 所有的客户端都维护了一份可用服务清单,这些清单都是从服务注册中心获取的。 负载均衡的时机 先将请求发送到负载均衡服务器,然后由负载均衡服务器通过负载均衡算法,在多个服务端之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。 简单点说就是,先发送请求,再进行负载均衡。 在发送请求前,由位于客户端的服务负载均衡器(例如 Ribbon)通过负载均衡算法选择一个服务器,然后进行访问。 客户端是否了解服务提供方信息 由于负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。 负载均衡是在客户端发送请求前进行的,因此客户端清楚的知道是哪个服务端提供的服务。
3.具体的代码实现
3.1 ribbon 可以与restTemplate结合使用
Ribbon 可以与 RestTemplate(Rest 模板)配合使用,以实现微服务之间的调用。
RestTemplate 是 Spring 家族中的一个用于消费第三方 REST 服务的请求框架。RestTemplate 实现了对 HTTP 请求的封装,提供了一套模板化的服务调用方法。通过它,Spring 应用可以很方便地对各种类型的 HTTP 请求进行访问。
RestTemplate 针对各种类型的 HTTP 请求都提供了相应的方法进行处理,例如 HEAD、GET、POST、PUT、DELETE 等类型的 HTTP 请求,分别对应 RestTemplate 中的 headForHeaders()、getForObject()、postForObject()、put() 以及 delete() 方法。
下面我们通过一个简单的实例,来演示 Ribbon 是如何实现服务调用的。
3.2 新增pom文件,引入需要jar信息
<dependencies> <!--公共子模块--> <dependency> <groupId>net.biancheng.c</groupId> <artifactId>micro-service-cloud-api</artifactId> <version>${project.version}</version> </dependency> <!--Spring Boot Web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--Spring Boot 测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--Spring Cloud Eureka 客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--Spring Cloud Ribbon 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
3.3 新增loadBalance 配置类
// 配置类注解 @Configuration public class ConfigBean { @Bean //将 RestTemplate 注入到容器中 @LoadBalanced //在客户端使用 RestTemplate 请求服务端时,开启负载均衡(Ribbon) public RestTemplate getRestTemplate() { return new RestTemplate(); } }
3.4 新增controller方法,注入RestTemplate类
@RestController public class TestRestTemplate { //面向微服务编程,即通过微服务的名称来获取调用地址 // 使用注册到 Spring Cloud Eureka 服务注册中心中的服务,即 application.name private static final String EUREKA_PRODUCER = "http://EUREKA-PRODUCER"; //RestTemplate是一种简单便捷的访问restful服务模板类,是 Spring 提供的用于访问 Rest 服务的客户端模板工具集,提供了多种便捷访问远程HTTP服务的方法 @Autowired private RestTemplate restTemplate; @RequestMapping(value = "/test/dept/get/{id}") public Dept get(@PathVariable("id") Integer id) { return restTemplate.getForObject(EUREKA_PRODUCER + "/test/get/" + id, Dept.class); } @RequestMapping(value = "/test/list") public List<Dept> list() { return restTemplate.getForObject(EUREKA_PRODUCER + "/test/list", List.class); } }
3.5 新增启动类
@SpringBootApplication @EnableEurekaClient public class EurekaProducer { public static void main(String[] args) { SpringApplication.run(EurekaProducer.class, args); } }
3.6 配置多个服务的application.yml文件
server: # 端口号修改为 8001 port: 8001 spring: application: # 微服务名称,不做修改,与 eureka-producer 的配置保持一致 name: eureka-producer eureka: # 将客户端注册到 eureka 服务列表内 client: service-url: #将服务注册到 Eureka 集群 defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/ instance: # 自定义服务名称信息,三个服务节点,端口由8001~8003 instance-id: eureka-producer-8001 # 显示访问路径的 ip 地址 prefer-ip-address: true
之后便可以访问接口,实现负载调用
4. Ribbon轮训策略
序号 | 实现类 | 负载均衡策略 |
---|---|---|
1 | RoundRobinRule | 按照线性轮询策略,即按照一定的顺序依次选取服务实例 |
2 | RandomRule | 随机选取一个服务实例 |
3 | RetryRule | 按照 RoundRobinRule(轮询)的策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试(重试时获取服务的策略还是 RoundRobinRule 中定义的策略),如果超过指定时间依然没获取到服务实例则返回 null 。 |
4 | WeightedResponseTimeRule | WeightedResponseTimeRule 是 RoundRobinRule 的一个子类,它对 RoundRobinRule 的功能进行了扩展。 根据平均响应时间,来计算所有服务实例的权重,响应时间越短的服务实例权重越高,被选中的概率越大。刚启动时,如果统计信息不足,则使用线性轮询策略,等信息足够时,再切换到 WeightedResponseTimeRule。 |
5 | BestAvailableRule | 继承自 ClientConfigEnabledRoundRobinRule。先过滤点故障或失效的服务实例,然后再选择并发量最小的服务实例。 |
6 | AvailabilityFilteringRule | 先过滤掉故障或失效的服务实例,然后再选择并发量较小的服务实例。 |
7 | ZoneAvoidanceRule | 默认的负载均衡策略,综合判断服务所在区域(zone)的性能和服务(server)的可用性,来选择服务实例。在没有区域的环境下,该策略与轮询(RandomRule)策略类似。 |
4.1 如何切换负载策略
只需要声明一个轮训策略类,注入到spring环境中
@Bean public IRule myRule() { // RandomRule 为随机策略 return new RandomRule(); }
4.2 如何自定义轮训策略
- 编写自定义策略类
/** * 定制 Ribbon 负载均衡策略 */ public class MyRandomRule extends AbstractLoadBalancerRule { // 总共被调用的次数,目前要求每台被调用5次 private int total = 0; // 当前提供服务的机器号 private int currentIndex = 0; /** * 策略具体实现 */ public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; //获取所有有效的服务实例列表 List<Server> upList = lb.getReachableServers(); //获取所有的服务实例的列表 List<Server> allList = lb.getAllServers(); // 具体逻辑省略。。。。。。 return server; } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { // TODO Auto-generated method stub } }
- 将自定义轮训类注入到spring容器之中
/** * 定制 Ribbon 负载均衡策略的配置类 * 否则所有的 Ribbon 客户端都会采用该策略,无法达到特殊化定制的目的 */ @Configuration public class MySelfRibbonRuleConfig { @Bean public IRule myRule() { // 自定义 Ribbon 负载均衡策略 return new MyRandomRule(); } }
- 指定哪些服务使用自定义负载策略
@SpringBootApplication @EnableEurekaClient //自定义 Ribbon 负载均衡策略在主启动类上使用 RibbonClient 注解,在该微服务启动时,就能自动去加载我们自定义的 Ribbon 配置类,从而是配置生效 // name 为需要定制负载均衡策略的微服务名称(application name) // configuration 为定制的负载均衡策略的配置类, // 且官方文档中明确提出,该配置类不能在 ComponentScan 注解(SpringBootApplication 注解中包含了该注解)下的包或其子包中 @RibbonClient(name = "eureka-producer...", configuration = MySelfRibbonRuleConfig.class) public class EurkeaProducer.... { public static void main(String[] args) { SpringApplication.run(MicroServiceCloudConsumerDept80Application.class, args); } }
三.OpenFeign Spring Cloud声明式服务调用组件
1.Feign简介
Netflix Feign 是 Netflix 公司发布的一种实现负载均衡和服务调用的开源组件。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Ribbon 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Feign。
Feign 对 Ribbon 进行了集成,利用 Ribbon 维护了一份可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。
Feign 是一种声明式服务调用组件,它在 RestTemplate 的基础上做了进一步的封装。通过 Feign,我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。
通过 Feign,我们可以像调用本地方法一样来调用远程服务,而完全感觉不到这是在进行远程调用。
Feign 支持多种注解,例如 Feign 自带的注解以及 JAX-RS 注解等,但遗憾的是 Feign 本身并不支持 Spring MVC 注解,这无疑会给广大 Spring 用户带来不便。
2019 年 Netflix 公司宣布 Feign 组件正式进入停更维护状态,于是 Spring 官方便推出了一个名为 OpenFeign 的组件作为 Feign 的替代方案。
2.OpenFeign 由来
OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。
OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。
OpenFeign 常用注解
使用 OpenFegin 进行远程服务调用时,常用注解如下表。
注解 说明 @FeignClient 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。 @EnableFeignClients 该注解用于开启 OpenFeign 功能,当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。 @RequestMapping Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。 @GetMapping Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。 @PostMapping Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。 Spring Cloud Finchley 及以上版本一般使用 OpenFeign 作为其服务调用组件。由于 OpenFeign 是在 2019 年 Feign 停更进入维护后推出的,因此大多数2019 年及以后的新项目使用的都是 OpenFeign,而 2018 年以前的项目一般使用 Feign。
3.Feign 与 OpenFeign 异同点
下面我们就来对比下 Feign 和 OpenFeign 的异同。
相同点
Feign 和 OpenFegin 具有以下相同点:
- Feign 和 OpenFeign 都是 Spring Cloud 下的远程调用和负载均衡组件。
- Feign 和 OpenFeign 作用一样,都可以实现服务的远程调用和负载均衡。
- Feign 和 OpenFeign 都对 Ribbon 进行了集成,都利用 Ribbon 维护了可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。
- Feign 和 OpenFeign 都是在服务消费者(客户端)定义服务绑定接口并通过注解的方式进行配置,以实现远程服务的调用。
不同点
Feign 和 OpenFeign 具有以下不同:
- Feign 和 OpenFeign 的依赖项不同,Feign 的依赖为 spring-cloud-starter-feign,而 OpenFeign 的依赖为 spring-cloud-starter-openfeign。
- Feign 和 OpenFeign 支持的注解不同,Feign 支持 Feign 注解和 JAX-RS 注解,但不支持 Spring MVC 注解;OpenFeign 除了支持 Feign 注解和 JAX-RS 注解外,还支持 Spring MVC 注解。
4.具体实现步骤
4.1在pom中加入需要使用的jar包
<dependencies> <dependency> <groupId>net.biancheng.c</groupId> <artifactId>micro-service-cloud-api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--Eureka Client 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- Ribbon 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <!--添加 OpenFeign 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
4.2 添加application.yml文件
server: port: 80 eureka: client: # 服务消费者可以不向服务注册中心注册服务 register-with-eureka: false service-url: defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/ # 服务消费者客户端需要去检索服务,获取在线服务列表信息 fetch-registry: true
4.3 编写调用类
//添加为容器内的一个组件 @Component // 服务提供者提供的服务名称,即 application.name @FeignClient(value = "EurekaProducer....") public interface DeptFeignService { // 调用对应服务提供者(8001、8002、8003)Controller 中定义的方法 @RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET) public Dept get(@PathVariable("id") int id); @RequestMapping(value = "/dept/list", method = RequestMethod.GET) public List<Dept> list(); }
在编写服务绑定接口时,需要注意以下 2 点:
- 在 @FeignClient 注解中,value 属性的取值为:服务提供者的服务名,即服务提供者配置文件(application.yml)中 spring.application.name 的取值。
- 接口中定义的每个方法都与服务提供者(即 eurekaProducer 等)中 Controller 定义的服务方法对应。
4.4 最后在服务启动类上添加启动注解即可
package net.biancheng.c; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients //开启 OpenFeign 功能 public class EurekaProducer.... { public static void main(String[] args) { SpringApplication.run(EurekaProducer.class, args); } }
4.5 超时设置
ribbon: # 建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间 ReadTimeout: 6000 # 建立连接后,服务器读取到可用资源的时间 ConnectionTimeout: 6000
4.6 日志增强
OpenFeign 提供了日志打印功能,我们可以通过配置调整日志级别,来了解请求的细节。
Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,通过它可以对 OpenFeign 服务绑定接口的调用情况进行监控。
logging: level: # feign 日志以什么样的级别监控该接口 feignn接口包路径e(cn.git.workflow....service): debug
以上配置说明如下:
- cn.git.workflow…service接口是开启 @FeignClient 注解的接口(即服务绑定接口)的完整类名。
- 也可以只配置部分路径,表示监控该路径下的所有服务绑定接口
- debug:表示监听该接口的日志级别。
4.6.1 编写日志配置类
@Configuration public class ConfigBean { /** * OpenFeign 日志增强 * 配置 OpenFeign 记录哪些内容 */ @Bean Logger.Level feginLoggerLevel() { return Logger.Level.FULL; } }
该配置的作用是通过配置的 Logger.Level 对象告诉 OpenFeign 记录哪些日志内容。
Logger.Level 的具体级别如下:
- NONE:不记录任何信息。
- BASIC:仅记录请求方法、URL 以及响应状态码和执行时间。
- HEADERS:除了记录 BASIC 级别的信息外,还会记录请求和响应的头信息。
- FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等等。
最终日志格式如下:
2021-10-12 14:33:07.408 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] ---> GET http://MICROSERVICECLOUDPROVIDERDEPT/dept/list HTTP/1.1 2021-10-12 14:33:07.408 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] ---> END HTTP (0-byte body) 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] <--- HTTP/1.1 200 (574ms) 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] connection: keep-alive 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] content-type: application/json 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] date: Tue, 12 Oct 2021 06:33:07 GMT 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] keep-alive: timeout=60 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] transfer-encoding: chunked 2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] 2021-10-12 14:33:07.991 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] [{"deptNo":1,"deptName":"开发部","dbSource":"bianchengbang_jdbc"},{"deptNo":2,"deptName":"人事部","dbSource":"bianchengbang_jdbc"},{"deptNo":3,"deptName":"财务部","dbSource":"bianchengbang_jdbc"},{"deptNo":4,"deptName":"市场部","dbSource":"bianchengbang_jdbc"},{"deptNo":5,"deptName":"运维部","dbSource":"bianchengbang_jdbc"}] 2021-10-12 14:33:07.991 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] <--- END HTTP (341-byte body)
四.Hystrix Spring Cloud服务熔断与降级组件
1. 基础概念
在微服务架构中,一个应用往往由多个服务组成,这些服务之间相互依赖,依赖关系错综复杂。
例如一个微服务系统中存在 A、B、C、D、E、F 等多个服务,它们的依赖关系如下图。
图1:服务依赖关系
通常情况下,一个用户请求往往需要多个服务配合才能完成。如图 1 所示,在所有服务都处于可用状态时,请求 1 需要调用 A、D、E、F 四个服务才能完成,请求 2 需要调用 B、E、D 三个服务才能完成,请求 3 需要调用服务 C、F、E、D 四个服务才能完成。
当服务 E 发生故障或网络延迟时,会出现以下情况:
- 即使其他所有服务都可用,由于服务 E 的不可用,那么用户请求 1、2、3 都会处于阻塞状态,等待服务 E 的响应。在高并发的场景下,会导致整个服务器的线程资源在短时间内迅速消耗殆尽。
- 所有依赖于服务 E 的其他服务,例如服务 B、D 以及 F 也都会处于线程阻塞状态,等待服务 E 的响应,导致这些服务的不可用。
- 所有依赖服务B、D 和 F 的服务,例如服务 A 和服务 C 也会处于线程阻塞状态,以等待服务 D 和服务 F 的响应,导致服务 A 和服务 C 也不可用。
从以上过程可以看出,当微服务系统的一个服务出现故障时,故障会沿着服务的调用链路在系统中疯狂蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。为了防止此类事件的发生,微服务架构引入了“熔断器”的一系列服务容错和保护机制。
2.熔断器
熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。
在微服务领域,熔断器最早是由 Martin Fowler 在他发表的 《Circuit Breaker》一文中提出。与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。
3.Spring Cloud Hystrix 简介
Spring Cloud Hystrix 是一款优秀的服务容错与保护组件,也是 Spring Cloud 中最重要的组件之一。
Spring Cloud Hystrix 是基于 Netflix 公司的开源组件 Hystrix 实现的,它提供了熔断器功能,能够有效地阻止分布式微服务系统中出现联动故障,以提高微服务系统的弹性。Spring Cloud Hystrix 具有服务降级、服务熔断、线程隔离、请求缓存、请求合并以及实时故障监控等强大功能。
Hystrix [hɪst’rɪks],中文含义是豪猪,豪猪的背上长满了棘刺,使它拥有了强大的自我保护能力。而 Spring Cloud Hystrix 作为一个服务容错与保护组件,也可以让服务拥有自我保护的能力,因此也有人将其戏称为“豪猪哥”。
在微服务系统中,Hystrix 能够帮助我们实现以下目标:
- 保护线程资源:防止单个服务的故障耗尽系统中的所有线程资源。
- 快速失败机制:当某个服务发生了故障,不让服务调用方一直等待,而是直接返回请求失败。
- 提供降级(FallBack)方案:在请求失败后,提供一个设计好的降级方案,通常是一个兜底方法,当请求失败后即调用该方法。
- 防止故障扩散:使用熔断机制,防止故障扩散到其他服务。
- 监控功能:提供熔断器故障监控组件 Hystrix Dashboard,随时监控熔断器的状态。
4. Hystrix 服务降级
Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。
服务降级的使用场景有以下 2 种:
- 在服务器压力剧增时,根据实际业务情况及流量,对一些不重要、不紧急的服务进行有策略地不处理或简单处理,从而释放服务器资源以保证核心服务正常运作。
- 当某些服务不可用时,为了避免长时间等待造成服务卡顿或雪崩效应,而主动执行备用的降级逻辑立刻返回一个友好的提示,以保障主体业务不受影响。
我们可以通过重写 HystrixCommand 的 getFallBack() 方法或 HystrixObservableCommand 的 resumeWithFallback() 方法,使服务支持服务降级。
Hystrix 服务降级 FallBack 既可以放在服务端进行,也可以放在客户端进行。
Hystrix 会在以下场景下进行服务降级处理:
- 程序运行异常
- 服务超时
- 熔断器处于打开状态
- 线程池资源耗尽
5.服务端降级具体实现
5.1 新增pom文件,引入jar包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>hystrix-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--添加 Spring Boot 的监控模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- eureka 客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--hystrix 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
5.2 编写yml文件
spring: application: #微服务名称,对外暴漏的微服务名称,十分重要 name: hystrix-server server: port: 8004 eureka: client: #将客户端注册到 eureka 服务列表内 service-url: # 将服务注册到 Eureka 集群 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: instance-id: hystrix-server-8004 #自定义服务名称信息 # 显示访问路径的 ip 地址 prefer-ip-address: true # Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点 management: endpoints: web: exposure: include: "*" # * 在yaml 文件属于关键字,所以需要加引号 info: app.name: hystrix-server company.name: lixuchun build.aetifactId: @project.artifactId@ build.version: @project.version@
5.3 编写service以及controller以及启动类行调用
/** * @description: Hytrix服务启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 08:48:26 */ @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class HystrixApplication { public static void main(String[] args) { SpringApplication.run(HystrixApplication.class, args); } }
package cn.git.hystrix.service.impl; import cn.git.hystrix.service.HystrixService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.stereotype.Service; /** * @description: 服务端熔断测试service实现 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 09:07:29 */ @Service public class HystrixServiceImpl implements HystrixService { /** * 正常方法 * @param id 标识id * @return 线程信息 */ @Override public String normalOk(String id) { return "线程:" + Thread.currentThread().getName() + ",正常方法调用,id:" + id; } /** * 超时方法,熔断方法 * @param id 线程标识id * @return 线程信息 */ @HystrixCommand(fallbackMethod = "timeOutFallbackHandler", commandProperties = {@HystrixProperty( // 规定 5 秒钟以内就不报错,正常运行,超过 5 秒就报错,调用指定的方法 name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") } ) @Override public String timeOut(String id) { try { // 设定超时6S Thread.sleep(6000); } catch (Exception e) { e.printStackTrace(); } return "线程:" + Thread.currentThread().getName() + ",超时方法,熔断方法,id:" + id; } /** * 超时执行方法 * @param id 标识id * @return 超时提示信息 */ public String timeOutFallbackHandler(String id) { return "5秒超时,系统繁忙哦!" + Thread.currentThread().getName() + ",id:" + id; } }
package cn.git.hystrix.controller; import cn.git.hystrix.service.HystrixService; import com.netflix.discovery.converters.Auto; import org.springframework.beans.factory.annotation.Autowired; 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; /** * @description: 熔断测试controller * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 09:15:05 */ @RestController @RequestMapping(value = "/hystrix") public class HystrixController { @Autowired private HystrixService hystrixService; /** * 正常方法校验 * @param id */ @GetMapping("/normal/ok/{id}") public String normalOk(@PathVariable(value = "id") String id) { return hystrixService.normalOk(id); } /** * 超时熔断校验 * @param id */ @GetMapping("/time/out/{id}") public String timeOut(@PathVariable(value = "id") String id) { return hystrixService.timeOut(id); } }
5.4 使用postman进行测试
由上两图所示,限流功能已经生效
6.客户端服务降级具体实现
6.1 新增client服务,新增pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>hystrix-client</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--添加 Spring Boot 的监控模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- eureka 客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--hystrix 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--添加 OpenFeign 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
6.2 新增服务yml文件信息
spring: application: #微服务名称,对外暴漏的微服务名称,十分重要 name: hystrix-client server: port: 8005 eureka: client: #将客户端注册到 eureka 服务列表内 service-url: # 将服务注册到 Eureka 集群 defaultZone: http://ip1:7001/eureka/,http://ip2:7002/eureka/,http://ip3:7003/eureka/ # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: instance-id: hystrix-client-8005 #自定义服务名称信息 # 显示访问路径的 ip 地址 prefer-ip-address: true # 开启客户端 hystrix feign: hystrix: enabled: true ribbon: # 建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间 ReadTimeout: 6000 # 建立连接后,服务器读取到可用资源的时间 ConnectionTimeout: 6000 hystrix: command: default: execution: isolation: thread: # 配置请求超时时间(全局) timeoutInMilliseconds: 7000 # 此处可以配置单独特定方法,此处设置为: HystrixClientService.timeOut(String id)方法 HystrixClientService#timeOut(String): execution: isolation: thread: # 超时时间设定为3秒钟 timeoutInMilliseconds: 3000 # Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点 management: endpoints: web: exposure: # * 在yaml 文件属于关键字,所以需要加引号 include: "*" info: app.name: hystrix-client company.name: lixuchun build.aetifactId: @project.artifactId@ build.version: @project.version@
6.3 新增controller以及service以及服务启动类信息
package cn.git.hystrix.controller; import cn.git.hystrix.service.HystrixClientService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; 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; /** * @description: hystrix 客户端调用controller * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 10:00:23 */ @RestController @RequestMapping("/hystrix/client") public class HystrixClientController { @Autowired private HystrixClientService hystrixClientService; /** * 正常方法 * @param id * @return */ @GetMapping(value = "/normal/ok/{id}") public String normalOk(@PathVariable(value = "id") String id) { return hystrixClientService.normalOk(id); } /** * 超时方法hystrix 客户端调用 * @param id * @return */ @HystrixCommand(fallbackMethod = "hystrixClientTimeOutHandler") @GetMapping(value = "/time/out/{id}") public String timeOut(@PathVariable(value = "id") String id) { return hystrixClientService.timeOut(id); } /** * 失败调用方法 * @return */ public String hystrixClientTimeOutHandler(@PathVariable(value = "id") String id) { return "hystrix client 客户端 调用超时出错啦! + " + id; } }
package cn.git.hystrix.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @description: 客户端hystrix service * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 09:55:54 */ @Component @FeignClient(value = "HYSTRIX-SERVER") public interface HystrixClientService { /** * feign hystrix ok 测试接口 * @param id * @return */ @GetMapping(value = "/hystrix/normal/ok/{id}") String normalOk(@PathVariable(value = "id") String id); /** * feign hystrix timeOut 测试接口 * @param id * @return */ @GetMapping(value = "/hystrix/time/out/{id}") String timeOut(@PathVariable(value = "id") String id); }
/** * @description: Hystrix客户端服务启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 09:49:05 */ @SpringBootApplication @EnableFeignClients @EnableHystrix public class HystrixClientApplication { public static void main(String[] args) { SpringApplication.run(HystrixClientApplication.class, args); } }
6.4 启动服务进行测试
由上两图可以查看,当前客户端服务也已经进行了限流。
在配置文件中设计请求的超时时间时,需要注意以下 2 点:
1)Hystrix 可以来为所有请求(方法)设置超时时间(单位为毫秒),若请求超时则触发全局的回退方法进行处理。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=mmm
2)Hystrix 还可以为某个特定的服务请求(方法)设置超时时间,格式如下:
hystrix.command.xxx#yyy(zzz).execution.isolation.thread.timeoutInMilliseconds=mmm
格式说明如下:
- xxx:为包含该服务方法的类的名称(通常为服务绑定接口的名称),例如 HystrixClientService接口。
- yyy:服务方法名,例如 timeOut() 方法。
- zzz:方法内的参数类型,例如 Integer、String 等等
- mmm:要设置的超时时间,单位为毫秒(1 秒 =1000 毫秒)
6.5 全局降级方法
在controller服务上面加上注释 @DefaultProperties(defaultFallback = “globalFallbackHandlerMethod”),在类中加入对应方法即可
package cn.git.hystrix.controller; import cn.git.hystrix.service.HystrixClientService; import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; 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; /** * @description: hystrix 客户端调用controller * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 10:00:23 */ @DefaultProperties(defaultFallback = "globalFallbackHandlerMethod") @RestController @RequestMapping("/hystrix/client") public class HystrixClientController { @Autowired private HystrixClientService hystrixClientService; /** * 正常方法 * @param id * @return */ @GetMapping(value = "/normal/ok/{id}") public String normalOk(@PathVariable(value = "id") String id) { return hystrixClientService.normalOk(id); } /** * 超时方法hystrix 客户端调用 * @param id * @return */ @HystrixCommand//(fallbackMethod = "hystrixClientTimeOutHandler") @GetMapping(value = "/time/out/{id}") public String timeOut(@PathVariable(value = "id") String id) { return hystrixClientService.timeOut(id); } /** * 失败调用方法 * @return */ public String hystrixClientTimeOutHandler(@PathVariable(value = "id") String id) { return "hystrix client 客户端 调用超时出错啦! + " + id; } /** * 全局的 fallback 方法, * 回退方法必须和 hystrix 的执行方法在相同类中 * DefaultProperties(defaultFallback = "globalFallbackHandlerMethod") 类上注解,请求方法上使用 @HystrixCommand 注解 */ public String globalFallbackHandlerMethod() { return "全局降级方法执行啦~~~~"; } }
调用测试
6.6 解耦降级逻辑
去除controller中的所有Hystrix的注解,只需要在调用service方法中加入 HystrixClientServiceFallback方法即可
package cn.git.hystrix.service; import cn.git.hystrix.fallback.HystrixClientServiceFallback; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @description: 客户端hystrix service * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 09:55:54 */ @Component @FeignClient(value = "HYSTRIX-SERVER", fallback = HystrixClientServiceFallback.class) public interface HystrixClientService { /** * feign hystrix ok 测试接口 * @param id * @return */ @GetMapping(value = "/hystrix/normal/ok/{id}") String normalOk(@PathVariable(value = "id") String id); /** * feign hystrix timeOut 测试接口 * @param id * @return */ @GetMapping(value = "/hystrix/time/out/{id}") String timeOut(@PathVariable(value = "id") String id); }
fallback方法具体实现
package cn.git.hystrix.fallback; import cn.git.hystrix.service.HystrixClientService; import org.springframework.stereotype.Component; /** * @description: 熔断测试类回退方法 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 11:04:50 */ @Component public class HystrixClientServiceFallback implements HystrixClientService { /** * feign hystrix ok 测试接口 * * @param id * @return */ @Override public String normalOk(String id) { return "feign hystrix ok 测试接口 HystrixClientServiceFallback 失败了"; } /** * feign hystrix timeOut 测试接口 * * @param id * @return */ @Override public String timeOut(String id) { return "feign hystrix timeOut 测试接口 HystrixClientServiceFallback 失败了"; } }
调用测试结果
7 Hystrix 服务熔断
7.1 概念介绍
熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制。
当微服务系统中的某个微服务不可用或响应时间太长时,为了保护系统的整体可用性,熔断器会暂时切断请求对该服务的调用,并快速返回一个友好的错误响应。这种熔断状态不是永久的,在经历了一定的时间后,熔断器会再次检测该微服务是否恢复正常,若服务恢复正常则恢复其调用链路。
在熔断机制中涉及了三种熔断状态:
- 熔断关闭状态(Closed):当服务访问正常时,熔断器处于关闭状态,服务调用方可以正常地对服务进行调用。
- 熔断开启状态(Open):默认情况下,在固定时间内接口调用出错比率达到一个阈值(例如 50%),熔断器会进入熔断开启状态。进入熔断状态后,后续对该服务的调用都会被切断,熔断器会执行本地的降级(FallBack)方法。
- 半熔断状态(Half-Open): 在熔断开启一段时间之后,熔断器会进入半熔断状态。在半熔断状态下,熔断器会尝试恢复服务调用方对服务的调用,允许部分请求调用该服务,并监控其调用成功率。如果成功率达到预期,则说明服务已恢复正常,熔断器进入关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。
三种熔断状态之间的转化关系如下图:
7.2 Hystrix 实现熔断机制
在 Spring Cloud 中,熔断机制是通过 Hystrix 实现的。Hystrix 会监控微服务间调用的状况,当失败调用到一定比例时(例如 5 秒内失败 20 次),就会启动熔断机制。
Hystrix 实现服务熔断的步骤如下:
- 当服务的调用出错率达到或超过 Hystix 规定的比率(默认为 50%)后,熔断器进入熔断开启状态。
- 熔断器进入熔断开启状态后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,该服务的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
- 当有请求再次调用该服务时,会直接调用降级逻辑快速地返回失败响应,以避免系统雪崩。
- 当休眠时间窗到期后,Hystrix 会进入半熔断转态,允许部分请求对服务原来的主业务逻辑进行调用,并监控其调用成功率。
- 如果调用成功率达到预期,则说明服务已恢复正常,Hystrix 进入熔断关闭状态,服务原来的主业务逻辑恢复;否则 Hystrix 重新进入熔断开启状态,休眠时间窗口重新计时,继续重复第 2 到第 5 步。
7.3 具体实现
在hystrixServer对应的service中新增breaker方法,并且在controller中添加调用方法
/** * 熔断方法测试 * @param id * @return 熔断响应信息 */ @Override @HystrixCommand(fallbackMethod = "breakerFallbackHandler", commandProperties = { // 是否开启熔断器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 统计时间窗 @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "10000"), // 统计时间窗内请求次数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 休眠时间窗口期 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 在统计时间窗口期以内,请求失败率达到 60% 时进入熔断状态 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), } ) public String breaker(String id) { if (id.length() < 2) { throw new RuntimeException("传入id长度小于2啦,报错啦! " + "id: " + id); } String bizId = UUID.randomUUID().toString(); return Thread.currentThread().getName() + " 处理逻辑成功,id信息为 :" + bizId; }
/** * 降级方法 * @param id * @return */ @GetMapping("/breaker/{id}") public String breaker(@PathVariable(value = "id") String id) { return hystrixService.breaker(id); }
在以上代码中,共涉及到了 4 个与 Hystrix 熔断机制相关的重要参数,这 4 个参数的含义如下表。
参数 | 描述 |
---|---|
metrics.rollingStats.timeInMilliseconds | 统计时间窗。 |
circuitBreaker.sleepWindowInMilliseconds | 休眠时间窗,熔断开启状态持续一段时间后,熔断器会自动进入半熔断状态,这段时间就被称为休眠窗口期。 |
circuitBreaker.requestVolumeThreshold | 请求总数阀值。 在统计时间窗内,请求总数必须到达一定的数量级,Hystrix 才可能会将熔断器打开进入熔断开启转态,而这个请求数量级就是 请求总数阀值。Hystrix 请求总数阈值默认为 20,这就意味着在统计时间窗内,如果服务调用次数不足 20 次,即使所有的请求都调用出错,熔断器也不会打开。 |
circuitBreaker.errorThresholdPercentage | 错误百分比阈值。 当请求总数在统计时间窗内超过了请求总数阀值,且请求调用出错率超过一定的比例,熔断器才会打开进入熔断开启转态,而这个比例就是错误百分比阈值。错误百分比阈值设置为 50,就表示错误百分比为 50%,如果服务发生了 30 次调用,其中有 15 次发生了错误,即超过了 50% 的错误百分比,这时候将熔断器就会打开。 |
测试:首先调用传值id长度为1 eg: 1,多次调用后,再转换为多位 eg: 12 发现服务仍然不可用,等待片刻之后,再调用12,发现服务恢复正常
8.监控故障信息
8.1 监控介绍
Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)功能,Hystrix 会持续地记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表的形式展示给用户,包括每秒执行请求的数量、成功请求的数量和失败请求的数量等。
8.2 监控服务搭建
- 先新建hystrix-dashboard服务,pom引入jar包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>hystrix-dashboard</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--Spring Boot 测试依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--hystrix-dashboard 监控的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <!--添加 Spring Boot 的监控模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- 再添加服务启动类以及服务配置类
package cn.git.hystrix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; /** * @description: hystrix dashboard服务启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 02:12:16 */ @SpringBootApplication @EnableHystrixDashboard public class HystrixDashboardApplication { public static void main(String[] args) { SpringApplication.run(HystrixDashboardApplication.class, args); } }
package cn.git.hystrix.config; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; /** * @description: dashboard配置类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-14 02:13:40 */ @Component public class HystrixDashboardConfig { /** * Hystrix dashboard 监控界面必须配置 * @return */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); // 访问路径 registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("hystrix.stream"); return registrationBean; } }
- 最后添加yml文件
server: # 监控服务端口号 port: 9002 # http://localhost:9002/hystrix 熔断器监控页面 # localhost:8004//actuator/hystrix.stream 监控地址 hystrix: dashboard: proxy-stream-allow-list: - "localhost"
- 启动服务,访问 http://localhost:9002/hystrix 地址查看
- 在搜索框中输入服务暴露信息 localhost:8004/actuator/hystrix.stream
图片参数信息解释
Hystrix Dashboard面板可分上下两部分来查询,上面部分是断路器方法调用的相关信息,Circuit,下面部分是Hystrix为断路器方法提供的线程池相关信息,Thread Pools。
Circuit介绍:
Circuit这里展示的当前方法的相对信息,如果有多个方法被断路器保护,那么这里将会依此展示多个方法的相关信息。
在图表中,左上角的圆圈代表了该方法的流量和状态:
- 圆圈越大代表方法流量越大
- 圆圈为绿色代表断路器健康、黄色代表断路器偶发故障、红色代表断路器故障
右上角的计数器(三列数字):
第一列从上到下
- 绿色代表当前成功调用的数量
- 蓝色代表短路请求的数量
- 蓝绿色代表错误请求的数量
第二列从上到下
- 黄色代表超时请求的数量
- 紫色代表线程池拒绝的数量
- 红色代表失败请求的数量
第三列
- 过去10s的错误请求百分比
Thread Pools:
Hystrix会针对一个受保护的类创建一个对应的线程池,这样做的目的是Hystrix的命令被调用的时候,不会受方法请求线程的影响(或者说Hystrix的工作线程和调用者线程相互之间不影响)在图表中,左上角的圆圈代表了该线程池的流量和状态:
- 圆圈越大代表线程池越活越,流量越大
圆圈颜色代表的是线程池的健康状况左下角从上至下:
- Active代表线程池中活跃线程的数量
- Queued代表排队的线程数量,该功能默认禁止,因此默认情况下始终为0
- Pool Size代表线程池中线程的数量(上面图我搞错了,困得死MMP)
右下角从上至下:
- Max Active代表最大活跃线程,这里展示的数据是当前采用周期中,活跃线程的最大值
- Execcutions代表线程池中线程被调用执行Hystrix命令的次数
- Queue Size代表线程池队列的大小,默认禁用,无意义
五. Config:Spring Cloud分布式配置组件
1. 基础概念
在分布式微服务系统中,几乎所有服务的运行都离不开配置文件的支持,这些配置文件通常由各个服务自行管理,以 properties 或 yml 格式保存在各个微服务的类路径下,例如 application.properties 或 application.yml 等。
这种将配置文件散落在各个服务中的管理方式,存在以下问题:
- 管理难度大:配置文件散落在各个微服务中,难以管理。
- 安全性低:配置跟随源代码保存在代码库中,容易造成配置泄漏。
- 时效性差:微服务中的配置修改后,必须重启服务,否则无法生效。
- 局限性明显:无法支持动态调整,例如日志开关、功能开关。
为了解决这些问题,通常我们都会使用配置中心对配置进行统一管理。市面上开源的配置中心有很多,例如百度的 Disconf、淘宝的 diamond、360 的 QConf、携程的 Apollo 等都是解决这类问题的。Spring Cloud 也有自己的分布式配置中心,那就是 Spring Cloud Config。
spring cloud config 介绍
Spring Cloud Config 是由 Spring Cloud 团队开发的项目,它可以为微服务架构中各个微服务提供集中化的外部配置支持。
简单点说就是,Spring Cloud Config 可以将各个微服务的配置文件集中存储在一个外部的存储仓库或系统(例如 Git 、SVN 等)中,对配置的统一管理,以支持各个微服务的运行。
Spring Cloud Config 包含以下两个部分:
- Config Server:也被称为分布式配置中心,它是个独立运行的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密信息和解密信息的访问接口
- Config Client:指的是微服务架构中的各个微服务,它们通过 Config Server 对配置进行管理,并从 Config Sever 中获取和加载配置信息。
Spring Cloud Config 默认使用 Git 存储配置信息,因此使用 Spirng Cloud Config 构建的配置服务器天然就支持对微服务配置的版本管理。我们可以使用 Git 客户端工具方便地对配置内容进行管理和访问。除了 Git 外,Spring Cloud Config 还提供了对其他存储方式的支持,例如 SVN、本地化文件系统等。
2. spring cloud config 工作原理
Spring Cloud Config 工作流程如下:
- 开发或运维人员提交配置文件到远程的 Git 仓库。
- Config 服务端(分布式配置中心)负责连接配置仓库 Git,并对 Config 客户端暴露获取配置的接口。
- Config 客户端通过 Config 服务端暴露出来的接口,拉取配置仓库中的配置。
- Config 客户端获取到配置信息,以支持服务的运行。
3. spring cloud config特点
Spring Cloud Config 具有以下特点:
- Spring Cloud Config 由 Spring Cloud 团队开发,可以说是 Spring 的亲儿子,能够与 Spring 的生态体系无缝集成。
- Spring Cloud Config 将所有微服务的配置文件集中存储在一个外部的存储仓库或系统(例如 Git)中,统一管理。
- Spring Cloud Config 配置中心将配置以 REST 接口的形式暴露给各个微服务,以方便各个微服务获取。
- 微服务可以通过 Spring Cloud Config 向配置中心统一拉取属于它们自己的配置信息。
- 当配置发生变化时,微服务不需要重启即可感知到配置的变化,并自动获取和应用最新配置。
- 一个应用可能有多个环境,例如开发(dev)环境、测试(test)环境、生产(prod)环境等等,开发人员可以通过 Spring Cloud Config 对不同环境的各配置进行管理,且能够确保应用在环境迁移后仍然有完整的配置支持其正常运行。
4. config服务搭建
新增config使用pom文件引入jar包,新增yml配置文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>config-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--配置中心服务器依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
server: # 端口号 port: 3344 spring: profiles: active: dev application: # 服务名 name: config-server cloud: config: server: # native: 本地模式 # search-locations: classpath:/config-server/ 本地服务类路径 # native: 本地服务 需要注意的是Windows中的绝对路径需要额外的/ # search-locations: file:///D:/spring-cloud-demo/config-server/src/main/resources/config-server git: uri: http://xxxxxxxx:8098/credit-rebuild/EbankForTransformation.git # 仓库名EbankForTransformation 项目下的 config文件夹下面,git仓库地址下的相对地址 多个用逗号","分割。 search-paths: config force-pull: true username: lixuchunxxxx password: lixuchunxxxx # 本地备份文件,如果git访问不了 则返回本地备份文件信息 basedir: D:\idea_workspace\EbankForTransformation\netflix-cloud\config-server\src\main\resources\profiles #分支名 label: master eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: # 集群版本 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka Server 集群 # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: # 自定义服务名称信息 instance-id: config-server # 显示访问路径的 ip 地址 prefer-ip-address: true
新增服务启动类
package cn.git.config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @description: config-server 服务启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-17 04:39:58 */ @SpringBootApplication @EnableEurekaClient @EnableConfigServer public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
Spring Cloud Config 规定了一套配置文件访问规则,如下表。
访问规则 | 示例 |
---|---|
/{application}/{profile}[/{label}] | /config/dev/master |
/{application}-{profile}.{suffix} | /config-dev.yml |
/{label}/{application}-{profile}.{suffix} | /master/config-dev.yml |
访问规则内各参数说明如下。
- {application}:应用名称,即配置文件的名称,例如 config-dev。
- {profile}:环境名,一个项目通常都有开发(dev)版本、测试(test)环境版本、生产(prod)环境版本,配置文件则以 application-{profile}.yml 的形式进行区分,例如 application-dev.yml、application-test.yml、application-prod.yml 等。
- {label}:Git 分支名,默认是 master 分支,当访问默认分支下的配置文件时,该参数可以省略,即第二种访问方式。
- {suffix}:配置文件的后缀,例如 config-dev.yml 的后缀为 yml。
启动服务,输入 http://localhost:3344/master/config-dev.yml 查看页面展示信息
输入 http://localhost:3344/config/dev/master 获得如下配置
格式化json后
{ "name": "config", "profiles": [ "dev" ], "label": "master", "version": "56d942260085f57da9c41fc1df25f84c95de365d", "state": null, "propertySources": [ { "name": "http://xxxx:8098/credit-rebuild/EbankForTransformation.git/config/config-dev.yml", "source": { "server.port": 7001, "eureka.instance.hostname": "xxxx", "eureka.client.register-with-eureka": false, "eureka.client.fetch-registry": false, "eureka.client.service-url.defaultZone": "http://${eureka.instance.hostname}:${server.port}/eureka/" } } ] }
5. config 客户端搭建
首先引入对应pom以及jar文件,新增服务启动类以及yml文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>config-client</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--Spring Cloud Config 客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- 暴露端点 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
server: # 端口号 port: 3355 spring: application: # 服务名 name: config-client cloud: config: # 分支名称 label: master # 配置文件名称,config-dev.yml 中的 config name: config #环境名 config-dev.yml 中的 dev profile: dev # Spring Cloud Config 服务端(配置中心)地址 uri: http://localhost:3344 eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: # 集群版本 defaultZone: http://xxxx:7001/eureka/,http://xxxx:7002/eureka/,http://xxxx:7003/eureka/ Eureka Server 集群 # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka
package cn.git.config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @description: config client 启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-23 11:31:14 */ @SpringBootApplication @EnableEurekaClient public class ConfigClientApplication { public static void main(String[] args) { SpringApplication.run(ConfigClientApplication.class, args); } }
编写controller 观察是否取到服务端配置值信息
package cn.git.config.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @description: config client 测试controller * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-23 01:55:09 */ @RestController @RequestMapping("/config") public class ConfigClientController { @Value("${cat.color}") private String color; @Value("${cat.name}") private String name; @GetMapping("/client/test") public String getConfigServerInfo() { return "cat color : " + color + ", name : " + name; } }
访问 http://localhost:3355/config/client/test 观察如下 已经获取配置信息
在config-server服务以及client服务调用过程中,修改config-dev.yml文件,client服务获取不到修改后的配置信息
可以使用 management 暴露的刷新端点进行刷新 curl -X POST “http://localhost:3355/actuator/refresh” 则可以刷新config-client客户端配置信息
手动刷新配置的问题
在上面的实例中,我们通过在 Config 客户端(端口号:3355)中引入 Spring Boot actuator 监控组件来监控配置的变化,使我们可以在不重启 Config 客户端的情况下获取到了最新配置,原理如下图。
图10:Spring Cloud Congfig 手动刷新这种方式虽然解决了重启 Config 客户端才能获取最新配置的问题,但另一个问题却也接踵而至,那就是只要配置仓库中的配置发生改变,就需要我们挨个向 Config 客户端手动发送 POST 请求,通知它们重新拉取配置。
我们知道,所谓的 Config 客户端其实就是一个一个的服务。在微服务架构中,一个系统往往包含十几甚至几十个服务,如果因为某一个配置文件的修改而向几十个微服务发送 POST 请求,这显然是不合理的。
6.config + bus 动态刷新配置
Spring Cloud Bus 又被称为消息总线,它能够通过轻量级的消息代理(例如 RabbitMQ、Kafka 等)将微服务架构中的各个服务连接起来,实现广播状态更改、事件推送等功能,还可以实现微服务之间的通信功能。
目前 Spring Cloud Bus 支持两种消息代理:RabbitMQ 和 Kafka。
Spring Cloud Bus 的基本原理
Spring Cloud Bus 会使用一个轻量级的消息代理来构建一个公共的消息主题 Topic(默认为“springCloudBus”),这个 Topic 中的消息会被所有服务实例监听和消费。当其中的一个服务刷新数据时,Spring Cloud Bus 会把信息保存到 Topic 中,这样监听这个 Topic 的服务就收到消息并自动消费。
Spring Cloud Bus 动态刷新配置的原理
利用 Spring Cloud Bus 的特殊机制可以实现很多功能,其中配合 Spring Cloud Config 实现配置的动态刷新就是最典型的应用场景之一。
当 Git 仓库中的配置发生了改变,我们只需要向某一个服务(既可以是 Config 服务端,也可以是 Config 客户端)发送一个 POST 请求,Spring Cloud Bus 就可以通过消息代理通知其他服务重新拉取最新配置,以实现配置的动态刷新。
Spring Cloud Bus 动态刷新配置的工作原理,如下图所示。
Bus+Config 实现配置的动态刷新
如图,利用 Spring Cloud Bus 实现配置的动态刷新需要以下步骤:
- 当 Git 仓库中的配置发生改变后,运维人员向 Config 服务端发送一个 POST 请求,请求路径为“/actuator/refresh”。
- Config 服务端接收到请求后,会将该请求转发给服务总线 Spring Cloud Bus。
- Spring Cloud Bus 接到消息后,会通知给所有 Config 客户端。
- Config 客户端接收到通知,请求 Config 服务端拉取最新配置。
- 所有 Config 客户端都获取到最新的配置。
这样很不方便。如下是bus整合config server服务 实现全局动态刷新,这样可以解决如上问题。
下面我们以 kafka为例,来演示如何使用 Config+Bus 实现配置的动态刷新。
6.1 在config-server服务改造
- 首先在服务端 新增kafka依赖包信息以及暴露端点信息
<!--添加消息总线(Bus)对 kafka 的支持--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-kafka</artifactId> </dependency> <!--添加Spring Boot actuator 监控模块的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
- 在application.yml中新增kafka以及bus以及端点信息配置
server: # 端口号 port: 3344 spring: kafka: # 配置 kafka 服务器的地址和端口 bootstrap-servers: 3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 consumer: group-id: SpringCloud-bus # bus 动态刷新 bus: refresh: enabled: true profiles: active: dev application: # 服务名 name: config-server cloud: config: server: git: # Git 地址,https://gitee.com/java-mohan/springcloud-config.git uri: http://3.1.101.56:8098/credit-rebuild/EbankForTransformation.git #仓库名 search-paths: config force-pull: true username: lixuchun password: lixuchun123456 # 本地备份文件,如果git访问不了 则返回本地备份文件信息 basedir: D:\idea_workspace\EbankForTransformation\netflix-cloud\config-server\src\main\resources\profiles #分支名 label: master eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: # 集群版本 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka Server 集群 # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: # 自定义服务名称信息 instance-id: config-server # 显示访问路径的 ip 地址 prefer-ip-address: true # 暴露端点监控配置 management: endpoints: web: exposure: include: '*'
6.2 config-client改造
- 新增kafka以及bus以及暴露端点jar包以及配置项
<!-- 暴露端点 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- 导入bus kafka 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-kafka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> </dependency>
- 修改bootstrap.yml配置文件,新增暴露端点,kafka以及bus配置
server: # 端口号 port: 3344 spring: kafka: # 配置 kafka 服务器的地址和端口 bootstrap-servers: 3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 consumer: group-id: SpringCloud-bus # bus 动态刷新 bus: refresh: enabled: true profiles: active: dev application: # 服务名 name: config-server cloud: config: server: git: # Git 地址,https://gitee.com/java-mohan/springcloud-config.git uri: http://3.1.101.56:8098/credit-rebuild/EbankForTransformation.git #仓库名 search-paths: config force-pull: true username: lixuchun password: lixuchun123456 # 本地备份文件,如果git访问不了 则返回本地备份文件信息 basedir: D:\idea_workspace\EbankForTransformation\netflix-cloud\config-server\src\main\resources\profiles #分支名 label: master eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: # 集群版本 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka Server 集群 # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: # 自定义服务名称信息 instance-id: config-server # 显示访问路径的 ip 地址 prefer-ip-address: true # 暴露端点配置 management: endpoints: web: exposure: include: '*'
6.3 动态刷新测试
分别启动config-server以及config-client服务,访问client服务 http://localhost:3355/config/client/test 观察配置信息
修改git配置信息代码 name为 jack7777
使用postman访问 http://localhost:3344/actuator/bus-refresh,再访问config-client端[http://localhost:3355/config/client/test],进行查看
或者使用窗口执行 curl -X POST “http://localhost:3344/actuator/bus-refresh”
6.4 Spring Cloud Bus 动态刷新配置(定点通知)
所谓定点通知,就是不再通知所有的 Config 客户端,而是根据需求只通知其中某一个 Config 客户端。
使用 Spring Cloud Bus 实现定点通知的方法十分简单,只要我们在发送 POST 请求时使用以下格式即可。
http://{hostname}:{port}/actuator/bus-refresh/{destination}
参数说明如下
- {hostname}: 表示 config-server 服务端的主机地址,既可以是域名,也可以是 IP 地址。
- {port}:表示 Config 服务端的端口号.
- {destination}:表示需要定点通知的 Config 客户端(微服务),由 Config 客户端的服务名(spring.application.name)+端口号(server.port)组成,例如只通知 config-client 刷新配置,则取值为 config-client:3355
执行 curl -X POST “http://localhost:3344/actuator/bus-refresh/config:3355” 即可
六. Zuul: 微服务网关组件
1. Zuul概念
官网解释:路由是微服务架构的一个组成部分。例如,/
可能映射到您的 Web 应用程序、/api/users
映射到用户服务和/api/shop
映射到商店服务。 Zuul是来自 Netflix 的基于 JVM 的路由器和服务器端负载均衡器
Zuul就是一个网关,那么到底什么是网关?
API网关为微服务架构中的服务提供了统一的访问入口
,客户端通过API网关访问相关服务。API网关的定义类似于设计模式中的门面模式
,它相当于整个微服务架构中的门面,所有客户端的访问都通过它来进行路由及过滤
。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。
Zuul包含了如下最主要的功能:路由、对请求过滤、负载均衡、灰度发布
什么是路由?
路由功能负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础,可以理解为就是请求转发
什么是请求过滤?
可以指定哪些请求允许访问,哪些请求不允许访问!
什么是负载均衡?
假如有多台机器部署了A服务,每次访问都访问到一台服务器上,那么多台机器根本没起到作用,搭建多台往往是为了减轻单机的压力,这时候就需要指定个策略,比如
轮询、随机、权重
等等,我们又称之为负载均衡策略
。网关为入口,由网关与微服务进行交互,所以网关必须要实现负载均衡的功能;
网关会获取微服务注册中心里面的服务连接地址,再配合一些算法选择其中一个服务地址,进行处理业务。这个属于客户端侧的负载均衡,由调用方去实现负载均衡逻辑
。
什么是灰度发布?
起源是,矿井工人发现,金丝雀对瓦斯气体很敏感,矿工会在下井之前,先放一只金丝雀到井中,如果金丝雀不叫了,就代表瓦斯浓度高。
在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。如果没有问题,那么可以将少量的用户流量导入到新版本上,然后再对新版本做运行状态观察,收集各种运行时数据,如果此时对新旧版本做各种数据对比,就是所谓的A/B测试。新版本没什么问题,那么逐步扩大范围、流量,把所有用户都迁移到新版本上面来。
Zuul现状
zuul截止cloud的 H.SR12
版本之后就彻底从官网移除了,假如你这时候还想使用zuul,需要注意cloud版本,springboot版本也需要注意,不可以高于2.3.12.RELEASE
。
2. zuul-server具体实现
2.1 新建zuul-server服务,新增zuul依赖使用pom对应jar包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>zuul-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <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> <!-- 暴露端点 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
2.2 新增服务启动类服务对应yml配置
服务启动类如下,注意添加**@EnableZuulProxy**注释
package cn.git.zuul; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; /** * @description: Zuul网关服务启动类 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 09:04:49 */ @SpringBootApplication @EnableZuulProxy public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
服务配置对应的application.yml配置如下
server: port: 3366 spring: application: name: zuul-server eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: defaultZone: http://3.2.26.244:7001/eureka instance: # 自定义服务名称信息 instance-id: zuul-server # 显示访问路径的 ip 地址 prefer-ip-address: true management: endpoints: web: exposure: # * 在yaml 文件属于关键字,所以需要加引号 include: "*" info: app.name: eureka-producer company.name: lixuchun build.aetifactId: @project.artifactId@ build.version: @project.version@ # 此配置访问路径为:http://localhost:3366/zuul/config/client/test # 原访问路径为 : http://localhost:3355/config/client/test #zuul: # routes: # # config-client是自己定义的路由名称 # config-client: # path: /** # url: http://localhost:3355 # 此配置访问路径为:http://localhost:3366/prop/config/client/test # 原访问路径为 : http://localhost:3355/config/client/test zuul: # 统一设置前缀,不使用前缀无法访问 prefix: /zuul-cus servlet-path: / # 路由映射配置 routes: # config-client 就是自己取的名字,这样就是可以设置多个路由,通过名称来区分 config-client: # /prop 被转发到服务名称为cloud-payment-service的服务 path: /prop/** #注册进eureka服务器的服务名称 serviceId: config-client # 使网关不可以使用服务名称进行访问 # http://localhost:3366/config-client/config/client/test 不能访问 # 也可以设置 ignored-services: "*" 全部服务都不可以使用services-id进行访问 ignored-services: config-client # 此配置项处理过滤器是否关闭 PreLogFilter: pre: disable: false
2.3 业务逻辑处理filter类
zuul的本质其实就是一个个的过滤器的集合,Zuul是一个业务网关, 而深入了解Zuul, 基本就是一系列过滤器的集合:
过滤器的执行时机以及具体在调用中的执行逻辑如下图:
zuul的过滤器, 主要有pre、rout、post、error四种过滤器类型(执行顺序如下)
正常的流程是pre–>route–>post
在pre过滤器阶段抛出异常,pre–> error -->post
在route过滤器阶段抛出异常,pre–>route–>error -->post
在post过滤器阶段抛出异常,pre–>route–>post–> error
过滤器类型定义在 filterType 方法中, 返回一个字符串代表过滤器的类型,在zuul中定义的四种不同生命周期的过滤器类型,主要功能如下:
pre
:在请求被路由之前调用, 利用这种路由器进行鉴权等服务, 记录日志、 限流route
:在路由请求时候被调用,作用是将请求路由到指定服务中去, 用于构建发送给微服务的请求, 并且用Http Client(或者Ribbon)请求微服务post
:在route和error过滤器之后被调用,可以用来添加Header , 记录日志, 将响应返回给客户端。error
:处理请求时发生错误时调用
从 pre 和 route 阶段抛出的异常将会进入 error 阶段,再进入到post阶段进行返回。由于 SendErrorFilter 需要判断请求上下文中是否包含
error.status_code
属性:有的话就用SendErrorFilter处理错误结果;没有的话就用SendResponseFilter返回正常结果,但是error.status_code
属性默认是在各个阶段过滤器中自己put进去的,这就导致,各个阶段过滤器抛出异常之后,是没有办法返回错误结果的。因此,我们扩展了一个ErrorFilter来捕获异常,然后手工的设置error.status_code
属性,让SendErrorFilter能正常运作。
每一种处理器具体细分多种, 参考图:
需要记住几个常见的, Route中有三种过滤器, 分别是:
- RibbonXXXFilter : 路由到服务
- SimpleHostRoutingFilter : 路由到URL地址
- SendForwardFilter : 转向Filter自己
下面展示两个自定义过滤器
package cn.git.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; /** * @description: 目标服务器返回数据后执行过滤器 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 11:27:53 */ @Slf4j @Component public class ZuulAfterFilter extends ZuulFilter { /** * 前置过滤器 * pre: 在请求被路由到目标服务前执行,比如权限校验、打印日志等功能; * routing: 在请求被路由到目标服务时执行 * post: 在请求被路由到自标服务后执行,比如给目标服务的响应添加头信息,收集统计数据等功能; * error: 请求在其他阶段发生错误时执行。 */ public static final String FILTER_TYPE_POST = "post"; @Override public String filterType() { return FILTER_TYPE_POST; } /** * 多个过滤器按照这个顺序执行 */ @Override public int filterOrder() { return 0; } /** * 过滤器是否开启 */ @Override public boolean shouldFilter() { return true; } /** * 自定义过滤器业务逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { // 获取响应信息 并且添加响应头信息 log.info("执行响应信息,添加响应头X-Test信息"); RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse response = requestContext.getResponse(); response.setHeader("X-Test", UUID.randomUUID().toString()); return null; } }
package cn.git.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @description: zuul 前置过滤器 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 11:19:00 */ @Slf4j @Component public class ZuulPrefixFilter extends ZuulFilter { /** * 前置过滤器 * pre: 在请求被路由到目标服务前执行,比如权限校验、打印日志等功能; * routing: 在请求被路由到目标服务时执行 * post: 在请求被路由到自标服务后执行,比如给目标服务的响应添加头信息,收集统计数据等功能; * error: 请求在其他阶段发生错误时执行。 */ public static final String FILTER_TYPE_PRE = "pre"; @Override public String filterType() { return FILTER_TYPE_PRE; } /** * 多个过滤器按照这个顺序执行 */ @Override public int filterOrder() { return 0; } /** * 过滤器是否开启 */ @Override public boolean shouldFilter() { return true; } /** * 自定义过滤器业务逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String host = request.getRemoteHost(); String method = request.getMethod(); String uri = request.getRequestURI(); log.info("前置过滤器执行,请求对应host[{}],method[{}],uri[{}]", host, method, uri); return null; } }
2.4 统一异常处理
统一异常处理需要禁用zuul自定义的异常处理类,需要在application.yml中配置
zuul: # 在自定义了异常处理后 需要禁用原有异常处理filter SendErrorFilter: error: disable: true
新增自定义异常处理类
package cn.git.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @description: 自定义异常处理 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 02:12:18 */ @Slf4j @Component public class ZuulCusErrorFilter extends ZuulFilter { /** * 前置过滤器 * pre: 在请求被路由到目标服务前执行,比如权限校验、打印日志等功能; * routing: 在请求被路由到目标服务时执行 * post: 在请求被路由到自标服务后执行,比如给目标服务的响应添加头信息,收集统计数据等功能; * error: 请求在其他阶段发生错误时执行。 */ public static final String FILTER_TYPE_ERROR = "error"; @Override public String filterType() { return FILTER_TYPE_ERROR; } /** * 多个过滤器按照这个顺序执行 */ @Override public int filterOrder() { return 0; } /** * 过滤器是否开启 */ @Override public boolean shouldFilter() { return true; } /** * 自定义过滤器业务逻辑 * @return */ @Override public Object run() { // 获取请求响应信息 try { RequestContext context = RequestContext.getCurrentContext(); ZuulException exception = this.findZuulException(context.getThrowable()); log.error("进入系统异常拦截", exception); HttpServletResponse response = context.getResponse(); response.setContentType("application/json; charset=utf8"); response.setStatus(exception.nStatusCode); PrintWriter writer = null; try { writer = response.getWriter(); writer.print("{code:"+ exception.nStatusCode +",message:\""+ exception.getMessage() +"\"}"); } catch (IOException e) { e.printStackTrace(); } finally { if(writer!=null){ writer.close(); } } } catch (Exception var5) { ReflectionUtils.rethrowRuntimeException(var5); } return null; } /** * 异常类获取 * @param throwable * @return */ public ZuulException findZuulException(Throwable throwable) { if (ZuulRuntimeException.class.isInstance(throwable.getCause())) { return (ZuulException)throwable.getCause().getCause(); } else if (ZuulException.class.isInstance(throwable.getCause())) { return (ZuulException)throwable.getCause(); } else { return ZuulException.class.isInstance(throwable) ? (ZuulException)throwable : new ZuulException(throwable, 500, (String)null); } } }
新增zuuException异常类
package cn.git.zuul.exception; import com.netflix.zuul.exception.ZuulException; /** * @description: 自定义zuul异常 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 04:09:10 */ public class CusZuulException extends ZuulException { public CusZuulException(Throwable throwable, String sMessage, int nStatusCode, String errorCause) { super(throwable, sMessage, nStatusCode, errorCause); } public CusZuulException(String sMessage, int nStatusCode, String errorCause) { super(sMessage, nStatusCode, errorCause); } public CusZuulException(Throwable throwable, int nStatusCode, String errorCause) { super(throwable, nStatusCode, errorCause); } public CusZuulException(String sMessage, int nStatusCode) { super(sMessage, nStatusCode, sMessage); } }
在preFilter中抛出异常信息然后请求接口进行观察
/** * 抛出异常 */ public void makeException() throws CusZuulException { throw new CusZuulException("自定义异常抛出啦!!!!!", 666); }
测试结果如下:
访问不存在路径时候报错信息不是统一异常返回,需要单独处理
新增统一处理其他异常统一处理controller
package cn.git.zuul.controller; import cn.git.zuul.result.Result; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 自定义error错误页面 * @author zhiguang */ @RestController public class ZuulErrorController implements ErrorController { /** * 出异常后进入该方法,交由下面的方法处理 */ @Override public String getErrorPath() { return "error"; } @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response){ Integer status = (Integer)request.getAttribute("javax.servlet.error.status_code"); return Result.error(status, status == 404 ? "访问地址不存在" : "内部服务器错误,正在处理"); } }
测试结果如下
如果访问地址匹配规则正确,但是服务提供方却没有此规则,则需要在postFilter中添加访问404业务逻辑错误响应信息,具体如下所示。
访问 http://localhost:3366/zuul-cus/prop/config/client/test2 请求结果如下
2.5 调用失败统一处理
package cn.git.zuul.fallback; import cn.git.zuul.result.Result; import com.alibaba.fastjson.JSONObject; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; /** * @description: zuul 调用失败统一处理 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-25 05:02:16 */ @Component public class ZuulFallback implements FallbackProvider { @Override public String getRoute() { // *代表全部服务,也可以指定具体的微服务,比如config-client return "*"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 888; } @Override public String getStatusText() throws IOException { return "zuul fallback"; } @Override public void close() {} @Override public InputStream getBody() throws IOException { //设置返回信息 String result = JSONObject.toJSONString(Result.error(999, "zuul统一fallback处理,系统繁忙,请稍后重试")); return new ByteArrayInputStream(result.getBytes()); } @Override public HttpHeaders getHeaders() { // headers设定,设定返回json HttpHeaders headers = new HttpHeaders(); MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8")); headers.setContentType(mt); return headers; } }; } }
关闭config-client ,再调用 路径 http://localhost:3366/zuul-cus/prop/config/client/test 则提示如下信息
2.6 zuul网关服务测试
启动服务,通过网关访问原config-client暴露的controller接口信息
原接口访问路径 http://localhost:3355/config/client/test
通过网关进行访问,路径 http://localhost:3366/zuul-cus/prop/config/client/test
七 . zipkin和sleuth链路追踪组件
1.链路追踪简介
Zipkin 最初是在 Twitter 上开发的,基于 Google 论文的概念,该论文描述了 Google 内部构建的分布式应用程序调试器 – 精简程序。 它管理此数据的收集和查找。 要使用 Zipkin,将对应用程序进行检测以向其报告计时数据。
如果要对生态系统中的延迟问题或错误进行故障排除,则可以根据应用程序,跟踪的长度,注解或时间戳来对所有跟踪进行过滤或排序。 通过分析这些跟踪,可以确定哪些组件未按预期执行,并可以对其进行修复。
内部有 4 个模块:
- 收集器 – 一旦任何组件将跟踪数据发送到 Zipkin 收集器守护程序,Zipkin 收集器就会对其进行验证,存储和索引以进行查找。
- 存储 – 此模块在后端存储和索引查找数据。 支持 Cassandra,ElasticSearch 和 MySQL。
- 搜索 – 此模块提供了一个简单的 JSON API,用于查找和检索存储在后端的跟踪。 该 API 的主要使用者是 Web UI。
- Web UI – 一个非常好的 UI 界面,用于查看轨迹。
Sleuth 是 Spring Cloud 系列的工具。 它用于生成跟踪 ID,span id,并将这些信息添加到标头和 MDC 中的服务调用中,以便诸如 Zipkin 和 [ELK] 等用于存储,索引和处理日志文件之类的工具可以使用它。由于它来自 Spring Cloud 系列,因此一旦添加到CLASSPATH
中,它就会自动集成到常见的通信渠道。
sleuth基本概念
Span(跨度):Span是基本的工作单元。Span包括一个64位的唯一ID,一个64位trace码,描述信息,时间戳事件,key-value 注解(tags),span处理者的ID(通常为IP)。
最开始的初始Span称为根span,此span中span id和 trace id值相同。
Trance(跟踪):包含一系列的span,它们组成了一个树型结构
Annotation(标注):用于及时记录存在的事件。常用的Annotation如下:
- CS(Client Sent 客户端发送):客户端发送一个请求,表示span的开始
- SR(Server Received 服务端接收):服务端接收请求并开始处理它。(SR - CS)等于网络的延迟
- SS(Server Sent 服务端发送):服务端处理请求完成,开始返回结束给服务端。(SR - SS)表示服务端处理请求的时间
- CR(Client Received 客户端接收):客户端完成接受返回结果,此时span结束。(CR -CS)表示客户端接收服务端数据的时间
sleuth 执行概念图如下:
服务调用图如下
执行顺序时序图则如下:
2. zipkin服务搭建
首先下载[zipkin jar]包,并且启动对应服务
下载到本地进行启动执行脚本如下
java -jar zipkin-server-2.24.0-exec.jar --KAFKA_BOOTSTRAP_SERVERS=3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 --KAFKA_TOPIC=zip-test-topic --STORAGE_TYPE=elasticsearch --ES_HOSTS=3.1.19.60:9200
3. sleuth zipkin 客户端搭建
在hystrix-client 以及 hystrix-server pom中引入对应jar包
<!-- zipkin sleuth 链路追踪 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>
在application.yml中引入zipkin 以及sleuth配置信息
spring: zipkin: base-url: http://3.2.26.244:9411 sender: type: kafka kafka: topic: zip-test-topic kafka: bootstrap-servers: 3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 sleuth: sampler: # sleuth采样比例 默认0.1 不是所有都会进行关注采样 probability: 1
zipkin的启动命令
java -jar zipkin-server-2.24.0-exec.jar --KAFKA_BOOTSTRAP_SERVERS=3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 --KAFKA_TOPIC=zip-test-topic --STORAGE_TYPE=elasticsearch --ES_HOSTS=3.1.19.60:9200,3.1.19.61:9200,3.1.19.62:9200 --ES_USERNAME=elastic --ES_PASSWORD=G1T@es2022#ccms
4. 链路测试
- 页面测试访问 http://localhost:8005/hystrix/client/normal/ok/777 hystrix-client调用 hystrix-server,观察链路信息
通过tid查询如下信息
访问阻塞接口 http://localhost:8005/hystrix/client/time/out/66666 hystrix-client调用 hystrix-server,观察链路信息
观察调用执行过程如下图所示:
八.系统监控admin
1.系统监控简介
springboot抽取了大部分监控系统的常用指标,提出了监控的总思想。然后就有好心的同志根据监控的总思想,制作了一个通用性很强的监控系统,因为是基于springboot监控的核心思想制作的,所以这个程序被命名为Spring Boot Admin。
Spring Boot Admin,这是一个开源社区项目,用于管理和监控SpringBoot应用程序。这个项目中包含有客户端和服务端两部分,而监控平台指的就是服务端。我们做的程序如果需要被监控,将我们做的程序制作成客户端,然后配置服务端地址后,服务端就可以通过HTTP请求的方式从客户端获取对应的信息,并通过UI界面展示对应信息。
2.新增监控服务
- 新增监控服务对应pom文件以及依赖jar包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netflix-cloud</artifactId> <groupId>org.springframework.boot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.git</groupId> <artifactId>admin-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- 暴露端点 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
- 新增服务对应的application.yml文件
spring: application: # 服务名 name: admin-server server: port: 9999 management: endpoints: web: exposure: include: '*' eureka: client: # 将客户端注册到 eureka 服务列表内 service-url: # 集群版本 defaultZone: http://xxxx:7001/eureka/,http://xxxx:7002/eureka/,http://xxxx:7003/eureka/ Eureka Server 集群 # 这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版) defaultZone: http://3.2.26.244:7001/eureka instance: # 自定义服务名称信息 instance-id: config-client # 显示访问路径的 ip 地址 prefer-ip-address: true
- 新增服务启动类
package cn.git.admin; import de.codecentric.boot.admin.server.config.EnableAdminServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @description: 微服务监控admin服务 * @program: bank-credit-sy * @author: lixuchun * @create: 2023-04-27 03:14:11 */ @SpringBootApplication @EnableAdminServer public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } }
3.客户端集成监控信息
在hystrix的客户端以及服务端新增监控配置信息
- 在 hystrix-client以及hystrix-server服务pom中新增admin监控使用jar包
<!-- zipkin sleuth 链路追踪 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>
- 首先在hystrix-client以及hystrix-server服务配置文件中新增监控配置
spring: application: #微服务名称,对外暴漏的微服务名称,十分重要 name: hystrix-client # zipkin 启动命令 nohup java -jar zipkin-server-2.24.0-exec.jar --KAFKA_BOOTSTRAP_SERVERS=3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 --KAFKA_TOPIC=zip-test-topic --STORAGE_TYPE=elasticsearch --ES_HOSTS=3.1.19.60:9200 & zipkin: base-url: http://3.2.26.244:9411 sender: type: kafka kafka: topic: zip-test-topic kafka: bootstrap-servers: 3.1.19.64:9092,3.1.19.65:9092,3.1.19.63:9092 sleuth: sampler: # 采样比例 probability: 1
4.启动服务进行测试
观察注册中心是否注册以及监控服务首页信息
访问 admin 首页