Spring Cloud 基础组件 - Hystrix

March . 25 . 2021

前言

      这篇继续介绍 Spring Cloud 相关的基础组件,我将用一个实际的Demo来记录一下如何使用 Spring Cloud 结合 Netflix Hystrix 来增强客户端的弹性。

项目地址 spring-cloud-hystrix-demo

      所有的系统,特别是分布式系统,都会遇到故障。如何构建应用程序来应对这种故障,是在开发的过程中需要着重考虑的。我们常见的处理方式是在应用程序的每一层构建冗余,利用集群、服务间的负载均衡等技术来提高程序的高可用性。

      但以上的几种方式只解决了构建弹性系统的一小部分问题,即当目标服务彻底崩溃时。当服务崩溃时,很容易就可以检测到该服务不在了,因此可以饶过他使用备用服务,然而,当服务仅仅运行缓慢,检测这个服务是否性能不佳并饶过它就不是那么容易了。

      性能不佳的服务所导致的潜在问题是,它不仅难以检测,还会触发连锁效应,从而影响整个应用程序的运行。如果没有适当的保护措施,一个性能不佳的服务很可能会迅速拖垮多个应用程序。尤其是在微服务的场景中,因为这些程序大多都是由多个细粒度的分布式服务所构成的。

客户端的弹性模式

     客户端弹性模式的重点在于,在远程服务发生错误或表现不佳时保护远程服务免于奔溃。实现方式就是让客户端“快速失败”,而不是消耗诸如数据库连接和线程池之类的宝贵资源,并且可以防止远程服务的问题影响到其他服务运行。

      客户端的弹性模式有四种,分别是:

  1.  客户端负载均衡模式
  2.  断路器模式
  3.  后备模式
  4.  舱壁模式

      下图详细的展示了这些模式如何用于客户端与微服务之间

1.png

图片来自 Spring Microservices In Action 一书

     

       这些模式是在调用远程资源的客户端中实现的,他们逻辑上位于消费远程资源的客户端和资源本身之间。



具体实现

  • 客户端的负载均衡模式

      在上一篇中我已经通过 Netflix Eureka 结合 Ribbon 实现了客户端的负载均衡,Ribbon 库是 Eureka 开箱即用的功能,这里就不再赘述, 后续我将在这个版本的基础上实现客户端弹性的其他三种模式。

      项目结构图如下所示:

Image



  • 断路器模式

      断路器模式是模仿电路断路器的客户端弹性模式。在电器系统中,断路器将检测是否有过多的电流流过电线。如果断路器检测到问题,它将断开与电器系统的其余部分连接,保护下游的部件不被烧毁。

     当远程服务被调用时,断路器将监视这个调用,如果调用的时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如果对某一个远程资源的调用失败次数足够多,那么断路器就会采取快速失败,阻止调用失败的远程资源。

      在 Netflix Hystrix 中实现断路器十分简单

ClientService.java

@HystrixCommand(
        commandProperties = {
                @HystrixProperty(
                  name = "execution.isolation.thread.timeoutInMilliseconds", 
                  value = "5000"
                 )
        }
)
public Map findClientbInfoByTimeOut() {

    log.info("断路器");

    // 睡眠 6s
    sleep6s();

    Map res = Maps.newHashMap();

    Object data = clientBRestTemplateClient.findClientB();
    res.put("status", "1");
    res.put("data", data);
    return res;
}

      只需要添加 @HystrixCommand 注解并对超时时间属性做配置即可,如上所示此处的超时时间是5s, 当方法执行超过5s时断路器就会介入并主动失败,抛出 HyStrixRuntimeException 的异常。



  • 后备模式

      当远程服务调用失败时,后备模式将提供代替的方法,并尝试通过其他的方式执行操作,而不是抛出一个异常。

      在 Netflix Hystrix 中后备模式的实现方式如下:

@HystrixCommand(fallbackMethod = "fallback")
public Map findClientbInfoByBack() {

    log.info("后备");

    // 睡眠 6s
    sleep6s();

    Map res = Maps.newHashMap();

    Object data = clientBRestTemplateClient.findClientB();
    res.put("status", "1");
    res.put("data", data);
    return res;
}

public Map fallback() {
    Map res = Maps.newHashMap();
    res.put("status", "3");
    res.put("data", "后备方法");
    return res;
}

      只需要添加 @HystrixCommand 注解并配置 fallbackMethod 属性,参数为与目标方法具有相同参数和返回值的方法名称,当方法调用超时或发生异常时,后备方法就会被调用并返回给用户。



  • 舱壁模式

      舱壁模式是建立在造船的概念基础上的。采用舱壁设计,一艘船被划分为完全隔离的隔间,这称为舱壁。即使船的一部分被击穿,舱壁会将水限制在被击穿的船的区域内,防止整艘船灌满并沉没。

      同样的概念可以应用于必须于多个远程资源交互的服务。通过使用舱壁模式,可以把远程资源的调用分到对应的线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。线程池充当服务的“舱壁”。每个远程资源都是隔离的,并分配给线程池。如果一个服务响应缓慢,那么这种服务调用的线程池就会饱和并停止处理请求,而对其他服务的线程池并不会有影响。

      在 Netflix Hystrix 中舱壁模式的实现方式如下:

@HystrixCommand(
        threadPoolKey = "myBulk",
        threadPoolProperties = {
                @HystrixProperty(name = "coreSize", value = "30"),
                @HystrixProperty(name = "maxQueueSize", value = "10")
        }
)
public Map findClientbInfoByBulk() {

    log.info("舱壁");

    Map res = Maps.newHashMap();

    Object data = clientBRestTemplateClient.findClientB();
    res.put("status", "1");
    res.put("data", data);
    return res;
}

     如果不配置 threadPoolKey 属性 Netflix Hystrix 会提供一个默认的线程池,并且这个线程池是共享的,当配置了该属性后 Netflix Hystrix 会新建一个线程池只处理该方法。

效果如下所示: 

2.png



小结

      断路器模式为远程调用提供了以下关键的能力:

  1. 快速失败 - 应用程序会很快速的失败,防止拖垮整个程序。
  2. 优雅的失败 - 通过超时和快速失败,短路器模式使应用程序有能力优雅地失败,通过替代机制来提高程序的用户体验。
  3. 无缝恢复 - 有了短路器作为中介,断路器可以定期体检所请求的资源是否重新上线,并在没有人为干预的情况下重新允许对该资源的访问。

      在大型的基于云的应用程序中可能运行着数十数百个服务,这种优雅的恢复能力至关重要,它可以显著的减少恢复服务所需的时间,并大大减少人为干预而可能造成更严重问题的风险。

      关于 Netflix Hystrix 的更多详细配置可以访问 官方的文档 查看。