服务容错和Hystrix

在微服务架构中,由于某个服务的不可用导致一系列的服务崩溃,被称之为雪崩效应。所以防御服务的雪崩效应是必不可少的,在Spring Cloud中防雪崩的利器就是Hystrix,Spring Cloud Hystri是基于Netflix Hystrix实现的。Hystrix的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务容错、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

Hystrix中的资源隔离:

在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.

关于服务降级:

  • 优先核心服务,非核心服务不可用或弱可用
  • 在Hystrix中可通过HystrixCommand注解指定可降级的服务
  • 在fallbackMethod(回退函数)中具体实现降级逻辑。对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存

触发降级

本小节我们来模拟一下触发服务降级的情况,首先在订单服务项目的pom.xml文件中,加入Spring Cloud Hystrix依赖。如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

添加好依赖后修改一下启动类的注解。修改后代码如下:

package org.zero.springcloud.order.server;

import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringCloudApplication
@EnableFeignClients(basePackages = "org.zero.springcloud.product.client")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

在controller包中,新建一个 HystrixController ,我们在这个类里做实验。在这个类里,我们调用了商品服务中的查询商品信息接口。为了模拟服务宕机触发降级,所以此时我已经把商品服务关闭了。具体代码如下:

package org.zero.springcloud.order.server.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;

/**
 * @program: sell_order
 * @description: Hystrix Demo
 * @author: 01
 * @create: 2018-08-28 20:10
 **/
@Slf4j
@RestController
@RequestMapping("/hystrix/demo")
public class HystrixController {

    /**
     * 通过@HystrixCommand注解指定可降级的服务,fallbackMethod参数指向的是回调函数,函数名称可自定义
     *
     * @return String
     */
    @HystrixCommand(fallbackMethod = "fallback")
    @GetMapping("/getProductInfoList")
    public String getProductInfoList() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.postForObject("http://127.0.0.1:8519/buyer/product/listForOrder",
                Collections.singletonList("157875196366160022"), String.class);
    }

    /**
     * 触发降级后的回调函数
     *
     * @return String
     */
    public String fallback() {
        return "太拥挤了, 请稍后重试~";
    }
}

启动项目,访问结果如下:

从测试结果可以看到,由于商品服务关闭了,导致无法调用相应的接口。触发了服务降级后,调用了注解中指定的回调函数,并返回了相应的提示。

触发服务降级不一定是服务调用失败,因为服务降级的主要触发原因是抛出了异常,所以只要这个方法中抛出了未被捕获的异常都会触发服务降级。如下示例:

@HystrixCommand(fallbackMethod = "fallback")
@GetMapping("/getProductInfoList")
public String getProductInfoList() {
    throw new RuntimeException("发生了异常");
}

在某些情况下,我们可能只需要定义一个默认的回调处理函数即可,那么我们就可以使用@DefaultProperties注解来定义默认的回调函数,这样就不需要每个 @HystrixCommand 注解都指定一个回调函数了。如下示例:

package org.zero.springcloud.order.server.controller;

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;

/**
 * @program: sell_order
 * @description: Hystrix Demo
 * @author: 01
 * @create: 2018-08-28 20:10
 **/
@Slf4j
@RestController
@RequestMapping("/hystrix/demo")
@DefaultProperties(defaultFallback = "defaultFallback")
public class HystrixController {

    /**
     * 定义了@DefaultProperties后,只需通过@HystrixCommand注解指定可降级的服务即可
     *
     * @return String
     */
    @HystrixCommand
    @GetMapping("/getProductInfoList")
    public String getProductInfoList() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.postForObject("http://127.0.0.1:8519/buyer/product/listForOrder",
                Collections.singletonList("157875196366160022"), String.class);
    }

    /**
     * 触发降级后的回调函数
     *
     * @return String
     */
    public String defaultFallback() {
        return "太拥挤了, 请稍后重试~";
    }
}

