Spring Cloud 简明教程

Spring Cloud - Circuit Breaker using Hystrix

Introduction

在分布式环境中,服务需要相互通信。通信可以同步或异步发生。当服务同步通信时,可能会出现多种造成问题的原因。例如:

  1. Callee service unavailable - 调用正在进行中的服务由于某种原因而关闭,例如错误、部署等。

  2. Callee service taking time to respond - 由于高负载或资源消耗,要调用的服务可能很慢,或者它正处于初始化服务的过程中。

在任何一种情况下,对于调用方来说,都浪费了时间和网络资源而等待被调用者做出响应。对于服务而言,更有意义的做法是退避并根据需要在一段时间后对被调用服务发出调用或共享默认响应。

Netflix Hystrix, Resilince4j 是两个众所周知且用于处理这种情况的断路器。在本教程中,我们将使用 Hystrix。

Hystrix – Dependency Setting

让我们使用我们之前用过的 Restaurant 的案例。让我们将 hystrix dependency 添加到我们的 Restaurant 服务中,这些服务会调用 Customer 服务。首先,让我们使用以下依赖关系更新服务的 pom.xml

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
   <version>2.7.0.RELEASE</version>
</dependency>

然后,使用正确的注释(即,@EnableHystrix)为我们的 Spring 应用程序类添加注释

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);
   }
}

Points to Note

  1. @ EnableDiscoveryClient@EnableFeignCLient ——我们在上一章已经查看了这些注释。

  2. @EnableHystrix ——此注释扫描我们的软件包,查找正在使用 @HystrixCommand 注释的方法。

Hystrix Command Annotation

完成之后,我们将重复使用之前在 Restaurant 服务中为客户服务类定义的 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);
}

现在,让我们在此处定义 service implementation 类,该类将使用 Feign 客户端。这将是 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");
   }
}

现在,让我们了解一下上面代码中的一些要点——

  1. HystrixCommand annotation ——此命令负责封装函数调用,即 getCustomerById ,并在其周围提供代理。然后,该代理提供各种挂钩,通过这些挂钩我们可以控制对客户服务的调用。例如,对请求进行超时设置,对请求进行池化,提供回退方法等。

  2. Fallback method ——当 Hystrix 确定调用者出了问题时,我们可以指定要调用的方法。此方法需要具有与带注释的方法相同的签名。在我们的例子中,我们决定将数据提供给控制器,作为纽约市的数据。

此注释提供了几个有用的选项——

  1. Error threshold percent ——在中断跳闸之前允许失败的请求的百分比,即调用回退方法。这可以通过使用 cicutiBreaker.errorThresholdPercentage 来控制

  2. 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());
   }
}

Circuit Tripping/Opening

现在我们已经完成了设置,让我们尝试一下。这里有一点背景知识,我们要执行以下操作——

  1. Start the Eureka Server

  2. Start the Customer Service

  3. 启动 Restaurant 服务,该服务将在内部调用客户服务。

  4. 对 Restaurant 服务进行 API 调用

  5. 关闭客户服务

  6. 对 Restaurant 服务进行 API 调用。鉴于客户服务已关闭,它会导致故障,最终将调用回退方法。

现在,让我们编译 Restaurant 服务代码,并使用以下命令执行

java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar

另外,启动客户服务和 Eureka 服务器。请注意,这些服务没有发生任何变化,它们与我们在前几章中看到的情况相同。

现在,让我们尝试为在华盛顿特区的简寻找餐馆。

{
   "id": 1,
   "name": "Jane",
   "city": "DC"
}

为此,我们将点击以下 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1

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

因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。

现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1

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

从输出中可以看到,我们拿到了位于纽约的餐厅,尽管我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了一个位于纽约的虚拟顾客。虽然没有用,但上面的示例显示后备按预期进行了调用。

Integrating Caching with 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();
   …
}

我们将遵循与上面相同步骤 −

  1. Start the Eureka Server.

  2. Start the Customer Service.

  3. 启动 Restaurant Service,该服务在内部调用 Customer Service。

  4. 对 Restaurant Service 进行一次 API 调用。

  5. 关闭 Customer Service。

  6. 对 Restaurant Service 进行一次 API 调用。假设 Customer Service 已关闭,但数据已被缓存,我们将得到一组有效数据。

现在,让我们进行同样的过程,直到步骤 3。

现在点击 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1

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

因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。

现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1

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

从输出中可以看到,我们拿到了位于华盛顿特区的餐厅,这正是我们预期的,因为我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了缓存的顾客数据。

Integrating Feign with Hystrix

我们发现了如何使用 @HystrixCommand 注解来触发断路并提供后备服务。但是,我们不得不另外定义一个 Service 类来封装我们的 Hystrix 客户端。但是,我们也可以通过简单的传递正确参数给 Feign 客户端来实现相同的目标。让我们尝试这么做。为此,首先通过添加注解 fallback class 来更新我们的 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

现在我们已经准备好设置,让我们来测试一下。我们将按以下步骤进行:

  1. Start the Eureka Server.

  2. 我们不启动客户服务。

  3. 启动 Restaurant 服务,该服务将在内部调用客户服务。

  4. 向餐馆服务进行 API 调用。鉴于客户服务已关闭,我们将注意到后备。

假设第一步已经完成,让我们继续进行第三步。让我们编译代码并执行以下命令:

java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar --
spring.config.location=classpath:application-circuit.yml

现在我们尝试点击 − [role="bare"] [role="bare"]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
…..