Tìm hiểu về cân bằng tải trong Spring Cloud với Ribbon và ví dụ

Xem thêm các chuyên mục:

1- Load Balancer là gì?

Hãy tưởng tượng rằng bạn có một hệ thống phân tán, nó gồm nhiều dịch vụ (ứng dụng) chạy trên nhiều máy tính khác nhau. Tuy nhiên khi số lượng người dùng lớn, một dịch vụ (ứng dụng) thường được tạo ra nhiều bản sao (replica), mỗi bản sao chạy trên một máy tính khác nhau. Lúc này xuất hiện "Load Balancer" (Bộ cân bằng tải), nó giúp phân phối lưu lượng đến (incomming traffic) đồng đều giữa các máy chủ.

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

Theo truyền thống các bộ cân bằng tải (Load Balancer) là các thành phần được đặt tại phía Server (Server Side). Khi các yêu cầu đến từ Client nó sẽ đi đến bộ cân bằng tải, và bộ cân bằng tải sẽ chỉ định một Server sẽ phục vụ yêu cầu này. Thuật toán đơn giản nhất mà bộ cân bằng tải sử dụng đó là chỉ định ngẫu nhiên. Hầu hết các bộ cân bằng tải trong trường hợp này là các phần cứng, tích hợp sẵn phần mềm điều khiển việc cân bằng tải (load balancing).

Bộ cân bằng tải phía Client (Client-Side Load Balancer):

Khi bộ cân bằng tải nằm phía Client (Client side) nó sẽ chủ động quyết định sẽ gửi yêu cầu tới máy chủ nào dựa trên một vài tiêu chí.
Thực sự các máy chủ có những khác biệt, chúng không giống nhau về các tiêu chí sau:
  1. Tính khả dụng (Availability): Không phải máy chủ nào cũng hoạt động trong mọi thời điểm.
  2. Hiệu xuất (Performance): Tốc độ của các máy chủ là khác nhau.
  3. Địa lý: Các máy chủ được đặt tại các vị trí khác nhau, chẳng hạn chúng được đặt tại các quốc gia khác nhau. Nó có thể gần với Client này nhưng xa so với Client khác.
Các bộ cân bằng tải phía Client thường gửi các yêu cầu tới các máy chủ cùng khu vực (Zone), hoặc có đáp ứng nhanh.

2- Netflix Ribbon

Ribbon là một phần trong gia đình Netflix Open Source Software (Netflix OSS). Nó là một thư viện cung cấp bộ cân bằng tải phía Client. Vì là một thành viên trong gia đình Netflix nên nó có thể tự động tương tác với Netflix Service Discovery (Eureka).
Spring Cloud đã tạo ra các API giúp bạn dễ dàng sử dụng các thư viện Ribbon.
OK, Chúng ta sẽ thảo luận về các khái niệm chính liên quan tới Ribbon:
  1. Danh sách các máy chủ.
  2. Danh sách đã được lọc các máy chủ.
  3. Bộ cân bằng tải (Load Balancer)
  4. Ping
-
  • Danh sách các máy chủ (List Of Servers):
Là một danh sách các máy chủ có thể đáp ứng một dịch vụ cụ thể cho 1 Client. Chẳng hạn 1 Client cần thông tin về thời tiết, sẽ có một danh sách các máy chủ có thể cung cấp thông tin này. Danh sách này bao gồm các máy chủ được cấu hình trực tiếp trong ứng dụng Client, và các máy chủ mà Client khám phá được.
  • Danh sách đã được lọc các máy chủ (Filtered List of Servers):
Chúng ta tiếp tục với ví dụ ở trên, 1 Client cần thông tin thời tiết, và có một danh sách các máy chủ có thể cung cấp thông tin đó. Tuy nhiên không phải tất cả các máy chủ đó đều hoạt động, hoặc máy chủ đó ở quá xa so với Client vì vậy nó đáp ứng rất chậm. Client sẽ loại bỏ các máy chủ này ra khỏi danh sách, và cuối cùng nó sẽ có một danh sách các máy chủ phù hợp hơn (Một danh sách đã được lọc).
  • Load Balancer (Ribbon):