超时设置

使用 @HystrixCommand 注解的接口是有一个默认超时时间的,当调用某个服务的耗时超过这个时间也会触发服务降级,默认的超时时间是1秒。我们也可以去自定义这个超时时间,如下示例:

@HystrixCommand(commandProperties = {
		// 设置超时时间为3秒
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
@GetMapping("/getProductInfoList")
public String getProductInfoList() {
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.postForObject("http://127.0.0.1:8519/buyer/product/listForOrder",
            Collections.singletonList("157875196366160022"), String.class);
}

Hystrix断路器

断路器就像电路中的断路器一样,当短路发生时,它第一时刻熔断,切断了故障电路,保护其他用电单元。

在分布式架构中,断路器的作用类似,当某个服务单元发生了故障,通过断路器的故障监控,直接切断原来的主逻辑调用,强迫以后的多个服务调用不再访问远程服务器,防止应用程序继续执行或等待超时。熔断器也可以监控服务单元的错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

在微服务架构中,系统被拆分成了一个个小的服务单元,各自运行在自己的线程中,各单元之间通过注册与订阅的方式互相远程调用,此时若网络故障或是某一服务挂掉则会出现调用延迟,进一步导致调用方的对外服务也出现延迟,如果调用方的请求不断增加,服务单元线程资源无法释放,队列装满,最终导致故障的蔓延,故断路器就是解决这种问题的。

断路器模式:

当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). 即有自我检测并恢复的能力.

代码示例:

@HystrixCommand(commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 开启熔断机制
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 设置当请求失败的数量达到10个后,打开断路器,默认值为20
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 设置打开断路器多久以后开始尝试恢复,默认为5s
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),  // 设置出错百分比阈值,当达到此阈值后,打开断路器,默认50%
})
@GetMapping("/getProductInfoList")
public String getProductInfoList(@RequestParam("number") Integer number) {
    if (number % 2 == 0) {
        return "success";
    }

    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.postForObject("http://127.0.0.1:8519/buyer/product/listForOrder",
            Collections.singletonList("157875196366160022"), String.class);
}

使用配置项

在代码里写配置可能不太方便维护,我们也可以在配置文件中使用配置项进行配置。例如超时时间配置如下:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

若指定配置某一个方法的超时时间,将default换成相应方法名即可。如下示例:

hystrix:
  command:
    getProductInfoList:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

断路器的的配置方式也是一样的,如下示例:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000
      circuitBreaker:
        enabled: true
        requestVolumeThreshold: 10
        sleepWindowInMilliseconds: 10000
        errorThresholdPercentage: 60

feign-hystrix的使用

我们在订单服务中,使用了feign组件去调用商品服务实现服务间的通信。而feign内部已包含了hystrix,所以也可以实现服务降级。首先在订单服务项目的配置文件中,增加如下配置:

feign:
  hystrix:
    enabled: true  # 开启hystrix

到商品服务项目的client模块中,新增一个 ProductClientFallback 类,并实现ProductClient接口。代码如下:

package org.zero.springcloud.product.client;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.zero.springcloud.product.common.DecreaseStockInput;
import org.zero.springcloud.product.common.ProductInfoOutput;

import java.util.List;

/**
 * @program: sell_product
 * @description: 触发服务降级时会调用相应的方法
 * @author: 01
 * @create: 2018-08-29 21:39
 **/
@Slf4j
@Component
public class ProductClientFallback implements ProductClient {

    @Override
    public List<ProductInfoOutput> productInfoList(List<String> productIdList) {
        log.info("productInfoList() 触发了服务降级");
        return null;
    }

    @Override
    public void decreaseStock(List<DecreaseStockInput> cartDTOList) {
        log.info("decreaseStock() 触发了服务降级");
    }
}

然后在 ProductClient 接口的@FeignClient注解里增加 fallback 属性,并指定以上编写的实现类。当某个接口触发降级时,就会调用实现类里的方法。代码如下:

