Microservices with Java — Part 2
Fault tolerance and Configuration Server
This is the second post of my microservices series, where I talk about my impressions of this architecture style using Java with Spring Boot.
The full series of articles are listed below:
- Part I: Project setup, REST communication and Service Discovery
- Part II: Fault tolerance and Configuration Server
- Part III: API Gateway and Centralized logs
- Part IV: Authentication and Authorization (it’s yet on my TODO list.. :-P)
The code implemented in each article is available on GitHub in a specific branch, and the final implementation is on the main branch.
In this article we will implement Fault Tolerance and Configuration Server, again using a practical approach using Java and Spring Boot framework.
The code of this article is available on GitHub in branch ‘ch-02’.
Fault Tolerance
Fault tolerance is the property that enables a system to continue operating properly in the event of the failure of some of its components [1].
In other words, it is the ability of the system of maintaining functionality when some parts of it breaks down, which means that the system is resilient.
In the scenario of the microservices that we built so far (in part 1 of this series), what if the products-catalog service goes down, or the user-info service becomes very slow, should it degrade the whole system and stop the purchase? As we already have the respective ids in the shopping-cart, maybe we can proceed with transaction even though some information could not be displayed.
So, we have to make a resilient architecture in case of failure during purchase transaction.
A very common solution is the Circuit Breaker Pattern. The solution involves to protected a request call by wrapping it in a circuit breaker object, and this object can detect if something is wrong (failure), and then it take temporary steps to avoid the situation getting worse, and then it deactivate the problematic component so that it does not affect the downstream components.
The circuit breaker monitors for failures, and since it reaches a specific threshold, it start to return a given fallback response for further calls. The circuit breaker monitors the protected call, and it can automatically resume to normal operation.
The fallback response is up to you: you can return an informative error, a default object or return some cached data.
The circuit breaker uses some parameters to take the decision to trip:
- last ‘n’ requests to consider for decision;
- how many of those should fail;
- timeout duration to be considered;
- how long after a circuit trip to try again.
In Spring Boot framework, the default implementation was Spring Cloud Netflix Hystrix, but since Spring Boot version 2.4.2 it’s used Spring Cloud Circuit breaker with Resilience4j as the underlying implementation, which is the one we are going to use in this article.
Let’s start adding the ‘circuitbreaker-resilience4j’ dependency in the pom.xml of shopping-cart microservice:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
Next, let’s create a configuration spring bean for circuit breaker with example parameters. We can place this bean at main class:
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration() {
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowSize(2)
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build());
}
Now, in the IntegrationService class, we can inject the circuit breaker factory and implement some fallback methods in case of failure of products or users requests, and wrap the call in a circuit breaker object, like this:
@Service @AllArgsConstructor
public class IntegrationService {
private final UserFeignClient userFeignClient;
private final ProductFeignClient productFeignClient;
private final CircuitBreakerFactory circuitBreakerFactory;
private final String USER_SERVICE_NAME = "USER-INFORMATION-SERVICE";
private final String PRODUCT_SERVICE_NAME = "PRODUCT-CATALOG-SERVICE";
public UserInfo getRemoteUserInfo(Long userId) {
// wrap in circuit breaker for fault tolerance
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
var user = circuitBreaker.run(() -> userFeignClient.findById(userId), throwable -> this.findUserByIdFallBack(userId));
return user;
}
public List<Item> getRemoteProductItemsInfo(List<Item> items) {
items.forEach(item -> {
// wrap in circuit breaker for fault tolerance
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
var product = circuitBreaker.run(() -> productFeignClient.findById(item.getProduct().getId()),
throwable -> this.findProductByIdFallBack(item.getProduct().getId()));
item.setProduct(product);
});
return items;
} // fallback: returns default object
private UserInfo findUserByIdFallBack(Long id) {
return new UserInfo(id, "name info unavailable", null, null);
}
private ProductOverview findProductByIdFallBack(Long id) {
return new ProductOverview(id, "product name unavailable", null);
}
}
And that’s it. If we launch all the microservices and POST a purchase request to shopping-cart service, it should run fine. But if we got down the products and/or user microservices, we should have the fallback response to these objects like shown below:
Configuration Server
Another improvement we can make in our system is to use a centralized configuration server.
Right now, we have local configurations for each microservice, usually at application.yml file. But we can change that to a client-server approach for storing and serving distributed configurations across multiple applications and environments, ideally versioned under Git version control and able to be modified at application runtime.
So, let’s set up a Config Server and configure the clients to consumes the configuration on startup and then refreshes the configuration without restarting the client. We will use Spring Cloud Config for this purpose:
After openning the project in IDE, let’s enable the config server with proper annotation ‘EnableConfigServer’ at main class:
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
It is possible to use a local directory as source of configs, or a git remote repository. In this case, we will use the same GitHub repository of this project, on a ‘configs’ folder. So, we need to set this configuration at servers application.yml file:
spring:
application:
name: CONFIG-SERVER-SERVICE
cloud:
config:
server:
git:
uri: https://github.com/tiagoamp/microservices-series
search-paths: configs
server:
port: 8084
And the server is read to go!
Now we need to extract client’s configuration in specific application.yml files, and store them in the configured git repository. But each microservice has to load the configuration at startup time, so we have to use another Spring config file that is load one step before: bootstrap.yml, which will have a reference to locate the config server:
spring:
profiles:
active: default
cloud:
config:
uri: http://localhost:8084/
So let’s create a remote application.yml config file using each service name, and set the bootstrap.yml file in each client to reach them via Config Server.
We have then at GitHub repository these external files: PRODUCT-CATALOG-SERVICE.yml, SHOPPING-CART-SERVICE.yml, USER-INFORMATION-SERVICE.yml.
For each microservice, we configure the bootstrap.yml the location to Config Server to retrieve its configs, and application.yml files must be as follows:
- product-catalog: bootstrap.yml and application.yml;
- users-info: bootstrap.yml and application.yml;
- shopping-cart: bootstrap.yml and application.yml.
Now, if we run all the microservices, everything should be running with no errors, but now the configurations are centralized at a git repository accessed by the Configuration Server.