资料参考:《Spring Cloud 微服务实战》
目录
服务降级
在HystrixCommand中可以通过重载getFallback()方法来实现服务降级逻辑。
在 HystrixObservableCommand 实现得 Hystrix 命令中,我们可以通过重载 resumenWithFallback 方法来实现服务降级逻辑。
使用注解来定义服务降级逻辑
异常处理
异常传播(就是不触发fallback)
异常获取
Hystrix依赖隔离
依赖隔离
如何使用
命令名称、分组和线程池划分
请求缓存
清理失效缓存功能
Hystrix断路器
fallbackMethod所描述的函数实际上是Hystrix命令执行失败时使用得后备方法,用来实现服务降级处理逻辑。
Hystrix会在执行 run() 得过程中,通过重载 getFallback() 方法来实现服务降级逻辑,Hystrix会在执行 run() 得过程中出现错误、超时、线程池拒绝、断路器熔断等情况时,执行 getFallback() 方法内得逻辑,比如:
public class MyCommand extends HystrixCommand {
private RestTemplate restTemplate;
protected MyCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected String getFallback() {
return "调用出异常啦,启用熔断器";
}
@Override
protected String run() throws Exception {
return restTemplate.getForEntity("http://PROVIDER-EUREKA/index",String.class).getBody();
}
}
该方法会返回一个 Onservable 对象,当命令执行失败得时候, Hystrix 会将 Observable 中得结果通知给所有得订阅者。
public class MyObservableCommand extends HystrixObservableCommand {
private RestTemplate restTemplate;
protected MyObservableCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected Observable construct() {
return Observable.create(new Observable.OnSubscribe() {
@Override
public void call(Subscriber super String> subscriber) {
try{
if (!subscriber.isUnsubscribed()){
String a = restTemplate.getForEntity("http://PROVIDER-EUREKA/index",String.class).getBody();
subscriber.onNext(a);
subscriber.onCompleted();
}
}catch (Exception e){
subscriber.onError(e);
}
}
});
}
@Override
protected Observable resumeWithFallback() {
return super.resumeWithFallback();
}
}
我们需要将具体得 Hystrix 命令与 fallback 实现函数定义再同一个类中,并且 fallbackMethod 得值必须与实现得 fallback 方法得名字相同。由于必须定义在一个类中,所有对 fallback得访问修饰符没有要求。
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(){
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-eureka");
StringBuilder sb = new StringBuilder();
sb.append("host: ").append(serviceInstance.getHost()).append(", ");
sb.append("port: ").append(serviceInstance.getPort()).append(", ");
sb.append("uri: ").append(serviceInstance.getUri());
System.out.println(sb.toString());
return restTemplate.getForEntity("http://PROVIDER-EUREKA/hello",String.class).getBody();
}
public String helloFallback(){
return "error";
}
}
在HystrixCommand 实现得 run() 方法中抛出异常时,除了 HystrixBadRequestException 之外,其他异常均会被 Hystyix 认为命令执行失败并触发服务降级逻辑。所有当需要在命令执行中抛出不触发服务降级得异常时使用它。
而在使用注册配置实现 Hystrix 命令时,它还支持忽略指定异常类型功能,只需要通过设置 @HystrixCommand 注解得 ignoreExceptions参数,比如:
@HystrixCommand(fallbackMethod = "helloFallback",ignoreExceptions = {NullPointerException.class})
public String helloService(){
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-eureka");
StringBuilder sb = new StringBuilder();
sb.append("host: ").append(serviceInstance.getHost()).append(", ");
sb.append("port: ").append(serviceInstance.getPort()).append(", ");
sb.append("uri: ").append(serviceInstance.getUri());
System.out.println(sb.toString());
return restTemplate.getForEntity("http://PROVIDER-EUREKA/hello",String.class).getBody();
}
public String helloFallback(){
return "error";
}
如上面得代码,当抛出得异常是NullPointException得时候,Hystrix会把它包装到 HystrixBadRequest- Exception 中,这样就不会触发 fallback逻辑了。
当 Hystrix 因为异常进入到服务降级得逻辑后,我们有时候需要获取到异常,对不同得异常进行针对性得处理,所以其实是可以获取到异常得。
我们已经体验了如何使用@HystrixCommand
来为一个依赖资源定义服务降级逻辑。实现方式非常简单,同时对于降级逻辑还能实现一些更加复杂的级联降级等策略。之前对于使用Hystrix来实现服务容错保护时,除了服务降级之外,我们还提到过线程隔离、断路器等功能。那么在本篇中我们就来具体说说线程隔离。
“舱壁模式”对于熟悉Docker的读者一定不陌生,Docker通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算某个在Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。
通过对依赖服务的线程池隔离实现,可以带来如下优势:
总之,通过对依赖服务实现线程池隔离,让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。
说了那么多依赖隔离的好处,那么我们如何使用Hystrix来实现依赖隔离呢?其实,我们在上一篇定义服务降级的时候,已经自动的实现了依赖隔离。
在上一篇的示例中,我们使用了@HystrixCommand来将某个函数包装成了Hystrix命令,这里除了定义服务降级之外,Hystrix框架就会自动的为这个函数实现调用的隔离。所以,依赖隔离、服务降级在使用时候都是一体化实现的,这样利用Hystrix来实现服务容错保护在编程模型上就非常方便的,并且考虑更为全面。除了依赖隔离、服务降级之外,还有一个重要元素:断路器。我们将在下一篇做详细的介绍,这三个重要利器构成了Hystrix实现服务容错保护的强力组合拳。
以继承方式实现的Hystrix命令使用类名作为默认的命令名称,我们也可以在构造函数中通过Setter静态类来设置,比如:
public HelloCommend() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")));
}
从上面Setter的使用中可以看到,我们并没有直接设置命令名称,而是先调用了withGroupkey来设置命令组名,然后才通过调用andCommandkey来设置命令名。这是因为在Setter的定义中,只有withGroupkey静态函数可以创建Setter的实例,听以
Groupkey是每个setter必需的参数,而CommandKey则是一个可选参数。
通过设置命令组, Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。那么为什么一定要设置命令组呢?因为除了根
据组能实现统计之外, Hystrix命令默认的线程划分也是根据命令分组来实现的。默认情况下, Hystrix会让相同组名的命令使用
同一个线程池,所以我们需要在创建Hystrix命令时为其指定命令组名来实现默认的线程池划分。
如果Hystrix的线程池分配仅仅依靠命令组来划分,那么它就显得不够灵活了,所以Hystrix还提供了HystrixThreadPoolKey来对
线程池进行设置,通过它我们可以实现更细粒度的线程池划分,比如:
public HelloCommend() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}
如果在没有特别指定HystrixThreadPoolkey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量通过
HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务
逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。
上面已经介绍了如何为通过继承实现的HystrixCommand设置命令名称、分组以及线程池划分,那么当我们使用
@HystrixCommand注解的时候,又该如何设置呢?只需设置@HystrixCommand注解的commandKey, groupkey以及
threadPoolKey属性即可,它们分别表示了命令名称、分组以及线程池划分,比如我们可以像下面这样进行设置:
@HystrixCommand(fallbackMethod = "helloFeedback1",groupKey = "",threadPoolKey = "",commandKey = "")
public String helloConsumer () {
return restTemplate.getForEntity("http://EUREKA-CLIENT/hello",
String.class).getBody();
}
高并发环境下如果能处理好缓存就可以有效的减小服务器的压力,Java中有许多非常好用的缓存工具,比如Redis、EHCache等,当然在Spring Cloud的Hystrix中也提供了请求缓存的功能,我们可以通过一个注解或者一个方法来开启缓存,进而减轻高并发环境下系统的压力。本文我们就来看看Hystrix中请求缓存的使用。
在 Hystrix 请求缓存的使用非常简单,我们只需要在实现 HystrixCommand 和 HystrixObservableCommand 命令中重载 getCacheKey()方法,就能实现缓存请求。
public class HelloCommend extends HystrixCommand{
private RestTemplate restTemplate;
public HelloCommend() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}
public HelloCommend(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
String forObject = restTemplate.getForObject("http://EUREKA-CLIENT/hello", String.class);
return forObject;
}
@Override
protected String getFallback() {
return "error";
}
@Override
protected String getCacheKey() {
return super.getCacheKey();
}
}
在上面得例子中,我们通过 getCacheKey 方法返回得请求缓存 key 值,就能让该请求命令具备缓存功能。系统在运行时会根据getCacheKey方法的返回值来判断这个请求是否和之前执行过的请求一样,即被缓存,如果被缓存,则直接使用缓存数据而不去请求服务提供者,那么很明显,getCacheKey方法将在run方法之前执行。
清理缓存,开启请求缓存之后,我们在读的过程中没有问题,但是我们如果是写,那么我们继续读之前的缓存了 ,我们需要把之前的cache清掉
说明 :
1.其中getInstance方法中的第一个参数的key名称要与实际相同
2.clear方法中的cacheKey要与getCacheKey方法生成的key方法相同
3.注意我们用了commandKey是test,大家要注意之后new这个Command的时候要指定相同的commandKey,否则会清除不成功
/**
* 清理缓存
* 开启请求缓存之后,我们在读的过程中没有问题,但是我们如果是写,那么我们继续读之前的缓存了
* 我们需要把之前的cache清掉
* 说明 : 1.其中getInstance方法中的第一个参数的key名称要与实际相同
* 2.clear方法中的cacheKey要与getCacheKey方法生成的key方法相同
* 3.注意我们用了commandKey是test,大家要注意之后new这个Command的时候要指定相同的commandKey,否则会清除不成功
*/
public static void flushRequestCache(Long id){
HystrixRequestCache.getInstance(
HystrixCommandKey.Factory.asKey("test"), HystrixConcurrencyStrategyDefault.getInstance())
.clear(String.valueOf(id));
}
我们来说说断路器的工作原理。当我们把服务提供者中加入了模拟的时间延迟之后,在服务消费端的服务降级逻辑因为hystrix命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于Hystrix超时时间的问题,我们的调用依然很有可能产生堆积。
这个时候断路器就会发挥作用,那么断路器是在什么情况下开始起作用呢?这里涉及到断路器的三个重要参数:快照时间窗、请求总数下限、错误百分比下限。这个参数的作用分别是:
那么当断路器打开之后会发生什么呢?我们先来说说断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当hystrix超时之后返回fallback
,每个请求时间延迟就是近似hystrix的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。当熔断器在10秒内发现请求总数超过20,并且错误百分比超过50%,这个时候熔断器打开。打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,hystrix也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
通过上面的一系列机制,hystrix的断路器实现了对依赖资源故障的端口、对降级策略的自动切换以及对主逻辑的自动恢复机制。这使得我们的微服务在依赖外部服务或资源的时候得到了非常好的保护,同时对于一些具备降级逻辑的业务需求可以实现自动化的切换与恢复,相比于设置开关由监控和运维来进行切换的传统实现方式显得更为智能和高效。