Spring Boot & Kubernetes

Spring Boot integrates with Resilience4J project to implement Circuit Breaker pattern.

When we created the project, we already set the circuit breaker dependencies, so we don’t need to register it in the pom.xml.

Let’s modify the previous application so the hello message is retrieved from an external service.

Rest Client

We need to implement a Rest client to request the message to the external service. In this case, RestTemplate approach is used, but you’ll see that any other approach works in a similar way from the point of view resiliency.

Create a new class named MessageService with the followign content:

org.acme.hellokubernetes.MessageService.java
package org.acme.hellokubernetes;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class MessageService {

    public String getMessage() {
        System.out.println("Get Message Remote call");

        final RestTemplate messageServiceGateway = new RestTemplate();
        return messageServiceGateway.getForEntity("http://localhost:8090", String.class).getBody();
    }

}

And add the following lines at the HelloController.java class to use the previous class to make requests to an external service:

org.acme.hellokubernetes.HelloController.java
@Autowired
MessageService messageService;

@GetMapping("/hello")
String hello() {
    return "This is Spring calling a " + messageService.getMessage();
}

Deploy External Service

Let’s deploy the external service to our Kubernetes cluster and expose the service so it’s accessible from localhost. This service returns a message when we query the default path. One of the important aspect of this service is a sleep call of three seconds, so every time we send a request to the service, it takes three seconds to return the response.

To deploy the service to Kubernetes run the following command in a new terminal window:

kubectl create deployment externalservice --image=quay.io/rhdevelopers/istio-tutorial-recommendation:v2.2-timeout
kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
externalservice-jd6jk   1/1     Running   0          18s

Then port forward the service so it’s accessible from localhost.

kubectl port-forward -n default externalservice-jd6jk  8090:8080

Access the Service

Let’s deploy the Spring Boot application:

Unresolved include directive in modules/ROOT/pages/05-resiliency.adoc - include::partial$package_run.adoc[]

Then curl the service, the response is sent back after 3 seconds:

This is Spring calling a recommendation v2 from 'd58cdc29c05f': 4

Resiliency

There are several resilience strategies to follow, but the most used are:

  • timeout

  • circuit breaker

  • retries

Timeout

We’ve seen that the external service takes three seconds to produce a response. Usually when we’re designing an application, if an external communication takes more than one second, then a timeout error should be rised.

Let’s configure Spring Boot to start using resiliency4j and add a timeout to the code that is accessing the external service.

Open HelloKubernetesApplication.java file and add the following method to configure resiliency4J factory to use timeout strategy configured in one second .

org.acme.hellokubernetes.HelloKubernetesApplication.java
package org.acme.hellokubernetes;

import java.time.Duration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;

@SpringBootApplication
public class HelloKubernetesApplication {

	@Bean
	public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() { (1)
    return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(1)).build())
            .build() (2)
        );
	}

	public static void main(String[] args) {
		SpringApplication.run(HelloKubernetesApplication.class, args);
	}

}
1 Creates a default circuit breaker factory.
2 Configures timeout to one second.

Then we need to wrap the code where circuit breaker must be applied. Open HelloController.java and wrap the hello() method:

org.acme.hellokubernetes.HelloController.java
package org.acme.hellokubernetes;

import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;

@RestController
public class HelloController {

    private CircuitBreaker messageCircuitBreaker; (1)

    private Set<String> names = new HashSet<>();

    public HelloController(MeterRegistry registry, CircuitBreakerFactory messageCircuitBreakerFactory) { (2)
      this.messageCircuitBreaker = messageCircuitBreakerFactory.create("message"); (3)
      registry.gaugeCollectionSize("names.size", Tags.empty(), names);
    }

    @Autowired
    MessageService messageService;

    @GetMapping("/hello")
    String hello() {
      return this.messageCircuitBreaker.run(() -> (4)
          "This is Spring calling a " + messageService.getMessage());
    }

    @GetMapping("/hello/{name}")
    String helloWithName(@PathVariable("name") String name) {
      names.add(name);
      return "Hello World " + name;
    }

}
1 Circuit breaker instance.
2 Pass the CircuitBreakerFactory instance.
3 Creates a circuit breaker instance for the message service.
4 Wrap the call.

Let’s deploy the Spring Boot application:

Unresolved include directive in modules/ROOT/pages/05-resiliency.adoc - include::partial$package_run.adoc[]

Then curl the service, the response is sent back after 1 second with an exception:

