Spring Cloud - 使用 Hystrix 实现断路器
简介
在分布式环境中,服务需要相互通信。通信可以是同步的,也可以是异步的。当服务同步通信时,可能有多种原因导致通信中断。例如:
被调用服务不可用 - 被调用的服务由于某种原因(例如:bug、部署等)而宕机。
被调用服务响应时间过长 - 被调用的服务由于负载过高、资源消耗过大或正在初始化服务而导致响应缓慢。
在任何一种情况下,对于调用方来说,等待被调用方响应都是浪费时间和网络资源的。更有意义的做法是让服务退避,并在一段时间后再次调用被调用服务,或者共享默认响应。
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>
然后,使用正确的注解,即 @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);
}
}
注意事项
@ 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 客户端。这将是对 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)并在其周围提供一个代理。然后,代理通过各种钩子提供控制我们对客户服务调用的方法。例如,请求超时、请求池化、提供回退方法等。
回退方法 - 当 Hystrix 确定被调用服务存在问题时,我们可以指定要调用的方法。此方法需要与被注解的方法具有相同的签名。在我们的例子中,我们决定将数据提供给纽约市的控制器。
此注解提供的一些有用选项:
错误阈值百分比 - 在断路器跳闸(即调用回退方法)之前允许请求失败的百分比。这可以通过使用 cicutiBreaker.errorThresholdPercentage 来控制。
在超时后放弃网络请求 - 如果被调用服务(在本例中为客户服务)速度缓慢,我们可以设置超时时间,在此时间之后我们将放弃请求并转到回退方法。这可以通过设置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 服务器
启动客户服务
启动餐厅服务,该服务将在内部调用客户服务。
对餐厅服务进行 API 调用
关闭客户服务
对餐厅服务进行 API 调用。由于客户服务已关闭,这将导致失败,最终将调用回退方法。
现在让我们编译餐厅服务代码并使用以下命令执行:
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
此外,启动客户服务和 Eureka 服务器。请注意,这些服务没有任何更改,它们与上一章中看到的一样。
现在,让我们尝试查找位于华盛顿特区的 Jane 的餐厅。
{
"id": 1,
"name": "Jane",
"city": "DC"
}
为此,我们将访问以下 URL:https://:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
所以,这里没有什么新内容,我们得到了位于华盛顿特区的餐厅。现在,让我们进入有趣的部分,即关闭客户服务。您可以通过按下 Ctrl+C 或简单地杀死 shell 来执行此操作。
现在让我们再次访问相同的 URL:https://:8082/restaurant/customer/1
{
"id": 4,
"name": "Pizzeria",
"city": "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 服务器。
启动客户服务。
启动餐厅服务,该服务在内部调用客户服务。
对餐厅服务进行 API 调用。
关闭客户服务。
对餐厅服务进行 API 调用。由于客户服务已关闭,但数据已缓存,我们将获得一组有效的数据。
现在,让我们按照步骤 3 之前的相同过程操作。
现在访问 URL:https://:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
所以,这里没有什么新内容,我们得到了位于华盛顿特区的餐厅。现在,让我们进入有趣的部分,即关闭客户服务。您可以通过按下 Ctrl+C 或简单地杀死 shell 来执行此操作。
现在让我们再次访问相同的 URL:https://:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
从输出中可以看出,我们得到了华盛顿特区的餐厅,这正是我们期望的,因为我们的客户来自华盛顿特区。这是因为我们的回退方法返回了缓存的客户数据。
将 Feign 与 Hystrix 集成
我们了解了如何使用 @HystrixCommand 注解来跳闸断路器并提供回退。但是,我们还必须另外定义一个服务类来包装我们的 Hystrix 客户端。但是,我们也可以通过简单地将正确的参数传递给 Feign 客户端来实现相同的效果。让我们尝试这样做。为此,首先通过添加回退类更新我们对 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: https://:8900/eureka
feign:
circuitbreaker:
enabled: true
现在,我们已经完成了设置,让我们测试一下。我们将按照以下步骤操作:
启动 Eureka 服务器。
我们不启动客户服务。
启动餐厅服务,该服务将在内部调用客户服务。
对餐厅服务进行 API 调用。由于客户服务已关闭,我们将注意到回退。
假设步骤 1 已完成,让我们转到步骤 3。让我们编译代码并执行以下命令:
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar -- spring.config.location=classpath:application-circuit.yml
现在让我们尝试访问:https://: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 …..