Client Side Load Balancing with Ribbon and Spring Cloud

Write a server service

Create the directory structure

mkdir -p say-hello/src/main/java/hello

Create a Gradle build file

vim say-hello/build.gradle
buildscript {
    ext {
        springBootVersion = '2.1.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'say-hello'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test') 
}


eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

Our "server" service is called Say Hello. It will return a random greeting (picked out of a static list of three) from an endpoint accessible at /greeting.

vim say-hello/src/main/java/hello/SayHelloApplication.java
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

@RestController
@SpringBootApplication
public class SayHelloApplication {

  private static Logger log = LoggerFactory.getLogger(SayHelloApplication.class);

  @RequestMapping(value = "/greeting")
  public String greet() {
    log.info("Access /greeting");

    List<String> greetings = Arrays.asList("Hi there", "Greetings", "Salutations");
    Random rand = new Random();

    int randomNum = rand.nextInt(greetings.size());
    return greetings.get(randomNum);
  }

  @RequestMapping(value = "/")
  public String home() {
    log.info("Access /");
    return "Hi!";
  }

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

The @RestController annotation gives the same effect as if we were using @Controller and @ResponseBody together. It marks SayHelloApplication as a controller class (which is what @Controller does) and ensures that return values from the class's @RequestMapping methods will be automatically converted appropriately from their original types and written directly to the response body (which is what @ResponseBody does). We have one @RequestMapping method for /greeting and then another for the root path /. (We'll want that second method when we get to working with Ribbon in just a bit.)

We're going to run multiple instances of this application locally alongside a client service application, so create the directory src/main/resources, create the file application.yml within it, and then in that file, set a default value for server.port. (We'll instruct the other instances of the application to run on other ports, as well, so that none of the Say Hello instances will conflict with the client when we get that running.) While we're in this file, we'll set the spring.application.name for our service too.

mkdir -p say-hello/src/main/resources/

vim say-hello/src/main/resources/application.yml
spring:
  application:
    name: say-hello

server:
  port: 8090

Access from a client service

Create the directory structure

mkdir -p user/src/main/java/hello

Create a Gradle build file

vim user/build.gradle
buildscript {
  ext {
    springBootVersion = '2.1.4.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
  baseName = 'user'
  version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}


dependencies {
  compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test') 
}

dependencyManagement {
  imports { 
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2"
  }
}

eclipse {
  classpath {
     containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
     containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
  }
}

To move beyond a single hard-coded server URL to a load-balanced solution, let's set up Ribbon.

mkdir -p user/src/main/resources/

vim user/src/main/resources/application.yml
spring:
  application:
    name: user

server:
  port: 8888

say-hello:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost:8090,localhost:9092,localhost:9999
    ServerListRefreshInterval: 15000

This configures properties on a Ribbon client. Spring Cloud Netflix creates an ApplicationContext for each Ribbon client name in our application. This is used to give the client a set of beans for instances of Ribbon components, including:

  • an IClientConfig, which stores client configuration for a client or load balancer,
  • an ILoadBalancer, which represents a software load balancer,
  • a ServerList, which defines how to get a list of servers to choose from,
  • an IRule, which describes a load balancing strategy, and
  • an IPing, which says how periodic pings of a server are performed.

In our case above, the client is named say-hello. The properties we set are eureka.enabled (which we set to false), listOfServers, and ServerListRefreshInterval. Load balancers in Ribbon normally get their server lists from a Netflix Eureka service registry. For our simple purposes here, we're skipping Eureka, so we set the ribbon.eureka.enabled property to false and instead give Ribbon a static listOfServers. ServerListRefreshInterval is the interval, in milliseconds, between refreshes of Ribbon's service list.

The User application will be what our user sees. It will make a call to the Say Hello application to get a greeting and then send that to our user when the user visits the endpoint at /hi.

vim user/src/main/java/hello/UserApplication.java
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@RestController
@RibbonClient(name = "say-hello", configuration = SayHelloConfiguration.class)
public class UserApplication {

  @LoadBalanced
  @Bean
  RestTemplate restTemplate(){
    return new RestTemplate();
  }

  @Autowired
  RestTemplate restTemplate;

  @RequestMapping("/hi")
  public String hi(@RequestParam(value="name", defaultValue="Artaban") String name) {
    String greeting = this.restTemplate.getForObject("http://say-hello/greeting", String.class);
    return String.format("%s, %s!", greeting, name);
  }

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

To get a greeting from Say Hello, we're using Spring's RestTemplate template class. RestTemplate makes an HTTP GET request to the Say Hello service's URL as we provide it and gives us the result as a String.

Our RestTemplate is also marked as LoadBalanced; this tells Spring Cloud that we want to take advantage of its load balancing support (provided, in this case, by Ribbon). The class is annotated with @RibbonClient, which we give the name of our client (say-hello) and then another class, which contains extra configuration for that client.

vim user/src/main/java/hello/SayHelloConfiguration.java
package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.AvailabilityFilteringRule;

public class SayHelloConfiguration {

  @Autowired
  IClientConfig ribbonClientConfig;

  @Bean
  public IPing ribbonPing(IClientConfig config) {
    return new PingUrl();
  }

  @Bean
  public IRule ribbonRule(IClientConfig config) {
    return new AvailabilityFilteringRule();
  }
}

We can override any Ribbon-related bean that Spring Cloud Netflix gives us by creating our own bean with the same name. Here, we override the IPing and IRule used by the default load balancer. The default IPing is a NoOpPing (which doesn't actually ping server instances, instead always reporting that they're stable), and the default IRule is a ZoneAvoidanceRule (which avoids the Amazon EC2 zone that has the most malfunctioning servers, and might thus be a bit difficult to try out in our local environment).

Our IPing is a PingUrl, which will ping a URL to check the status of each server. Say Hello has a method mapped to the / path; that means that Ribbon will get an HTTP 200 response when it pings a running Say Hello server. The IRule we set up, the AvailabilityFilteringRule, will use Ribbon's built-in circuit breaker functionality to filter out any servers in an "open-circuit" state: if a ping fails to connect to a given server, or if it gets a read failure for the server, Ribbon will consider that server "dead" until it begins to respond normally.

Trying it out

Run the Say Hello service

cd say-hello
gradle bootRun

Run other instances on ports 9092 and 9999

cd say-hello

SERVER_PORT=9092 gradle bootRun
SERVER_PORT=9999 gradle bootRun

Then start up the User service.

cd user
gradle bootRun

Access localhost:8888/hi and then watch the Say Hello service instances. You can see Ribbon's pings arriving every 15 seconds:

curl localhost:8888/hi
Greetings, Artaban!
2019-04-30 14:56:25.684  INFO 81063 --- [nio-8090-exec-4] hello.SayHelloApplication                : Access /greeting
...
2019-04-30 14:58:06.481  INFO 81063 --- [nio-8090-exec-3] hello.SayHelloApplication                : Access /
2019-04-30 14:58:21.493  INFO 81063 --- [nio-8090-exec-5] hello.SayHelloApplication                : Access /
2019-04-30 14:58:36.503  INFO 81063 --- [nio-8090-exec-9] hello.SayHelloApplication                : Access /

And your requests to the User service should result in calls to Say Hello being spread across the running instances in round-robin form:

Now shut down a Say Hello server instance. Once Ribbon has pinged the down instance and considers it down, you should see requests begin to be balanced across the remaining instances.

results matching ""

    No results matching ""