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文件导入
<!-- config-server 启动引入 -->
<!-- Eureka客户端启动类 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- feign 启动类 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 修改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<Test> collapseFindAll(@RequestParam(value = "ids") List<Integer> ids);
}

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

    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand
    public ResultInfo getTest(Integer id){
        log.info(">>>>>>>>> 进入restTemplate 方法调用 >>>>>>>>>>>>");
        ResponseEntity<ResultInfo> 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<Test> findAll(List<Integer> 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上下文的传递

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容