使用 Spring Cloud LoadBalancer 進行客戶端負載均衡

本指南將引導您完成建立負載均衡微服務的流程。

您將構建什麼

您將構建一個微服務應用,該應用使用 Spring Cloud LoadBalancer 在呼叫另一個微服務時提供客戶端負載均衡。

您將需要什麼

  • 約 15 分鐘

  • 您喜歡的文字編輯器或 IDE

  • JDK 1.8 或更高版本

  • Gradle 6+ 或 Maven 3.5+

  • 您也可以直接將程式碼匯入到您的 IDE 中

  • Spring Tool Suite (STS) 或 IntelliJ IDEA

建立一個根專案

本指南將逐步構建兩個專案,其中一個專案是另一個專案的依賴項。因此,您需要在根專案下建立兩個子專案。首先,在頂層建立構建配置。對於 Maven,您需要一個包含列出子目錄的 <modules> 標籤的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-spring-cloud-loadbalancer</artifactId>
    <version>0.1.0</version>
    <packaging>pom</packaging>

    <modules>
      <module>say-hello</module>
      <module>user</module>
    </modules>
</project>

對於 Gradle,您需要一個包含相同目錄的 settings.gradle 檔案

rootProject.name = 'gs-spring-cloud-loadbalancer'

include 'say-hello'
include 'user'

(可選)您可以包含一個空的 build.gradle 檔案(以幫助 IDE 識別根目錄)。

建立目錄結構

在您想要作為根目錄的目錄中,建立以下子目錄結構(例如,在 *nix 系統上使用 mkdir say-hello user 命令)

└── say-hello
└── user

在專案的根目錄中,您需要設定一個構建系統,本指南將向您展示如何使用 Maven 或 Gradle。

從 Spring Initializr 開始

如果您為 Say Hello 專案使用 Maven,請訪問 Spring Initializr 生成一個包含所需依賴項 (Spring Web) 的新專案。

以下列表顯示了選擇 Maven 時建立的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-loadbalancer-say-hello</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-cloud-loadbalancer-say-hello</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<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>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

如果您為 Say Hello 專案使用 Gradle,請訪問 Spring Initializr 生成一個包含所需依賴項 (Spring Web) 的新專案。

以下列表顯示了選擇 Gradle 時建立的 build.gradle 檔案

plugins {
	id 'org.springframework.boot' version '3.2.0'
	id 'io.spring.dependency-management' version '1.1.4'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

bootJar {
	enabled = false
}

如果您為 User 專案使用 Maven,請訪問 Spring Initializr 生成一個包含所需依賴項 (Cloud Loadbalancer 和 Spring Reactive Web) 的新專案。

以下列表顯示了選擇 Maven 時建立的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-loadbalancer-user</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-cloud-loadbalancer-user</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
		<java.version>17</java.version>
		<spring-cloud.version>2023.0.0</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-loadbalancer</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-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>
	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
		</repository>
	</repositories>

</project>

如果您為 User 專案使用 Gradle,請訪問 Spring Initializr 生成一個包含所需依賴項 (Cloud Loadbalancer 和 Spring Reactive Web) 的新專案。

以下列表顯示了選擇 Gradle 時建立的 build.gradle 檔案

plugins {
	id 'org.springframework.boot' version '3.2.0'
	id 'io.spring.dependency-management' version '1.1.4'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/milestone' }
}

ext {
	set('springCloudVersion', "2023.0.0")
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
}

dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}

test {
	useJUnitPlatform()
}

bootJar {
	enabled = false
}

手動初始化 (可選)

如果您想手動初始化專案而不是使用前面顯示的連結,請按照以下步驟操作

  1. 導航到 https://start.spring.io。此服務會獲取應用程式所需的所有依賴項,併為您完成大部分設定工作。

  2. 選擇 Gradle 或 Maven 以及您想要使用的語言。本指南假設您選擇了 Java。

  3. 點選 Dependencies 並選擇 Spring Web (用於 Say Hello 專案) 或 Cloud LoadbalancerSpring Reactive Web (用於 User 專案)。

  4. 點選 Generate

  5. 下載生成的 ZIP 檔案,該檔案是根據您的選擇配置好的 Web 應用程式的壓縮包。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此過程。

實現“Say Hello”服務

我們的“伺服器”服務名為 Say Hello。它從 /greeting 路徑下的端點返回一個隨機問候語(從三個固定列表中選取一個)。

src/main/java/hello 目錄中,建立檔案 SayHelloApplication.java

以下列表顯示了 say-hello/src/main/java/hello/SayHelloApplication.java 檔案的內容

package hello;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SayHelloApplication {

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

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

  @GetMapping("/greeting")
  public String greet() {
  log.info("Access /greeting");

  List<String> greetings = Arrays.asList("Hi there", "Greetings", "Salutations");
  Random rand = new Random();

  int randomNum = rand.nextInt(greetings.size());
  return greetings.get(randomNum);
  }

  @GetMapping("/")
  public String home() {
  log.info("Access /");
  return "Hi!";
  }
}

它是一個簡單的 @RestController,其中我們有一個用於 /greeting@RequestMapping 方法和另一個用於根路徑 / 的方法。

我們將與客戶端服務應用程式一起在本地執行此應用程式的多個例項。要開始,請執行以下步驟

  1. 建立 src/main/resources 目錄。

  2. 在該目錄中建立 application.yml 檔案。

  3. 在該檔案中,為 server.port 設定預設值。

(我們將指示應用程式的其他例項在其他埠上執行,以便在客戶端執行時,任何 Say Hello 例項都不會與客戶端衝突)。在此檔案中,我們還可以為我們的服務設定 spring.application.name

以下列表顯示了 say-hello/src/main/resources/application.yml 檔案的內容

spring:
  application:
    name: say-hello

server:
  port: 8090

從客戶端服務訪問

使用者看到的是 User 應用程式。它呼叫 Say Hello 應用程式獲取問候語,然後在使用者訪問 /hi/hello 路徑下的端點時將問候語傳送給使用者。

在 User 應用程式目錄下,在 src/main/java/hello 目錄中,新增 UserApplication.java 檔案

以下列表顯示了 user/src/main/java/hello/UserApplication.java 檔案的內容

package hello;

import reactor.core.publisher.Mono;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;

/**
 * @author Olga Maciaszek-Sharma
 */
@SpringBootApplication
@RestController
public class UserApplication {

  private final WebClient.Builder loadBalancedWebClientBuilder;
  private final ReactorLoadBalancerExchangeFilterFunction lbFunction;

  public UserApplication(WebClient.Builder webClientBuilder,
      ReactorLoadBalancerExchangeFilterFunction lbFunction) {
    this.loadBalancedWebClientBuilder = webClientBuilder;
    this.lbFunction = lbFunction;
  }

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

  @RequestMapping("/hi")
  public Mono<String> hi(@RequestParam(value = "name", defaultValue = "Mary") String name) {
    return loadBalancedWebClientBuilder.build().get().uri("http://say-hello/greeting")
        .retrieve().bodyToMono(String.class)
        .map(greeting -> String.format("%s, %s!", greeting, name));
  }

  @RequestMapping("/hello")
  public Mono<String> hello(@RequestParam(value = "name", defaultValue = "John") String name) {
    return WebClient.builder()
        .filter(lbFunction)
        .build().get().uri("http://say-hello/greeting")
        .retrieve().bodyToMono(String.class)
        .map(greeting -> String.format("%s, %s!", greeting, name));
  }
}

我們還需要一個 @Configuration 類來設定一個負載均衡的 WebClient.Builder 例項

以下列表顯示了 user/src/main/java/hello/WebClientConfig.java 檔案的內容

package hello;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)
public class WebClientConfig {

  @LoadBalanced
  @Bean
  WebClient.Builder webClientBuilder() {
    return WebClient.builder();
  }

}

該配置提供了一個 @LoadBalanced WebClient.Builder 例項,當用戶訪問 UserApplication.javahi 端點時,我們將使用它。一旦訪問了 hi 端點,我們使用此構建器建立一個 WebClient 例項,該例項向 Say Hello 服務的 URL 傳送 HTTP GET 請求,並將結果作為 String 返回。

UserApplication.java 中,我們還添加了一個 /hello 端點,執行相同的操作。然而,我們不是使用 @LoadBalanced 註解,而是使用一個 @Autowired 負載均衡交換過濾器函式 (lbFunction),我們透過使用 filter() 方法將其傳遞給一個我們以程式設計方式構建的 WebClient 例項。

儘管我們為這兩個端點設定負載均衡的 WebClient 例項的方式略有不同,但兩者的最終行為完全相同。Spring Cloud LoadBalancer 用於選擇 Say Hello 服務的合適例項。

spring.application.nameserver.port 屬性新增到 src/main/resources/application.propertiessrc/main/resources/application.yml 檔案中

以下列表顯示了 user/src/main/resources/application.yml 檔案的內容

spring:
  application:
    name: user

server:
  port: 8888

跨伺服器例項進行負載均衡

現在我們可以訪問 User 服務上的 /hihello,並看到友好的問候語

$ curl https://:8888/hi
Greetings, Mary!

$ curl https://:8888/hi?name=Orontes
Salutations, Orontes!

WebClientConfig.java 檔案中,我們使用 @LoadBalancerClient 註解傳遞 LoadBalancer 的自定義配置

@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)

這意味著,每當聯絡名為 say-hello 的服務時,Spring Cloud LoadBalancer 將使用 SayHelloConfiguration.java 中提供的配置,而不是使用預設設定。

以下列表顯示了 user/src/main/java/hello/SayHelloConfiguration.java 檔案的內容

package hello;

import java.util.Arrays;
import java.util.List;

import reactor.core.publisher.Flux;

import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

/**
 * @author Olga Maciaszek-Sharma
 */
public class SayHelloConfiguration {

  @Bean
  @Primary
  ServiceInstanceListSupplier serviceInstanceListSupplier() {
    return new DemoServiceInstanceListSuppler("say-hello");
  }

}

class DemoServiceInstanceListSuppler implements ServiceInstanceListSupplier {

  private final String serviceId;

  DemoServiceInstanceListSuppler(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", 8090, false),
            new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 9092, false),
            new DefaultServiceInstance(serviceId + "3", serviceId, "localhost", 9999, false)));
  }
}

在該類中,我們提供了一個自定義的 ServiceInstanceListSupplier,其中包含三個硬編碼的例項,Spring Cloud LoadBalancer 在呼叫 Say Hello 服務時會從中進行選擇。

新增此步驟是為了解釋如何將自己的自定義配置傳遞給 Spring Cloud LoadBalancer。但是,您不必使用 @LoadBalancerClient 註解併為 LoadBalancer 建立自己的配置。最典型的方式是結合服務發現使用 Spring Cloud LoadBalancer。如果您的類路徑中有任何 DiscoveryClient,預設的 Spring Cloud LoadBalancer 配置會使用它來檢查服務例項。因此,您只從正在執行的例項中進行選擇。您可以檢視此指南,瞭解如何使用 ServiceDiscovery

我們還添加了一個包含預設 server.portspring.application.nameapplication.yml 檔案。

以下列表顯示了 user/src/main/resources/application.yml 檔案的內容

spring:
  application:
    name: user

server:
  port: 8888

測試負載均衡器

以下列表顯示瞭如何使用 Gradle 執行 Say Hello 服務

$ ./gradlew bootRun

以下列表顯示瞭如何使用 Maven 執行 Say Hello 服務

$ mvn spring-boot:run

要實現負載均衡,您需要運行同一應用程式的兩個獨立的伺服器例項。您可以透過在不同埠上執行 Say Hello 服務的第二個例項來實現。在本例中,我們使用埠 9999。

要使用 Gradle 執行此操作,請開啟新的終端並執行以下命令

export SERVER_PORT=9092
./gradlew bootRun

要使用 Maven 執行此操作,請開啟新的終端並執行以下命令

export SERVER_PORT=9999
mvn spring-boot:run

然後您可以啟動 User 服務。此時,您應該有三個終端:兩個用於兩個 Say Hello 例項,一個用於 User。然後您可以訪問 localhost:8888/hi 並觀察 Say Hello 服務例項。

您對 User 服務的請求應該會以輪詢的方式分散到正在執行的 Say Hello 例項中進行呼叫

2016-03-09 21:15:28.915  INFO 90046 --- [nio-8090-exec-7] hello.SayHelloApplication                : Access /greeting

總結

恭喜!您剛剛開發了一個 Spring Loadbalancer 應用程式!

想要撰寫新的指南或為現有指南做貢獻?請檢視我們的貢獻指南

所有指南的程式碼均遵循 ASLv2 許可釋出,文字內容遵循 署名-禁止演繹知識共享許可 釋出。