Spring Cloud Netflix - Hystrix
1. Overview
In this tutorial, we’ll cover the Spring Cloud Netflix Hystrix – the fault tolerance library. We’ll use the library and implement the Circuit Breaker enterprise pattern, which is describing a strategy against failure cascading at different levels in an application.
The principle is analogous to electronics: Hystrix is watching methods for failing calls to related services. If there is such a failure, it will open the circuit and forward the call to a fallback method.
The library will tolerate failures up to a threshold. Beyond that, it leaves the circuit open. Which means, it will forward all subsequent calls to the fallback method, to prevent future failures. This creates a time buffer for the related service to recover from its failing state.
2. REST Producer
To create a scenario, which demonstrates the Circuit Breaker pattern, we need a service first. We’ll name it ‘REST Producer’, because it provides data for the Hystrix-enabled ‘REST Consumer’ – which we’ll create in the next step.
Let’s create a new Maven project using the spring-boot-starter-web dependency:
1
2
3
4
5
| < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > < version >1.4.0.RELEASE</ version > </ dependency > |
The project itself is intentionally kept simple. It consists of a controller interface with one @RequestMapping annotated GET method returning simply a String, a @RestController implementing this interface and a @SpringBootApplication.
We’ll begin with the interface:
1
2
3
4
| public interface GreetingController { @GetMapping ( "/greeting/{username}" ) String greeting( @PathVariable ( "username" ) String username); } |
And the implementation:
1
2
3
4
5
6
7
8
| @RestController public class GreetingControllerImpl implements GreetingController { @Override public String greeting( @PathVariable ( "username" ) String username) { return String.format( "Hello %s!\n" , username); } } |
Next, we’ll write down the main application class:
1
2
3
4
5
6
| @SpringBootApplication public class RestProducerApplication { public static void main(String[] args) { SpringApplication.run(RestProducerApplication. class , args); } } |
To complete this section, the only thing left to do is to configure an application-port on which we’ll be listening. We won’t use the default port 8080 because the port should remain reserved for the application described in the next step.
Furthermore, we’re defining an application name to be able to look-up our ‘REST Producer’ from the client application that we’ll introduce, later. Let’s create an application.properties with the following content:
1
2
| server.port=9090 spring.application.name=rest-producer |
Now we’re able to test our ‘REST Producer’ using curl:
1
2
| $> curl http: //localhost :9090 /greeting/Cid Hello Cid! |
3. REST Consumer with Hystrix
For our demonstration scenario, we’ll be implementing a web-application, which is consuming the REST service from the previous step using RestTemplate and Hystrix. For the sake of simplicity, we’ll call it the ‘REST Consumer’.
Consequently, we create a new Maven project with spring-cloud-starter-hystrix,spring-boot-starter-web and spring-boot-starter-thymeleaf as dependencies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| < dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-hystrix</ artifactId > < version >1.1.5.RELEASE</ version > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > < version >1.4.0.RELEASE</ version > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-thymeleaf</ artifactId > < version >1.4.0.RELEASE</ version > </ dependency > |
For the Circuit Breaker to work, Hystix will scan @Component or @Service annotated classes for @HystixCommand annotated methods, implement a proxy for it and monitor its calls.
We’re going to create a @Service class first, which will be injected to a @Controller. Since we’re building a web-application using Thymeleaf, we also need an HTML template to serve as a view.
This will be our injectable @Service implementing a @HystrixCommand with an associated fallback method. This fallback has to use the same signature as the ‘original’:
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Service public class GreetingService { @HystrixCommand (fallbackMethod = "defaultGreeting" ) public String getGreeting(String username) { return new RestTemplate() .getForObject( "http://localhost:9090/greeting/{username}" , String. class , username); } private String defaultGreeting(String username) { return "Hello User!" ; } } |
RestConsumerApplication will be our main application class. The @EnableCircuitBreaker annotation will scan the classpath for any compatible Circuit Breaker implementation.
To use Hystrix explicitly, you have to annotate this class with @EnableHystrix:
1
2
3
4
5
6
7
| @SpringBootApplication @EnableCircuitBreaker public class RestConsumerApplication { public static void main(String[] args) { SpringApplication.run(RestConsumerApplication. class , args); } } |
We’ll set up the controller using our GreetingService:
1
2
3
4
5
6
7
8
9
10
11
12
| @Controller public class GreetingController { @Autowired private GreetingService greetingService; @GetMapping ( "/get-greeting/{username}" ) public String getGreeting(Model model, @PathVariable ( "username" ) String username) { model.addAttribute( "greeting" , greetingService.getGreeting(username)); return "greeting-view" ; } } |
And here’s the HTML template:
1
2
3
4
5
6
7
8
9
| <!DOCTYPE html> < html xmlns:th = "http://www.thymeleaf.org" > < head > < title >Greetings from Hystrix</ title > </ head > < body > < h2 th:text = "${greeting}" /> </ body > </ html > |
To ensure that the application is listening on a defined port, we put the following in an application.properties file:
1
| server.port=8080 |
To see a Hystix circuit breaker in action, we’re starting our ‘REST Consumer’ and pointing our browser to http://localhost:8080/get-greeting/Cid. Under normal circumstances, the following will be shown:
1
| Hello Cid! |
To simulate a failure of our ‘REST Producer’, we’ll simply stop it, and after we finished refreshing the browser we should see a generic message, returned from the fallback method in our @Service:
1
| Hello User! |
4. REST Consumer with Hystrix and Feign
Now, we’re going to modify the project from the previous step to use Spring Netflix Feign as declarative REST client, instead of Spring RestTemplate.
The advantage is that we’re later able to easily refactor our Feign Client interface to use Spring Netflix Eureka for service discovery.
To start the new project, we’ll make a copy of our ‘REST Consumer’, and add the ‘REST Producer’ and spring-cloud-starter-feign as dependencies:
1
2
3
4
5
6
7
8
9
10
| < dependency > < groupId >com.baeldung.spring.cloud</ groupId > < artifactId >spring-cloud-hystrix-rest-producer</ artifactId > < version >1.0.0-SNAPSHOT</ version > </ dependency > < dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-feign</ artifactId > < version >1.1.5.RELEASE</ version > </ dependency > |
Now, we’re able to use our GreetingController to extend a Feign Client. We’ll implement Hystrix fallback as a static inner class annotated with @Component.
Alternatively, we could define a @Bean annotated method returning an instance of this fallback class.
The name property of the @FeignClient is mandatory. It is used, to look-up the application either by service discovery via a Eureka Client or via URL, if this property is given. For more on using Spring Netflix Eureka for service discovery have a look at this article:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @FeignClient ( name = "rest-producer" url = "http://localhost:9090" , fallback = GreetingClient.GreetingClientFallback. class ) public interface GreetingClient extends GreetingController { @Component public static class GreetingClientFallback implements GreetingController { @Override public String greeting( @PathVariable ( "username" ) String username) { return "Hello User!" ; } } } |
In the RestConsumerFeignApplication, we’ll put an additional annotation to enable Feign integration, in fact, @EnableFeignClients, to the main application class:
1
2
3
4
5
6
7
8
9
| @SpringBootApplication @EnableCircuitBreaker @EnableFeignClients public class RestConsumerFeignApplication { public static void main(String[] args) { SpringApplication.run(RestConsumerFeignApplication. class , args); } } |
We’re going to modify the controller to use an auto-wired Feign Client, rather than the previously injected @Service, to retrieve our greeting:
1
2
3
4
5
6
7
8
9
10
11
| @Controller public class GreetingController { @Autowired private GreetingClient greetingClient; @GetMapping ( "/get-greeting/{username}" ) public String getGreeting(Model model, @PathVariable ( "username" ) String username) { model.addAttribute( "greeting" , greetingClient.greeting(username)); return "greeting-view" ; } } |
To distinguish this example from the previous, we’ll alter the application listening port in the application.properties:
1
| server.port=8082 |
Finally, we’ll test this Feign-enabled ‘REST Consumer’ like the one from the previous section. The expected result should be the same.
com/spring-cloud-netflix-hystrix
5. Cache Fallback with Hystrix
Now, we are going to add Hystrix to our Spring Cloud project. In this cloud project, we have a rating service that talks to the database and gets ratings of books.
Let’s assume that our database is a resource under demand, and its response latency might vary in time or might not be available in times. We’ll handle this scenario with the Hystrix Circuit-Breaker falling back to a cache for the data.
5.1. Setup and Configuration
Let us add the spring-cloud-starter-hystrix dependency to our rating module:
1
2
3
4
<
dependency
>
<
groupId
>org.springframework.cloud</
groupId
>
<
artifactId
>spring-cloud-starter-hystrix</
artifactId
>
</
dependency
>
When ratings are inserted/updated/deleted in the database, we’ll replicate the same to the Redis cache with a Repository. If you want to learn more about Redis, check this article.
Let’s update the RatingService to wrap the database querying methods in a Hystrix command with @HystrixCommand and configure it with a fallback to reading from Redis:
1
2
3
4
5
6
7
8
9
10
11
12
13
@HystrixCommand
(
commandKey =
"ratingsByIdFromDB"
,
fallbackMethod =
"findCachedRatingById"
,
ignoreExceptions = { RatingNotFoundException.
class
})
public
Rating findRatingById(Long ratingId) {
return
Optional.ofNullable(ratingRepository.findOne(ratingId))
.orElseThrow(() ->
new
RatingNotFoundException(
"Rating not found. ID: "
+ ratingId));
}
public
Rating findCachedRatingById(Long ratingId) {
return
cacheRepository.findCachedRatingById(ratingId);
}
Note that the fallback method should have the same signature of a wrapped method and must reside in the same class. Now when the findRatingById fails or gets delayed more than a given threshold, Hystrix fallbacks to findCachedRatingById.
As the Hystrix capabilities are transparently injected as AOP advice, we have to adjust the order in which the advice is stacked, in case if we have other advice like Spring’s transactional advice. Here we have adjusted the Spring’s transaction AOP advice to have lower precedence than Hystrix AOP advice:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@EnableHystrix
@EnableTransactionManagement
(
order=Ordered.LOWEST_PRECEDENCE,
mode=AdviceMode.ASPECTJ)
public
class
RatingServiceApplication {
@Bean
@Primary
@Order
(value=Ordered.HIGHEST_PRECEDENCE)
public
HystrixCommandAspect hystrixAspect() {
return
new
HystrixCommandAspect();
}
// other Beans, Configurations
}
Here, we have adjusted the Spring’s transaction AOP advice to have lower precedence than Hystrix AOP advice.
1
2
3
4
| < dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-hystrix</ artifactId > </ dependency > |
1
2
3
4
5
6
7
8
9
10
11
12
13
| @HystrixCommand ( commandKey = "ratingsByIdFromDB" , fallbackMethod = "findCachedRatingById" , ignoreExceptions = { RatingNotFoundException. class }) public Rating findRatingById(Long ratingId) { return Optional.ofNullable(ratingRepository.findOne(ratingId)) .orElseThrow(() -> new RatingNotFoundException( "Rating not found. ID: " + ratingId)); } public Rating findCachedRatingById(Long ratingId) { return cacheRepository.findCachedRatingById(ratingId); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @EnableHystrix @EnableTransactionManagement ( order=Ordered.LOWEST_PRECEDENCE, mode=AdviceMode.ASPECTJ) public class RatingServiceApplication { @Bean @Primary @Order (value=Ordered.HIGHEST_PRECEDENCE) public HystrixCommandAspect hystrixAspect() { return new HystrixCommandAspect(); } // other Beans, Configurations } |
Comments
Post a Comment