springcloud系列—Hystrix—第3章-3: Hystrix 服务降级(fallback)与异常处理,Hystrix依赖隔离(命令名称-分组和线程池)、请求缓存与清除缓存、断路器

资料参考:《Spring Cloud 微服务实战》

目录

服务降级

在HystrixCommand中可以通过重载getFallback()方法来实现服务降级逻辑。

在 HystrixObservableCommand 实现得 Hystrix 命令中,我们可以通过重载 resumenWithFallback 方法来实现服务降级逻辑。

使用注解来定义服务降级逻辑

异常处理

异常传播(就是不触发fallback)

异常获取

Hystrix依赖隔离

依赖隔离

如何使用

命令名称、分组和线程池划分

请求缓存

清理失效缓存功能

Hystrix断路器


 

服务降级

fallbackMethod所描述的函数实际上是Hystrix命令执行失败时使用得后备方法,用来实现服务降级处理逻辑。

  • 在HystrixCommand中可以通过重载getFallback()方法来实现服务降级逻辑。

Hystrix会在执行 run() 得过程中,通过重载 getFallback() 方法来实现服务降级逻辑,Hystrix会在执行 run() 得过程中出现错误、超时、线程池拒绝、断路器熔断等情况时,执行 getFallback() 方法内得逻辑,比如:

public class MyCommand extends HystrixCommand<String> {

    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();
    }
}
  • 在 HystrixObservableCommand 实现得 Hystrix 命令中,我们可以通过重载 resumenWithFallback 方法来实现服务降级逻辑。

该方法会返回一个 Onservable 对象,当命令执行失败得时候, Hystrix 会将 Observable 中得结果通知给所有得订阅者。

public class MyObservableCommand extends HystrixObservableCommand<String> {

    private RestTemplate restTemplate;

    protected MyObservableCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @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<String> 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";
    }
}

异常处理

异常传播(就是不触发fallback)

在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 因为异常进入到服务降级得逻辑后,我们有时候需要获取到异常,对不同得异常进行针对性得处理,所以其实是可以获取到异常得。

 

Hystrix依赖隔离

我们已经体验了如何使用@HystrixCommand来为一个依赖资源定义服务降级逻辑。实现方式非常简单,同时对于降级逻辑还能实现一些更加复杂的级联降级等策略。之前对于使用Hystrix来实现服务容错保护时,除了服务降级之外,我们还提到过线程隔离、断路器等功能。那么在本篇中我们就来具体说说线程隔离。

依赖隔离

“舱壁模式”对于熟悉Docker的读者一定不陌生,Docker通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算某个在Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。

通过对依赖服务的线程池隔离实现,可以带来如下优势:

  • 应用自身得到完全的保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的额其余部分。
  • 可以有效的降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响到应用其他的请求。
  • 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下容器级别的清理恢复速度要慢得多。
  • 当依赖的服务出现配置错误的时候,线程池会快速的反应出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新(后续会通过Spring Cloud Config与Spring Cloud Bus的联合使用来介绍)来处理它。
  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,此时线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
  • 除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步的访问。

总之,通过对依赖服务实现线程池隔离,让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

如何使用

说了那么多依赖隔离的好处,那么我们如何使用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<String>{

    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方法之前执行。

  • 减少重复得请求数,降低依赖服务得并发度。
  • 在同一用户得请求中,相同依赖服务得返回数据始终保持一致。
  • 请求缓存在 run() 和 construct() 执行之前生效,所以可以有效得减少不必要得现场开销。

清理失效缓存功能

清理缓存,开启请求缓存之后,我们在读的过程中没有问题,但是我们如果是写,那么我们继续读之前的缓存了 ,我们需要把之前的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超时时间的问题,我们的调用依然很有可能产生堆积。

这个时候断路器就会发挥作用,那么断路器是在什么情况下开始起作用呢?这里涉及到断路器的三个重要参数:快照时间窗、请求总数下限、错误百分比下限。这个参数的作用分别是:

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
  • 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用此时不足20次,即时所有的请求都超时或其他原因失败,断路器都不会打开。
  • 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。

那么当断路器打开之后会发生什么呢?我们先来说说断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当hystrix超时之后返回fallback,每个请求时间延迟就是近似hystrix的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。当熔断器在10秒内发现请求总数超过20,并且错误百分比超过50%,这个时候熔断器打开。打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,hystrix也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

通过上面的一系列机制,hystrix的断路器实现了对依赖资源故障的端口、对降级策略的自动切换以及对主逻辑的自动恢复机制。这使得我们的微服务在依赖外部服务或资源的时候得到了非常好的保护,同时对于一些具备降级逻辑的业务需求可以实现自动化的切换与恢复,相比于设置开关由监控和运维来进行切换的传统实现方式显得更为智能和高效。

猜你喜欢

转载自blog.csdn.net/weixin_40663800/article/details/84644213