Service Integration With Netflix Feign and Ribbon
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.
Join the DZone community and get the full member experience.
Join For FreeThe 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 extensively 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
and @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 work through the following steps
- 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.
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 are covered later.
Creating the Bank Account Service
The purpose of this service is to allow 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);
}
}
This is standard Spring Boot setup with the addition of @EnableFeignClients("com.briansjavablog.microservices.client")
This is a component scanner that specifically looks for interfaces annotated with @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);
}
}
The AccountIdentifierServiceClient
interface injected on line 6 is used to call the AccountIdentifier service. We'll look at the AccountIdentifierServiceClient
interface in detail later. On line 17 the AccountIdentifier
entity returned from the AccountIdentifierService
is passed back to the client.
Creating the Feign Client Interface
The next step is to create the AccountIdentifierServiceClient
interface that was injected into the BankAccountController
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);
}
As part of component scanning, Spring will look for interfaces annotated with @FeignClient
and using the configuration in @FeignConfiguration.class
, create a REST client implementation. This REST client will be injected anywhere the AccountIdentifierServiceClient
is Auto Wired, so in this app, it'll be injected into the BankAccountController
we created earlier.
We added a single interface method and annotate it with @GetMapping(path = "account-identifier/accountType/{accountType}")
. The @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 an HTTP GET request to account-identifier/accountType/
with a path variable for accountType.
Configuring Feign
In the @FeignClient
annotation above we referenced FeignConfiguration.class
. We'll now define that class to configure the behavior 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
annotation, as we already referenced it directly from the @FeignClient
annotation in the AccountIdentifierServieClient
.
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;
}
The first configuration Bean we configure is Logger.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);
}
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.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 RequestInterceptor
bean. The sample RequestInterceptor
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");
}
};
}
Basic Auth
Feign provides the BasicAuthRequestInterceptor
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 BasicAuthRequestInterceptor
just supply the username and password.
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
Retries
The final piece of Feign configuration, we'll look at is the Retryer
. 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);
}
Configuring the default Retryer
is probably good enough for most uses cases, but if you want to roll your own, you can implement the Retryer
interface.
Configuring Ribbon
You can configure Ribbon via the native Netflix API, but the easiest way is to use the @RibbonClient
annotation provided by Spring Cloud. Similar to the @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();
}
The IRule
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;
}
The IPing
bean defines how we'll ping service instances to check their health. Here I create a PingUrl
and supply the path to the health check endpoint. Ribbon will send an HTTP request to http://my-server:port/actuator/health. An 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>
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));
}
};
}
The getUpdatedListOfServers
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
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 AccountType
parameter and returns an AccountIdentifier
. The AccountIdentifier
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 AccountIdentifierController
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);
}
}
Earlier we defined a PingUrl
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();
}
}
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-ribbon
Now build the Bank Account Service Jar.
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
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
and the second instance by running java -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=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 an 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
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.
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 we look at the Account Identifier Service log for the instance on port 8091 we should see the request being processed.
{"accountNumber":"C19637","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.
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)]
If we look at the Account Identifier Service log for the instance on port 8092 we should see the request being processed.
{"accountNumber":"C18492","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 an IPing
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.
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.
Published at DZone with permission of Brian Hannaway, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments