Spring-cloud 微服务架构搭建 03 - Hystrix 深入理解与配置使用

1. hystrix简介

分布式的服务系统中,出现服务宕机是常有的事情,hystrix提供的客户端弹性模式设计可以快速失败客户端,保护远程资源,防止服务消费进行“上游”传播。

Hystrix库是高度可配置的,可以让开发人员严格控制使用它定义的断路器模式和舱壁模式 的行为 。 开发人员可以通过修改 Hystrix 断路器的配置,控制 Hystrix 在超时远程调用之前需要等 待的时间 。 开发人员还可以控制 Hystrix 断路器何时跳闸以及 Hystrix何时尝试重置断路器 。

使用 Hystrix, 开发人员还可以通过为每个远程服务调用定义单独的线程组,然后为每个线程组配置相应的线程数来微调舱壁实现。 这允许开发人员对远程服务调用进行微调,因为某些远 程资源调用具有较高的请求量 。

客户端弹性模式?有以下四点:

  1. 客户端负载均衡模式,由ribbon模块提供;
  2. 断路器模式(circuit breaker);
  3. 快速失败模式(fallback);
  4. 舱壁模式(bulkhead);
  • 下面我们通过Feign-service(Feign结合Hystrix)模块和Demo-dervice(上篇文章的基础服务模块)对hystix组件进行功能测试和理解。

2. hystrix-service 模块快速搭建

注:本文项目采用idea工具进行搭建

  • 使用idea自身的spring initializr进行项目的初始化,项目名为:feign-service,其主要测试基于feign的远程调用,也有restTemplate的测试;
  • 初始化完成项目之后进行pom文件导入



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    org.springframework.cloud
    spring-cloud-starter-openfeign


    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix

  • 修改application.yml文件,添加如下配置:
management:
  endpoints:
    web:
      exposure:
        include: "*"  # 暴露所有服务监控端口,也可以只暴露 hystrix.stream端口
  endpoint:
    health:
      show-details: ALWAYS
# feign配置
feign:
  compression:
    request:
      enabled:  true  #开启请求压缩功能
      mime-types: text/xml;application/xml;application/json #指定压缩请求数据类型
      min-request-size: 2048  #如果传输超过该字节,就对其进行压缩
    response:
    #开启响应压缩功能
      enabled:  true
  hystrix:
    # 在feign中开启hystrix功能,默认情况下feign不开启hystrix功能
  • 修改bootstrap.yml文件,链接eureka-config,添加如下配置:
# 指定服务注册中心的位置。
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    hostname: localhost
    preferIpAddress: true
  • 最后修改服务启动类:
@ServletComponentScan
@EnableFeignClients
@SpringCloudApplication
public class FeignServiceApplication {

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

    /**
     * 设置feign远程调用日志级别
     * Logger.Level有如下几种选择:
     * NONE, 不记录日志 (默认)。
     * BASIC, 只记录请求方法和URL以及响应状态代码和执行时间。
     * HEADERS, 记录请求和应答的头的基本信息。
     * FULL, 记录请求和响应的头信息,正文和元数据。
     */
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.HEADERS;
    }

    /**
     * 引入restTemplate负载均衡
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

最后添加远程调用客户端接口如下:

/**
 * feign 注解来绑定该接口对应 demo-service 服务
 * name 为其它服务的服务名称
 * fallback 为熔断后的回调
 */
@FeignClient(value = "demo-service",
//        configuration = DisableHystrixConfiguration.class, // 局部关闭断路器
        fallback = DemoServiceHystrix.class)
public interface DemoClient {

    @GetMapping(value = "/test/hello")
    ResultInfo hello();

    @GetMapping(value = "/test/{id}",consumes = "application/json")
    ResultInfo getTest(@PathVariable("id") Integer id);

    @PostMapping(value = "/test/add")
    ResultInfo addTest(Test test);

    @PutMapping(value = "/test/update")
    ResultInfo updateTest(Test test);

    @GetMapping(value = "/test/collapse/{id}")
    Test collapse(@PathVariable("id") Integer id);

    @GetMapping(value = "/test/collapse/findAll")
    List collapseFindAll(@RequestParam(value = "ids") List ids);
}

/**
 * restTemplate 客户端
*/
@Component
public class RestClient {

    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand
    public ResultInfo getTest(Integer id){
        log.info(">>>>>>>>> 进入restTemplate 方法调用 >>>>>>>>>>>>");
        ResponseEntity restExchange =
                restTemplate.exchange(
                        "http://demo-service/test/{id}",
                        HttpMethod.GET,
                        null, ResultInfo.class, id);
        return restExchange.getBody();
    }
}
  • 添加测试接口
@Log4j2
@RestController
@RequestMapping("/test")
public class FeignController {

    @Autowired
    private DemoClient demoClient;

    @Autowired
    private RestClient restClient;

    @HystrixCommand
    @GetMapping("/feign/{id}")
    public ResultInfo testFeign(@PathVariable("id") Integer id){
        log.info("使用feign进行远程服务调用测试。。。");
        ResultInfo test = demoClient.getTest(id);
        log.info("服务调用获取的数据为: " + test);
        /**
         * hystrix 默认调用超时时间为1秒
         * 此处需要配置 fallbackMethod 属性才会生效
         */
        //log.info("服务延时:" + randomlyRunLong() + " 秒");
        return test;
    }

    @HystrixCommand
    @GetMapping("/rest/{id}")
    public ResultInfo testRest(@PathVariable("id") Integer id){
        log.info("使用restTemplate进行远程服务调用测试。。。");
        return restClient.getTest(id);
    }
  • 到此配置完成hystrix,可以启动进行测试使用,测试远程调用的服务实例为demo-service,通过postman或者其他工具我们可以发现配置很完美,下面我们分别介绍hystrix的具体配置和使用。

3. hystrix 回退机制

  • 回退机制也叫后备机制,就是在我们的服务调用不可达或者服务调用超时失败的情况下的后备操作。有两种fallback定义方式:
  1. feign的@FeignClient中定义fallback属性,定义一个实现c次client接口的类。
  2. @HystrixCommand(fallbackMethod = "buildFallbackMethod")方式;
  • 我们使用服务调用延时的机制处理如下:
@HystrixCommand(
            // 开启此项 feign调用的回退处理会直接调用此方法
            fallbackMethod = "buildFallbacktestFeign",
    )
    @GetMapping("/feign/{id}")
    public ResultInfo testFeign(@PathVariable("id") Integer id){
        ... 省略代码
        /**
         * hystrix 默认调用超时时间为1秒
         * 此处需要配置 fallbackMethod 属性才会生效
         */
        log.info("服务延时:" + randomlyRunLong() + " 秒");
        return test;
    }
        ... 省略代码
    /**
     * testFeign 后备方法
     * @return
     */
    private ResultInfo buildFallbacktestFeign(Integer id){
        return ResultUtil.success("testFeign 接口后备处理,参数为: " + id );
    }
    // 模拟服务调用延时
    private Integer randomlyRunLong(){
        Random rand = new Random();
        int randomNum = rand.nextInt(3) + 1;
        if (randomNum==3) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return randomNum;
    }
  • 如上我们在调用testfeign接口时,当随机数为3时会进行线程的休眠,那么会超过hystrix 默认调用超时时间,接口返回后台方法buildFallbacktestFeign的返回值。

注意:我们在回退方法中进行远程接口的调用时,也需要使用@HystrixCommand进行包裹,不然出现问题会吃大亏。

4. hystrix 线程池隔离和参数微调

  • 线程池隔离可以全局设置也可以在@HystrixCommand中下架如下参数进行配置,如果需要动态配置可以利用aop进行配置,配置参数如下:
// 以下为 舱壁模式配置配置单独的线程池
threadPoolKey = "test",
threadPoolProperties = {
        @HystrixProperty(name = "coreSize",value="30"),
        @HystrixProperty(name="maxQueueSize", value="10")},
// 以下为断路器相关配置 可以根据系统对参数进行微调
commandProperties={
        // 设置hystrix远程服务调用超时时间,一般不建议修改
//  @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="4000"),
        // 请求必须达到以下参数以上才有可能触发,也就是10秒內发生连续调用的最小参数
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
        // 请求到达requestVolumeThreshold 上限以后,调用失败的请求百分比
        @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
        // 断路由半开后进入休眠的时间,期间可以允许少量服务通过
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
        // 断路器监控时间 默认10000ms
        @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
        // timeInMilliseconds的整数倍,此处设置越高,cpu占用资源越多我
        @HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")}
  • 除了在方法中添加为,还可以在类上进行类的全局控制
// 类级别属性配置
@DefaultProperties(
    commandProperties={
            // 请求必须达到以下参数以上才有可能触发,也就是10秒內发生连续调用的最小参数
            @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
            // 请求到达requestVolumeThreshold 上限以后,调用失败的请求百分比
            @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75")}
)

5. hystrix 缓存配置

Hystrix请求缓存不是只写入一次结果就不再变化的,而是每次请求到达Controller的时候,我们都需要为HystrixRequestContext进行初始化,之前的缓存也就是不存在了,我们是在同一个请求中保证结果相同,同一次请求中的第一次访问后对结果进行缓存,缓存的生命周期只有一次请求!与使用redis进行url缓存的模式不同
测试代码如下:

@RestController
@RequestMapping("/cache")
public class CacheTestController {
    @Autowired
    private CacheService cacheService;
    @GetMapping("/{id}")
    public ResultInfo testCache(@PathVariable("id") Integer id){
        // 查询缓存数据
        log.info("第一次查询: "+cacheService.testCache(id));
        // 再次查询 查看日志是否走缓存
        log.info("第二次查询: "+cacheService.testCache(id));
        // 更新数据
        cacheService.updateCache(new Test(id,"wangwu","121"));
        // 再次查询 查看日志是否走缓存,不走缓存则再次缓存
        log.info("第二次查询: "+cacheService.testCache(id));
        // 再次查询 查看日志是否走缓存
        log.info("第二次查询: "+cacheService.testCache(id));
        return ResultUtil.success("cache 测试完毕!!!");
    }
}

@Service
public class CacheService {
    @Autowired
    private DemoClient demoClient;
    
    /**
     * commandKey 指定命令名称
     * groupKey 分组
     * threadPoolKey 线程池分组
     *
     * CacheResult 设定请求具有缓存
     *  cacheKeyMethod 指定请求缓存key值设定方法 优先级大于 @CacheKey() 的方式
     *
     * CacheKey() 也是指定缓存key值,优先级较低
     * CacheKey("id") Integer id 出现异常,测试CacheKey()读取对象属性进行key设置
     *  java.beans.IntrospectionException: Method not found: isId
     *
     * 直接使用以下配置会出现异常:
     *   java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
     *
     * 原因:请求缓存不是只写入一次结果就不再变化的,而是每次请求到达Controller的时候,我们都需要为
     *      HystrixRequestContext进行初始化,之前的缓存也就是不存在了,我们是在同一个请求中保证
     *      结果相同,同一次请求中的第一次访问后对结果进行缓存,缓存的生命周期只有一次请求!
     *      与使用redis进行url缓存的模式不同。
     * 因此,我们需要做过滤器进行HystrixRequestContext初始化。
     */
    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(commandKey = "testCache", groupKey = "CacheTestGroup", threadPoolKey = "CacheTestThreadPool")
    public ResultInfo testCache(Integer id){
        log.info("test cache 服务调用测试。。。");
        return demoClient.getTest(id);
    }
    
    /**
     * 这里有两点要特别注意:
     * 1、这个方法的入参的类型必须与缓存方法的入参类型相同,如果不同被调用会报这个方法找不到的异常,
     *    等同于fallbackMethod属性的使用;
     * 2、这个方法的返回值一定是String类型,报出如下异常:
     *    com.netflix.hystrix.contrib.javanica.exception.HystrixCachingException:
     *            return type of cacheKey method must be String.
     */
    private String getCacheKey(Integer id){
        log.info("进入获取缓存key方法。。。");
        return String.valueOf(id);
    }

    @CacheRemove(commandKey = "testCache")
    @HystrixCommand(commandKey = "updateCache", groupKey = "CacheTestGroup", threadPoolKey = "CacheTestThreadPool")
    public ResultInfo updateCache(@CacheKey("id") Test test){
        log.info("update cache 服务调用测试。。。");
        return demoClient.updateTest(test);
    }
}
  • 使用postman进行测试调用http://localhost:8090/cache/1;可以发现demo-service服务响应了两次,说明在第一次缓存chen成功,update后删除了缓存。测试过程中遇到的问题已经注释,读者可以自行测试;

6. hystrix 异常抛出处理

  • @HystrixCommandignoreExceptions属性会将忽略的异常包装成HystrixBadRequestException,从而不执行回调.
@RestController
@RequestMapping("/exception")
public class ExceptionTestController {

    @Autowired
    private DemoClient demoClient;

    /**
     * ignoreExceptions 属性会将RuntimeException包装
     *   成HystrixBadRequestException,从而不执行回调.
     */
    @HystrixCommand(ignoreExceptions = {RuntimeException.class},
                    fallbackMethod = "buildFallbackTestException")
    @GetMapping("/{id}")
    public ResultInfo testException(@PathVariable("id") Integer id){
        log.info("test exception 服务调用异常抛出测试。。。");
        if (id == 1){
            throw new RuntimeException("测试服务调用异常");
        }
        return demoClient.getTest(id);
    }

    /**
     * testFeign 后备方法
     * @return
     */
    private ResultInfo buildFallbackTestException(Integer id){
        return ResultUtil.success("testException 接口后备处理,参数为: " + id );
    }
}
  • 接口调用http://localhost:8090/exception/1 服务抛出异常,接口接收到异常信息;如果去除ignoreExceptions = {RuntimeException.class},再次调用接口,发现执行了buildFallbackTestException回退方法。

8. hystrix 请求合并

注意:请求合并方法本身时高延迟的命令,对于一般请求延迟低的服务需要考虑延迟时间合理化以及延迟时间窗內的并发量

  • 请求合并的测试
@RestController
@RequestMapping("/collapse")
public class CollapseController {

    @Autowired
    private CollapseService collapseService;

    @GetMapping("/{id}")
    public ResultInfo testRest(@PathVariable("id") Integer id){
        log.info("进行 Collapse 远程服务调用测试,开始时间: " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        Test test = collapseService.testRest(id);
        log.info("进行 Collapse 远程服务调用测试,结束时间: " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        /**
         * 启用请求合并:
         *      开始时间: 2018-10-18T10:40:12.374
         *      结束时间: 2018-10-18T10:40:13.952
         * 不使用请求合并:
         *      开始时间: 2018-10-18T10:43:41.472
         *      结束时间: 2018-10-18T10:43:41.494
         */
        return ResultUtil.success(test);
    }
}

@Service
public class CollapseService {

    @Autowired
    private DemoClient demoClient;

    @HystrixCollapser(
            // 指定请求合并的batch方法
            batchMethod = "findAll",
            collapserProperties = {
                    // 请求合并时间窗为 100ms ,需要根据请求的延迟等进行综合判断进行设置
                    @HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")
            })
    public Test testRest(Integer id){
        Test test = demoClient.collapse(id);
        return test;
    }

    // batch method must be annotated with HystrixCommand annotation
    @HystrixCommand
    private List findAll(List ids){
        log.info("使用 findAll 进行远程服务 Collapse 调用测试。。。");

        return demoClient.collapseFindAll(ids);
    }
}

-调用接口 http://localhost:8090/collapse/1 ,可以使用并发测试工具进行测试,比如jmeter;返回以上注释信息结果,说明我们在设置这些参数需要进行多方面的测试。

9. Hystrix ThreadLocal上下文的传递

具体内容可以参考下面的参考博文,也可以下载我github代码进行测试。

注意:配置ThreadLocal上下文的传递之后,我们在回过头测试hystrix的cache测试,发现清理缓存的功能失效了。希望有思路的博友可以提供建议,谢谢

本文github代码地址:
我的github:spring-cloud 基础模块搭建 ---- 欢迎指正

参考博文:
SpringCloud (八) Hystrix 请求缓存的使用
Hystrix实现ThreadLocal上下文的传递

你可能感兴趣的:(Spring-cloud 微服务架构搭建 03 - Hystrix 深入理解与配置使用)