一、Hystrix的作用和目标
Hystrix是一种延迟、容错处理解决方案,能有效地阻止级联故障,保护整个系统处于可用的稳定状态。
- 对延迟和故障进行控制,保护应用系统;
- 在一个复杂的分布式系统中阻止级联故障;
- 快速失败和迅速恢复;
- 在合理的情况下回退和优雅降级;
- 准实时监控告警;
二、Hystrix入门案例
我们从start.spring.io
上下载一个项目,引入的依赖有Web和Hystrix(circuit-breaker),需要注意的是SpringBoot的版本不能高于2.5.0,本案例使用的SpringBoot是2.4.8,SpringCloud是2020.0.3。
2.1 注解方式
首先新建一个Controller:
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/getUserAddress/{userName}")
public String getUserAddress(@PathVariable String userName) throws Exception {
return userService.getUserAddress(userName);
}
}
再建立Service处理类,里面包含处理方法和降级方法:
@Service
public class UserService {
@HystrixCommand(fallbackMethod = "handleIllegalUserName")
public String getUserAddress(String userName) throws Exception {
if(ObjectUtils.isEmpty(userName) || !"Hystrix".equals(userName)){
throw new Exception("userName is null");
}
return "Hystrix";
}
public String handleIllegalUserName(String userName){
return "您输入的userName为空:" + userName;
}
}
最后,在启动类上增加开启Hystrix的注解即可:
@EnableHystrix
@SpringBootApplication
public class HystrixDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDemoApplication.class, args);
}
}
启动应用后,我们访问http://localhost:8080/getUserAddress/Hystrix
可以得到正常的处理结果,但如果是一个userName,就会走handleIllegalUserName
降级处理方法,需要注意的是,Exception并没有被抛出。
2.2 Command方式
首先我们创建一个Command,继承HystrixCommand,并实现自己需要的远程调用逻辑和降级逻辑。
public class AccountCommand extends HystrixCommand {
private String name;
public AccountCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() throws Exception {
if(ObjectUtils.isEmpty(name) || !"zhangxun".equals(name)){
throw new Exception("Account is illegal!");
}
return "zhangxun login success!";
}
@Override
protected String getFallback() {
return "please check your account name!";
}
}
然后复用2.1节的controller,里面新增一个mapping:
@GetMapping("/getAccount/{accountName}")
public String getAccount(@PathVariable String accountName) throws Exception {
// run方法的同步执行
// return new AccountCommand(accountName).execute();
// 异步执行run方法,适合需要同时执行多个Command的run方法场景
Future future = new AccountCommand(accountName).queue();
return future.get();
}
此时,我们访问该地址,对于错误的accountName就可以走降级处理方法getFallBack了。
三、在Feign中使用Hystrix
Feign自身就集成了Hystrix,只要在使用Feign的时候开启Hystrix即可。我们还是在上面的例子中进行示例演示。
首先,我们引入Feign的依赖:
org.springframework.cloud
spring-cloud-starter-openfeign
3.0.3
然后创建对应的Controller和Service:
@RestController
public class GitHubController {
@Autowired
private GitHubFeignService gitHubFeignService;
@GetMapping("/getGitHubInfo/{repoName}")
public String getGitHubInfo(@PathVariable String repoName) throws Exception {
return gitHubFeignService.searchRepo(repoName);
}
}
@FeignClient(name = "github", url = "https://api.github.com", fallback = GitHubFeignServiceFallBack.class)
public interface GitHubFeignService {
/**
* 等同于调用https://api.github.com/search/repositories?q=xxx
* @param queryString
* @return
*/
@RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
String searchRepo(@RequestParam("q") String queryString);
}
然后我们需要一个Fallback类,实现Service接口,定义降级处理方案:
@Component
public class GitHubFeignServiceFallBack implements GitHubFeignService {
@Override
public String searchRepo(String queryString) {
return "请检查github地址是否正确?!";
}
}
最后,我们在启动类上开启Feign的使用:
@EnableHystrix
@EnableFeignClients
@SpringBootApplication
public class HystrixDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDemoApplication.class, args);
}
}
默认情况下,Feign的Hystrix是关闭的,所以此时我们调用Controller接口http://localhost:8081/getGitHubInfo/Hystrix
,是能正常返回github关于Hystrix的搜索结果的,然后我们故意将GitHubFeignService的url参数写错为githug,此时我们重启后访问就会报错如下内容:
java.net.UnknownHostException: api.githug.com
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184) ~[na:1.8.0_181]
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) ~[na:1.8.0_181]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[na:1.8.0_181]
然后我们在配置文件中开启Hystrix:
# SpringCloud2020以后的版本开启方式
feign.circuitbreaker.enabled=true
# SpringCloud2021以前的版本开启方式
# feign.hystrix.enabled=true
然后再次重启访问,就会发现降级方案生效了,后台也不再抛出异常。
四、熔断
上面讲述的都是Hystrix降级的例子,现在来介绍下熔断。
默认情况下,只要Hystrix开启了熔断,那么在10秒内如果请求书超过20个,有50%以上的请求发生了异常,Hystrix就会对其进行熔断,并执行fallBack降级方案。然后每隔5秒会半开一次,请求会去真的调用关联方,算是进行服务可用性的试探,如果请求成功了,那么就会自动关闭熔断。
如下是一些配置项可以用来控制Hystrix:
#是否启用熔断器,默认true
circuitBreaker.enabled=true
#是否强制打开熔断器,始终保持打开状态,默认为false
circuitBeaker.forceOpen=false
#是否强制关闭熔断器,始终保持关闭状态,默认为false
circuitBeaker.forceClosed=false
#单位时间内触发熔断器的错误率,默认50%
circuitBreaker.errorThresholdPercentage=50
#单位时间内出发熔断器的基数,只有满足这么多请求时才会计算错误率,默认20
circuitBreaker.requestVolumeThreshold=20
#表示熔断器开启后,多少时间之后开始半开试探,默认5000ms
circuitBreaker.sleepWindowInMilliseconds=5000
五、限流
在前面讲解RateLimiter的时候,我们知道了限流的两种基本算法:令牌桶和漏桶算法。令牌桶是可以将前一段时间没有用掉的令牌余额在当前时间继续使用;漏桶是只允许使用当前时间的资源,前面时间即使有余额也不能使用了。Hystrix则是利用web容器的线程数来实现限流的,Hystrix的限流又称为资源隔离,隔离的方式有如下两种。
5.1 信号量资源隔离
前面文章有讲到过信号量来实现限流的实例,可以先行参考。
Hystrix默认是使用线程池做资源隔离的,所以如果要使用信号量做资源隔离的话,需要在配置文件中做如下的配置:
# THREAD是默认配置
# execution.isolation.strategy=ExecutionIsolationStrategy.THREAD
execution.isolation.strategy=ExecutionIsolationStrategy.SEMAPHORE
信号量其实就是一个计数器,限制某个服务同一时间最多只能有多少个并发,从而保护服务器不会因为某个单独服务的并发量太高而挂掉,从而影响其他的服务。当一个请求达到服务器的时候,接受请求和执行下游服务调用的线程是同一个线程,不存在线程切换的性能开销。我们可以通过如下配置来设置每个服务的最大信号量:
# 默认值为10
execution.isolation.semaphore.maxConcurrentRequests=100
当某个服务的并发请求数达到阈值的时候,就可以快速失败,执行降级。
总结:
- 优点
- 实现简单,符合大部分的使用场景;
- 没有线程切换,效率高,性能开销小;
- 缺点
- 不支持异步,比如需要同时调用多个依赖方,不能异步同时发起,必须同步发起;
- 不支持超时设置,调用依赖一旦执行只能等待服务响应或者从网络超时中返回,可能出现长时间的等待情况;
5.2 线程池资源隔离
由于如上信号量的缺点,才有了现在Hystrix的默认线程池限流方案,会为每一个Command服务创建一个线程池。
其中,接收用户请求的线程和执行依赖调用的线程是分开的。前者只负责把请求任务提交到对应Command的线程池中,然后接待其它用户请求任务去了,因此不论当前Command线程池是否忙碌,都不影响其它Command服务的执行。如果当前Command的线程池资源耗尽,并且队列满时,直接就快速失败执行降级策略。
我们可以通过如下的配置来设置线程池的参数:
#并发执行的最大线程数,默认10
hystrix.threadpool.default.coreSize=200
#BlockingQueue的最大队列数,默认值-1
hystrix.threadpool.default.maxQueueSize=1000
#即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝,默认值5
hystrix.threadpool.default.queueSizeRejectionThreshold=800
总结:
- 优点
- 支持异步;
- 支持设置超时,不会存在长时间的等待;
- 缺点
- 存在线程切换,有额外的性能开销;
- 容易造成线程池资源浪费;