Load balancing is required to service requests in a manner that maximizes speed, capacity utilization, and it makes sure that no server is overloaded with requests. The load balancer also redirects requests to the remaining host servers if a server goes down. In microservice architecture, a microservice can serve internal or external requests. Based on this, we can have two types of load balancing – client-side and server-side load balancing.
Microservices need interprocess communication so that services can communicate with each other. Spring Cloud uses Netflix Ribbon, a client-side load balancer that plays this critical role and can handle both HTTP and TCP. Ribbon is cloud-enabled and provides built-in failure resiliency. Ribbon also allows you to use multiple and pluggable load balancing rules. It integrates clients with load balancers.
In the last chapter, we added Eureka Server. Ribbon is integrated with Eureka Server in Spring Cloud by default. This integration provides the following features:
application.yml
if required.ribbonServerList
with DiscoveryEnabledNIWSServerList
.DiscoveryEnabledNIWSServerList
interface is used in place of Ribbon's IPing
.There are different clients available in Spring Cloud that use Ribbon, such as RestTemplate or FeignClient. These clients allow microservices to communicate with each other. Clients use instance IDs in place of hostnames and ports for making an HTTP call to service instances when Eureka Server is used. The client passes the service ID to Ribbon, Ribbon then uses the load balancer to pick the instance from the Eureka Server.
If there are multiple instances of services available in Eureka, as shown in the following screenshot, Ribbon picks only one for the request, based on load balancing algorithms:
We can use DiscoveryClient
to find all the available service instances in Eureka Server, as shown in the following code. Method getLocalServiceInstance()
of class DiscoveryClientSample
returns the all local service instances available in Eureka Server.
DiscoveryClient sample:
@Component class DiscoveryClientSample implements CommandLineRunner { @Autowired private DiscoveryClient; @Override public void run(String... strings) throws Exception { //print the Discovery Client Description System.out.println(discoveryClient.description()); // Get restaurant-service instances and prints its info discoveryClient.getInstances("restaurant-service").forEach((ServiceInstance serviceInstance) -> { System.out.println(new StringBuilder("Instance --> ").append(serviceInstance.getServiceId()) .append(" Server: ").append(serviceInstance.getHost()).append(":").append(serviceInstance.getPort()) .append(" URI: ").append(serviceInstance.getUri()).append(" ")); }); } }
When executed, this code prints the following information. It shows two instances of the Restaurant service:
Spring Cloud Eureka Discovery Client Instance: RESTAURANT-SERVICE Server: SOUSHARM-IN:3402 URI: http://SOUSHARM-IN:3402 Instance --> RESTAURANT-SERVICE Server: SOUSHARM-IN:3368 URI: http://SOUSHARM-IN:3368
The following samples showcase how these clients can be used. You can see that in both clients, the service name restaurant-service
is used in place of a service hostname and port. These clients call /v1/restaurants
to get a list of restaurants containing the name given in the name query parameter:
Rest Template sample:
@Override public void run(String... strings) throws Exception { ResponseEntity<Collection<Restaurant>> exchange = this.restTemplate.exchange( "http://restaurant-service/v1/restaurants?name=o", HttpMethod.GET, null, new ParameterizedTypeReference<Collection<Restaurant>>() { }, ( "restaurants"); exchange.getBody().forEach((Restaurant restaurant) -> { System.out.println(new StringBuilder(" [ ").append(restaurant.getId()).append(" ").append(restaurant.getName()).append("]")); }); }
FeignClient sample:
@Component class FeignSample implements CommandLineRunner { @Autowired private RestaurantClient restaurantClient; @Override public void run(String... strings) throws Exception { this.restaurantClient.getRestaurants("o").forEach((Restaurant restaurant) -> { System.out.println(restaurant); }); } } @FeignClient("restaurant-service") interface RestaurantClient { @RequestMapping(method = RequestMethod.GET, value = "/v1/restaurants") Collection<Restaurant> getRestaurants(@RequestParam("name") String name); }
All preceding examples will print the following output:
[ 1 Big-O Restaurant] [ 2 O Restaurant]
After client-side load balancing, it is important for us to define server-side load balancing. In addition, from the microservice architecture's point of view, it is important to define the routing mechanism for our OTRS app. For example, /
may be mapped to our UI application, /restaurantapi
is mapped to restaurant service, and /userapi
is mapped to user service.
We'll use the Netflix Zuul Server as our Edge Server. Zuul is a JVM-based router and server-side load balancer. Zuul supports any JVM language for writing rules and filters and having the in-built support for Java and Groovy.
The external world (the UI and other clients) calls the Edge server, which uses the routes defined in application.yml
to call internal services and provide the response. Your guess is right if you think it acts as a proxy server, carries gateway responsibility for internal networks, and calls internal services for defined and configured routes.
Normally, it is recommended to have a single Edge Server for all requests. However, few companies use a single Edge Server per client to scale. For example, Netflix uses a dedicated Edge Server for each device type.
An Edge Server will also be used in the next chapter, when we configure and implement microservice security.
Configuring and using the Edge Server is pretty simple in Spring Cloud. You need to use the following steps:
pom.xml
:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency>
@EnableZuulProxy
annotation in your application class. It also internally uses @EnableDiscoveryClient
: therefore it is also registered to Eureka Server automatically. You can find the registered Zuul Server in the last figure: Multiple service registration – Restaurant service".application.yml
, as the following shows:zuul:ignoredServices
: This skips the automatic addition of services. We can define service ID patterns here. *
denotes that we are ignoring all services. In the following sample, all services are ignored except restaurant-service
.Zuul.routes
: This contains the path
attribute that defines the URI's pattern. Here, /restaurantapi
is mapped to Restaurant Service using serviceId
. serviceId
represents the service in Eureka Server. You can use a URL in place of a service, if Eureka Server is not used. We have also used the stripPrefix
attribute to strip the prefix (/restaurantapi
), and the resultant /restaurantapi/v1/restaurants/1
call converts to /v1/restaurants/1
while calling the service:application.yml
info:
component: Zuul Server
# Spring properties
spring:
application:
name: zuul-server # Service registers under this name
endpoints:
restart:
enabled: true
shutdown:
enabled: true
health:
sensitive: false
zuul:
ignoredServices: "*"
routes:
restaurantapi:
path: / restaurantapi/**
serviceId: restaurant-service
stripPrefix: true
server:
port: 8765
# Discovery Server Access
eureka:
instance:
leaseRenewalIntervalInSeconds: 3
metadataMap:
instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${random.value}}}
serviceUrl:
defaultZone: http://localhost:8761/eureka/
fetchRegistry: false
Let's see a working Edge Server. First, we'll call the restaurant service deployed on port 3402
, shown as follows:
Then, we'll call the same service using the Edge Server that is deployed on port 8765
. You can see that the /restaurantapi
prefix is used for calling /v1/restaurants?name=o
, and it gives the same result: