Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we're going to see how Spring Boot 2.3 integrates with Kubernetes probes to create an even more pleasant cloud-native experience.

First, we'll start with a little bit of a background on Kubernetes probes. Then we'll switch gears and see how Spring Boot 2.3 supports those probes.

2. Kubernetes Probes

When using Kubernetes as our orchestration platform, the kubelet in each node is responsible for keeping the pods in that node healthy.

For instance, sometimes our apps may need a little bit of time before being able to accept requests. The kubelet can make sure that the application receives requests only when it's ready. Also, if the main process of a pod crashes for any reason, the kubelet will restart the container.

In order to fulfill these responsibilities, Kubernetes has two probes: liveness probes and readiness probes.

The kubelet will use the readiness probe to determine when the application is ready to accept requests. More specifically, a pod is ready when all of its containers are ready.

Similarly, the kubelet can check if a pod is still alive through liveness probes. Basically, the liveness probe helps the kubelet know when it should restart a container.

Now that we are familiar with the concepts, let's see how the Spring Boot integration works.

3. Liveness and Readiness in Actuator

As of Spring Boot 2.3, LivenessStateHealthIndicator and ReadinessStateHealthIndicator classes will expose the liveness and readiness state of the application. When we deploy our application to Kubernetes, Spring Boot will automatically register these health indicators.

As a result, we can use /actuator/health/liveness and /actuator/health/readiness endpoints as our liveness and readiness probes, respectively.

For instance, we can add these to our pod definition to configure the liveness probe as an HTTP GET request:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
    initialDelaySeconds: 3
    periodSeconds: 3

We'll usually let Spring Boot decide when to stand up these probes for us. But, if we want to, we can enable them manually in our application.properties:

management.health.probes.enabled=true

3.1. Readiness and Liveness State Transitions

Spring Boot uses two enums to encapsulate different readiness and liveness states. For readiness state, there is an enum called ReadinessState with the following values:

  • The ACCEPTING_TRAFFIC state represents that the application is ready to accept traffic
  • The REFUSING_TRAFFIC state means that the application is not willing to accept any requests yet

Similarly, the LivenessState enum represents the liveness state of the app with two values:

  • The CORRECT value means the application is running and its internal state is correct
  • On the other hand, the BROKEN value means the application is running with some fatal failures

Here's how readiness and liveness state changes in terms of application lifecycle events in Spring:

  1. Registering listeners and initializers
  2. Preparing the Environment
  3. Preparing the ApplicationContext
  4. Loading bean definitions
  5. Changing the liveness state to CORRECT
  6. Calling the application and command-line runners
  7. Changing the readiness state to ACCEPTING_TRAFFIC

Once the application is up and running, we (and Spring itself) can change these states by publishing appropriate AvailabilityChangeEvents.

4. Managing the Application Availability

Application components can retrieve the current readiness and liveness state by injecting the ApplicationAvailability interface:

@Autowired private ApplicationAvailability applicationAvailability;

Then we can use it as follows:

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.CORRECT);
assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);
assertThat(applicationAvailability.getState(ReadinessState.class))
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);

4.1. Updating the Availability State

We can also update the application state by publishing an AvailabilityChangeEvent event:

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.CORRECT);
mockMvc.perform(get("/actuator/health/liveness"))
  .andExpect(status().isOk())
  .andExpect(jsonPath("$.status").value("UP"));

AvailabilityChangeEvent.publish(context, LivenessState.BROKEN);

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.BROKEN);
mockMvc.perform(get("/actuator/health/liveness"))
  .andExpect(status().isServiceUnavailable())
  .andExpect(jsonPath("$.status").value("DOWN"));

As shown above, before publishing any event, the /actuator/health/liveness endpoint returns a 200 OK response with the following JSON:

{
    "status": "OK"
}

Then after breaking the liveness state, the same endpoint returns a 503 service unavailable response with the following JSON:

{
    "status": "DOWN"
}

When we change to a readiness state of REFUSING_TRAFFIC, the status value will be OUT_OF_SERVICE:

assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);
mockMvc.perform(get("/actuator/health/readiness"))
  .andExpect(status().isOk())
  .andExpect(jsonPath("$.status").value("UP"));

AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);

assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.REFUSING_TRAFFIC);
mockMvc.perform(get("/actuator/health/readiness"))
  .andExpect(status().isServiceUnavailable())
  .andExpect(jsonPath("$.status").value("OUT_OF_SERVICE"));

4.2. Listening to a Change

We can register event listeners to be notified when an application availability state changes:

@Component
public class LivenessEventListener {
    
    @EventListener
    public void onEvent(AvailabilityChangeEvent<LivenessState> event) {
        switch (event.getState()) {
        case BROKEN:
            // notify others
            break;
        case CORRECT:
            // we're back
        }
    }
}

Here we're listening to any change in application liveness state.

5. Auto-Configurations

Before wrapping up, let's see how Spring Boot automatically configures these probes in Kubernetes deployments. The AvailabilityProbesAutoConfiguration class is responsible for registering the liveness and readiness probes conditionally.

As a matter of fact, there is a special condition there that registers the probes when one of the following is true:

When an application meets either of these conditions, the auto-configuration registers beans of  LivenessStateHealthIndicator and ReadinessStateHealthIndicator.

6. Conclusion

In this article, we saw how we can use Spring Boot provides two health probes for Kubernetes integration.

As usual, all the examples are available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
6 Comments
Oldest
Newest
Inline Feedbacks
View all comments
N B
N B
5 months ago

How heavy are those liveness/readiness calls? O remember that /health by itself is quite heavy, because it might connect to DB, might call config-server, etc

Loredana Crusoveanu
5 months ago
Reply to  N B

Hi,
Individual Spring Boot Actuator endpoints are now configurable and you can choose to enable, disable or expose them. The efficiency of the health call will be determined by the application architecture. For example, If your application uses Redis, the RedisHealthIndicator will be used as part of the health check-up.
Liveness and readiness calls will similarly use the LivenessStateHealthIndicator and ReadinessStateHealthIndicator respectively to make use of these probes Kubernetes provides.
Cheers!

Jon A
Jon A
5 months ago

SpringBoot needs to be dramatically faster at starting up in order to be useful in a k8s cluster but this is pretty handy to at least try to optimize state transitions. Any plans to solve the startup time so that managing status via restart loops is viable again?

Loredana Crusoveanu
5 months ago
Reply to  Jon A

Hi Jon,

Thanks for the feedback.
As far as reducing the startup times, that would be more of a question for the Spring Boot community. You may find their suggestions here useful: https://spring.io/blog/2018/12/12/how-fast-is-spring
Cheers.

Rakesh
Rakesh
4 months ago

The github link is missing. Can you please update that?

Loredana Crusoveanu
4 months ago
Reply to  Rakesh

Hi Rakesh,
The GitHub link is right there in the last section.
Cheers

Comments are closed on this article!