使用Hystrix实现服务容错
Hystrix
Hystrix是运行在客户端的,通过一定的手段,规避在不同但互相关联的服务调用中出现的故障和过分的延迟响应,从而防止发生服务雪崩。
Hystrix实现容错的手段
- 服务降级
- 服务熔断
- 请求合并
- 线程池隔离
- 信号量隔离
SpringCloud集成Hystrix
- 导入相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
注意,因为Fegin中包含了Hystrix,如果使用了Fegin客户端请求框架,只需要在配置文件中设置
feign.hystrix.enabled=true
开启Hystrix即可。
- 在启动类上添加
@EnableHystrix
注解
服务降级
所谓服务降级就是说当服务迟迟没有响应,不会继续等待,而是会执行一个兜底的方法避免请求的阻塞。一定程度上降低了故障的发生。
实现步骤
在客户端的Service层创建一个方法并为其添加@HystrixCommand
注解
@HystrixCommand(fallbackMethod = "error")
public String helloService(){
return restTemplate.getForEntity("http://myservice/hello",String.class).getBody();
}
其中fallbackMethod属性为这个rest请求指定了一个失败的回调方法error()
public String error(){
return "error";
}
紧接着在Controller层创建一个接口,并调用刚才的业务方法helloService()
@RequestMapping(value = "/consumer",method = RequestMethod.GET)
public String helloConsumer(){
return helloService.helloService();
}
为了测试需要,将myservice
的hello
接口做一些小小的修改:在方法中让他沉睡1000ms
(这是因为Hystrix的默认超时时间为1000ms,达到了这个时间就会进行降级)
@GetMapping("/hello")
public String index() throws InterruptedException {
client.getServices().forEach(System.out::println);
Thread.sleep(1000);
return "Hello world";
}
现在,启动注册中心、客户端、服务端,开始测试
测试达到预期结果!
服务熔断
服务熔断跟服务降级很相似,不同的地方在于熔断表示会断开与服务的连接,不过这种断开一般是暂时的,后续还会尝试与服务进行连接,如果还是不行则继续保持熔断状态,反复如此。
实现步骤
在客户端的Service层创建一个方法并为其添加@HystrixCommand
注解,并为其设置一些参数
@HystrixCommand(fallbackMethod = "error",commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "2"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "30")
})
public String helloService(){
return restTemplate.getForEntity("http://myservice/hello",String.class).getBody();
}
其中circuitBreaker.requestVolumeThreshold
表示请求数量、execution.isolation.thread.timeoutInMilliseconds
表示请求超时时间、circuitBreaker.errorThresholdPercentage
表示请求失败数所占请求数量的百分比。综合来看,大意就是:请求数量达到了2
,且失败的请求数占其30%
,则会熔断当前请求。
同样为了便于测试,将myservice
的hello
接口做一些小小的修改:设置其睡眠时间为2000ms
@GetMapping("/hello")
public String index() throws InterruptedException {
System.out.println(client.getServices().get(0)+"已收到请求");
Thread.sleep(2000);
return "Hello world";
}
测试,请求两次后的结果如下
发现服务端只接受到了一次请求,这是因为在第二次请求时由于失败的请求比例已经大于等于阈值,Hystrix
已经对服务做了熔断。但正如之前所述,熔断不是永久的,默认5000ms
后会暂时解除熔断如果请求仍失败,则再次熔断。
5000ms
后再次发起请求
再次接收到请求
请求合并
将多个请求合并为一个请求进行服务的访问,能一定程度上减少网络开销
实现步骤
在客户端的Service层创建一个方法并为其添加@HystrixCollapser
注解,并对其属性进行设置。需要注意的是,这个方法中的代码不会被执行
@HystrixCollapser(batchMethod = "mybatch",scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,collapserProperties = {
@HystrixProperty(name = "maxRequestsInBatch",value = "200"),
@HystrixProperty(name = "timerDelayInMilliseconds",value = "10"),
})
public Future<String> helloService2(String name){
System.out.println("是否被执行?");
return null;
}
其中,batchMethod
属性表示执行合并后请求的方法;maxRequestsInBatch
表示批处理最大请求数,就是说最大能合并多少条请求;timerDelayInMilliseconds
指执行合并操作的毫秒数。需要注意的是方法的返回类型必须为Future<T>
类型。
创建一个mybatch
方法,并挂上@HystrixCommand
注解,这个方法中的参数、返回值是一个集合类型。这是因为多个参数被合并为一个集合,多个返回值也被合并为一个集合的原因
@HystrixCommand
public List<String> mybatch(List<String> names){
List<String> list = restTemplate.postForObject("http://myservice/hello2",names,List.class);
return list;
}
在Controller中新建一个接口,调用两次helloService2()
方法,意为将这两个请求合并为一个请求
@RequestMapping(value = "/consumer2")
public String helloConsumer2() throws ExecutionException, InterruptedException {
Future<String> result1 = helloService.helloService2("张三");
Future<String> result2 = helloService.helloService2("李四");
System.out.println(result1.get());
System.out.println(result2.get());
return "OK";
}
而在服务端,仍然要返回合并了多条数据的集合类型
@PostMapping("/hello2")
public List<String> index2(@RequestBody List<String> names) throws InterruptedException {
System.out.println("已经接收到批处理请求");
List<String> list = new ArrayList<>();
list.add(names.get(0));
list.add(names.get(1));
return list;
}
打开浏览器访问/consumer2
接口
服务端只接收到了一次请求,所以测试成功。
线程池隔离
默认情况下,web程序需要依赖tomcat的内置线程池发起服务的调用,这就导致程序中所有的请求都是使用同一个线程池,如果程序中存在一个请求占用的线程比较多,就会间接影响到其他的请求调用。通过为不同请求指定Hystrix
的线程池可以实现线程池的隔离,使不同请求互不影响。
实现步骤
在客户端的Service层创建一个方法并为其添加@HystrixCommand
注解,并对其属性进行设置。
@HystrixCommand(groupKey = "apple",threadPoolKey = "apple",threadPoolProperties = {
@HystrixProperty(name = "coreSize",value = "4"),
})
public String thread1(){
System.out.println(Thread.currentThread().getName());
return "thread1";
}
public String thread2(){
System.out.println(Thread.currentThread().getName());
return "thread2";
}
其中groupKey
表示分组名称、threadPoolKey
表示线程池名称,HystrixProperty
则用来指定线程数量。上面的示例为thread1
指定了Hystrix
线程池,thread2
则使用tomcat线程池,从而实现请求之间互不影响。
在Controller中添加一个接口,分别调用thread1
和thread2
@RequestMapping("/thread")
public String threadPool(){
helloService.thread1();
helloService.thread2();
return "OK";
}
打开浏览器访问/thread
接口
结果显示,thread1
和thread2
分别使用了不同的线程池,不会互相影响。
信号量隔离
可以将信号量理解为线程计数器,假设1s
内最多允许2条线程访问请求,第3条线程则会调用fallbackMethod
指定的兜底方法。所以说信号量隔离实现了服务的限流。
实现步骤
在客户端的Service层创建一个方法并为其添加@HystrixCommand
注解,并对其属性进行设置,并指定兜底方法
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHORE"),
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests",value = "2")
},fallbackMethod = "down")
public String semaphore() throws InterruptedException {
System.out.println("OK");
Thread.sleep(900);
return null;
}
其中,execution.isolation.strategy
指定使用信号量的方式实现服务的降级(默认使用线程池),execution.isolation.semaphore.maxConcurrentRequests
则表示1s
内最多只能有2个线程访问服务。
创建一个降级方法
public String down(){
System.out.println("执行降级");
return null;
}
在Controller中添加一个新的接口
@RequestMapping("/semaphore")
public String semaphore() throws InterruptedException {
helloService.semaphore();
return "OK";
}
使用apache-jmeter工具进行请求测试。因为设置了信号量最多为2,所以这里指定3个请求线程
设置请求地址并发起请求
控制台打印信息:
由于1s内只允许2个线程请求服务,所以当第3个发起请求会被降级处理。
谢谢大家
转载自:https://juejin.cn/post/7242969515345084477