1. 序章

マイクロサービスアーキテクチャの人気が高まるにつれ、さまざまなサーバーに分散された複数のサービスを実行することが一般的になりつつあります。 このクイックチュートリアルでは、 Spring Cloudロードバランサーを使用して、よりフォールトトレラントなアプリケーションを作成する方法について説明します。

2. 負荷分散とは何ですか?

負荷分散は、同じアプリケーションの異なるインスタンス間でトラフィックを分散するプロセスです。

フォールトトレラントシステムを作成するには、各アプリケーションの複数のインスタンスを実行するのが一般的です。 したがって、あるサービスが別のサービスと通信する必要がある場合は常に、特定のインスタンスを選択して要求を送信する必要があります。

負荷分散に関しては、多くのアルゴリズムがあります。

  • ランダム選択:インスタンスをランダムに選択
  • ラウンドロビン:毎回同じ順序でインスタンスを選択する
  • 最小の接続:現在の接続が最も少ないインスタンスを選択する
  • 加重メトリック:加重メトリックを使用して最適なインスタンスを選択します(たとえば、CPUまたはメモリの使用量)
  • IPハッシュ:クライアントIPのハッシュを使用してインスタンスにマップする

これらは負荷分散アルゴリズムのほんの一例であり、それぞれに長所と短所があります

ランダム選択とラウンドロビンは実装が簡単ですが、サービスを最適に使用できない場合があります。 逆に、最小の接続と重み付けされたメトリックはより複雑ですが、一般的に、より最適なサービス使用率を作成します。 また、サーバーの粘着性が重要な場合、IPハッシュは優れていますが、フォールトトレラントではありません。

3. Spring CloudLoadBalancerの概要

Spring Cloud Load Balancerライブラリを使用すると、他のアプリケーションと負荷分散された方法で通信するアプリケーションを作成できます。 必要なアルゴリズムを使用して、リモートサービス呼び出しを行うときに負荷分散を簡単に実装できます。

説明のために、いくつかのサンプルコードを見てみましょう。 簡単なサーバーアプリケーションから始めましょう。 サーバーには単一のHTTPエンドポイントがあり、複数のインスタンスとして実行できます。

次に、Spring Cloudロードバランサーを使用してサーバーの異なるインスタンス間でリクエストを交互に行うクライアントアプリケーションを作成します。

3.1. サーバーの例

サンプルサーバーでは、単純なSpringBootアプリケーションから始めます。

@SpringBootApplication
@RestController
public class ServerApplication {

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

    @Value("${server.instance.id}")
    String instanceId;

    @GetMapping("/hello")
    public String hello() {
        return String.format("Hello from instance %s", instanceId);
    }
}

まず、 instanceId。という名前の構成可能な変数を挿入します。これにより、実行中の複数のインスタンスを区別できます。 次に、メッセージとインスタンスIDをエコーバックする単一のHTTPGETエンドポイントを追加します。

デフォルトのインスタンスは、IDが1のポート8080で実行されます。 2番目のインスタンスを実行するには、いくつかのプログラム引数を追加する必要があります

--server.instance.id=2 --server.port=8081

3.2. クライアントの例

それでは、クライアントコードを見てみましょう。 ここでSpring Cloudロードバランサーを使用するので、アプリケーションに含めることから始めましょう。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

次に、ServiceInstanceListSupplierの実装を作成します。 これは、Spring Cloud LoadBalancerの主要なインターフェースの1つです。 利用可能なサービスインスタンスを見つける方法を定義します。

サンプルアプリケーションでは、サンプルサーバーの2つの異なるインスタンスをハードコーディングします。 それらは同じマシンで実行されますが、異なるポートを使用します。

class DemoInstanceSupplier implements ServiceInstanceListSupplier {
    private final String serviceId;

    public DemoInstanceSupplier(String serviceId) {
        this.serviceId = serviceId;
    }

    @Override
    public String getServiceId() {
        return serviceId;
    }

    @Override
        public Flux<List<ServiceInstance>> get() {
          return Flux.just(Arrays
            .asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8080, false),
              new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 8081, false)));
    }
}

実際のシステムでは、サービスアドレスをハードコーディングしない実装を使用する必要があります。 これについては後でもう少し見ていきます。

