Понимание балансировки нагрузки в Spring Cloud с лентой и примером

1- Что такое Load Balancer?

Представьте, что у вас есть распределенная система, которая имеет много услуг (приложений) работающих на разных компьютерах. Но если количество пользователей большое, услуга (приложение) обычно cоздает разные реплики (replica), каждая реплика работает на отдельном компьютере. В это время появляется  "Load Balancer" (Балансировка нагрузки), которая помогает распределять входящий траффик (incomming traffic) равно между серверами.

Bộ cần bằng tải phía Server (Server side Load Balancer)

Традиционно, Балансировка нагрузки (Load Balancer) это компоненты расположенные на стороне сервера (Server Side). Когда запросы поступают от Client они придут к балансировке нагрузки, и она определит один сервер для этого запроса. Самый простой алгоритм используемый балансировкой нагрузки это случайное распределение. Почти все балансировки нагрузки являются аппаратным обеспечением, интегрирующим программным обеспечением, которые контролируют балансировку нагрузки (load balancing).

Балансировка на стороне клиента (Client-Side Load Balancer):

Когда балансирвока нагрузки находится на стороне  Клиента (Client side) она сама решает к какому серверу отправить запрос, основываясь на некоторых критериях.
На самом деле сервера имеют разницу, они отличаются друг от друга по следющим критериям:
  1. Доступность (Availability): Не все сервера работают все время.
  2. Производительность (Performance): Скорость серверов разная.
  3. География: Сервера расположены в разных местах, например расположены в разных странах. Они могут быть рядом с одним Клиентом, но далеко от других.
Балансировка на клиентской стороне обыно отправляет запросы к серверам одной зоны (Zone), или имеет быстрый ответ.

2- Netflix Ribbon

Ribbon это часть семьи  Netflix Open Source Software (Netflix OSS). Является библиотекой предоставляющей балансировку на клиентской стороне. Так как он является частью семьи  Netflix то он может автоматически взаимодействовать с  Netflix Service Discovery (Eureka).
Spring Cloud создал  API, чтобы помочь вам легко использовать библиотеки  Ribbon.
OK, мы обсудим главные принципы связанные с  Ribbon:
  1. Список серверов.
  2. Фильтрованный список серверов.
  3. Балансировка нагрузки (Load Balancer)
  4. Ping
-
  • Список серверов​​​​​​​ (List Of Servers):
Список серверов, которые могут предоставить определенную услугу для 1 Client. Например 1 Client нужна информация про погоду, будет список серверов которые смогут предоставить данную информацию. Даннный список включает сервера напрямую конфигурированные в приложении  Client, и сервера которые Client нашел.
  • Фильтрованный список серверов (Filtered List of Servers):
Мы продолжаем пример выше, 1 Client нужна информация погоды и есть список серверов, которые могут предоставить эту информацию. Но не все эти сервера работают, или тот сервер слишком далеко от Client, из-за этого отвечает очень медленно. Client отбросит эти сервера из списка, и в конце будет список более подходящих серверов (Фильтрованнный список).
  • Load Balancer (Ribbon):
Ribbon это балансировка нагрузки, является компонентоом расположенным на стороне Client (клиента), он решает как сервер будет вызван (из списка фильтрованных серверов). 
Есть некоторые стратегии (strategy) для решения. Но они обычно основываются на  "Rule Component" (Компонент правил) чтобы создать настоящее решение. По умолчанию  Spring Cloud Ribbon использует стратегию  ZoneAwareLoadBalancer (Сервера в одной зоне (zone) с Client).
Rule Component это умный модуль, который создает решение  "Вызвать или не вызывать". По умолчанию  Spring Cloud использует правило  ZoneAvoidanceRule.
  • Ping:
Ping это способ, который  Client использует для быстрой проверки работает ли на тот момент сервер или нет? Поведение по умолчанию у Spring Cloud это уполномочить  Eureka автоматически проверять данную информацию. Но  Spring Cloud позволяет вам кастомизировать проверку по вашему.

3- Цель статьи

Заметьте, что данная статья напрямую связана с 2-мя предыдущими статьями.
  1. Статья (1): Мы создали "Service Registration" (Eureka Server).
  2. Статья (2): Мы создали приложение "ABC Service", являющееся Discovery Client (Eureka Client).
В данной статье мы создадим приложение  "XYZ Service", оно так же является  Discovery Client (Eureka Client) и вызовет  "ABC Service". На приложении  "XYZ Service" мы будет использовать балансировку нагрузки.
Чтобы протестировать данное приложение, нужно запустить 3 приложения. При этом, создается 5 реплик (replica) приложения   "ABC Service" (Симулирующих работу на 5 разных компьютерах). Когда  Client (XYZ Service) вызывает  "ABC Service", балансировка нагрузки решает какую реплику  "ABC Service" вызвать.
OK!, Довольно интересная и так же легкая работа.

4- Создать проект Spring Boot

На  Eclipse создать проект  Spring Boot.
Ввести:
  • Name: SpringCloudLoadBalancerRibbon
  • Group: org.o7planning
  • Artifact: SpringCloudLoadBalancerRibbon
  • Description: Spring Cloud Load Balancer (Ribbon)
  • Package: org.o7planning.ribbon
  • Приложение, которое мы создаем вызывает к "ABC Service", ему понадобится Load Balancer (Балансировка нагрузки), поэтому мы объявим использование библиотеки Ribbon.
  • Данное приложение должно найти другие услуги (приложения) работающие на распределенной системе, поэтому ему нужно использовать библиотеку Discovery, мы будем использовать Eureka Discovery (Eureka Client).
OK, проект создан.
pom.xml
<?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>org.o7planning</groupId>
    <artifactId>SpringCloudLoadBalancerRibbon</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringCloudLoadBalancerRibbon</name>
    <description>Spring Cloud Load Balancer (Ribbon)</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.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>Edgware.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <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>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</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>

5- Конфигурация Ribbon

application.yml
spring:
  application:
    name: XYZ-SERVICE
 
server:
  port: 5555

# -- Configure for Ribbon:
 
ping-server:
  ribbon:
    eureka:
      enabled: false # Disable Default Ping
    listOfServers: localhost:8000,localhost:8001,localhost:8002,,localhost:8003
    ServerListRefreshInterval: 15000
    
# -- Configure Discovery Client (Eureka Client).    
# Configure this application to known "Service Registration".

eureka:
  instance:
    appname: XYZ-SERVICE  # ==> This is an instance of XYZ-SERVICE
  client:    
    fetchRegistry: true
    serviceUrl:
#      defaultZone: http://my-eureka-server.com:9000/eureka
      defaultZone: http://my-eureka-server-us.com:9001/eureka
#      defaultZone: http://my-eureka-server-fr.com:9002/eureka
#      defaultZone: http://my-eureka-server-vn.com:9003/eureka   
SpringCloudLoadBalancerRibbonApplication.java
package org.o7planning.ribbon;

import org.o7planning.ribbon.config.RibbonConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@RibbonClient(name = "ping-a-server", configuration = RibbonConfiguration.class)
@EnableEurekaClient
@SpringBootApplication
public class SpringCloudLoadBalancerRibbonApplication {

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

}
RibbonConfiguration.java
package org.o7planning.ribbon.config;

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.WeightedResponseTimeRule;

 
public class RibbonConfiguration {
    
    @Autowired
    private IClientConfig ribbonClientConfig;
 
    @Bean
    public IPing ribbonPing(IClientConfig config) {
        return new PingUrl();
    }
 
    @Bean
    public IRule ribbonRule(IClientConfig config) {
        return new WeightedResponseTimeRule();
    }
    
}

6- Controller

Example1Controller.java
package org.o7planning.ribbon.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class Example1Controller {

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private LoadBalancerClient loadBalancer;

    @ResponseBody
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home() {

        return "<a href='testCallAbcService'>/testCallAbcService</a>";
    }

    @ResponseBody
    @RequestMapping(value = "/testCallAbcService", method = RequestMethod.GET)
    public String showFirstService() {

        String serviceId = "ABC-SERVICE".toLowerCase();

        // (Need!!) eureka.client.fetchRegistry=true
        List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);

        if (instances == null || instances.isEmpty()) {
            return "No instances for service: " + serviceId;
        }
        String html = "<h2>Instances for Service Id: " + serviceId + "</h2>";

        for (ServiceInstance serviceInstance : instances) {
            html += "<h3>Instance :" + serviceInstance.getUri() + "</h3>";
        }

        // Create a RestTemplate.
        RestTemplate restTemplate = new RestTemplate();

        html += "<br><h4>Call /hello of service: " + serviceId + "</h4>";

        try {
            // May be throw IllegalStateException (No instances available)
            ServiceInstance serviceInstance = this.loadBalancer.choose(serviceId);

            html += "<br>===> Load Balancer choose: " + serviceInstance.getUri();

            String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/hello";

            html += "<br>Make a Call: " + url;
            html += "<br>";

            String result = restTemplate.getForObject(url, String.class);

            html += "<br>Result: " + result;
        } catch (IllegalStateException e) {
            html += "<br>loadBalancer.choose ERROR: " + e.getMessage();
            e.printStackTrace();
        } catch (Exception e) {
            html += "<br>Other ERROR: " + e.getMessage();
            e.printStackTrace();
        }
        return html;
    }

}

7- Запуск приложения

Для того, чтобы полностью протестировать данное приложение, вам нужно запустить 2 приложения с предыдущей статьи ( "Service Registration" и "ABC Service")
Потом вы можете запустить приложение напрямую на Eclipse и пройти по следующим ссылкам чтобы посмотреть  Eureka Monitor ( Eureka монитор).

Test Load Balancer: