微服务系统通常会运行每个服务的多个实例。这是增强弹性所必需的。因此,在这些实例之间分配负载非常重要。执行此操作的组件是负载均衡器。Spring 提供了一个 Spring Cloud Load Balancer 库。在本文中,您将学习如何使用它在Spring Boot项目中实现客户端负载平衡。
客户端和服务器端负载平衡
当一个微服务调用另一个部署了多个实例的服务并在这些实例上分配负载而不依赖外部服务器来完成工作时,我们谈论的是客户端负载平衡。相反,在服务器端模式下,平衡功能被委托给单独的服务器,该服务器负责调度传入的请求。在本文中,我们将讨论一个基于客户端场景的示例。
负载平衡算法
有多种方法可以实现负载平衡。我们在此列出一些可能的算法:
- 循环:按循环方式依次选择实例(调用序列中的最后一个实例后,从第一个实例重新开始)。
- 随机选择:随机选择实例。
- 加权:根据某些数量(例如 CPU 或内存负载)为每个节点分配权重,从而做出选择。
- 相同实例:如果可用,则选择先前调用的相同实例。
Spring Cloud 为上述所有场景提供了易于配置的实现。
Spring Cloud 负载均衡器入门
假设您使用 Maven,将 Spring Cloud Load Balancer 集成到您的 Spring Boot 项目中,您应该首先在依赖管理部分中定义发布:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
spring-cloud-starter-loadbalancer
然后您应该包含依赖项列表中命名的启动器:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
...
</dependencies>
负载平衡配置
我们可以使用application.yaml文件配置我们的组件。@LoadBalancerClients
注释激活负载均衡器功能并通过参数定义配置类defaultConfiguration
。
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = BookClientConfiguration.class)
@LoadBalancerClients(defaultConfiguration = LoadBalancerConfiguration.class)
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}
配置类定义了一个类型的 bean ServiceInstanceListSupplier
,并允许我们设置要使用的特定平衡算法。在下面的示例中,我们使用该weighted
算法。该算法根据分配给每个节点的权重来选择服务。
@Configuration
public class LoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withWeighted()
.build(context);
}
}
测试客户端负载平衡
我们将展示一个使用两个简单微服务的示例,一个充当服务器,另一个充当客户端。我们将客户端想象为调用作者服务的图书馆应用程序的图书服务。我们将使用JUnit测试实现此演示。您可以在本文底部的链接中找到示例。
客户端将通过 OpenFeign 调用服务器。我们将使用 API 模拟工具Hoverfly实现一个模拟两个服务器实例上的调用的测试用例。该示例使用以下版本的 Java、Spring Boot 和Spring Cloud。
- Spring Boot:3.2.1
- Spring Cloud:2023.0.0
- Java 17
要在我们的 JUnit 测试中使用 Hoverfly,我们必须包含以下依赖项:
<dependencies>
<!-- Hoverfly -->
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我们将使用该算法在客户端应用程序中配置负载均衡器withSameInstancePreference
。这意味着如果可用,它将始终优先选择之前选择的实例。您可以使用如下配置类实现该行为:
@Configuration
public class LoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withSameInstancePreference()
.build(context);
}
}
我们希望独立于外部环境测试客户端组件。为此,我们通过将属性设置为 来禁用application.yaml文件中的发现服务器客户端功能。然后我们在端口 8091 和 8092 上静态定义两个作者服务实例:eureka.client.enabled
false
spring:
application:
name: book-service
cloud:
discovery:
client:
simple:
instances:
author-service:
- service-id: author-service
uri: http://author-service:8091
- service-id: author-service
uri: http://author-service:8092
eureka:
client:
enabled: false
我们用 注释我们的测试类,它将启动客户端的应用程序上下文。要使用application.yaml@SpringBootTest
文件中配置的端口,我们将参数设置为 的值。 我们还用 注释它,以将 Hoverfly 集成到运行环境中。 webEnvironmentSpringBootTest.WebEnvironment.DEFINED_PORT@ExtendWith(HoverflyExtension.class)
使用 Hoverfly 领域特定语言,我们模拟了服务器应用程序的两个实例,公开了端点/authors/getInstanceLB
。我们通过方法为两者设置了不同的延迟endDelay
。在客户端,我们定义一个/library/getAuthorServiceInstanceLB
端点,该端点通过负载均衡器转发调用并将其定向到 REST 服务的一个或另一个实例getInstanceLB
。
我们将在 for 循环中执行 10 次调用/library/getAuthorServiceInstanceLB
。由于我们为两个实例配置了非常不同的延迟,因此我们预计大多数调用都会落在延迟最小的服务上。我们可以在下面的代码中看到测试的实现:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@ExtendWith(HoverflyExtension.class)
class LoadBalancingTest {
private static Logger logger = LoggerFactory.getLogger(LoadBalancingTest.class);
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testLoadBalancing(Hoverfly hoverfly) {
hoverfly.simulate(dsl(
service("http://author-service:8091").andDelay(10000, TimeUnit.MILLISECONDS)
.forAll()
.get(startsWith("/authors/getInstanceLB"))
.willReturn(success("author-service:8091", "application/json")),
service("http://author-service:8092").andDelay(50, TimeUnit.MILLISECONDS)
.forAll()
.get(startsWith("/authors/getInstanceLB"))
.willReturn(success("author-service:8092", "application/json"))));
int a = 0, b = 0;
for (int i = 0; i < 10; i++) {
String result = restTemplate.getForObject("http://localhost:8080/library/getAuthorServiceInstanceLB", String.class);
if (result.contains("8091")) {
++a;
} else if (result.contains("8092")) {
++b;
}
logger.info("Result: ", result);
}
logger.info("Result: author-service:8091={}, author-service:8092={}", a, b);
}
}
如果我们运行测试,我们可以看到所有针对该实例的调用都有 20 毫秒的延迟。您可以通过设置两个延迟之间的较低范围来更改值,以查看结果如何变化。
结论
客户端负载平衡是微服务系统的重要组成部分。当为应用程序提供服务的一个或多个节点发生故障时,它可以保证系统的弹性。在本文中,我们展示了如何使用 Spring Cloud Load Balancer 来实现它。