SpringCloud学习(四)容错保护

SpringCloud 容错保护

  1. Hystrix

    1. 容错保护

      微服务架构的系统之间通过服务注册与订阅的方式相互依赖,由于服务在不同进程运行且采用远程调用的方式,可能因为网络、服务自身故障延迟造成请求任务堆积导致服务瘫痪

      微服务架构中若一个服务出现故障,很容易因为依赖关系引发故障蔓延,最终导致整个系统故障,因此产生了断路器等一系列的服务保护机制

      若某个服务单元发生故障,经过断路器的故障监控,会向消费服务方返回一个错误响应,而不是长时间的等待

      Spring Cloud Hystrix实现了断路器、线程隔离等服务保护功能,对延迟和故障提供了强大的容错能力,具体功能有服务降级、服务熔断、线程/信号隔离、请求缓存、请求合并、服务监控等

    2. 断路器(服务降级)

      消费服务方(调用服务者)通过断路器实现服务降级回调功能,实际使用中我们会为大多数在执行过程中可能会失败的Hystrix命令实现服务降级逻辑,但有些情况可以不去实现服务降级,如

      • 写操作:Hystrix命令用来调用服务执行写操作时,通常返回类型为void或者空的Observable,实现服务降级的意义不大,通常写操作失败时我们通知调用者即可
      • 批处理/离线计算:Hystrix命令用来执行一些批处理操作生成文件或者进行一些离线计算失败时,只需要将错误传播给调用者,如生成文件失败/计算失败参数有误等,让调用者稍后重试

      接下来我们引入断路器来实现一般Hystrix命令的服务降级操作

      1. 引入hystrix依赖

        1
        2
        3
        4
        5
        6
        <!--hystrix容错保护模块-->
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix</artifactId>
        <version>1.3.5.RELEASE</version>
        </dependency>
      2. 开启断路器:在Spring Boot启动类中添加注解

        • @EnableCircuitBreaker:开启断路器功能
        • @SpringCloudApplication:相当于 @SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker,开启Spring Boot、服务治理、断路器功能
      3. 添加服务降级方法:通过在消费服务的方法上添加注解@HystrixCommand配置若服务不可用或者超时执行服务降级方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        @HystrixCommand(fallbackMethod = "getHystrixDemoError")
        public String getHystrixDemo(String name){
        ResponseEntity<String> response=restTemplate.getForEntity("http://TEST-SERVICE/hystrixDemo?name="+name,String.class);
        return response.getBody();
        }

        //需要与原方法统一参数类型、个数 否则会造成异常fallback method wasn't found:
        public String getHystrixDemoError(String name){
        return "error";
        }
      4. 配置Hystrix调用超时时间:调用服务时响应时间超过超时时间自动调用服务降级方法

        1
        2
        3
        4
        5
        # Hystrix
        # 默认服务降级时间
        hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
        # 设置某个方法的降级时间
        hystrix.command.getHystrixDemo.execution.isolation.thread.timeoutInMilliseconds=1000
      5. 服务提供者增加随机相应时间:用于测试服务降级功能

        1
        2
        3
        4
        5
        6
        7
        8
        //让线程等待一会 触发消费服务的断路器 测试服务降级功能
        int sleepTime=new Random().nextInt(3000);
        log.info("SleepTime is "+sleepTime);
        try {
        Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
      6. 调用服务:可以发现服务响应时间超过1000ms时会出发断路器调用服务降级方法,返回error字符串,响应时间小于1000ms时可以返回正确相应内容

    3. 请求命令Command(封装服务调用请求)

      Hystrix使用请求命令HystrixCommand 或者HystrixObservableCommand 来封装具体的依赖服务调用逻辑

      这里主要采取注解方式实现封装服务调用,较为优雅,还可以通过继承方式实现封装

      • HystrixCommand:封装一个返回单个操作结果的服务调用,有两种执行方式

        • 同步执行:在调用服务的方法上加上@HystrixCommand注解

          1
          2
          3
          4
          5
          @HystrixCommand(fallbackMethod = "getHystrixDemoError")
          public String getHystrixDemo(String name){
          ResponseEntity<String> response=restTemplate.getForEntity("http://TEST-SERVICE/hystrixDemo?name="+name,String.class);
          return response.getBody();
          }
        • 异步执行:在调用服务的方法上加上@HystrixCommand注解,返回一个AsynResult对象,并在AsynResult对象内重写invoke方法,调用封装的服务得到一个Future<>类型的对象,可用其get()方法得到执行结果

          1
          2
          3
          4
          5
          6
          7
          8
          9
          @HystrixCommand(fallbackMethod = "getHystrixDemoError")
          public Future<String> getHystrixDemoAsync(String name){
          return new AsyncResult<String>(){
          @Override
          public String invoke(){
          return restTemplate.getForEntity("http://TEST-SERVICE/hystrixDemo?name="+name,String.class).getBody();
          }
          };
          }
      • HystrixObservableCommand:封装一个返回多个操作结果的服务调用

        • HotObservable:返回Hot Observable,命令立即执行,使用@HystrixCommand注解,通过将注解中的ObservableExecutionMode属性设置为EAGER表示这是一个同步执行的服务调用,返回Observable<>对象,可使用Iterator将其内容遍历输出

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          //EAGER表示同步执行
          @HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER,fallbackMethod = "getHystrixDemoErrorObservable")
          public Observable<User> getHystrixDemoObservable(String name){
          return Observable.create(new Observable.OnSubscribe<User>(){
          @Override
          public void call(Subscriber<? super User> observer){
          try{
          if(!observer.isUnsubscribed()){
          User user=restTemplate.getForObject("http://TEST-SERVICE/hystrixDemo?name="+name,User.class);
          observer.onNext(user);
          observer.onCompleted();
          }
          }catch(Exception e){
          observer.onError(e);
          }
          }
          });
          }
          1
          2
          //Observable使用方式
          Iterator<User> iterator=consumerService.getHystrixDemoObservable(name).toBlocking().getIterator();
        • ColdObservable:返回Cold Observable,命令不会立即执行,只有当所有订阅者都订阅后才会执行,定义方法和使用方法与Hot Observable基本相同,仅仅将@HystrixCommand注解的ObservableExecutionMode属性设置为LAZY

          1
          @HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY,fallbackMethod = "getHystrixDemoErrorObservable")
    4. 异常处理

      HystrixCommand封装的服务调用方法中,若出现除了HystrixBadRequestException的异常,一律视作HystrixCommand命令执行失败,不抛出异常而是触发服务降级处理,而我们对于是什么异常造成服务降级往往很感兴趣,因此可以在服务降级方法中使用Throwable参数将造成服务命令失败的异常信息传递给服务降级的方法,从而进行相应处理

      1
      2
      3
      4
      5
      6
      //在服务降级的方法中增加Throwable参数
      public Observable<User> getHystrixDemoErrorObservable(String name,Throwable e){
      //使用Throwable参数可将高级服务造成服务降级的异常传递下来 使用log打印出或者在返回的数据/对象属性中存储相应的信息提示调用者
      log.error("getHystrixDemoObservableAsync command fail "+e.toString());
      return Observable.just(User.setErrorMsg(e.toString));
      }
    5. 请求缓存

      微服务的依赖服务调用相比于进程内调用会引起一部分性能损失,HTTP相比于其他高性能通信协议在速度上没有任何优势,在分布式、高并发环境下,对于数据库这样的外部资源进行读写操作存在性能瓶颈,微服务怎么解决这样的问题呢?

      Hystrix提供了类似数据访问的缓存保护的功能对请求进行缓存,我们可以开启和使用请求缓存来优化系统,达到高并发时减轻请求线程消耗、降低请求相应时间等效果

      • 开启请求缓存:对服务调用方法使用@CacheResult注解
      • 设置缓存Key:在服务调用方法的参数前加上@CacheKey,对于对象参数缓存标识可以使用@CacheKey(“缓存标识”)标识对象的内部属性
      • 缓存清理:如果查询服务调用方法开启了请求缓存,其余会影响到查询服务调用数据的方法(如对数据进行修改、更新、增加)需要主动使查询服务调用方法的缓存失效,保证其缓存的数据的有效性,可以使用@CacheRemove(commandKey=”查询服务方法名”)清除失效的缓存

      我们尝试着测试一下缓存功能,具体思路是注册TEST-SERVICE功能是返回一个随机数,ConsumerService调用TEST-SERVICE服务,ConsumerController调用ConsumerService将结果输出

      1. 第一种情况:ConsumerService缓存服务,ConsumerController调用一次ConsumerService将结果输出,我们通过多次调用ConsumerController观察服务调用结果是否缓存

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        //TEST-SERVICE返回随机数
        Integer randNum=new Random().nextInt(3000);
        return randNum.toString();

        //ConsumerService调用服务且缓存
        @CacheResult
        @HystrixCommand(commandKey="cacheDemoKey")
        public String cacheDemo(@CacheKey String name){
        log.info("cacheDemo!!!!");
        //调用TEST-SERVICE的cacheDemo接口得到随机数
        return restTemplate.getForEntity("http://TEST-SERVICE/cacheDemo?name="+name,String.class).getBody();
        }

        //ConsumerController调用ConsumerService
        @RequestMapping(value = "/getCacheDemo",method = RequestMethod.GET)
        public String getCacheDemo(
        @RequestParam("name") String name
        ){
        //初始化上下文环境 否则缓存会报错
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        String response=consumerService.cacheDemo(name);
        context.close();
        return "response: "+response;
        }
        根据运行结果可以看出,缓存并不成功,可能由于每次调用ConsumerController均用的是新的上下文环境,即每次执行ConsumerController均是在不同的进程,不能跨进程缓存
      2. 第二种情况:ConsumerService缓存服务,ConsumerController调用多次ConsumerService将结果输出,我们调用一次ConsumerController观察服务调用结果是否缓存

        1
        2
        3
        4
        5
        6
        //修改ConsumerController多次调用ConsumerService
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        String response1=consumerService.cacheDemo(name);
        String response2=consumerService.cacheDemo(name);
        context.close();
        return "response1: "+response1+"\n response2: "+response2;
        结果如下,缓存成功,只有在同一进程,同一上下文下才能进行请求缓存

      3. 第三种情况:测试缓存清除功能

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        @CacheResult
        @HystrixCommand(commandKey="cacheDemoKey")
        public String cacheDemo(@CacheKey String name){
        return restTemplate.getForEntity("http://TEST-SERVICE/cacheDemo?name="+name,String.class).getBody();
        }

        //在ConsumerService中增加缓存清除功能
        //CacheRemove的commandKey与缓存服务的HystrixCommand的commandKey一致
        @CacheRemove(commandKey = "cacheDemoKey")
        @HystrixCommand
        public void removeCacheKey(@CacheKey String name){//CacheKey与缓存服务的参数一致
        log.info("removeCache!!!!");
        }

        //ConsumerController在两次请求中增加清除缓存的调用
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        String response1=consumerService.cacheDemo(name);
        consumerService.removeCacheKey(name);
        String response2=consumerService.cacheDemo(name);
        context.close();
        return "response1: "+response1+"\n response2: "+response2;
        结果如下,成功清除缓存,清除缓存的函数要点有两点,需要指定commandKey与请求缓存的@HystrixCommand的commandKey属性一致,需要入参中包含与请求缓存的@CacheKey一致的参数