Spring Cloud - 使用 Hystrix 的断路器

简介

在分布式环境中,服务需要相互通信。 通信可以同步或异步发生。 当服务同步通信时,可能有多种原因导致事情中断。 例如 −

  • Callee service unavailable − 正在调用的服务由于某种原因而关闭,例如 − 错误,部署等

  • Callee service taking time to respond − 由于高负载或资源消耗,正在调用的服务可能会很慢,或者它正在初始化服务。

在任何一种情况下,调用者等待被调用者响应都是浪费时间和网络资源。 服务在一段时间后退出并调用被调用者服务或共享默认响应更有意义。

Netflix Hystrix、Resilence4j 是两个著名的断路器,用于处理此类情况。 在本教程中,我们将使用 Hystrix。


Hystrix – 依赖设置

让我们使用我们之前使用的餐厅案例。 让我们将 hystrix 依赖项 添加到调用客户服务的餐厅服务中。 首先,让我们使用以下依赖更新服务的 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.tutorialspoint;
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);
   }
}

注意事项

  • @ EnableDiscoveryClient@EnableFeignCLient − 我们已经在前一章看过这些注解。

  • @EnableHystrix − 这个注解扫描我们的包并寻找使用@HystrixCommand 注解的方法。


Hystrix 命令注解

完成后,我们将重用我们之前在餐厅服务中为客户服务类定义的 Feign 客户端,这里没有更改 −

package com.tutorialspoint;
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);
}

现在,让我们在这里定义将使用 Feign 客户端的 service implementation 类。 这将是对 feign 客户端的简单包装。

package com.tutorialspoint;
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 注解 − 这负责包装 getCustomerById 的函数调用并围绕它提供代理。 然后代理提供各种钩子,我们可以通过这些钩子来控制我们对客户服务的调用。 例如请求超时、请求池化、提供回退方法等。

  • Fallback 方法 − 当 Hystrix 确定被调用者有问题时,我们可以指定我们想要调用的方法。 此方法需要与被注解的方法具有相同的签名。 在我们的案例中,我们决定将数据提供给我们纽约市的控制器。

此注解提供了几个有用的选项 −

  • Error threshold percent − 在电路被触发之前允许失败的请求百分比,即调用回退方法。 这可以通过使用 cicutiBreaker.errorThresholdPercentage 来控制

  • Giving up on the network request after timeout − 如果被调用者服务(在我们的例子中为客户服务)很慢,我们可以设置超时时间,之后我们将放弃请求并转移到回退方法。 这是通过设置 execution.isolation.thread.timeoutInMilliseconds 来控制的

最后,这是我们的控制器,我们称之为 CustomerServiceImpl

package com.tutorialspoint;
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());
   }
}

电路跳闸/打开

现在我们已经完成了设置,让我们试一试。 这里只是一点背景,我们要做的是以下 −

  • 启动 Eureka 服务

  • 启动 Customer 服务

  • 启动将在内部调用客户服务的餐厅服务。

  • 对餐厅服务进行 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"
   }
]

所以,这里没有什么新鲜事,我们得到了位于 DC 的餐厅。 现在,让我们转到有趣的部分,即关闭客户服务。 您可以通过按 Ctrl+C 或简单地杀死 shell 来做到这一点。

现在让我们再次点击相同的 URL − http://localhost:8082/restaurant/customer/1

{
   "id": 4,
   "name": "Pizzeria",
   "city": "NY"
}

从输出中可以看出,尽管我们的客户来自 DC,但我们从 NY 获得了餐馆。这是因为我们的后备方法返回了一个位于 NY 的虚拟客户。 虽然没有用,但上面的示例显示回退已按预期调用。


将缓存与 Hystrix 集成

为了让上面的方法更有用,我们可以在使用 Hystrix 的时候集成缓存。 当底层服务不可用时,这可能是一种有用的模式,可以提供更好的答案。

首先,让我们创建服务的缓存版本。

package com.tutorialspoint;
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();
   …
}

我们将按照与上述相同的步骤 −

  • 启动 Eureka 服务。

  • 启动 Customer 服务。

  • 启动内部调用客户服务的餐厅服务。

  • 对餐厅服务进行 API 调用。

  • 关闭客户服务。

  • 对餐厅服务进行 API 调用。 鉴于客户服务已关闭但数据已缓存,我们将获得一组有效的数据。

现在,让我们按照相同的过程直到第 3 步。

现在点击 URL:http://localhost:8082/restaurant/customer/1

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

所以,这里没有什么新鲜事,我们得到了位于 DC 的餐厅。 现在,让我们转到有趣的部分,即关闭客户服务。 您可以通过按 Ctrl+C 或简单地杀死 shell 来做到这一点。

现在让我们再次点击相同的 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 客户端来实现相同的目的。 让我们尝试这样做。 为此,首先通过添加 fallback 类来更新我们的 CustomerService 的 Feign 客户端。

package com.tutorialspoint;
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.tutorialspoint;
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

现在,我们已经准备好设置,让我们测试一下。 我们将按照这些步骤 −

  • 启动 Eureka 服务。

  • 我们不启动客户服务。

  • 启动将在内部调用客户服务的餐厅服务。

  • 对餐厅服务进行 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

由于我们尚未启动客户服务,因此将调用回退并且回退将 NY 作为城市发送,这就是为什么我们在以下输出中看到 NY 餐馆。

{
   "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
…..