package org.zero.springcloud.product.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.zero.springcloud.product.common.DecreaseStockInput;
import org.zero.springcloud.product.common.ProductInfoOutput;

import java.util.List;

/**
 * @program: sell_order
 * @description: 配置需要调用的接口地址
 * @author: 01
 * @create: 2018-08-19 12:14
 **/
@FeignClient(name = "PRODUCT", fallback = ProductClientFallback.class)
public interface ProductClient {

    /**
     * 调用商品服务-按id查询商品列表
     * 注意,接口地址需要填写完整
     *
     * @param productIdList productIdList
     * @return List<ProductInfo>
     */
    @PostMapping("/buyer/product/listForOrder")
    List<ProductInfoOutput> productInfoList(@RequestBody List<String> productIdList);

    /**
     * 调用商品服务-扣库存
     *
     * @param cartDTOList cartDTOList
     */
    @PostMapping("/buyer/product/decreaseStock")
    void decreaseStock(@RequestBody List<DecreaseStockInput> cartDTOList);
}

编写完以上的代码后,不要忘了安装到maven本地仓库中,安装命令如下:

mvn clean -Dmaven.test.skip=true install

回到订单服务,在启动类上增加@ComponentScan注解,扩大包扫描范围:

package org.zero.springcloud.order.server;

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@SpringCloudApplication
@ComponentScan(basePackages = "org.zero.springcloud")
@EnableFeignClients(basePackages = "org.zero.springcloud.product.client")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

重启订单服务项目,访问创建订单接口,如下:

控制台输出如下:

注:此时我已关闭了商品服务,所以才会触发服务降级

如果是超时导致服务降级的话,可以在配置文件中配置feign的超时时间,如下:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

hystrix-dashboard

hystrix-dashboard是一个可视化的熔断监视工具,我们本小节来看看如何在项目中使用这个工具。我们以订单服务项目为例,首先在pom.xml文件中,增加如下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在启动类中,增加@EnableHystrixDashboard注解。代码如下:

package org.zero.springcloud.order.server;

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@EnableHystrixDashboard
@SpringCloudApplication
@ComponentScan(basePackages = "org.zero.springcloud")
@EnableFeignClients(basePackages = "org.zero.springcloud.product.client")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

在config包下新建一个 HystrixConfig 配置类,用于配置 HystrixMetricsStreamServlet 。代码如下:

package org.zero.springcloud.order.server.config;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @program: sell_order
 * @description: 配置HystrixMetricsStreamServlet
 * @author: 01
 * @create: 2018-08-29 22:22
 **/
@Configuration
public class HystrixConfig {
    @Bean
    public HystrixMetricsStreamServlet hystrixMetricsStreamServlet() {
        return new HystrixMetricsStreamServlet();
    }

    @Bean
    public ServletRegistrationBean registration(HystrixMetricsStreamServlet servlet) {
        ServletRegistrationBean<HystrixMetricsStreamServlet> registrationBean = new ServletRegistrationBean<>();
        registrationBean.setServlet(servlet);
        //是否启用该registrationBean
        registrationBean.setEnabled(true);
        registrationBean.addUrlMappings("/hystrix.stream");
        return registrationBean;
    }
}

完成以上代码的编写后,重启项目,访问http://localhost:9080/hystrix,会进入到如下页面中:

通过Hystrix Dashboard主页面的文字介绍,我们可以知道,Hystrix Dashboard共支持三种不同的监控方式:

  • 默认的集群监控:通过URL:http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
  • 指定的集群监控:通过URL:http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
  • 单体应用的监控:通过URL:http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。
  • Delay:控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗。
  • Title:该参数可以展示合适的标题。

我这里使用的是单体应用的监控,点击Monitor Stream后,进入到如下页面,在此页面可以看到这个项目的请求信息:

我们来对这些指标进行一个简单的说明: