Spring Cloud Netflix - Ribbon

1. Introduction

spring-cloud

In Microservice architecture, a monolithic application is split into multiple Smaller and independent applications. Each application manages a certain functionality independently and can be deployed/scaled independently.

However in order to accomplish a business functionality, it's often required for multiple services to communicate with one another.

Spring Cloud Netflix provides Netflix integration with spring boot applications which ensure seamless Java web Development Company of Microservice.It provides common design patterns for developing the applications

  1. Eureka - For service discovery
  2. Hystrix - For implementing circuit breaker pattern
  3. Ribbon - For client side load balancing
  4. Zuul - Intelligent routing.

In this article, we’ll be exploring more about Ribbon.

2. Netflix Ribbon

One of the advantages of microservices is that they can be scaled vertically (increasing allocated heap memory, threads) or horizontally (deploying multiple instances of same service) very easily.

We can bring up multiple instances of your service with any instance capable of handling any request. To leverage this horizontal scaling, Load balancers are employed. In traditional load balancers client only knows 1 target. The target in-turn distributes the request among multiple instances.

Netflix Ribbon is an alternate approach to Traditional Load Balancers, where client is aware of multiple instances of a service and chooses a particular instance of it. One advantage of this is client controls the load balancing algorithm.

Netflix Ribbon is a Client Side Load Balancer.

Following are salient features of Ribbon :

  1. Source Code : https://github.com/Netflix/ribbon
  2. Client Side Load Balancer
  3. Can be used with Eureka or without it. In case Eureka isn’t configured, the list of service instances needs to be explicitly provided to client.

3. Project

In our project, we’ll be creating 3 microservices

  1. Eureka Server - service registry
  2. User Service - A Eureka registered microservice, which provides User specific endpoints. For employing ribbon, we’ll be deploying 3 instances of this service on different ports.
  3. Simple Ribbon Client - This will be a client service, which will consume endpoints of User Service. Here we will employ default configuration of ribbon.
  4. In the final section, we’ll explore customizing Ribbon configuration.

The general flow of client side load balancer with Eureka (for service discovery) is:

  1. Eureka Server acts as Service Discovery which maintains the list of all the microservices as KEY-VALUE pair. Key is serviceId and value is the instance information (host, port).
  2. User microservice has multiple instances deployed.
  3. The client microservice employs Ribbon as Load balancer and consumes the user service endpoints.
  4. Ribbon talks to Eureka Server and gets information about instances of the User Service to choose from.
  5. Even if instance of User Service is added or goes down, Eureka updates the same and Ribbon uses the updated information to choose an instance of it. Thus no manual intervention is needed.

3.1 Flow Diagram:

spring-cloud

4. Eureka Server:

To implement the Eureka server, all we need to do is:

  1. Add spring-cloud-starter-eureka-server dependency
  2. Enable Eureka Server by adding annotation @EnableEurekaServeron our main Spring boot application class.

The project structure is as follows:

spring-cloud

4.1 Maven dependencies

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hemant</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-server</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</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>

4.2 Spring Boot Main Class

packagecom.hemant.eurekaserver; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer publicclassEurekaServerApplication { publicstaticvoidmain(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }

4.3 Application Properties

# Server port server: port: 8761 eureka: client: # Dont register itself with eureka registerWithEureka: false fetchRegistry: false

Thus Eureka Server will run on port 8761.

4.4 Starting Eureka Server

1. To start the Eureka Server application, go to the project’s root directory and execute

>>>mvn clean install

2. If the build is success, we can launch the application by:

>>>java -jar target/eureka-server-0.0.1-SNAPSHOT.jar

3. The following logs indicate that application is up and running

c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 8761 c.h.e.EurekaServerApplication : Started EurekaServerApplication in 13.399 seconds (JVM running for 14.281)

You can access, the Eureka Server console at http://localhost:8761/

spring-cloud

5. User Service

User Service is Eureka Registered micro service.

It will also expose some endpoints which will then be consumed by the client application. Also we’ll be deploying multiple instances of this.

The project structure is as follows:

spring-cloud

5.1 Maven Dependencies

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hemant</groupId> <artifactId>user-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>user-service</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> <relativePath /><!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

5.2 Spring Boot Main Class

packagecom.hemant.userservice; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication @EnableEurekaClient publicclassUserServiceApplication { publicstaticvoidmain(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }

The convenience annotation @EnableEurekaClient makes this application as a Eureka Client which can be registered with our Eureka Server.

5.3 User Model Class:

This is the model class which will be rendered via controller endpoints.

packagecom.hemant.userservice.model; publicclassUser { private String id; private String name; private String email; public String getId() { return id; } publicvoidsetId(String id) { this.id = id; } public String getName() { return name; } publicvoidsetName(String name) { this.name = name; } public String getEmail() { return email; } publicvoidsetEmail(String email) { this.email = email; } @Override publicinthashCode() { finalint prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override publicbooleanequals(Object obj) { if (this == obj) returntrue; if (obj == null) returnfalse; if (!(objinstanceof User)) returnfalse; User other = (User) obj; if (id == null) { if (other.id != null) returnfalse; } elseif (!id.equals(other.id)) returnfalse; returntrue; } @Override public String toString() { return"User [id=" + id + ", name=" + name + ", email=" + email + "]"; } publicUser(String id, String name, String email) { super(); this.id = id; this.name = name; this.email = email; } }

5.4 Controller

This exposes User service endpoints

packagecom.hemant.userservice.controller; importjava.util.ArrayList; importjava.util.HashMap; importjava.util.List; importjava.util.Map; importjavax.annotation.PostConstruct; importorg.springframework.web.bind.annotation.PathVariable; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RequestMethod; importorg.springframework.web.bind.annotation.RestController; importcom.hemant.userservice.model.User; @RestController publicclassUserController { private Map<String, User>userMap; @PostConstruct publicvoidinitMap() { userMap = newHashMap<>(); for (inti = 1; i<= 5; i++) { String id = "user-"+ i; userMap.put(id, new User(id, "User" + i, "user" + i + "@gmail.com")); } } @RequestMapping(value = "", method = RequestMethod.GET) public List<User>getAllUsers() { returnnew ArrayList<>(userMap.values()); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User getUserById(@PathVariable("id") String id) { User user = userMap.get(id); if (null == user) { thrownewIllegalArgumentException("User not found for : " + id); } return user; } }

5.5 Application Properties:

server : port : 8083 spring : application : name : user-service eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka

5.6 Multiple instances of User Services

We’ll be creating 3 instances of User Service. The code base of them is same, only their application properties are different.

All the multiple instances will have the same service ID. The only difference is in port. To create multiple instances of the same, simple copy-paste the folder.

We have created 3 instances/folders of User service.

Following are the application properties of them.

5.6.1 User Service 1

server : port : 8081 spring : application : name : user-service eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka

5.6.2 User Service 2

server : port : 8082 spring : application : name : user-service eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka

5.6.3 User Service 3

server : port : 8083 spring : application : name : user-service eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka

5.7 Deploying the User Services:

The process to deploy each user Microservice is similar.

1. Go to the project’s root directory and execute

>>>mvn clean install

2. If the build is success, we can launch the application by:

>>>java -jar target/user-service-0.0.1-SNAPSHOT.jar

3. The following logs indicate that application is up and running and also registered with Eureka server.

c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 8081 c.h.userservice.UserServiceApplication : Started UserServiceApplication in 10.22 seconds (JVM running for 11.243) com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/192.168.1.117: user-service:8080 - registration status: 204

4. At the same point, in Eureka Server logs, you can see that User service has registered itself.

2018-07-26 23:53:19.165 INFO 5715 --- [nio-8761-exec-4] c.n.e.registry.AbstractInstanceRegistry : Registered instance USER- SERVICE/192.168.1.117:user-service:8081 with status UP (replication=false)

5. Repeat the same process by visiting individual directories of User Service instances.

6. Upon refreshing the Eureka server console (http://localhost:8761/), you can see all the UserService instances are now available.

spring-cloud

6. Simple Ribbon Client MicroService

This micro service consumes the endpoints of User service. It uses Ribbon with default configuration to select apt instance of User service.

The project structure is

spring-cloud

6.1 Maven Dependencies

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hemant</groupId> <artifactId>simple-ribbon-client</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>simple-ribbon-client</name> <description>A simple ribbon client service for accessing User Service endpoints</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> <relativePath /><!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring.cloud.version>1.2.6.RELEASE</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-eureka</artifactId> <version>${spring.cloud.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

By including dependency spring-cloud-starter-eureka, the Ribbon dependencies are also included.

6.2 Spring Boot Main Class:

packagecom.hemant.simpleribbonclient; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication @EnableEurekaClient publicclassSimpleRibbonClientApp { publicstaticvoidmain(String[] args) { SpringApplication.run(SimpleRibbonClientApp.class, args); } }

6.3 User Client Controller:

This controller act as client and consumes the endpoints from User Service.

packagecom.hemant.simpleribbonclient.controller; importjava.util.LinkedHashMap; importjava.util.Map; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; importorg.springframework.http.HttpMethod; importorg.springframework.http.ResponseEntity; importorg.springframework.web.bind.annotation.PathVariable; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RequestMethod; importorg.springframework.web.bind.annotation.RestController; importorg.springframework.web.client.RestTemplate; /** * In typical Eureka Client Application, * we autowire #{org.springframework.cloud.client.discovery.DiscoveryClient} * * The instance is selected via following logic: * <code> * @Autowired * private DiscoveryClientdiscoveryClient; * * @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) * public Map<String, String>getUserById(@PathVariable String id) { * List<ServiceInstance>serviceInstances = discoveryClient.getInstances(userServiceName); * if (serviceInstances.isEmpty()) { * throw new IllegalArgumentException("No instance found with serviceId :" + userServiceName); * } * ServiceInstanceuserServiceInstance = serviceInstances.get(0); * String url = userServiceInstance.getUri().toString() + "/" + id; * ... * * </code> * The drawback of this approach is at line: * ServiceInstanceuserServiceInstance = serviceInstances.get(0); * * even if multiple service instances are available, we are choosing the same every time. * * With Ribbon, we use * 1. #{LoadBalancerClient} instead of #{DiscoveryClient} * 2. The instance is chosen via following line : * ServiceInstanceuserServiceInstance = loadBalancerClient.choose(userServiceName); * * Thus load-balancer determines which instance to choose. * * @author hemant * */ @RestController publicclassUserClientController { @Autowired privateLoadBalancerClientloadBalancerClient; privateRestTemplaterestTemplate = newRestTemplate(); privatefinal String userServiceName = "user-service"; @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) public Map<String, String>getUserById(@PathVariable String id) { ServiceInstanceuserServiceInstance = loadBalancerClient.choose(userServiceName); String url = userServiceInstance.getUri().toString() + "/" + id; ResponseEntity<String>authResponse = restTemplate.exchange(url, HttpMethod.GET, null, String.class); Map<String, String> map = newLinkedHashMap<>(); map.put("url", url); map.put("user-service-response", authResponse.getBody()); return map; } }

This uses Ribbon loadBalancerClient to choose an instance of User Service.

By default, the most well known and basic load balancing strategy, i.e. Round Robin Rule is used.

6.4 Application Properties

server.port : 8091 spring.application.name : simple-ribbon-client eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka

Ribbon can be used without Eureka (In that case explicit list of service instances needs to be provided). However since we are using Eureka, no explicit configuration of User service instances is needed.

6.5 Deploying the Application

1. Go to the project’s root directory and execute

>>>mvn clean install

2. If the build is success, we can launch the application by:

>>>java -jar target/info-service-0.0.1-SNAPSHOT.jar

6.6 Testing the Client:

Once the service is deployed, we can test the load balancer as follows:

1. Hit the client service endpoint

GET http://localhost:8091/user/user-1 { "url": "http://192.168.43.110:8081/user-1", "user-service-response": "{\"id\":\"user-1\",\"name\":\"User1\",\"email\":\"user1@gmail.com\"}" } Thus the user information was rendered by instance running on 8081

2. Hit it second time

GET http://localhost:8091/user/user-1 { "url": "http://192.168.43.110:8082/user-1", "user-service-response": "{\"id\":\"user-1\",\"name\":\"User1\",\"email\":\"user1@gmail.com\"}" } Thus the user information was rendered by instance running on 8082

3. Hit it third time

GET http://localhost:8091/user/user-1 { "url": "http://192.168.43.110:8083/user-1", "user-service-response": "{\"id\":\"user-1\",\"name\":\"User1\",\"email\":\"user1@gmail.com\"}" } Thus the user information was rendered by instance running on 8083

4. Hit it forth time

GET http://localhost:8091/user/user-1 { "url": "http://192.168.43.110:8081/user-1", "user-service-response": "{\"id\":\"user-1\",\"name\":\"User1\",\"email\":\"user1@gmail.com\"}" } Thus the user information was rendered again by instance running on 8081.

On further re-tries, we can infer that the load balancer is consuming the user-service instances using Round-Robin strategy. Even if instance is removed/new instance is added, Ribbon will get updated instance information via Eureka and continue its operation without any hassle.

7. Customizing Ribbon Configuration

We didn’t do much Load Balancer configuration in previous application. However Ribbon allows us to configure the following components:

  1. Rule – component which specifies load balancing rule (implementation of com.netflix.loadbalancer.IRule). The implementations are AbstractLoadBalancerRule, AvailabilityFilteringRule, ClientConfigEnabledRoundRobinRule, RandomRule, ResponseTimeWeightedRule, RetryRule, RoundRobinRule, WeightedResponseTimeRule, ZoneAvoidanceRule
  2. Ping – Component which specifies mechanism to determine the server’s availability in real-time (Implementation of com.netflix.loadbalancer.IPing). The implementations are bstractLoadBalancerPing, DummyPing, NoOpPing, PingConstant
  3. ServerList – can be dynamic or static. In case Eureka is used, then this need not be configured.

7.1 Customizing Application Properties:

With spring, some part of configuration can be done in application properties.

The properties are prefixed as <client>.ribbon.*

Following is the sample configuration used for our User Service:

# user-service - is the serviceId (spring-application-name) #ribbon will use Eureka to get instances of user-service user-service.ribbon.eureka.enabled=true # refresh the server list every 10 sec user-service.ribbon.ServerListRefreshInterval=10000

7.2 Advanced Customization:

We can also take full control of the client by declaring additional configuration.

The Spring Boot application class is as follows:

@SpringBootApplication @EnableEurekaClient @RibbonClient(name = "user-service", configuration = UserRibbonClientConfig.class) publicclassCustomRibbonClientApp { publicstaticvoidmain(String[] args) { SpringApplication.run(CustomRibbonClientApp.class, args); } // Annotation to mark a RestTemplate bean to be configured to use a // LoadBalancerClient @LoadBalanced @Bean RestTemplaterestTemplate() { returnnew RestTemplate(); } }

The ribbon configuration class is as follows:

packagecom.hemant.customribbonclient; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.context.annotation.Bean; importcom.netflix.client.config.IClientConfig; importcom.netflix.loadbalancer.BestAvailableRule; importcom.netflix.loadbalancer.IPing; importcom.netflix.loadbalancer.IRule; importcom.netflix.loadbalancer.PingUrl; /** * We have to make sure that this class is not ComponentScanned * @author hemant * */ publicclassUserRibbonClientConfig { @Autowired IClientConfigribbonClientConfig; @Bean publicIPingribbonPing(IClientConfig config) { returnnewPingUrl(); } @Bean publicIRuleribbonRule(IClientConfig config) { returnnewBestAvailableRule(); } }

This configuration is used in client logic as follows:

@Autowired privateRestTemplaterestTemplate; // inside the function String url = "http://user-service" + "/" + id; ResponseEntity<String>authResponse = restTemplate.exchange(url, HttpMethod.GET, null, String.class);

8. Conclusion:

Thus we have used Ribbon for client side load balancing and accessing a specific instance of the service. The default load balancing strategy used is Round-Robin.

In advanced configuration section, we have also explored how to take full control of ribbon configuration.