Spring Cloud - 使用 Hystrix 的断路器

  • 介绍

    在分布式环境中,服务之间需要相互通信。通信可以同步或异步发生。当服务同步通信时,可能有多种原因导致事情中断。例如 -
    • Callee service unavailable - 被调用的服务由于某种原因关闭,例如 - 错误、部署等。
    • Callee service taking time to respond − 正在调用的服务可能因高负载或资源消耗而变慢,或者正在初始化服务。
    在任何一种情况下,调用者等待被调用者响应都是浪费时间和网络资源。服务在一段时间后退出并调用被调用者服务或共享默认响应更有意义。
    Netflix Hystrix, Resilince4j是两个众所周知的断路器,用于处理这种情况。在本教程中,我们将使用 Hystrix。
  • Hystrix - 依赖设置

    让我们使用我们之前使用过的 Restaurant 案例。让我们加上hystrix dependency到我们的餐厅服务部,该服务部致电客户服务部。首先,让我们更新pom.xml 具有以下依赖项的服务 -
    
    
    <dependency>
    
       <groupId>org.springframework.cloud</groupId>
    
       <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    
       <version>2.7.0.RELEASE</version>
    
    </dependency>
    
    
    然后,使用正确的注解来注解我们的 Spring 应用程序类,即@EnableHystrix
    
    
    package com.jc2182;
    
    import org.springframework.boot.SpringApplication;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
    
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    import org.springframework.cloud.netflix.hystrix.EnableHystrix;
    
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    
    @EnableFeignClients
    
    @EnableDiscoveryClient
    
    @EnableHystrix
    
    public class RestaurantService{
    
       public static void main(String[] args) {
    
          SpringApplication.run(RestaurantService.class, args);
    
       }
    
    }
    
    
    Points to Note
    • @ EnableDiscoveryClient@EnableFeignCLient - 我们已经在上一章中查看了这些注解。
    • @EnableHystrix - 此注解扫描我们的包并查找使用@HystrixCommand 注解的方法。
  • Hystrix 命令注解

    完成后,我们将重用我们之前在 Restaurant 服务中为客户服务类定义的 Feign 客户端,这里没有变化 -
    
    
    package com.jc2182;
    
    import org.springframework.cloud.openfeign.FeignClient;
    
    import org.springframework.web.bind.annotation.PathVariable;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @FeignClient(name = "customer-service")
    
    public interface CustomerService {
    
       @RequestMapping("/customer/{id}")
    
       public Customer getCustomerById(@PathVariable("id") Long id);
    
    }
    
    
    现在,让我们定义 service implementation这里的类将使用 Feign 客户端。这将是一个围绕假客户端的简单包装器。
    
    
    package com.jc2182;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.stereotype.Service;
    
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    
    @Service
    
    public class CustomerServiceImpl implements CustomerService {
    
       @Autowired
    
       CustomerService customerService;
    
       @HystrixCommand(fallbackMethod="defaultCustomerWithNYCity")
    
       public Customer getCustomerById(Long id) {
    
          return customerService.getCustomerById(id);
    
       }
    
       // assume customer resides in NY city
    
       public Customer defaultCustomerWithNYCity(Long id) {
    
          return new Customer(id, null, "NY");
    
       }
    
    }
    
    
    现在,让我们从上面的代码中了解几点 -
    • HystrixCommand annotation - 这负责包装函数调用 getCustomerById并提供一个围绕它的代理。然后代理提供各种钩子,我们可以通过这些钩子控制我们对客户服务的调用。例如请求超时、请求池化、提供回退方法等。
    • Fallback method- 我们可以指定当 Hystrix 确定被调用者有问题时要调用的方法。此方法需要与被注解的方法具有相同的签名。在我们的案例中,我们决定将数据提供回纽约市的控制者。
    此注解提供了几个有用的选项 -
    • Error threshold percent− 在电路跳闸之前允许失败的请求百分比,即调用回退方法。这可以通过使用 cucitiBreaker.errorThresholdPercentage 来控制
    • Giving up on the network request after timeout− 如果被调用方服务(在我们的案例中为客户服务)很慢,我们可以设置超时时间,在此之后我们将删除请求并转向回退方法。这是通过设置控制的execution.isolation.thread.timeoutInMilliseconds
    最后,这是我们的控制器,我们称之为 CustomerServiceImpl
    
    
    package com.jc2182;
    
    import java.util.HashMap;
    
    import java.util.List;
    
    import java.util.stream.Collectors;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.web.bind.annotation.PathVariable;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    
    class RestaurantController {
    
       @Autowired
    
       CustomerServiceImpl customerService;
    
       static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
    
       static{
    
          mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC"));
    
          mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO"));
    
          mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC"));
    
          mockRestaurantData.put(3L, new Restaurant(4, "Pizeeria", "NY"));
    
       }
    
       @RequestMapping("/restaurant/customer/{id}")
    
       public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long
    
    id)
    
    {
    
       System.out.println("Got request for customer with id: " + id);
    
       String customerCity = customerService.getCustomerById(id).getCity();
    
       return mockRestaurantData.entrySet().stream().filter(
    
          entry -> entry.getValue().getCity().equals(customerCity))
    
          .map(entry -> entry.getValue())
    
          .collect(Collectors.toList());
    
       }
    
    }
    
    
  • 断路/开路

    现在我们已经完成了设置,让我们试一试。这里只是一点背景,我们将做的是以下 -
    • 启动尤里卡服务器
    • 开始客户服务
    • 启动将在内部调用客户服务的餐厅服务。
    • 对餐厅服务进行 API 调用
    • 关闭客户服务
    • 对餐厅服务进行 API 调用。鉴于客户服务已关闭,它将导致失败,最终将调用回退方法。
    现在让我们编译餐厅服务代码并使用以下命令执行 -
    
    
    java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
    
    
    此外,启动客户服务和 Eureka 服务器。请注意,这些服务没有变化,它们与前几章中看到的一样。
    现在,让我们尝试为位于 DC 的 Jane 寻找餐厅。
    
    
    {
    
       "id": 1,
    
       "name": "Jane",
    
       "city": "DC"
    
    }
    
    
    为此,我们将访问以下 URL:http://localhost:8082/restaurant/customer/1
    
    
    [
    
       {
    
          "id": 1,
    
          "name": "Pandas",
    
          "city": "DC"
    
       },
    
       {
    
          "id": 3,
    
          "name": "Little Italy",
    
          "city": "DC"
    
       }
    
    ]
    
    
    所以,这里没什么新鲜事,我们有华盛顿特区的餐厅。现在,让我们转到关闭客户服务的有趣部分。您可以通过按 Ctrl+C 或简单地杀死外壳来实现。
    现在让我们再次访问相同的 URL - http://localhost:8082/restaurant/customer/1
    
    
    {
    
       "id": 4,
    
       "name": "Pizzeria",
    
       "city": "NY"
    
    }
    
    
    从输出中可以看出,虽然我们的客户来自 DC,但我们从纽约获得了餐厅。这是因为我们的回退方法返回了一个位于纽约的虚拟客户。虽然没有用,但上面的示例显示按预期调用了回退。
  • 将缓存与 Hystrix 集成

    为了使上述方法更有用,我们可以在使用 Hystrix 时集成缓存。当底层服务不可用时,这可能是提供更好答案的有用模式。
    首先,让我们创建服务的缓存版本。
    
    
    package com.jc2182;
    
    import java.util.HashMap;
    
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.stereotype.Service;
    
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    
    @Service
    
    public class CustomerServiceCachedFallback implements CustomerService {
    
       Map<Long, Customer> cachedCustomer = new HashMap<>();
    
       @Autowired
    
       CustomerService customerService;
    
       @HystrixCommand(fallbackMethod="defaultToCachedData")
    
       public Customer getCustomerById(Long id) {
    
          Customer customer = customerService.getCustomerById(id);
    
          // cache value for future reference
    
          cachedCustomer.put(customer.getId(), customer);
    
          return customer;
    
       }
    
       // get customer data from local cache
    
       public Customer defaultToCachedData(Long id) {
    
          return cachedCustomer.get(id);
    
       }
    
    }
    
    
    我们使用 hashMap 作为存储来缓存数据。这是出于开发目的。在生产环境中,我们可能希望使用更好的缓存解决方案,例如 Redis、Hazelcast 等。
    现在,我们只需要更新控制器中的一行即可使用上述服务 -
    
    
    @RestController
    
    class RestaurantController {
    
       @Autowired
    
       CustomerServiceCachedFallback customerService;
    
       static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
    
       …
    
    }
    
    
    我们将遵循与上述相同的步骤 -
    • 启动尤里卡服务器。
    • 启动客户服务。
    • 启动内部调用客户服务的餐厅服务。
    • 对餐厅服务进行 API 调用。
    • 关闭客户服务。
    • 对餐厅服务进行 API 调用。鉴于客户服务已关闭但数据已缓存,我们将获得一组有效数据。
    现在,让我们按照相同的过程直到第 3 步。
    现在点击 URL:http://localhost:8082/restaurant/customer/1
    
    
    [
    
       {
    
          "id": 1,
    
          "name": "Pandas",
    
          "city": "DC"
    
       },
    
       {
    
          "id": 3,
    
          "name": "Little Italy",
    
          "city": "DC"
    
       }
    
    ]
    
    
    所以,这里没什么新鲜事,我们有华盛顿特区的餐厅。现在,让我们转到关闭客户服务的有趣部分。您可以通过按 Ctrl+C 或简单地杀死外壳来实现。
    现在让我们再次访问相同的 URL - http://localhost:8082/restaurant/customer/1
    
    
    [
    
       {
    
          "id": 1,
    
          "name": "Pandas",
    
          "city": "DC"
    
       },
    
       {
    
          "id": 3,
    
          "name": "Little Italy",
    
          "city": "DC"
    
       }
    
    ]
    
    
    从输出中可以看出,我们从 DC 获得了餐厅,这是我们期望的,因为我们的客户来自 DC。这是因为我们的回退方法返回了缓存的客户数据。
  • 将 Feign 与 Hystrix 集成

    我们看到了如何使用 @HystrixCommand 注解来跳闸并提供回退。但是我们必须另外定义一个 Service 类来包装我们的 Hystrix 客户端。但是,我们也可以通过简单地将正确的参数传递给 Feign 客户端来实现相同的效果。让我们尝试这样做。为此,首先通过添加一个更新我们的 CustomerService 的 Feign 客户端fallback class.
    
    
    package com.jc2182;
    
    import org.springframework.cloud.openfeign.FeignClient;
    
    import org.springframework.web.bind.annotation.PathVariable;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @FeignClient(name = "customer-service", fallback = FallBackHystrix.class)
    
    public interface CustomerService {
    
       @RequestMapping("/customer/{id}")
    
       public Customer getCustomerById(@PathVariable("id") Long id);
    
    }
    
    
    现在,让我们为 Feign 客户端添加回退类,当 Hystrix 电路跳闸时将调用该类。
    
    
    package com.jc2182;
    
    import org.springframework.stereotype.Component;
    
    @Component
    
    public class FallBackHystrix implements CustomerService{
    
       @Override
    
       public Customer getCustomerById(Long id) {
    
          System.out.println("Fallback called....");
    
          return new Customer(0, "Temp", "NY");
    
       }
    
    }
    
    
    最后,我们还需要创建 application-circuit.yml 启用 hystrix。
    
    
    spring:
    
       application:
    
          name: restaurant-service
    
    server:
    
       port: ${app_port}
    
    eureka:
    
       client:
    
          serviceURL:
    
             defaultZone: http://localhost:8900/eureka
    
    feign:
    
       circuitbreaker:
    
          enabled: true
    
    
    现在,我们已经准备好了设置,让我们测试一下。我们将遵循以下步骤 -
    • 启动尤里卡服务器。
    • 我们不启动客户服务。
    • 启动将在内部调用客户服务的餐厅服务。
    • 对餐厅服务进行 API 调用。鉴于客户服务已关闭,我们会注意到回退。
    假设第一步已经完成,让我们进入第三步。让我们编译代码并执行以下命令 -
    
    
    java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar --
    
    spring.config.location=classpath:application-circuit.yml
    
    
    现在让我们尝试点击 - http://localhost:8082/restaurant/customer/1
    由于我们尚未启动客户服务,因此将调用回退,并且回退将纽约作为城市发送,这就是为什么我们在以下输出中看到纽约餐厅。
    
    
    {
    
       "id": 4,
    
       "name": "Pizzeria",
    
       "city": "NY"
    
    }
    
    
    此外,为了确认,在日志中,我们会看到 -
    
    
    ….
    
    2021-03-13 16:27:02.887 WARN 21228 --- [reakerFactory-1]
    
    .s.c.o.l.FeignBlockingLoadBalancerClient : Load balancer does not contain an
    
    instance for the service customer-service
    
    Fallback called....
    
    2021-03-13 16:27:03.802 INFO 21228 --- [ main]
    
    o.s.cloud.commons.util.InetUtils : Cannot determine local hostname
    
    …..