Spring Cloud Ribbon によるクライアントサイドロードバランシングの実装手法

Ribbon と Eureka の連携概要

Ribbon は Netflix が公開したクライアントサイドのロードバランシングライブラリであり、HTTP や TCP クライアントの通信制御を支援します。サービスプロバイダーのアドレスを設定することで、定義されたアルゴリズムに基づき、サービスコンシューマーが自動的に適切なインスタンスへリクエストを送信できるようになります。標準ではラウンドロビンやランダムなどのアルゴリズムが用意されていますが、独自の戦略を実装することも可能です。

Spring Cloud 環境において、Ribbon は Eureka と連携することで、Eureka Server から動的にサービスインスタンスのリストを取得し、ロードバランシングアルゴリズムに従って特定のインスタンスへ通信を行います。

ロードバランシングコンシューマーの構築

サービス登録・発見機能を持つ Eureka サーバーを前提として、新たに Ribbon を利用したコンシューマーモジュール(client-loadbalancer)を作成します。Maven プロジェクトの構成は以下の通りです。

<?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>com.example.cloud</groupId>
    <artifactId>client-loadbalancer</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>client-loadbalancer</name>
    <description>Spring Cloud Ribbon Consumer Demo</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

ロードバランシング対応 RestTemplate の設定

起動クラスにおいて、@LoadBalanced アノテーションを付与した RestTemplate ビーンを定義します。これにより、HTTP リクエスト時に Ribbon による負荷分散が自動で適用されます。

package com.example.cloud.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class ClientApplication {

    @Bean
    @LoadBalanced
    public RestTemplate balancedRestTemplate() {
        return new RestTemplate();
    }

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

サービス呼び出しコントローラーの実装

外部 API を呼び出すためのコントローラーを作成します。サービス名(Eureka に登録されたアプリケーション名)を URL のホスト部分に指定することで、Ribbon が適切なインスタンスへルーティングします。

package com.example.cloud.consumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class ProxyController {

    @Autowired
    private RestTemplate balancedRestTemplate;

    @GetMapping("/call-service")
    public String invokeRemoteService() {
        return balancedRestTemplate.getForObject(
            "http://service-provider/api/info",
            String.class
        );
    }
}

設定ファイルの準備

コンシューマーアプリケーションが Eureka サーバーに登録され、サービスプロバイダーを検出できるよう application.properties を設定します。

spring.application.name=client-loadbalancer
server.port=9001
eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka/

サービスプロバイダー側の修正

負荷分散の動作を確認するため、サービスプロバイダー(service-provider)側で現在のポート番号やインスタンス情報を返すエンドポイントを用意します。

package com.example.cloud.provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class InstanceInfoController {

    private static final Logger log = LoggerFactory.getLogger(InstanceInfoController.class);

    @Autowired
    private DiscoveryClient discoveryClient;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/api/info")
    public String getServiceInfo() {
        StringBuilder response = new StringBuilder();
        response.append("Hello from port: ").append(serverPort).append("<br>");

        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
        response.append("Total instances: ").append(instances.size()).append("<br>");

        for (ServiceInstance instance : instances) {
            response.append("Host: ").append(instance.getHost());
            response.append(", Port: ").append(instance.getPort());
            response.append(", URI: ").append(instance.getUri()).append("<br>");
        }
        return response.toString();
    }
}

複数インスタンスの起動設定

ロードバランシングを検証するため、同一のサービスプロバイダーアプリケーションを異なるポートで複数起動します。プロパティファイルを分けて設定します。

application-node1.properties:

spring.application.name=service-provider
server.port=8081
eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka/

application-node2.properties:

spring.application.name=service-provider
server.port=8082
eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka/

IDE の起動設定において、それぞれのプロパティファイルをアクティブなプロファイルとして指定し、Eureka サーバー起動後に 2 つのプロバイダーインスタンスを起動します。Eureka ダッシュボード(http://localhost:8080/)にて、service-provider が 2 つのインスタンスとして登録されていることを確認します。

負荷分散動作の確認

コンシューマーアプリケーション(client-loadbalancer)を起動し、http://localhost:9001/call-service にアクセスします。ブラウザでページを複数回リフレッシュすると、レスポンス内に含まれるポート番号が 8081 と 8082 で交互、またはランダムに変化します。これにより、Ribbon が複数のサービスインスタンスに対して負荷分散を行っていることが確認できます。

タグ: Spring-Cloud Netflix-Ribbon Eureka load-balancing RestTemplate

6月15日 16:45 投稿