Spring Cloud Kubernetes Load Balancer Guide
Spring Cloud Kubernetes Load Balancer support has been added in the last release of Spring cloud Hoxton.SR8
. It was probably the last project in Spring Cloud that used Ribbon as a client-side load balancer. The current implementation is based on the Spring Cloud LoadBalancer project. It provides two modes of communication. First of them detects IP addresses of all pods running within a given service. The second of them use Kubernetes Service
name for searching all the target instances.
In this article, I’m going to show you how to use the Spring Cloud Kubernetes Load Balancer module in your application. First, I will demonstrate the differences between POD
and SERVICE
modes. Then we will enable load balancing across multiple namespaces. Finally, we will implement a fault tolerance mechanism with the Spring Cloud Circuit Breaker project.
Source code
If you would like to try it by yourself, you may always take a look at my source code example. In order to do that you need to clone my repository sample-spring-microservices-kubernetes. Then just follow my instructions 🙂 The good idea is to read the article Microservices with Spring Cloud Kubernetes before you move on.
Step 1. Enable Spring Cloud Kubernetes Load Balancer
You need to include a single Spring Boot Starter to enable Spring Cloud Kubernetes Load Balancer. It is spring-cloud-starter-kubernetes-loadbalancer
. Consequently, you also need to have a REST client on the classpath. Spring RestTemplate
is automatically included with the Spring Web module. We will also use the OpenFeign client, so you should include the right starter.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Step 2. Implement and use the REST client
Spring Cloud OpenFeign is a declarative REST client. Therefore, you need to create an interface with methods and Spring MVC annotations. It is important to set the right name inside @FeignClient
annotation. This name needs to be the same as the name of the target Kubernetes Service. In the following code example, you see an implementation of the employee
service client inside department-service
.
@FeignClient(name = "employee")
public interface EmployeeClient {
@GetMapping("/department/{departmentId}")
List<Employee> findByDepartment(@PathVariable("departmentId") String departmentId);
}
OpenFeign client is enabled after annotating the main class with @EnableFeignClients
. After that you can inject it to the @RestController
class. Finally, you may use its method findByDepartment
to find employees assigned to the particular department.
@RestController
public class DepartmentController {
private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);
DepartmentRepository repository;
EmployeeClient employeeClient;
public DepartmentController(DepartmentRepository repository, EmployeeClient employeeClient) {
this.repository = repository;
this.employeeClient = employeeClient;
}
@GetMapping("/{id}/with-employees")
public Department findByIdWithEmployees(@PathVariable("id") String id) {
LOGGER.info("Department findByIdWithEmployees: id={}", id);
Optional<Department> optDepartment = repository.findById(id);
if (optDepartment.isPresent()) {
Department department = optDepartment.get();
department.setEmployees(employeeClient.findByDepartment(department.getId()));
return department;
}
return null;
}
@GetMapping("/organization/{organizationId}/with-employees")
public List<Department> findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId) {
LOGGER.info("Department find: organizationId={}", organizationId);
List<Department> departments = repository.findByOrganizationId(organizationId);
departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
return departments;
}
}
Opposite to the OpenFeign, the RestTemplate
is a low-level HTTP client. We need to enable Spring Cloud load balancing for it. To do that just annotate RestTemplate
bean with @LoadBalanced
.
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
Here’s the similar implementation of @RestController
class, but this time with the RestTemplate
.
public class DepartmentWithRestTemplateController {
private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);
DepartmentRepository repository;
RestTemplate restTemplate;
public DepartmentWithRestTemplateController(DepartmentRepository repository, RestTemplate restTemplate) {
this.repository = repository;
this.restTemplate = restTemplate;
}
@GetMapping("/{id}/with-employees")
public Department findByIdWithEmployees(@PathVariable("id") String id) {
LOGGER.info("Department findByIdWithEmployees: id={}", id);
Optional<Department> optDepartment = repository.findById(id);
if (optDepartment.isPresent()) {
Department department = optDepartment.get();
department.setEmployees(findEmployeesByDepartment(department.getId()));
return department;
}
return null;
}
@GetMapping("/organization/{organizationId}/with-employees")
public List<Department> findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId) {
LOGGER.info("Department find: organizationId={}", organizationId);
List<Department> departments = repository.findByOrganizationId(organizationId);
departments.forEach(d -> d.setEmployees(findEmployeesByDepartment(d.getId())));
return departments;
}
private List<Employee> findEmployeesByDepartment(String departmentId) {
Employee[] employees = restTemplate
.getForObject("http://employee//department/{departmentId}", Employee[].class, departmentId);
return Arrays.asList(employees);
}
}
Step 3. Deploy Spring Cloud applications on Kubernetes
The project is ready to be used with Skaffold. Therefore, you don’t have to worry about the deployment process. You just need to run a single command skaffold dev --port-forward
to deploy our applications on Kubernetes. But before deploying them on the cluster, we perform a short overview of the communication process. We have three microservices. Each of them is running in two instances. They are using Mongo as a backend store. There is also the gateway application. It is built on top of Spring Cloud Gateway. It provides a single API endpoint to all downstream services. Of course, all our applications are using Spring Cloud Load Balancer for traffic management. To clarify, you can take a look at the picture below.
Here’s the Kubernetes deployment manifest for the employee-service
. It sets two running pods in replicas
parameter, and references ConfigMap
and Secret
to inject Mongo credentials.
apiVersion: apps/v1
kind: Deployment
metadata:
name: employee
labels:
app: employee
spec:
replicas: 2
selector:
matchLabels:
app: employee
template:
metadata:
labels:
app: employee
spec:
containers:
- name: employee
image: piomin/employee
ports:
- containerPort: 8080
env:
- name: MONGO_DATABASE
valueFrom:
configMapKeyRef:
name: mongodb
key: database-name
- name: MONGO_USERNAME
valueFrom:
secretKeyRef:
name: mongodb
key: database-user
- name: MONGO_PASSWORD
valueFrom:
secretKeyRef:
name: mongodb
key: database-password
The most important element in the communication between applications is Kubernetes Service
object. The name of service must be the same as the hostname used by the RestTemplate
or OpenFeign client. In our case that name is employee
.
apiVersion: v1
kind: Service
metadata:
name: employee
labels:
app: employee
spring-boot: "true"
spec:
ports:
- port: 8080
protocol: TCP
selector:
app: employee
type: ClusterIP
Let’s verify the status after deploying all the applications. Here’s the list of running pods.
Here’s the list of Kubernetes Endpoints
.
Step 4. Communication in the POD mode
By default, Spring Cloud Kubernetes Load Balancer uses the POD mode. In this mode, it gets the list of Kubernetes endpoints to detect the IP address of all the application pods. In that case, the only thing you need to do is to disable the Ribbon load balancer. Spring Cloud Kubernetes is still using it as a default client-side load balancer. To disable it you need to set the following property.
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
After adding a test data we may send some requests to the endpoint GET /{id}/with-employees
. It finds a department by the id. Then it communicates with the employee-service
endpoint GET /department/{departmentId}
to search all the employees assigned to the current department. The department-service
is exposed on port 8081, since I enabled option port-forward
on Skaffold.
$ curl http://localhost:8081/5f5896b3cb8caf7e6f6b9e1c/with-employees
{"id":"5f5896b3cb8caf7e6f6b9e1c","organizationId":"1","name":"test1","employees":[{"id":"5f5896e26092716e54f60a9a","name":"test1","age":30,"position":"d
eveloper"},{"id":"5f5896f29625e62c7d373906","name":"test2","age":40,"position":"tester"},{"id":"5f5897266092716e54f60a9b","name":"test3","age":45,"posit
ion":"developer"}]}
Let’s take a look on the logs from employee-service
. I repeated the request visible above several times. The requests are load balanced between two available instances of employee-service
. Skaffold prints the id of every pod in the logs, so you can verify that everything works fine.
Step 5. Communication across multiple namespaces
By default, Spring Cloud Kubernetes allows load balancing within the same namespace. You may enable discovery across multiple namespaces. To do that you need to use the property following property.
spring:
cloud:
kubernetes:
discovery:
all-namespaces: true
After setting that, we will deploy the employee-service
application in the different namespace than department-service
. To do that you need to set parameter -n
on the skaffold
command.
$ skaffold dev -n test-a
Here’s the current list of running pods.
You just need to send the same request as before curl http://localhost:8081/5f5896b3cb8caf7e6f6b9e1c/with-employees
. Of course, the Kubernetes service name must be unique across all the namespaces.
Step 6. Load balancer on Spring Cloud Gateway
Spring Cloud Gateway uses the same load balancing mechanism as other Spring Cloud applications. To enable it on Kubernetes we need to include dependency spring-cloud-starter-kubernetes-loadbalancer
. We should also enable Spring Cloud DiscoveryClient
and disable Ribbon. Here’s the configuration of the gateway-service
.
spring:
application:
name: gateway
cloud:
loadbalancer:
ribbon:
enabled: false
gateway:
discovery:
locator:
enabled: true
routes:
- id: employee
uri: lb://employee
predicates:
- Path=/employee/**
filters:
- RewritePath=/employee/(?.*), /$\{path}
- id: department
uri: lb://department
predicates:
- Path=/department/**
filters:
- RewritePath=/department/(?.*), /$\{path}
- id: organization
uri: lb://organization
predicates:
- Path=/organization/**
filters:
- RewritePath=/organization/(?.*), /$\{path}
A gateway acts as an entry point to our system. It performs routing and load balancing to all the downstream services. Therefore, we can call any of our applications using already defined routes. Since gateway-service
is available on port 8080, I can call any of the endpoints using the following requests.
We can expose API documentation of all the microservices on the gateway. To do that we may use the SpringFox project. First, we need to include SpringFox Starter to Maven dependencies.
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
Swagger documentation is usually generated for a single application. Because we want to expose documentation of all our microservices we need to override the Swagger resource provider. The custom implementation is visible below. It uses a discovery mechanism to the names of running services.
@Configuration
public class GatewayApi {
@Autowired
RouteDefinitionLocator locator;
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider() {
return () -> {
List<SwaggerResource> resources = new ArrayList<>();
Flux<RouteDefinition> definitions = locator.getRouteDefinitions();
definitions
.filter(routeDefinition -> !routeDefinition.getId().startsWith("ReactiveCompositeDiscoveryClient_"))
.subscribe(routeDefinition -> resources.add(createResource(routeDefinition.getId(), "2.0")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return resources;
};
}
private SwaggerResource createResource(String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(location);
swaggerResource.setLocation("/" + location + "/v2/api-docs");
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
Here’s the Swagger UI available at http://localhost:8080/swagger-ui/index.html
on my local machine.
Step 7. Enabling circuit breaker
We can use a circuit breaker component with Spring Cloud Load Balancer. The default Spring Cloud implementation is based on Resilience4j. In order to enable it for your application, you need to include the following dependency.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
The next step is to provide a configuration of a circuit breaker.
@Bean
Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id ->
new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(1000))
.build())
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.failureRateThreshold(66.6F)
.slowCallRateThreshold(66.6F)
.build())
.build()
);
}
We need to inject Resilience4JCircuitBreakerFactory
to the Spring controller class. Then we are creating the circuit breaker instance using an injected factory. Finally, the client calling method is running inside the circuit breaker run
method.
@RestController
public class DepartmentController {
private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);
DepartmentRepository repository;
EmployeeClient employeeClient;
Resilience4JCircuitBreakerFactory circuitBreakerFactory;
public DepartmentController(DepartmentRepository repository, EmployeeClient employeeClient,
Resilience4JCircuitBreakerFactory circuitBreakerFactory) {
this.repository = repository;
this.employeeClient = employeeClient;
this.circuitBreakerFactory = circuitBreakerFactory;
}
@GetMapping("/{id}/with-employees-and-delay")
public Department findByIdWithEmployeesAndDelay(@PathVariable("id") String id) {
LOGGER.info("Department findByIdWithEmployees: id={}", id);
Optional<Department> optDepartment = repository.findById(id);
if (optDepartment.isPresent()) {
Department department = optDepartment.get();
Resilience4JCircuitBreaker circuitBreaker = circuitBreakerFactory.create("delayed-circuit");
List<Employee> employees = circuitBreaker.run(() ->
employeeClient.findByDepartmentWithDelay(department.getId()));
department.setEmployees(employees);
return department;
}
return null;
}
}
Conclusion
Load balancing is one of the key patterns in a microservices architecture. Spring Cloud Load Balancer is replacing the Ribbon client. By default, load balancing in Kubernetes is based on Services. Therefore, you need to use additional tools for more advanced routing mechanisms. Spring Cloud Kubernetes comes with some interesting features. One of them is the ability to load balance across multiple namespaces. You can also use the additional Spring Cloud components like a circuit breaker. In comparison with tools like Istio, it is still not much. Is it a chance for improvement? We will see. Nevertheless, Spring Cloud Kubernetes is currently one of the most popular Spring Cloud projects. It may be a good choice if you are migrating your Spring Cloud microservices architecture to Kubernetes. Support for the load balancer may be an important step during such a migration.
Leave a ReplyCancel reply