3

Service Integration with Netflix Feign and Ribbon

 3 years ago
source link: https://www.briansdevblog.com/2019/04/service-integration-with-netflix-feign-and-ribbon/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The guys at Netflix have developed and open sourced (among many other things) Feign and Ribbon.  These libraries can help you as a developer, to build robust, fault tolerant service integrations.  Best of all, they’ve been tested in the wild by Netflix, who use both libraries extensivley in their own microservices architecture. In this post we’ll look at Feign and Ribbon to see how they can be used in the context of a Spring Boot application.

What is Feign?

Feign is a library that helps developers create declarative HTTP clients by simply defining an interface and annotating it. At runtime, Feign creates the HTTP client implementation for the interface. We’ll look at this in detail with some sample code later.

What is Ribbon?

Ribbon is a library that provides client-side load balancing and fault tolerance for HTTP Clients. You can choose from a number of load balancing algorithms out of the box or even provide your own. For dynamic service discovery, you can integrate with Eureka or you can simply configure a list of available service instances. Ribbon provides Fault tolerance by periodically pinging the health endpoint of registered services and removing unhealthy instances from the load balancer.

Spring Support

While you can use Feign and Ribbon directly via the native Netflix API, if you’re using Spring it makes sense to use Feign and Ribbon via the Spring Cloud project. Spring Cloud helps you easily integrate Feign and Ribbon into your Spring apps with helpful annotations like @FeignClient@FeignClientand  @RibbonClient@RibbonClient . This post will look at Feign and Ribbon in the context of a Spring Boot application.

Sample App

The best way to explore Feign and Ribbon is to put them to work in a sample app. To get a useful working example up and running we’ll

  • Create a simple Bank Account Service that exposes a single REST endpoint for creating a Bank Account
  • Create a simple Account Identifier Service that generates a new account number for each new bank account
  • Use Feign and Ribbon to call the Account Identifier Service from the Bank Account Service.
  • Run 2 instances of the Account Identifier Service so that we can see Ribbon load balancing the calls to the Account Identifier Service.

The diagram below describes how the various components interact.

Sample App

Source Code

You’ll find the full source code for the Bank Account Service and Account Identifier Service on Github.  I’d recommend you pull the code down so that you can run the sample services locally. Instructions for running the services is covered later.

Creating the Bank Account Service

The purpose of this service is to allows clients to create a new Bank Account entity, via a single RESTful endpoint.  The main application class is defined as follows.

@SpringBootApplication
@EnableFeignClients("com.briansjavablog.microservices.client")
public class BankAccountServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(BankAccountServiceApplication.class, args);
  }
}
  1. @SpringBootApplication
  2. @EnableFeignClients("com.briansjavablog.microservices.client")
  3. public class BankAccountServiceApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(BankAccountServiceApplication.class, args);
@SpringBootApplication
@EnableFeignClients("com.briansjavablog.microservices.client")
public class BankAccountServiceApplication {

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

This is standard Spring Boot setup with the addition of @EnableFeignClients("com.briansjavablog.microservices.client")@EnableFeignClients("com.briansjavablog.microservices.client") This is a component scanner that specifically looks for interfaces annotated with  @FeignClient@FeignClient .

Creating the Bank Account Controller

The REST controller exposes a single operation for creating a new Bank Account.

@Slf4j
@RestController
public class BankAccountController {

  @Autowired
  public AccountIdentifierServiceClient accountIdentifierServiceClient; 
  
  
  @PostMapping("/bank-account")
  public ResponseEntity<AccountIdentifier> createBankAccount(@RequestBody BankAccount bankAccount) throws URISyntaxException {
    
    log.info("creating bank account {}", bankAccount);
    
    AccountIdentifier accountIdentifier = accountIdentifierServiceClient.getAccountIdentifier(bankAccount.getAccountType().getValue());
    
    log.info("created Account Identifier [{}]", accountIdentifier);
    
    return ResponseEntity.ok(accountIdentifier);				
  }
    
}
  1. @Slf4j
  2. @RestController
  3. public class BankAccountController {
  4. @Autowired
  5. public AccountIdentifierServiceClient accountIdentifierServiceClient;
  6. @PostMapping("/bank-account")
  7. public ResponseEntity<AccountIdentifier> createBankAccount(@RequestBody BankAccount bankAccount) throws URISyntaxException {
  8. log.info("creating bank account {}", bankAccount);
  9. AccountIdentifier accountIdentifier = accountIdentifierServiceClient.getAccountIdentifier(bankAccount.getAccountType().getValue());
  10. log.info("created Account Identifier [{}]", accountIdentifier);
  11. return ResponseEntity.ok(accountIdentifier);
@Slf4j
@RestController
public class BankAccountController {

  @Autowired
  public AccountIdentifierServiceClient accountIdentifierServiceClient; 
  
  
  @PostMapping("/bank-account")
  public ResponseEntity<AccountIdentifier> createBankAccount(@RequestBody BankAccount bankAccount) throws URISyntaxException {
    
    log.info("creating bank account {}", bankAccount);
    
    AccountIdentifier accountIdentifier = accountIdentifierServiceClient.getAccountIdentifier(bankAccount.getAccountType().getValue());
    
    log.info("created Account Identifier [{}]", accountIdentifier);
    
    return ResponseEntity.ok(accountIdentifier);				
  }
    
}

The AccountIdentifierServiceClient AccountIdentifierServiceClient  interface injected on line 6 is used to call the AccountIdentifier service. We’ll look at the AccountIdentifierServiceClient AccountIdentifierServiceClient  interface in detail later. On line 18 the  AccountIdentifierAccountIdentifier entity returned from the AccountIdentifierServiceAccountIdentifierServiceis passed back to the client.

Creating  the Feign Client Interface

The next step is to create the AccountIdentifierServiceClientAccountIdentifierServiceClient interface that was injected into the BankAccountControllerBankAccountController above.

@FeignClient(name="account-identifier-service", configuration=FeignConfiguration.class)
public interface AccountIdentifierServiceClient {
  
  @GetMapping(path = "account-identifier/accountType/{accountType}")
  public AccountIdentifier getAccountIdentifier(@PathVariable("accountType") String accountType);
}
  1. @FeignClient(name="account-identifier-service", configuration=FeignConfiguration.class)
  2. public interface AccountIdentifierServiceClient {
  3. @GetMapping(path = "account-identifier/accountType/{accountType}")
  4. public AccountIdentifier getAccountIdentifier(@PathVariable("accountType") String accountType);
@FeignClient(name="account-identifier-service", configuration=FeignConfiguration.class)
public interface AccountIdentifierServiceClient {
  
  @GetMapping(path = "account-identifier/accountType/{accountType}")
  public AccountIdentifier getAccountIdentifier(@PathVariable("accountType") String accountType);
}

As part of component scanning, Spring will look for interfaces annotated with @FeignClient@FeignClient and using the configuration in  @FeignConfiguration.class@FeignConfiguration.class, create a REST client implementation. This REST client will be injected anywhere the AccountIdentifierServiceClientAccountIdentifierServiceClient is Auto Wired, so in this app, it’ll be injected into the BankAccountControllerBankAccountController we created earlier.

We added a single interface method and annotate it with @GetMapping(path = "account-identifier/accountType/{accountType}")@GetMapping(path = "account-identifier/accountType/{accountType}") .  The  @GetMapping@GetMapping annotation is a standard Spring Web annotation (the same as you’d use to create a REST controller), but in this instance, it’s used to describe the operations we want to perform with the REST client.  Spring will use Feign and this information to build a REST client that sends a HTTP GET request to  account-identifier/accountType/account-identifier/accountType/ with a path variable for accountType.

Configuring Feign

In the @FeignClient@FeignClientannotation above we referenced FeignConfiguration.classFeignConfiguration.class.  We’ll now define that class to configure the behaviour of the Feign client. Spring Cloud allows you to override a number of different beans, we’ll look at some of the most useful ones below.

Note that you don’t need to mark the class with the @Configuration@Configuration annotation, as we already referenced it directly from the @FeignClient@FeignClient annotation in the AccountIdentifierServieClientAccountIdentifierServieClient.

Logging

package com.briansjavablog.microservices.client;

import org.springframework.context.annotation.Bean;

import feign.Logger;
import feign.Request;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FeignConfiguration {

  
  @Bean
  public Logger.Level configureLogLevel(){
    return  Logger.Level.FULL;
  }
  1. package com.briansjavablog.microservices.client;
  2. import org.springframework.context.annotation.Bean;
  3. import feign.Logger;
  4. import feign.Request;
  5. import feign.RequestInterceptor;
  6. import feign.RequestTemplate;
  7. import feign.Retryer;
  8. import feign.auth.BasicAuthRequestInterceptor;
  9. import lombok.extern.slf4j.Slf4j;
  10. @Slf4j
  11. public class FeignConfiguration {
  12. @Bean
  13. public Logger.Level configureLogLevel(){
  14. return Logger.Level.FULL;
package com.briansjavablog.microservices.client;

import org.springframework.context.annotation.Bean;

import feign.Logger;
import feign.Request;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FeignConfiguration {

  
  @Bean
  public Logger.Level configureLogLevel(){
    return  Logger.Level.FULL;
  }

The first configuration Bean we configure is Logger.LevelLogger.Level . Here you can set the level of logging required to one of the following

  • NONE – no logging
  • BASIC – log request method, URL, response code and execution time
  • HEADERS – same as BASIC, plus request & response headers
  • FULL – log headers, body and metadata for request & response

Timeouts

@Bean 
public Request.Options timeoutConfiguration(){
  
  return new Request.Options(5000, 30000);
}
  1. @Bean
  2. public Request.Options timeoutConfiguration(){
  3. return new Request.Options(5000, 30000);
@Bean 
public Request.Options timeoutConfiguration(){
  
  return new Request.Options(5000, 30000);
}

Another important piece of configuration for any REST client is the connect and read timeout values. These tell your app to wait a sensible amount of time for a connection or response, without blocking indefinitely.  You can set both the connect and read timeout values by supplying a  Request.OptionsRequest.Options bean. Both values are in milliseconds.

Request Interceptors

Request interceptors are commonly used to tweak a request before its sent. Adding custom HTTP headers is a common use case. Feign allows you to provide your own interceptors via the RequestInterceptorRequestInterceptor bean. The sample  RequestInterceptorRequestInterceptor below adds a dummy HTTP header to the request.

@Bean
public RequestInterceptor requestLoggingInterceptor() {
    
  return new RequestInterceptor() {
      
    @Override
    public void apply(RequestTemplate template) {
      
      log.info("Adding header [testHeader / testHeaderValue] to request");
      template.header("testHeader", "testHeaderValue");
    }
  };
}
  1. @Bean
  2. public RequestInterceptor requestLoggingInterceptor() {
  3. return new RequestInterceptor() {
  4. @Override
  5. public void apply(RequestTemplate template) {
  6. log.info("Adding header [testHeader / testHeaderValue] to request");
  7. template.header("testHeader", "testHeaderValue");
@Bean
public RequestInterceptor requestLoggingInterceptor() {
    
  return new RequestInterceptor() {
      
    @Override
    public void apply(RequestTemplate template) {
      
      log.info("Adding header [testHeader / testHeaderValue] to request");
      template.header("testHeader", "testHeaderValue");
    }
  };
}

Basic Auth

Feign provides the BasicAuthRequestInterceptorBasicAuthRequestInterceptor out of the box to allow you to easily configure basic auth for your client. This is a nice convenience and saves you having to roll your own. To configure  BasicAuthRequestInterceptorBasicAuthRequestInterceptor just supply the username and password.

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    
  return new BasicAuthRequestInterceptor("user", "password");
}
  1. @Bean
  2. public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
  3. return new BasicAuthRequestInterceptor("user", "password");
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    
  return new BasicAuthRequestInterceptor("user", "password");
}

Retries

The final piece of Feign configuration we’ll look at is the RetryerRetryer. Like the timeout configuration we looked at earlier, a retry policy is important for robust integrations. Thankfully Feign provides a default implementation out of the box that retries the request a number of times, with an exponential backoff between attempts. The default implementation allows you to specify the initial period to wait, the max period to wait and the max number of retries.

@Bean
public Retryer retryer() {
  
  return new Retryer.Default(1000, 8000, 3);		
}
  1. @Bean
  2. public Retryer retryer() {
  3. return new Retryer.Default(1000, 8000, 3);
@Bean
public Retryer retryer() {
  
  return new Retryer.Default(1000, 8000, 3);		
}

Configuring the default RetryerRetryer is probably good enough for most uses cases, but if you want to roll your own, you can implement the  RetryerRetryer interface.

Configuring Ribbon

You can configure Ribbon via the native Netflix API, but the easiest way is to use the @RibbonClient@RibbonClient annotation provided by Spring Cloud. Similar to the @FeignClient@FeignClient we looked at earlier, you can configure a number of key components via Bean configuration. We’ll look at a few of the most useful below.

Load Balancing Algorithm

@Slf4j
@RibbonClient(name="account-identifier-service")
public class RibbonConfiguration {

  
  @Bean
  public IRule loadBlancingRule() {
    		
    return new RoundRobinRule();
  }
  1. @Slf4j
  2. @RibbonClient(name="account-identifier-service")
  3. public class RibbonConfiguration {
  4. @Bean
  5. public IRule loadBlancingRule() {
  6. return new RoundRobinRule();
@Slf4j
@RibbonClient(name="account-identifier-service")
public class RibbonConfiguration {

  
  @Bean
  public IRule loadBlancingRule() {
    		
    return new RoundRobinRule();
  }

The IRuleIRule bean defines the load balancing algorithm that Ribbon will use. There are a number of common load balancing rules available out of the box. In this instance, I’ve used round-robin to evenly distribute requests across all available instances.

Health Checks

@Bean
public IPing pingConfiguration(ServerList<Server> servers) {
    
  String pingPath = "/actuator/health";
  IPing ping = new PingUrl(false, pingPath);				
  log.info("Configuring ping URI to [{}]", pingPath);
    
  return ping;		
}
  1. @Bean
  2. public IPing pingConfiguration(ServerList<Server> servers) {
  3. String pingPath = "/actuator/health";
  4. IPing ping = new PingUrl(false, pingPath);
  5. log.info("Configuring ping URI to [{}]", pingPath);
  6. return ping;
@Bean
public IPing pingConfiguration(ServerList<Server> servers) {
    
  String pingPath = "/actuator/health";
  IPing ping = new PingUrl(false, pingPath);				
  log.info("Configuring ping URI to [{}]", pingPath);
    
  return ping;		
}

The IPingIPing bean defines how we’ll ping service instances to check their health. Here I create a PingUrlPingUrl and supply the path to the health check endpoint. Ribbon will send a HTTP request to http://my-server:port/actuator/health.  A HTTP 200 response to the ping will tell Ribbon that the instance is healthy and available to receive requests. An unsuccessful response will tell Ribbon that the instance is unhealthy and it will be removed from the load balancers list of available instances.

Service Discovery

So how do we get the list of available instances in the first place? The ServerList<T>ServerList<T> interface defines methods for retrieving the list of available instances. We can provide an implementation of this interface to get the list of instances from any source. In a cloud environment, you’d likely want dynamic service discovery using something like Eureka. In a non-cloud environment, you might be able to get away with a list of target instances configured in a properties file.

To keep things simple I’ve returned a hard-coded list of instances. Note that we supply only the scheme, host and port. The remainder of the URL is derived from the Feign configuration we defined earlier.

@Bean
public ServerList<Server> serverList() {
    
  return new ServerList<Server>() {
      
    @Override
    public List<Server> getUpdatedListOfServers() {

      List<Server> serverList = Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));				
      log.info("Returning updated list of servers [{}]", serverList);
      return serverList;
    }
      
    @Override
    public List<Server> getInitialListOfServers() {

      return Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));
    }
  };
}
  1. @Bean
  2. public ServerList<Server> serverList() {
  3. return new ServerList<Server>() {
  4. @Override
  5. public List<Server> getUpdatedListOfServers() {
  6. List<Server> serverList = Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));
  7. log.info("Returning updated list of servers [{}]", serverList);
  8. return serverList;
  9. @Override
  10. public List<Server> getInitialListOfServers() {
  11. return Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));
@Bean
public ServerList<Server> serverList() {
    
  return new ServerList<Server>() {
      
    @Override
    public List<Server> getUpdatedListOfServers() {

      List<Server> serverList = Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));				
      log.info("Returning updated list of servers [{}]", serverList);
      return serverList;
    }
      
    @Override
    public List<Server> getInitialListOfServers() {

      return Arrays.asList(new Server("http", "localhost", 8091), new Server("http", "localhost", 8092));
    }
  };
}

The getUpdatedListOfServersgetUpdatedListOfServers is called periodically by Ribbon to pick up the latest instances. The period is configurable via the ribbon.ServerListRefreshInterval in application.peroperties

# check for updated list of servers every 10 seconds
account-identifier-service.ribbon.ServerListRefreshInterval=10000
  1. # check for updated list of servers every 10 seconds
  2. account-identifier-service.ribbon.ServerListRefreshInterval=10000
# check for updated list of servers every 10 seconds
account-identifier-service.ribbon.ServerListRefreshInterval=10000

After Ribbon has retrieved the latest list of instances, it pings the health check of each to ensure the instance is still alive. If the instance is healthy it remains eligible for requests otherwise it’s regarded as dead until the next ping cycle runs 10 seconds later. This continuous ping cycle allows Ribbon to ignore unhealthy instances when they’re down, but re-register them if they become available again.

Account Identifier Service

The Bank Account Service will use the Feign/Ribbon client to load balance calls to the Account Identifier Service. The Account Identifier Service exposes a single REST endpoint that takes an AccountTypeAccountType parameter and returns an AccountIdentifierAccountIdentifier . The AccountIdentifierAccountIdentifier contains a randomly generated account number and the port of the instance that handled the request. Including the port in the response will allow us to see which instance handled the request and will prove that Ribbon is load balancing the requests as expected. The AccountIdentifierControllerAccountIdentifierController is shown below.

@Slf4j
@RestController
public class AccountIdentifierController {

  @Autowired	
  private Environment environment;
  
  
  @GetMapping("/account-identifier/accountType/{accountType}")
  public ResponseEntity<AccountIdentifier> createAccountIdentifier(@PathVariable("accountType") EnumAccountType accountType) 
                                                                                                   throws URISyntaxException {
    
    log.info("creating Account Identifier for account type [{}]", accountType);
    
    Random random = new Random(System.currentTimeMillis());
    int randomId = 10000 +  random.nextInt(20000);
    
    AccountIdentifier accountIdentifier = new AccountIdentifier	();
    
    if(accountType.equals(EnumAccountType.CURRENT_ACCOUNT)) {
      accountIdentifier.setAccountNumber("C" + randomId);
    }
    else if(accountType.equals(EnumAccountType.SAVINGS_ACCOUNT)) {
      accountIdentifier.setAccountNumber("S" + randomId);
    }
    
    accountIdentifier.setAccountIdentifierServicePort(environment.getProperty("local.server.port"));
    
    log.info("generated Account Identifier [{}]", accountIdentifier);
    
    return ResponseEntity.ok(accountIdentifier);				
  }
    
}
  1. @Slf4j
  2. @RestController
  3. public class AccountIdentifierController {
  4. @Autowired
  5. private Environment environment;
  6. @GetMapping("/account-identifier/accountType/{accountType}")
  7. public ResponseEntity<AccountIdentifier> createAccountIdentifier(@PathVariable("accountType") EnumAccountType accountType)
  8. throws URISyntaxException {
  9. log.info("creating Account Identifier for account type [{}]", accountType);
  10. Random random = new Random(System.currentTimeMillis());
  11. int randomId = 10000 + random.nextInt(20000);
  12. AccountIdentifier accountIdentifier = new AccountIdentifier ();
  13. if(accountType.equals(EnumAccountType.CURRENT_ACCOUNT)) {
  14. accountIdentifier.setAccountNumber("C" + randomId);
  15. else if(accountType.equals(EnumAccountType.SAVINGS_ACCOUNT)) {
  16. accountIdentifier.setAccountNumber("S" + randomId);
  17. accountIdentifier.setAccountIdentifierServicePort(environment.getProperty("local.server.port"));
  18. log.info("generated Account Identifier [{}]", accountIdentifier);
  19. return ResponseEntity.ok(accountIdentifier);
@Slf4j
@RestController
public class AccountIdentifierController {

  @Autowired	
  private Environment environment;
  
  
  @GetMapping("/account-identifier/accountType/{accountType}")
  public ResponseEntity<AccountIdentifier> createAccountIdentifier(@PathVariable("accountType") EnumAccountType accountType) 
                                                                                                   throws URISyntaxException {
    
    log.info("creating Account Identifier for account type [{}]", accountType);
    
    Random random = new Random(System.currentTimeMillis());
    int randomId = 10000 +  random.nextInt(20000);
    
    AccountIdentifier accountIdentifier = new AccountIdentifier	();
    
    if(accountType.equals(EnumAccountType.CURRENT_ACCOUNT)) {
      accountIdentifier.setAccountNumber("C" + randomId);
    }
    else if(accountType.equals(EnumAccountType.SAVINGS_ACCOUNT)) {
      accountIdentifier.setAccountNumber("S" + randomId);
    }
    
    accountIdentifier.setAccountIdentifierServicePort(environment.getProperty("local.server.port"));
    
    log.info("generated Account Identifier [{}]", accountIdentifier);
    
    return ResponseEntity.ok(accountIdentifier);				
  }
    
}

Earlier we defined a PingUrlPingUrl bean to allow Ribbon to ping the health endpoint of each Account Service instance. Spring Boot provides a health endpoint out of the box but I’ve added my own implementation to log the health check and the port the app is running on. The custom health check always returns UP.

@Slf4j
@Component
public class HealthIndicator extends AbstractHealthIndicator {

  @Autowired	
  private Environment environment;
  
  @Override
  protected void doHealthCheck(Builder builder) throws Exception {

    log.info("running health check for {}", environment.getProperty("local.server.port"));
    builder.up();
  }
}
  1. @Slf4j
  2. @Component
  3. public class HealthIndicator extends AbstractHealthIndicator {
  4. @Autowired
  5. private Environment environment;
  6. @Override
  7. protected void doHealthCheck(Builder builder) throws Exception {
  8. log.info("running health check for {}", environment.getProperty("local.server.port"));
  9. builder.up();
@Slf4j
@Component
public class HealthIndicator extends AbstractHealthIndicator {

  @Autowired	
  private Environment environment;
  
  @Override
  protected void doHealthCheck(Builder builder) throws Exception {

    log.info("running health check for {}", environment.getProperty("local.server.port"));
    builder.up();
  }
}

This will be useful later when we run the Bank Account Service and 2 instances of the Account Identifier Service. The health check logging will show us Ribbon calling the health endpoint of each registered instance.

Pulling the Sample Code and Building

You can grab the sample services from Github with git clone https://github.com/briansjavablog/client-side-load-balancing-netflix-feign-and-ribbongit clone https://github.com/briansjavablog/client-side-load-balancing-netflix-feign-and-ribbon.

git clone https://github.com/briansjavablog/client-side-load-balancing-netflix-feign-and-ribbon
  1. git clone https://github.com/briansjavablog/client-side-load-balancing-netflix-feign-and-ribbon
git clone https://github.com/briansjavablog/client-side-load-balancing-netflix-feign-and-ribbon

Now build the Bank Account Service Jar.

cd client-side-load-balancing-netflix-feign-and-ribbon/bank-account-service/
mvn clean install
  1. cd client-side-load-balancing-netflix-feign-and-ribbon/bank-account-service/
  2. mvn clean install
cd client-side-load-balancing-netflix-feign-and-ribbon/bank-account-service/
mvn clean install

Followed by the Account Identifier Service Jar

cd ../account-identifier-service/
mvn clean install
  1. cd ../account-identifier-service/
  2. mvn clean install
cd ../account-identifier-service/
mvn clean install

Starting the Services

To see the load balancing in action, we’re going to run 2 instances of the Account Identifier Service. One app will run on port 8091 and the other on port 8092. It’s important you use these specific ports as these are the ports we used to configure Ribbon earlier. Start the first instance by running java -jar target/account-identifier-service-1.0.0.jar --server.port=8091
java -jar target/account-identifier-service-1.0.0.jar --server.port=8091<br> and the second instance by running  java -jar target/account-identifier-service-1.0.0.jar --server.port=8092java -jar target/account-identifier-service-1.0.0.jar --server.port=8092.

Next, we’ll start a single instance of the Bank Account Service and use it to call the 2 Account Identifier Services instances.  Start the Bank Account Service by running java -jar target/bank-account-service-1.0.0.jar --server.port=8080java -jar target/bank-account-service-1.0.0.jar --server.port=8080 Make sure you don’t try to use port 8091 or 8092 as these are already used by the two Account Identifier instances.

Testing the Services

We’re finally ready to put Feign and Ribbon to the test by sending some requests to the Bank Account Service. We’ll test a few different scenarios to see how they’re handled.

Load Balancing

The Bank Account Service should load balance requests evenly across the two Account Identifier Service instances. Let’s see that in action. We’ll start by sending a HTTP POST to create a new Bank Account using the cURL command below.

curl -i -H "Content-Type: application/json" -X POST -d '{"accountId":"B12345","accountName":"Joe Bloggs","accountType":"CURRENT_ACCOUNT","accountBlance":1250.38}' localhost:8080/bank-account
  1. curl -i -H "Content-Type: application/json" -X POST -d '{"accountId":"B12345","accountName":"Joe Bloggs","accountType":"CURRENT_ACCOUNT","accountBlance":1250.38}' localhost:8080/bank-account
curl -i -H "Content-Type: application/json" -X POST -d '{"accountId":"B12345","accountName":"Joe Bloggs","accountType":"CURRENT_ACCOUNT","accountBlance":1250.38}' localhost:8080/bank-account

If the request is processed successfully you should see a JSON response with a randomly generated account number and the port of the Account Identifier Service instance that handled the request. The response below tells us that the Bank Account Service sent the first request to the Account Identifier instance on port 8091.

{"accountNumber":"C19637","accountIdentifierServicePort":"8091"}
  1. {"accountNumber":"C19637","accountIdentifierServicePort":"8091"}
{"accountNumber":"C19637","accountIdentifierServicePort":"8091"}

If we look at the Account Identifier Service log for the instance on port 8091 we should see the request being processed.

2019-04-11 08:07:29.263  INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController   : creating Account Identifier for account type [CURRENT_ACCOUNT]
2019-04-11 08:07:29.263  INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController   : generated Account Identifier [AccountIdentifier(accountNumber=C19205, accountIdentifierServicePort=8091)]
  1. 2019-04-11 08:07:29.263 INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController : creating Account Identifier for account type [CURRENT_ACCOUNT]
  2. 2019-04-11 08:07:29.263 INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController : generated Account Identifier [AccountIdentifier(accountNumber=C19205, accountIdentifierServicePort=8091)]
2019-04-11 08:07:29.263  INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController   : creating Account Identifier for account type [CURRENT_ACCOUNT]
2019-04-11 08:07:29.263  INFO 2112 --- [nio-8091-exec-9] c.b.a.rest.AccountIdentifierController   : generated Account Identifier [AccountIdentifier(accountNumber=C19205, accountIdentifierServicePort=8091)]

Now if we send a second request to the Bank Account Service it should use the Round Robin load balancing we configured earlier, to send the request to the Account Identifier Service instance on port 8092.

{"accountNumber":"C18492","accountIdentifierServicePort":"8092"}
  1. {"accountNumber":"C18492","accountIdentifierServicePort":"8092"}
{"accountNumber":"C18492","accountIdentifierServicePort":"8092"}

If we look at the Account Identifier Service log for the instance on port 8092 we should see the request being processed.

2019-04-11 08:18:31.170  INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController   : creating Account Identifier for account type [CURRENT_ACCOUNT]
2019-04-11 08:18:31.171  INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController   : generated Account Identifier [AccountIdentifier(accountNumber=C28287, accountIdentifierServicePort=8092)]
  1. 2019-04-11 08:18:31.170 INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController : creating Account Identifier for account type [CURRENT_ACCOUNT]
  2. 2019-04-11 08:18:31.171 INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController : generated Account Identifier [AccountIdentifier(accountNumber=C28287, accountIdentifierServicePort=8092)]
2019-04-11 08:18:31.170  INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController   : creating Account Identifier for account type [CURRENT_ACCOUNT]
2019-04-11 08:18:31.171  INFO 24256 --- [io-8092-exec-10] c.b.a.rest.AccountIdentifierController   : generated Account Identifier [AccountIdentifier(accountNumber=C28287, accountIdentifierServicePort=8092)]

We can see that the first request was sent to the instance on port 8091 and the second request was sent to the instance on port 8092. Feel free to experiment by sending a few more requests to the Bank Account Service. You’ll see that the Account Identifier Service instance called alternates between requests as expected.

Health Checks / Ping Cycle

Another key feature we should test is health checks. We configured a IPingIPing bean earlier to tell Ribbon what health endpoint to call on the Account Identifier Service. If we look at the console output for both Account Identifier Service instances, we should see the health check called by the Ribbon Ping cycle every 10 seconds.

Account Identifier Service Health Checks

Account Identifier Service Health Checks

The screenshot below shows the console output from the 2 instances I have running locally. If you look at the time stamp for each log entry you’ll see that the health check is being called every 10 seconds as expected.

Fault Tolerance

If we stop one of the Account Identifier Service instances, the health check will fail and Ribbon will stop sending requests to that instance. All subsequent requests to the Account Identifier Service will be routed to the single healthy instance. Ribbon will continue to ping the unhealthy instance every 10 seconds and if at some point it becomes healthy, Ribbon will add it back into the pool of available servers.

Wrapping Up

In this post, we looked at how Feign can be used to build simple, declarative HTTP clients. We also saw how Ribbon can be used alongside Feign to load balance calls across multiple instances and deal with failures via periodic health checks. If you’ve any questions or comments please leave a note below.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK