Spring Cloud 简明教程
Spring Cloud - Circuit Breaker using Hystrix
Introduction
在分布式环境中,服务需要相互通信。通信可以同步或异步发生。当服务同步通信时,可能会出现多种造成问题的原因。例如:
In a distributed environment, services need to communicate with each other. The communication can either happen synchronously or asynchronously. When services communicate synchronously, there can be multiple reasons where things can break. For example −
-
Callee service unavailable − The service which is being called is down for some reason, for example − bug, deployment, etc.
-
Callee service taking time to respond − The service which is being called can be slow due to high load or resource consumption or it is in the middle of initializing the services.
在任何一种情况下,对于调用方来说,都浪费了时间和网络资源而等待被调用者做出响应。对于服务而言,更有意义的做法是退避并根据需要在一段时间后对被调用服务发出调用或共享默认响应。
In either of the cases, it is waste of time and network resources for the caller to wait for the callee to respond. It makes more sense for the service to back off and give calls to the callee service after some time or share default response.
Netflix Hystrix, Resilince4j 是两个众所周知且用于处理这种情况的断路器。在本教程中,我们将使用 Hystrix。
Netflix Hystrix, Resilince4j are two well-known circuit breakers which are used to handle such situations. In this tutorial, we will use Hystrix.
Hystrix – Dependency Setting
让我们使用我们之前用过的 Restaurant 的案例。让我们将 hystrix dependency 添加到我们的 Restaurant 服务中,这些服务会调用 Customer 服务。首先,让我们使用以下依赖关系更新服务的 pom.xml :
Let us use the case of Restaurant that we have been using earlier. Let us add hystrix dependency to our Restaurant Services which call the Customer Service. First, let us update the pom.xml of the service with the following dependency −
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.7.0.RELEASE</version>
</dependency>
然后,使用正确的注释(即,@EnableHystrix)为我们的 Spring 应用程序类添加注释
And then, annotate our Spring application class with the correct annotation, i.e., @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);
}
}
Points to Note
Points to Note
-
@ EnableDiscoveryClient and @EnableFeignCLient − We have already looked at these annotations in the previous chapter.
-
@EnableHystrix − This annotation scans our packages and looks out for methods which are using @HystrixCommand annotation.
Hystrix Command Annotation
完成之后,我们将重复使用之前在 Restaurant 服务中为客户服务类定义的 Feign 客户端,这里不作任何更改——
Once done, we will reuse the Feign client which we had defined for our customer service class earlier in the Restaurant service, no changes here −
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 客户端的一个简单包装。
Now, let us define the service implementation class here which would use the Feign client. This would be a simple wrapper around the feign client.
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");
}
}
现在,让我们了解一下上面代码中的一些要点——
Now, let us understand couple of points from the above code −
-
HystrixCommand annotation − This is responsible for wrapping the function call that is getCustomerById and provide a proxy around it. The proxy then gives various hooks through which we can control our call to the customer service. For example, timing out the request,pooling of request, providing a fallback method, etc.
-
Fallback method − We can specify the method we want to call when Hystrix determines that something is wrong with the callee. This method needs to have same signature as the method which is annotated. In our case, we have decided to provide the data back to our controller for the NY city.
此注释提供了几个有用的选项——
Couple of useful options this annotation provides −
-
Error threshold percent − Percentage of request allowed to fail before the circuit is tripped, that is, fallback methods are called. This can be controlled by using cicutiBreaker.errorThresholdPercentage
-
Giving up on the network request after timeout − If the callee service, in our case Customer service, is slow, we can set the timeout after which we will drop the request and move to fallback method. This is controlled by setting execution.isolation.thread.timeoutInMilliseconds
最后,这里是我们称为 CustomerServiceImpl 的控制器
And lastly, here is our controller which we call the 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
现在我们已经完成了设置,让我们尝试一下。这里有一点背景知识,我们要执行以下操作——
Now that we are done with the setup, let us give this a try. Just a bit background here, what we will do is the following −
-
Start the Eureka Server
-
Start the Customer Service
-
Start the Restaurant Service which will internally call Customer Service.
-
Make an API call to Restaurant Service
-
Shut down the Customer Service
-
Make an API call to Restaurant Service. Given that Customer Service is down, it would cause failure and ultimately, the fallback method would be called.
现在,让我们编译 Restaurant 服务代码,并使用以下命令执行
Let us now compile the Restaurant Service code and execute with the following command −
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
另外,启动客户服务和 Eureka 服务器。请注意,这些服务没有发生任何变化,它们与我们在前几章中看到的情况相同。
Also, start the Customer Service and the Eureka server. Note that there are no changes in these services and they remain same as seen in the previous chapters.
现在,让我们尝试为在华盛顿特区的简寻找餐馆。
Now, let us try to find restaurant for Jane who is based in DC.
{
"id": 1,
"name": "Jane",
"city": "DC"
}
为此,我们将点击以下 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
For doing that, we will hit the following URL: [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。
So, nothing new here, we got the restaurants which are in DC. Now, let’s move to the interesting part which is shutting down the Customer service. You can do that either by hitting Ctrl+C or simply killing the shell.
现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
Now let us hit the same URL again − [role="bare"]http://localhost:8082/restaurant/customer/1
{
"id": 4,
"name": "Pizzeria",
"city": "NY"
}
从输出中可以看到,我们拿到了位于纽约的餐厅,尽管我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了一个位于纽约的虚拟顾客。虽然没有用,但上面的示例显示后备按预期进行了调用。
As is visible from the output, we have got the restaurants from NY, although our customer is from DC.This is because our fallback method returned a dummy customer who is situated in NY. Although, not useful, the above example displays that the fallback was called as expected.
Integrating Caching with Hystrix
为了让上面这个方法更有用,我们可以在使用 Hystrix 时整合缓存。当底层服务不可用时,这会是一个提供更好答案的有用模式。
To make the above method more useful, we can integrate caching when using Hystrix. This can be a useful pattern to provide better answers when the underlying service is not available.
首先,让我们创建该服务的缓存版本。
First, let us create a cached version of the service.
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 等。
We are using hashMap as the storage to cache the data. This for developmental purpose. In Production environment, we may want to use better caching solutions, for example, Redis, Hazelcast, etc.
现在,我们只需要更新控制器中的一行来使用上面的服务 −
Now, we just need to update one line in the controller to use the above service −
@RestController
class RestaurantController {
@Autowired
CustomerServiceCachedFallback customerService;
static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
…
}
我们将遵循与上面相同步骤 −
We will follow the same steps as above −
-
Start the Eureka Server.
-
Start the Customer Service.
-
Start the Restaurant Service which internally call Customer Service.
-
Make an API call to the Restaurant Service.
-
Shut down the Customer Service.
-
Make an API call to the Restaurant Service. Given that Customer Service is down but the data is cached, we will get a valid set of data.
现在,让我们进行同样的过程,直到步骤 3。
Now, let us follow the same process till step 3.
现在点击 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
Now hit the URL: [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。
So, nothing new here, we got the restaurants which are in DC. Now, let us move to the interesting part which is shutting down the Customer service. You can do that either by hitting Ctrl+C or simply killing the shell.
现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
Now let us hit the same URL again − [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
从输出中可以看到,我们拿到了位于华盛顿特区的餐厅,这正是我们预期的,因为我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了缓存的顾客数据。
As is visible from the output, we have got the restaurants from DC which is what we expect as our customer is from DC. This is because our fallback method returned a cached customer data.
Integrating Feign with Hystrix
我们发现了如何使用 @HystrixCommand 注解来触发断路并提供后备服务。但是,我们不得不另外定义一个 Service 类来封装我们的 Hystrix 客户端。但是,我们也可以通过简单的传递正确参数给 Feign 客户端来实现相同的目标。让我们尝试这么做。为此,首先通过添加注解 fallback class 来更新我们的 CustomerService 的 Feign 客户端。
We saw how to use @HystrixCommand annotation to trip the circuit and provide a fallback. But we had to additionally define a Service class to wrap our Hystrix client. However, we can also achieve the same by simply passing correct arguments to Feign client. Let us try to do that. For that, first update our Feign client for CustomerService by adding a fallback class.
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 断路触发时将调用该类。
Now, let us add the fallback class for the Feign client which will be called when the Hystrix circuit is tripped.
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。
Lastly, we also need to create the application-circuit.yml to enable hystrix.
spring:
application:
name: restaurant-service
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
feign:
circuitbreaker:
enabled: true
现在我们已经准备好设置,让我们来测试一下。我们将按以下步骤进行:
Now, that we have the setup ready, let us test this out. We will follow these steps −
-
Start the Eureka Server.
-
We do not start the Customer Service.
-
Start the Restaurant Service which will internally call Customer Service.
-
Make an API call to Restaurant Service. Given that Customer Service is down, we will notice the fallback.
假设第一步已经完成,让我们继续进行第三步。让我们编译代码并执行以下命令:
Assuming 1st step is already done, let’s move to step 3. Let us compile the code and execute the following command −
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
Let us now try to hit − [role="bare"]http://localhost:8082/restaurant/customer/1
由于我们尚未启动客户服务,因此将调用后备,并且后备将 NY 作为城市发送过来,这就是为什么我们在以下输出中看到 NY 餐馆的原因。
As we have not started Customer Service, fallback would be called and the fallback sends over NY as the city, which is why, we see NY restaurants in the following output.
{
"id": 4,
"name": "Pizzeria",
"city": "NY"
}
此外,为了确认,在日志中,我们会看到:
Also, to confirm, in the logs, we would see −
….
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
…..