Spring Cloud Hystrix——快速入门

在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,最终导致自身服务的瘫痪。

举个例子,在一个电商网站中,我们可能会将系统拆分成,用户、订单、库存、积分、评论等一系列的服务单元。用户创建一个订单的时候,在调用订单服务创建订单的时候,会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因网络原因无法被访问到,导致创建订单服务的线程进入等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些等待线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用,这就是所谓的雪崩效应。
如下图所示:

造成雪崩原因可以归结为以下三个:

  • 服务提供者不可用(硬件故障,程序Bug,缓存击穿,用户大量请求)
  • 重试加大流量(用户重试,代码逻辑重试)
  • 服务调用者不可用(同步等待造成的资源耗尽)
    在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就会因依赖关系形成故障蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器等一系列的服务保护机制。

什么是断路器

断路器模式源于Martin Fowler的Circuit Breaker一文。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时的切断故障电路,防止发生过载、发热、甚至起火等严重后果。

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

在Spring Cloud中使用了Hystrix 来实现断路器的功能。Hystrix是Netflix开源的微服务框架套件之一,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

快速入门

首先构建一个如下图架构所示的服务调用关系。

  • eureka-server 工程:服务注册中心,端口为8761。
  • hello-service 工程:HELLO-SERVICE 的服务单元,两个实例启动端口分别为 8081 和 8082.
  • ribbon-consumer 工程:使用 Ribbon 实现的服务消费者,端口为 9000
    在未加入断路器之前,关闭8081的实例,发送 GET 请求到http://localhost:9000/ribbon-consumer ,可以获取下面的输入。 下面开始引入Spring Cloud Hystrix
  • 在ribbon-consumer引入hystrix依赖
    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
  • 在ribbon-consumer的主类中使用@EnableCircuitBreaker注解开启断路器功能:
  • 改造原来的服务消费方式,新增HelloService类,注入RestTemplate实例。将在ConsumerController(原来注入RestTemplate实例的controller)中对RestTemplate的使用迁移到hello函数中,最后,在hello函数上增加@HystrixCommand注解创建HystrixCommand的实现,同时利用fallbackMethod属性指定服务降级的实现方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class HelloService {
    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "helloFallback")
    public String hello() {
    return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody();
    }
    public String helloFallback() {
    return "error";
    }
    }
  • 修改ConsumerController类(原注入RestTemplate实例的类),注入上面实现的HelloService实例,并在helloConsumer中调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class ConsumerController {
    @Autowired
    HelloService helloService;

    @RequestMapping(value = "ribbon-consumer")
    public String helloConsumer() {
    return helloService.hello();
    }
    }
  • 验证断路器的回调

    • 重启之前关闭的8081工程,确保所有服务均已启动
    • 访问http://localhost:9000/ribbon-consumer 可以轮询两个HELLO-SERVICE,返回Hello World
    • 再次断开8081的HELLO-SERVICE
    • 访问http://localhost:9000/ribbon-consumer 返回error
      上面是通过断开具体的服务实例来模拟某个节点无法访问的情况,下面模拟服务阻塞(长时间未响应)的情况。修改HELLO-SERVICE的/hello接口
      1
      2
      3
      4
      5
      6
      7
      @RequestMapping(value = "/sleep", method = RequestMethod.GET)
      public String hello() throws Exception {
      int sleepTime = new Random().nextInt(3000);
      System.out.println("sleepTime : "+sleepTime);
      Thread.sleep(sleepTime);
      return "Hello Word"+sleepTime;
      }

通过Thread.sleep() 函数可让 /sleep 接口的处理线程不是马上返回内容,而是在阻塞几秒后才返回内容。由于 Hystrix 默认超时时间为 2000 毫秒,所以这里采用了 0 至 3000 的随机数以让处理过程有一定概率发生超时来触发断路器。为了更精确的观察断路器的触发,在消费者调用函数中做一些时间记录,具体如下:

1
2
3
4
@HystrixCommand(fallbackMethod = "sleepFallback")
public String sleep() {
return restTemplate.getForEntity("http://HELLO-SERVICE/sleep", String.class).getBody();
}

重新启动各服务,连续访问http://localhost:9000/ribbon-consumer 几次,可以观察到控制台中输出的sleepTime大于2000时,会返回error,即服务消费者因调用的服务超时从而触发熔断请求,并调用回调逻辑返回结果。