Client Side Load Balancing with Ribbon
In Part 1 of this tutorial, we already covered the basic concepts of micro services. As a hands on, we created a sample application(TEST-APP) and applied those concepts and registered the instances of the application in the service registry.
Anyway micro services are loosely coupled, independent deployable components. Since each service is responsible to have a small functionality, it might need to involve a lot of services to complete a single transaction. So inter service communication is really important in micro service architecture which is a big challenging topic.
3.1 Client Side vs Server Side Load Balancing
Refer the my load balancing blog post to get more idea about the client side vs server side load balancing.
Suppose there is a scenario which Service A wants to access the Service B. There are multiple instances in Service B. Anyway, to properly handle the incoming traffic, we can distribute the incoming requests from Service A among the server instances of Service B.
Anyway, there are two different ways to do the load balancing.
Server side load balancing
There is a load balancer in between the Service A and Service B. All the instances are registered in the service registry. Load balancer fetches the available instances of Service B from the service registry and distribute the incoming traffic from Service A.
Client side load balancing
Client side load balancing
There is no separate server to load balance in between services. Here the service A works as the client and the load balancing handle from their side itself. Service A fetches the available server instances of Service B from service registry and distributes the load among the Service B instances.
3.2 What is the RestTemplate?
RestTemplate is the basic Spring class for simultaneous client side HTTP access. It simplifies the interaction with HTTP servers and enforces RESTful systems.
3.3 Client side load balancing with Ribbon
Let's apply the load balancing to our sample application created in Part 1. Ribbon is one of the client side load balancer, which facilitate to do the load balancing in micro service architecture.
Create a new spring boot application as mentioned in Part 1. We are going to invoke this new service from our previous TEST-APP service.
Project structure of the new spring boot application(ribbonApp)
Sample Java Code
SalaryController.java
package com.sample.ribon.controller;
import com.sample.ribon.model.Salary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SalaryController {
@Autowired
Environment environment;
@Autowired
Salary salary;
@RequestMapping("/salary")
public Salary getSalary() {
System.out.println("hitting get salary operation");
salary.setId("emp1");
salary.setMonth("January");
salary.setAmount(1000);
return salary;
}
@RequestMapping("/portNumber")
public String getPortNumber() {
System.out.println("hitting get port number operation");
String serverPort = environment.getProperty("local.server.port");
return " Host : localhost " + " :: Port : " + serverPort;
}
}
Salary.java
package com.sample.ribon.model;
import org.springframework.stereotype.Component;
@Component
public class Salary {
private String id;
private int amount;
private String month;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
}
RibonApplication.java
package com.sample.ribon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class RibonApplication {
public static void main(String[] args) {
SpringApplication.run(RibonApplication.class, args);
}
}
Application.properties
spring.application.name=ribonapp
server.port = 8768
eureka.client.serviceUrl.defaultZone= http://${registry.host:localhost}:${registry.port:8765}/eureka/
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.sample</groupId> <artifactId>ribon</artifactId> <version>0.0.1-SNAPSHOT</version> <name>ribon</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Make the following changes in the existing sample application(TEST-APP) to represent it as a ribbon client.
Project structure
RibbonConfiguration.java
Add a new java class called RibbonConfiguration.java.This class contains the ribbon client configuration for client side load balancing.
package com.test.sample;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
public class RibbonConfiguration {
@Autowired
IClientConfig ribbonClientConfig;
@Bean
public IPing ribbonPing(IClientConfig config) {
return new PingUrl();
}
@Bean
public IRule ribbonRule(IClientConfig config) {
return new WeightedResponseTimeRule();
}
}
Do the below changes in the existing files.
pom.xml
Add below highlighted dependency to the pom.xml. This dependency related to the ribbon client.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.test</groupId>
<artifactId>sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sample</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EmployeeController.java
package com.test.sample.controller;
import com.test.sample.model.Department;
import com.test.sample.model.Employee;
import com.test.sample.model.Salary;
import com.test.sample.service.DepartmentService;
import com.test.sample.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@Autowired
DepartmentService departmentService;
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
RestTemplate restTemplate;
@RequestMapping("/personalInfo")
public Employee getEmployeeDetails() {
return employeeService.getEmployeePersonalInfo();
}
@RequestMapping("/departmentInfo")
public Department getDepartmentDetails() {
return departmentService.getDepartmentInfo();
}
@RequestMapping("/salaryInfo")
public Object getsalary() {
Object salary = this.restTemplate.getForObject("http://ribonapp/salary", Object.class);
return salary;
}
@RequestMapping("/portInfo")
public String getPortNumber() {
String randomString = this.restTemplate1.getForObject("http://ribonapp/portNumber", String.class);
return "Server Response :: " + randomString;
}
}
We are using rest template to make the http request to the external service. Anyway, it has annotated with @LoadBalanced to use the RibbonLoadBalancerClient to interact with the external services.
Object salary = this.restTemplate.getForObject("http://ribonapp/salary", Salary.class);
spring.application.name=ribonapp
When invoking the services via restTemplate.getForObject(), need to pass the value set in the "spring.application.name" property of the calling service's application.proprerties file as the host name of the url.
SampleApplication.java
package com.test.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = "com.test.sample")
@RibbonClient(name="ribbonService",configuration = RibbonConfiguration.class)
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
@RibbonClient annotation is set to make this application as a ribbon client. Here we are passing a
name and configuration parameters. You can choose any meaningful name. But as configuration, you need to pass the exact ribbon configuration class name which we created before.
Testing
1. Start the Eureka registry instance by right click and press run on the "EurekaApplication.java" class.
2. Start the SampleService(TEST-APP) by right click and press run on the "SampleApplication.java" class.
3. We are going to start the two instances of Ribbon application to check the load balancing functionality.
To start two instances in different ports, first build the project and create a jar file. Run "mvn clean install" from the location of pom.xml file.
4. Once you have built that, you can see the "ribon-0.0.1-SNAPSHOT.jar" created in target directory.
5. Go into that directory and run execute the jar. To start two instances, change the port number and execute.
- java -jar -Dserver.port=8768 ribon-0.0.1-SNAPSHOT.jar
- java -jar -Dserver.port=8769 ribon-0.0.1-SNAPSHOT.jar
Once you started all the service instances, go and check the service registry. You can see the 3 service instances(1 sample application & 2 ribbon application instances) have registered in Eureka.
Let' invoke the salaryInfo operation. sample application will invoke the ribbon applicaiton and you can get the below response from ribbon application.
But how can we verify the requests to the salaryInfo are distributed among the two ribbon instances. To verify that, let's invoke portInfo operation which is returning the port of the responding server.
Send multiple request and you will see the responses are returned by two servers in round robin manner.