熔断
熔断机制是应对雪崩效应的一种微服务链路保护机制。当服务调用者调用链路的某个服务不可用或响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。
在 SpringCloud 框架里熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用满足一定阈值,缺省是5秒内20次调用失败就会启动熔断机制。熔断机制的注解是 HystrixCommand。
熔断机制是针对服务提供者 Provider 做的修改,我们根据 springcloud-provider-dept-8001 项目复制新增一个名为 springcloud-provider-dept-hystrix-8001 的项目,然后修改新项目。
1、导入 hystrix 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>1.4.6.RELEASE</version> </dependency> 复制代码
2、修改 DeptServiceImpl
@Service public class DeptServiceImpl implements DeptService { @Autowired DeptMapper deptMapper; @Override public boolean addDept(Dept dept) { return deptMapper.addDept(dept); } @HystrixCommand(fallbackMethod = "getDeptHystrix", commandProperties = { //默认 20 个;10s 内请求数大于 20 个时就启动熔断器,当请求符合熔断条件时将触发 getFallback()。 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), //请求错误率大于 50%时就熔断,然后 for 循环发起请求,当请求符合熔断条件时将触发 getFallback()。 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"), //默认 5 秒;熔断多少秒后去尝试请求 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000"), }) @Override public Dept queryDept(long id) { Dept dept = deptMapper.queryDept(id); if (dept == null){ throw new RuntimeException("id="+id+"=>没有对应的信息,null"); } return dept; } public Dept getDeptHystrix(long id){ return new Dept().setDeptId(id). setDpName("id="+id+"=>没有对应的信息,null"). setDbSource("no database in MySQL"); } @Override public List<Dept> queryAll() { return deptMapper.queryAll(); } } 复制代码
可以将@HystrixCommand 注解加在 Service 类上,也可以直接加在 controller 方法上,上述代码中我只加了关于 @GetMapping("/dept/get/{id}") 请求的熔断方法。
3、修改入口类
@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker //开启熔断器 public class DeptProviderHystrix_8001 { public static void main(String[] args) { SpringApplication.run(DeptProviderHystrix_8001.class,args); } } 复制代码
4、启动一个服务注册中心、本项目以及一个服务消费者即可,访问 http://localhost/consumer/dept/get/1 可以正常获取数据,但是访问 http://localhost/consumer/dept/get/10 页面显示内容为:
{"deptId":10,"dpName":"id=10=>没有对应的信息,null","dbSource":"no database in MySQL"} 复制代码
请求合并
没合并的请求与合并的请求
img
Hystrix中的请求合并,就是利用一个合并处理器,将对同一个服务发起的连续请求合并成一个请求进行处理(这些连续请求的时间窗默认为10ms),在这个过程中涉及到的一个核心类就是HystrixCollapser。
什么情况下使用请求合并
在微服务架构中会拆分成多个小的模块,各个模块之间进行调用是需要通信的,但是在并发量大的情况下会令大量线程处于等待状态,这就会增加响应时间,所以可以使用请求合并将多次请求合并为一次请求。
请求合并的代价是什么
在设置了请求合并以后,本来一次请求只需要5ms就搞定了,但是使用了请求合并后可能还需要再等待10ms,看看还有没有其他请求一起,这样一次请求就从5ms增加到了15ms。不过如果我们发起的命令本来就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间窗的时间消耗就显得微不足道了。
所以总结:请求合并适合在高延迟 + 大量并发的情况下使用。
服务提供者
我们根据 springcloud-provider-dept-8001 项目复制一个名为 springcloud-provider-dept-dgrade-8001 的新项目,作为服务提供者。
1、相关依赖不变
2、在 DeptService 接口上增加一个方法,用于多个 deptId 查询。
public interface DeptService { ..... List<Dept> getDepts(List<Long> ids); } 复制代码
3、同理修改 DeptMapper
@Mapper @Repository public interface DeptMapper { ..... List<Dept> getDepts(List<Long> ids); } 复制代码
4、修改 mapper 映射文件
<select id="getDepts" parameterType="list" resultType="Dept"> select * from springcloud.dept <where> deptId in <foreach collection="list" item="id" open="(" close=")" separator=","> #{id} </foreach> </where> </select> 复制代码
5、DeptServiceImpl
@Service public class DeptServiceImpl implements DeptService { @Autowired DeptMapper deptMapper; ..... @Override public List<Dept> getDepts(List<Long> ids) { return deptMapper.getDepts(ids); } } 复制代码
6、DeptController
@RequestMapping("/dept/getList/{ids}") public List<Dept> getDepts2(@PathVariable("ids") String sid) throws ExecutionException, InterruptedException { List<Long> ids = Arrays.stream(sid.split(",")).map(Long::parseLong).collect(Collectors.toList()); List<Dept> list = deptService.getDepts(ids); return list; } 复制代码
用户可以在 url 地址中输入多个 id,来查询多条数据。
7、启动该项目,在浏览器中输入 http://localhost:8001/dept/getList/1,2 ,则可以得到数据显示。
服务消费者
新建项目名为 springcloud-consumer-dept-80-syn。
1、导入依赖
<dependencies> <dependency> <groupId>com.msdn</groupId> <artifactId>springcloud-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!--ribbon依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>1.4.6.RELEASE</version> </dependency> </dependencies> 复制代码
2、application.yml
server: port: 80 #eureka配置 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001:7001/eureka/,http://eureka7002:7002/eureka/,http://eureka7003:7003/eureka/ 复制代码
3、Myconfig
package com.msdn.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author hresh * @date 2020/4/28 21:27 * @description */ @Configuration public class MyConfig { //配置负载均衡实现RestTemplate @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } } 复制代码
4、DeptConsumerService 请求合并逻辑
@Service public class DeptConsumerService { @Autowired RestTemplate restTemplate; private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT"; //利用 hystrix 合并请求 @HystrixCollapser(batchMethod = "batchDept", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, collapserProperties = { //请求时间间隔在 2s 之内的请求会被合并为一个请求,默认为 10ms @HystrixProperty(name = "timerDelayInMilliseconds", value = "2000"), //设置触发批处理执行之前,在批处理中允许的最大请求数。 @HystrixProperty(name = "maxRequestsInBatch", value = "200"), }) public Future<Dept> getDept2(Long id){ return null; } @HystrixCommand public List<Dept> batchDept(List<Long> ids){ System.out.println("batchDept---------"+ids+"Thread.currentThread().getName():" + Thread.currentThread().getName()); List<Dept> list = new ArrayList<>(); String sid = Joiner.on(",").join(ids); Dept[] depts = restTemplate.getForObject(REST_URL_PREFIX+"/dept/getList/"+sid, Dept[].class); list = Arrays.asList(depts); return list; } } 复制代码
关于@HystrixCollapser 注解的参数详解,可以参考下图:
5、DeptConsumerController
@RestController public class DeptConsumerController { @Autowired DeptConsumerService service; @RequestMapping("/consumer/dept/get/{id}") public Dept getDept(@PathVariable("id") long id) throws ExecutionException, InterruptedException { Future<Dept> dept1 = service.getDept2(id); return dept1.get(); } } 复制代码
6、入口类
@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class DeptConsumerSyn_80 { public static void main(String[] args) { SpringApplication.run(DeptConsumerSyn_80.class,args); } } 复制代码
7、启动一个注册中心,以及上述的服务提供者,和本项目,注意服务消费者项目中需要进入 debug 状态,并在 controller 类中的 getDept 方法处打上断点,然后在浏览器中打开两个网页分别输入 http://localhost/consumer/dept/get/3 和 http://localhost/consumer/dept/get/2 ,然后来到 IDEA 中通过断点调试,查看控制台内容输出。
通过结果可知,两次请求会合并为一次请求,减少了对数据库的访问次数。本例中通过 debug 设置断点将两次请求拦截下来,合并为一次请求进行处理。让业务逻辑层把单个查询的 sql,改为批量查询的 sql。
疑惑:本来想通过 Jmeter 进行高并发测试的,不过没有得到想要的结果,可能是我应用场景处理的有问题,所以只能通过 debug 来实现。
基于Feign实现的负载均衡
降级
关于 Feign 的实现可以参考我的上一篇文章,其中主要修改了 springcloud-api 项目。
1、导入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> <version>1.4.6.RELEASE</version> </dependency> 复制代码
2、添加 DeptServiceFallbackFactory
//降级 @Component public class DeptServiceFallbackFactory implements FallbackFactory { @Override public DeptFeignService create(Throwable throwable) { return new DeptFeignService() { @Override public boolean addDept(Dept dept) { return false; } @Override public Dept queryDept(long id) { return new Dept().setDeptId(id). setDpName("id="+id+"=>没有对应的信息,null"). setDbSource("服务降级处理"); } @Override public List<Dept> queryAll() { return null; } }; } } 复制代码
3、修改 DeptFeignService
@Service @FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT",fallbackFactory = DeptServiceFallbackFactory.class) public interface DeptFeignService { @PostMapping("/dept/add") boolean addDept(@RequestBody Dept dept); @GetMapping("/dept/get/{id}") Dept queryDept(@PathVariable("id") long id); @GetMapping("/dept/list") List<Dept> queryAll(); } 复制代码
接下来我们修改服务消费者 springcloud-consumer-dept-80-feign 项目。
1、修改配置文件
server: port: 80 #feign默认是不开启hystrix,默认值为false feign: hystrix: enabled: true #eureka配置 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001:7001/eureka/,http://eureka7002:7002/eureka/,http://eureka7003:7003/eureka/ 复制代码
2、修改 DeptConsumerController
@RestController public class DeptConsumerController { @Autowired DeptFeignService service; @RequestMapping("/consumer/dept/get/{id}") public Dept getDept(@PathVariable("id") long id) { return service.queryDept(id); } @RequestMapping("/consumer/dept/list") public List<Dept> queryAll() { return service.queryAll(); } @RequestMapping(name = "/consumer/dept/add") public boolean addDept(Dept dept) { return service.addDept(dept); } } 复制代码
3、启动一个注册中心和一个服务提供者,以及本项目,然后访问 http://localhost/consumer/dept/get/3,页面正常显示数据,当关闭服务提供者,再次访问上述网址,页面会显示如下内容:
{"deptId":3,"dpName":"id=3=>没有对应的信息,null","dbSource":"服务降级处理"} 复制代码
Hystrix-dashboard
Hystrix-dashboard 是一款针对 Hystrix 进行实时监控的工具,通过 Hystrix-dashboard 我们可以直观的看到各 Hystrix Command 的请求响应时间,请求成功率等数据。
新建一个名为 springcloud-consumer-hystrix-dashboard 的普通 maven 项目,当作监控中心。
1、导入相关依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>1.4.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId> <version>1.4.6.RELEASE</version> </dependency> </dependencies> 复制代码
2、新增配置文件,设置端口号
server: port: 9001 复制代码
3、新增启动类
package com.msdn; @SpringBootApplication @EnableHystrixDashboard @EnableHystrix public class DeptConsumerDashboard { public static void main(String[] args) { SpringApplication.run(DeptConsumerDashboard.class); } } 复制代码
4、启动该项目,访问 http://localhost:9001/hystrix,页面内容如下:
监控项目
对 springcloud-provider-dept-hystrix-8001 项目进行扩增。
在入口类 DeptProviderHystrix_8001 中增加 ServletRegistrationBean 注入。
@SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient @EnableCircuitBreaker //开启熔断器 public class DeptProviderHystrix_8001 { public static void main(String[] args) { SpringApplication.run(DeptProviderHystrix_8001.class,args); } @Bean public ServletRegistrationBean getServletRegistrationBean(){ ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet()); servletRegistrationBean.addUrlMappings("/actuator/hystrix.stream"); return servletRegistrationBean; } } 复制代码
启动一个注册中心,并启动该监控项目,在监控中心页面输入上述图片中内容,然后点击按钮跳转。
当你不断访问 http://localhost:8001/dept/get/1,可以观察上述页面变化。
详情代码访问: https://github.com/Acorn2/springcloud-eureka ,如有疑惑可联系我。