netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git eureka-server 8 8 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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/
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); } }
服务启动一段时间后发现有红字产生
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.
翻译:自我保护模式已关闭。 在网络/其他问题的情况下,这可能无法保护实例到期,意思就是可能提供不可用的服务给客户端调用
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 也是基于它实现的。
在任何一个系统中,负载均衡都是一个十分重要且不得不去实施的内容,它是系统处理高并发、缓解网络压力和服务端扩容的重要手段之一。
负载均衡(Load Balance) ,简单点说就是将用户的请求平摊分配到多个服务器上运行,以达到扩展服务器带宽、增强数据处理能力、增加吞吐量、提高网络的可用性和灵活性的目的。
负载均衡分为 客户端以及服务端 如下图:
当客户端发送请求时,该请求不会直接发送到服务端进行处理,而是全部交给负载均衡服务器,由负载均衡服务器按照某种算法(例如轮询、随机等),从其维护的可用服务清单中选择一个服务端,然后进行转发。
服务端负载均衡具有以下特点:
- 需要建立一个独立的负载均衡服务器。
- 负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。
- 可用服务端清单存储在负载均衡服务器上。
客户端负载均衡的工作原理如下图。
客户端负载均衡是将负载均衡逻辑以代码的形式封装到客户端上,即负载均衡器位于客户端。客户端通过服务注册中心(例如 Eureka Server)获取到一份服务端提供的可用服务清单。有了服务清单后,负载均衡器会在客户端发送请求前通过负载均衡算法选择一个服务端实例再进行访问,以达到负载均衡的目的;
客户端负载均衡也需要心跳机制去维护服务端清单的有效性,这个过程需要配合服务注册中心一起完成。
客户端负载均衡具有以下特点:
- 负载均衡器位于客户端,不需要单独搭建一个负载均衡服务器。
- 负载均衡是在客户端发送请求前进行的,因此客户端清楚地知道是哪个服务端提供的服务。
- 客户端都维护了一份可用服务清单,而这份清单都是从服务注册中心获取的。
Ribbon 就是一个基于 HTTP 和 TCP 的客户端负载均衡器,当我们将 Ribbon 和 Eureka 一起使用时,Ribbon 会从 Eureka Server(服务注册中心)中获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
服务端负载均衡 VS 客户端负载均衡
不同点 服务端负载均衡 客户端负载均衡 是否需要建立负载均衡服务器 需要在客户端和服务端之间建立一个独立的负载均衡服务器。 将负载均衡的逻辑以代码的形式封装到客户端上,因此不需要单独建立负载均衡服务器。 是否需要服务注册中心 不需要服务注册中心。 需要服务注册中心。
在客户端负载均衡中,所有的客户端和服务端都需要将其提供的服务注册到服务注册中心上。可用服务清单存储的位置 可用服务清单存储在位于客户端与服务器之间的负载均衡服务器上。 所有的客户端都维护了一份可用服务清单,这些清单都是从服务注册中心获取的。 负载均衡的时机 先将请求发送到负载均衡服务器,然后由负载均衡服务器通过负载均衡算法,在多个服务端之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。 简单点说就是,先发送请求,再进行负载均衡。 在发送请求前,由位于客户端的服务负载均衡器(例如 Ribbon)通过负载均衡算法选择一个服务器,然后进行访问。 客户端是否了解服务提供方信息 由于负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。 负载均衡是在客户端发送请求前进行的,因此客户端清楚的知道是哪个服务端提供的服务。
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 是如何实现服务调用的。
net.biancheng.c micro-service-cloud-api ${project.version} org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
// 配置类注解 @Configuration public class ConfigBean { @Bean //将 RestTemplate 注入到容器中 @LoadBalanced //在客户端使用 RestTemplate 请求服务端时,开启负载均衡(Ribbon) public RestTemplate getRestTemplate() { return new 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 list() { return restTemplate.getForObject(EUREKA_PRODUCER + "/test/list", List.class); } }
@SpringBootApplication @EnableEurekaClient public class EurekaProducer { public static void main(String[] args) { SpringApplication.run(EurekaProducer.class, args); } }
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
之后便可以访问接口,实现负载调用
序号 | 实现类 | 负载均衡策略 |
---|---|---|
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)策略类似。 |
只需要声明一个轮训策略类,注入到spring环境中
@Bean public IRule myRule() { // RandomRule 为随机策略 return new RandomRule(); }
/** * 定制 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 upList = lb.getReachableServers(); //获取所有的服务实例的列表 List 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 } }
/** * 定制 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); } }
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 的替代方案。
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。
下面我们就来对比下 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 注解。
net.biancheng.c micro-service-cloud-api ${project.version} org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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
//添加为容器内的一个组件 @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 list(); }
在编写服务绑定接口时,需要注意以下 2 点:
- 在 @FeignClient 注解中,value 属性的取值为:服务提供者的服务名,即服务提供者配置文件(application.yml)中 spring.application.name 的取值。
- 接口中定义的每个方法都与服务提供者(即 eurekaProducer 等)中 Controller 定义的服务方法对应。
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); } }
ribbon: # 建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间 ReadTimeout: 6000 # 建立连接后,服务器读取到可用资源的时间 ConnectionTimeout: 6000
OpenFeign 提供了日志打印功能,我们可以通过配置调整日志级别,来了解请求的细节。
Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,通过它可以对 OpenFeign 服务绑定接口的调用情况进行监控。
logging: level: # feign 日志以什么样的级别监控该接口 feignn接口包路径e(cn.git.workflow....service): debug
以上配置说明如下:
- cn.git.workflow…service接口是开启 @FeignClient 注解的接口(即服务绑定接口)的完整类名。
- 也可以只配置部分路径,表示监控该路径下的所有服务绑定接口
- debug:表示监听该接口的日志级别。
@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)
在微服务架构中,一个应用往往由多个服务组成,这些服务之间相互依赖,依赖关系错综复杂。
例如一个微服务系统中存在 A、B、C、D、E、F 等多个服务,它们的依赖关系如下图。
图1:服务依赖关系
通常情况下,一个用户请求往往需要多个服务配合才能完成。如图 1 所示,在所有服务都处于可用状态时,请求 1 需要调用 A、D、E、F 四个服务才能完成,请求 2 需要调用 B、E、D 三个服务才能完成,请求 3 需要调用服务 C、F、E、D 四个服务才能完成。
当服务 E 发生故障或网络延迟时,会出现以下情况:
从以上过程可以看出,当微服务系统的一个服务出现故障时,故障会沿着服务的调用链路在系统中疯狂蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。为了防止此类事件的发生,微服务架构引入了“熔断器”的一系列服务容错和保护机制。
熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。
在微服务领域,熔断器最早是由 Martin Fowler 在他发表的 《Circuit Breaker》一文中提出。与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。
Spring Cloud Hystrix 是一款优秀的服务容错与保护组件,也是 Spring Cloud 中最重要的组件之一。
Spring Cloud Hystrix 是基于 Netflix 公司的开源组件 Hystrix 实现的,它提供了熔断器功能,能够有效地阻止分布式微服务系统中出现联动故障,以提高微服务系统的弹性。Spring Cloud Hystrix 具有服务降级、服务熔断、线程隔离、请求缓存、请求合并以及实时故障监控等强大功能。
Hystrix [hɪst’rɪks],中文含义是豪猪,豪猪的背上长满了棘刺,使它拥有了强大的自我保护能力。而 Spring Cloud Hystrix 作为一个服务容错与保护组件,也可以让服务拥有自我保护的能力,因此也有人将其戏称为“豪猪哥”。
在微服务系统中,Hystrix 能够帮助我们实现以下目标:
Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。
服务降级的使用场景有以下 2 种:
我们可以通过重写 HystrixCommand 的 getFallBack() 方法或 HystrixObservableCommand 的 resumeWithFallback() 方法,使服务支持服务降级。
Hystrix 服务降级 FallBack 既可以放在服务端进行,也可以放在客户端进行。
Hystrix 会在以下场景下进行服务降级处理:
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git hystrix-server 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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@
/** * @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); } }
由上两图所示,限流功能已经生效
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git hystrix-client 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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@
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); } }
由上两图可以查看,当前客户端服务也已经进行了限流。
在配置文件中设计请求的超时时间时,需要注意以下 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 毫秒)
在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 "全局降级方法执行啦~~~~"; } }
调用测试
去除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 失败了"; } }
调用测试结果
熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制。
当微服务系统中的某个微服务不可用或响应时间太长时,为了保护系统的整体可用性,熔断器会暂时切断请求对该服务的调用,并快速返回一个友好的错误响应。这种熔断状态不是永久的,在经历了一定的时间后,熔断器会再次检测该微服务是否恢复正常,若服务恢复正常则恢复其调用链路。
在熔断机制中涉及了三种熔断状态:
三种熔断状态之间的转化关系如下图:
在 Spring Cloud 中,熔断机制是通过 Hystrix 实现的。Hystrix 会监控微服务间调用的状况,当失败调用到一定比例时(例如 5 秒内失败 20 次),就会启动熔断机制。
Hystrix 实现服务熔断的步骤如下:
在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,发现服务恢复正常
Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)功能,Hystrix 会持续地记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表的形式展示给用户,包括每秒执行请求的数量、成功请求的数量和失败请求的数量等。
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git hystrix-dashboard 8 8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.projectlombok lombok org.springframework.boot spring-boot-maven-plugin
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; } }
server: # 监控服务端口号 port: 9002 # http://localhost:9002/hystrix 熔断器监控页面 # localhost:8004//actuator/hystrix.stream 监控地址 hystrix: dashboard: proxy-stream-allow-list: - "localhost"
图片参数信息解释
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代表线程池队列的大小,默认禁用,无意义
在分布式微服务系统中,几乎所有服务的运行都离不开配置文件的支持,这些配置文件通常由各个服务自行管理,以 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 包含以下两个部分:
Spring Cloud Config 工作流程如下:
Spring Cloud Config 具有以下特点:
新增config使用pom文件引入jar包,新增yml配置文件
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git config-server 8 8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-config-server org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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 |
访问规则内各参数说明如下。
启动服务,输入 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/" } } ] }
首先引入对应pom以及jar文件,新增服务启动类以及yml文件
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git config-client 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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 请求,这显然是不合理的。
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 实现配置的动态刷新需要以下步骤:
这样很不方便。如下是bus整合config server服务 实现全局动态刷新,这样可以解决如上问题。
下面我们以 kafka为例,来演示如何使用 Config+Bus 实现配置的动态刷新。
org.springframework.cloud spring-cloud-starter-bus-kafka org.springframework.boot spring-boot-starter-actuator
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: '*'
org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-bus-kafka org.springframework.cloud spring-cloud-starter-stream-kafka
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: '*'
分别启动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”
所谓定点通知,就是不再通知所有的 Config 客户端,而是根据需求只通知其中某一个 Config 客户端。
使用 Spring Cloud Bus 实现定点通知的方法十分简单,只要我们在发送 POST 请求时使用以下格式即可。
http://{hostname}:{port}/actuator/bus-refresh/{destination}
参数说明如下
执行 curl -X POST “http://localhost:3344/actuator/bus-refresh/config:3355” 即可
官网解释:路由是微服务架构的一个组成部分。例如,/
可能映射到您的 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
。
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git zuul-server 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-zuul org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
服务启动类如下,注意添加**@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
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中有三种过滤器, 分别是:
下面展示两个自定义过滤器
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; } }
统一异常处理需要禁用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 请求结果如下
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 则提示如下信息
启动服务,通过网关访问原config-client暴露的controller接口信息
原接口访问路径 http://localhost:3355/config/client/test
通过网关进行访问,路径 http://localhost:3366/zuul-cus/prop/config/client/test
Zipkin 最初是在 Twitter 上开发的,基于 Google 论文的概念,该论文描述了 Google 内部构建的分布式应用程序调试器 – 精简程序。 它管理此数据的收集和查找。 要使用 Zipkin,将对应用程序进行检测以向其报告计时数据。
如果要对生态系统中的延迟问题或错误进行故障排除,则可以根据应用程序,跟踪的长度,注解或时间戳来对所有跟踪进行过滤或排序。 通过分析这些跟踪,可以确定哪些组件未按预期执行,并可以对其进行修复。
内部有 4 个模块:
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如下:
sleuth 执行概念图如下:
服务调用图如下
执行顺序时序图则如下:
首先下载[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
在hystrix-client 以及 hystrix-server pom中引入对应jar包
org.springframework.cloud spring-cloud-starter-sleuth org.springframework.cloud spring-cloud-starter-zipkin org.springframework.kafka spring-kafka
在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
通过tid查询如下信息
访问阻塞接口 http://localhost:8005/hystrix/client/time/out/66666 hystrix-client调用 hystrix-server,观察链路信息
观察调用执行过程如下图所示:
springboot抽取了大部分监控系统的常用指标,提出了监控的总思想。然后就有好心的同志根据监控的总思想,制作了一个通用性很强的监控系统,因为是基于springboot监控的核心思想制作的,所以这个程序被命名为Spring Boot Admin。
Spring Boot Admin,这是一个开源社区项目,用于管理和监控SpringBoot应用程序。这个项目中包含有客户端和服务端两部分,而监控平台指的就是服务端。我们做的程序如果需要被监控,将我们做的程序制作成客户端,然后配置服务端地址后,服务端就可以通过HTTP请求的方式从客户端获取对应的信息,并通过UI界面展示对应信息。
netflix-cloud org.springframework.boot 1.0-SNAPSHOT 4.0.0 cn.git admin-server 8 8 org.springframework.boot spring-boot-starter-web de.codecentric spring-boot-admin-starter-server 2.2.0 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
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); } }
在hystrix的客户端以及服务端新增监控配置信息
org.springframework.cloud spring-cloud-starter-sleuth org.springframework.cloud spring-cloud-starter-zipkin org.springframework.kafka spring-kafka
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
观察注册中心是否注册以及监控服务首页信息
访问 admin 首页