{"timestamp":"2021-05-31T14:23:09.929+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}

And if we check the Spring Boot logs, the timeout exception is thrown instead of the message:

java.util.concurrent.TimeoutException: TimeLimiter 'message' recorded a timeout exception.
	at io.github.resilience4j.timelimiter.TimeLimiter.createdTimeoutExceptionWithName(TimeLimiter.java:221) ~[resilience4j-timelimiter-1.7.0.jar:1.7.0]

Fallback

Sometimes we can provide some fallback method in case of an error, this fallback method could be a default value or a call to another system to try to get a valid value (ie distributed cache is down then the fallback may try to get the value from the database).

To provide a fallback run method provides an overload version of the method to set the fallback method:

Open HelloController.java file and change hello() method:

org.acme.hellokubernetes.HelloController.java
@GetMapping("/hello")
String hello() {
    return this.messageCircuitBreaker.run(() ->
        "This is Spring calling a " + messageService.getMessage()
        , throwable -> "Default" (1)
    );
}
1 Fallback method. In this case a static value is provided.

Let’s deploy the Spring Boot application:

Unresolved include directive in modules/ROOT/pages/05-resiliency.adoc - include::partial$package_run.adoc[]

Then access the endpoint:

curl localhost:8080/hello

The output is the value provided in the fallback part.

Default

Circuit Breaker

We can also use Circuit Breaker pattern in resiliency4j as a resiliency strategy.

To make the RestTemplate fail, let’s stop the external service by stopping the port-forward process. Move to the terminal where kubectl port-forward was run and push Ctrl+C to stop it.

Open HelloKubernetesApplication.java file and configure circuit breaker:

org.acme.hellokubernetes.HelloKubernetesApplication.java
package org.acme.hellokubernetes;

import java.time.Duration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;

@SpringBootApplication
public class HelloKubernetesApplication {

	@Bean
	public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
    return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .circuitBreakerConfig(CircuitBreakerConfig.custom() (1)
                                    .failureRateThreshold(50) (2)
                                    .ringBufferSizeInClosedState(5) (3)
                                    .build()
                                )
            .build());
	}

	public static void main(String[] args) {
		SpringApplication.run(HelloKubernetesApplication.class, args);
	}

}
1 Configures Circuit breaker.
2 Rate threshold to 50% of calls.
3 Configures the size of the sliding window.

Let’s deploy the Spring Boot application:

Unresolved include directive in modules/ROOT/pages/05-resiliency.adoc - include::partial$package_run.adoc[]

And run the following command in a terminal:

for i in {1..10}; do
curl localhost:8080/hello
done

The output is obviously 10 errors:

{"timestamp":"2021-06-01T08:48:59.417+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.435+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.448+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.468+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.479+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.491+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.504+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.515+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.527+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}{"timestamp":"2021-06-01T08:48:59.539+00:00","status":500,"error":"Internal Server Error","message":"","path":"/hello"}

But if we check the Spring Boot logs, we’ll see that first java.net.ConnectException: Connection refused exteption is thrown and then CallNotPermittedException exception is thrown as the circuit is open and the real call isn’t executed:

Get Message Remote call (1)
exception org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8090": Connection refused; nested exception is java.net.ConnectException: Connection refused
...
(2)
exception io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'message' is OPEN and does not permit further calls
1 The message is printed as the real logic is executed.
2 No message as circuit is open and MessageService method isn’t called.

Retry

Another strategy used for resiliency is automatic retries in case of an error is reported. Spring Boot relies on Spring Retry and Spring Aspects projects for automatic retries.

Open pom.xml file and register the following dependencies:

pom.xml
<dependency>
    <groupId>org.springframework.retry</groupId>
	<artifactId>spring-retry</artifactId>
	<version>1.3.1</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.3.7</version>
</dependency>

To enable retries, annotate the HelloKubernetesApplication.java wfile with @EnableRetry annotation:

org.acme.hellokubernetes.HelloKubernetesApplication.java
@org.springframework.retry.annotation.EnableRetry
@SpringBootApplication

The final step is to annotate the retryable method with @Retryable annotation:

org.acme.hellokubernetes.HelloController.java
package org.acme.hellokubernetes;

import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;

@RestController
public class HelloController {

    private CircuitBreaker messageCircuitBreaker;

    private Set<String> names = new HashSet<>();

    public HelloController(MeterRegistry registry, CircuitBreakerFactory messageCircuitBreakerFactory) {
      this.messageCircuitBreaker = messageCircuitBreakerFactory.create("message");
      registry.gaugeCollectionSize("names.size", Tags.empty(), names);
    }

    @Autowired
    MessageService messageService;

    @GetMapping("/hello")
    String hello() {
      return this.messageCircuitBreaker.run(() ->
          "This is Spring calling a " + messageService.getMessage());
    }

    @GetMapping("/hello/{name}")
    String helloWithName(@PathVariable("name") String name) {
      names.add(name);
      return "Hello World " + name;
    }

    @GetMapping("/hello/error")
    @Retryable(maxAttempts = 4, (1) (2)
                backoff = @Backoff(delay = 1000)) (3)
    String helloWithException() {
      System.out.println("Method with error");
      throw new IllegalArgumentException("Error");
    }

}
1 Annotates the method with @Retryable annotation.
2 Executes four retries.
3 With an sleep of 1 second between retries.

Let’s deploy the Spring Boot application:

Unresolved include directive in modules/ROOT/pages/05-resiliency.adoc - include::partial$package_run.adoc[]

Then access the created endpoint:

curl localhost:8080/hello/error

Before the error is returned, there is an sleep of 4 seconds (1 second for each retry).

But if we check the Spring Boot logs, we’ll see that Method with error is printed four times, one for each retry.

Method with error
Method with error
Method with error
Method with error


java.lang.IllegalArgumentException: Error

Clean-Up

Before stepping to the following section, stop the kubectl port-forward process by typing Ctrl+C on the terminal.

Undeploy the service by deleteing all the resources created in the namespace:

kubectl delete all --all -n default