それでは、LoadBalancerConfigurationクラスを作成しましょう。

@Configuration
@LoadBalancerClient(name = "example-service", configuration = DemoServerInstanceConfiguration.class)
class WebClientConfig {
    @LoadBalanced
    @Bean
    WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

このクラスには1つの役割があります。負荷分散されたWebClientビルダーを作成してリモートリクエストを作成します。 アノテーションがサービスに疑似名を使用していることに注意してください。

これは、インスタンスを実行するための実際のホスト名とポートが事前にわからない可能性があるためです。 そのため、プレースホルダーとして疑似名を使用し、フレームワークは実行中のインスタンスを選択するときに実際の値に置き換えます。

次に、サービスインスタンスサプライヤをインスタンス化するConfigurationクラスを作成しましょう。 上記と同じ疑似名を使用していることに注意してください。

@Configuration
class DemoServerInstanceConfiguration {
    @Bean
    ServiceInstanceListSupplier serviceInstanceListSupplier() {
        return new DemoInstanceSupplier("example-service");
    }
}

これで、実際のクライアントアプリケーションを作成できます。 上からWebClient beanを使用して、サンプルサーバーに10個のリクエストを送信してみましょう。

@SpringBootApplication
public class ClientApplication {

    public static void main(String[] args) {

        ConfigurableApplicationContext ctx = new SpringApplicationBuilder(ClientApplication.class)
          .web(WebApplicationType.NONE)
          .run(args);

        WebClient loadBalancedClient = ctx.getBean(WebClient.Builder.class).build();

        for(int i = 1; i <= 10; i++) {
            String response =
              loadBalancedClient.get().uri("http://example-service/hello")
                .retrieve().toEntity(String.class)
                .block().getBody();
            System.out.println(response);
        }
    }
}

出力を見ると、2つの異なるインスタンス間で負荷分散されていることが確認できます。

Hello from instance 2
Hello from instance 1
Hello from instance 2
Hello from instance 1
Hello from instance 2
Hello from instance 1
Hello from instance 2
Hello from instance 1
Hello from instance 2
Hello from instance 1

4. その他の機能

サンプルサーバーとクライアントは、Spring Cloud LoadBalancerの非常に簡単な使用法を示しています。 しかし、他のライブラリ機能は言及する価値があります。

手始めに、サンプルクライアントはデフォルトのRoundRobinLoadBalancerポリシーを使用しました。 ライブラリは、RandomLoadBalancerクラスも提供します。 また、任意のアルゴリズムを使用して、ReactorServiceInstanceLoadBalancerの独自の実装を作成することもできます。

さらに、ライブラリはサービスインスタンスを動的に検出する方法を提供します。 これは、DiscoveryClientServiceInstanceListSupplierインターフェイスを使用して行います。 これは、EurekaZookeeperなどのサービス検出システムと統合する場合に便利です。

さまざまな負荷分散およびサービス検出機能に加えて、ライブラリは基本的な再試行機能も提供します。 内部的には、最終的にはSpringRetryライブラリに依存しています。 これにより、失敗したリクエストを再試行できます。待機期間が経過した後、同じインスタンスを使用する可能性があります。

もう1つの組み込み機能は、Micrometerライブラリの上に構築されたメトリックです。 箱から出して、各インスタンスの基本的なサービスレベルのメトリックを取得しますが、独自のメトリックを追加することもできます。

最後に、Spring Cloud Load Balancerライブラリは、LoadBalancerCacheManagerインターフェイスを使用してサービスインスタンスをキャッシュする方法を提供します。 実際には、利用可能なサービスインスタンスの検索にはリモートコールが含まれる可能性があるため、これは重要です。 これは、頻繁に変更されないデータを検索するのにコストがかかる可能性があることを意味し、アプリケーションで発生する可能性のある障害点を表します。 サービスインスタンスのキャッシュを使用することで、アプリケーションはこれらの欠点のいくつかを回避できます。

5. 結論

負荷分散は、最新のフォールトトレラントシステムを構築する上で不可欠な部分です。 Spring Cloud Load Balancerを使用すると、さまざまな負荷分散技術を使用してさまざまなサービスインスタンスにリクエストを分散するアプリケーションを簡単に作成できます

そしてもちろん、ここにあるすべてのサンプルコードは、GitHubにあります。