Ribbon là một bộ cân bằng tải, nó là một thành phần đặt tại phía Client, nó quyết định máy chủ nào sẽ được gọi (Trong danh sách đã được lọc các máy chủ). 
Có một vài chiến lược (strategy) để đưa ra quyết định. Nhưng chúng thường dựa trên một "Rule Component" (Thành phần quy tắc) để tạo một quyết định thực sự. Theo mặc định Spring Cloud Ribbon sử dụng chiến lược ZoneAwareLoadBalancer (Các máy chủ cùng khu vực (zone) với Client).
Rule Component là một module thông minh nó tạo một quyết định "Gọi hoặc không gọi". Theo mặc định Spring Cloud sử dụng quy tắc ZoneAvoidanceRule.
  • Ping:
Ping là cách mà Client sử dụng để kiểm tra nhanh một máy chủ có hoạt động tại thời điểm đó hay không? Hành vi mặc định của Spring Cloud là ủy quyền cho Eureka tự động kiểm tra các thông tin này. Tuy nhiên Spring Cloud cho phép bạn tùy biến để kiểm tra theo cách của bạn.

3- Mục tiêu của bài học

Chú ý rằng bài học này có liên quan trực tiếp tới 2 bài học trước.
  1. Bài học (1): Chúng ta đã tạo một "Service Registration" (Eureka Server).
  2. Bài học (2): Chúng ta đã tạo một ứng dụng "ABC Service" nó là một Discovery Client (Eureka Client).
Bài học này chúng ta sẽ tạo một ứng dụng "XYZ Service", nó cũng là một Discovery Client (Eureka Client) và nó sẽ gọi đến "ABC Service". Trên ứng dụng "XYZ Service" chúng ta sẽ sử dụng một bộ cân bằng tải.
Để có thể test được ứng dụng này chúng ta cần chạy cả 3 ứng dụng. Trong đó ứng dụng "ABC Service" sẽ được tạo 5 bản sao (replica) (Mô phỏng nó đang được chạy trên 5 máy tính khác nhau). Khi Client (XYZ Service) gọi đến "ABC Service", bộ cân bằng tải sẽ quyết định bản sao nào của "ABC Service" sẽ được gọi.
OK!, Đúng là một công việc thú vị, và cũng khá dễ dàng.

4- Tạo dự án Spring Boot

Trên Eclipse tạo một dự án Spring Boot.
Nhập vào:
  • Name: SpringCloudLoadBalancerRibbon
  • Group: org.o7planning
  • Artifact: SpringCloudLoadBalancerRibbon
  • Description: Spring Cloud Load Balancer (Ribbon)
  • Package: org.o7planning.ribbon
  • Ứng dụng mà chúng ta đang tạo sẽ gọi tới "ABC Service", nó sẽ cần tới một Load Balancer (Bộ cân bằng tải), vì vậy chúng ta sẽ khai báo sử dụng thư viện Ribbon.
  • Ứng dụng này cũng cần phải khám phá ra các dịch vụ (ứng dụng) khác đang chạy trong hệ thống phân tán, vì vậy nó cần sử dụng thư viện Discovery, chúng ta sẽ sử dụng Eureka Discovery (Eureka Client).
OK, project đã được tạo ra.
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- Cấu hình 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- Chạy ứng dụng

Để test được đầy đủ ứng dụng này, bạn cần phải chạy 2 ứng dụng của 2 bài học trước ( "Service Registration""ABC Service")
Sau đó bạn có thể chạy ứng dụng này trực tiếp trên Eclipse. Và truy cập vào đường dẫn dưới đây để xem Eureka Monitor (Trình giám sát Eureka).

Test Load Balancer:

Xem thêm các chuyên mục: