微服务系统通常运行每个服务的多个实例。这是实施弹性所必需的。因此,在这些实例之间分配负载非常重要。执行此操作的组件是负载均衡器。Spring 提供了一个 Spring Cloud Load Balancer 库。在本文中,您将学习如何使用它在 Spring Boot 项目中实现客户端负载均衡。
客户端和服务器端负载均衡
当一个微服务调用另一个部署了多个实例的服务并在这些实例上分配负载而不依赖外部服务器来完成这项工作时,我们讨论了客户端负载均衡。相反,在服务器端模式下,平衡功能被委托给一个单独的服务器,该服务器分派传入的请求。在本文中,我们将讨论一个基于客户端场景的示例。
负载均衡算法
有几种方法可以实现负载平衡。我们在这里列出了一些可能的算法:
- Round robin:以循环方式依次选择实例(在调用序列中的最后一个实例后,我们从第一个实例重新开始)。
- Random choice:实例是随机选择的。
- Weighted:根据某个数量(例如 CPU 或内存负载)分配给每个节点的权重进行选择。
- Same instance:如果可用,则选择之前调用的相同实例。
Spring Cloud 为上述所有场景提供了易于配置的实现。
Spring Cloud Load Balancer 启动器
假设您使用 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>
然后,您应该在依赖项列表中包含 name:spring-cloud-starter-loadbalancer
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
...
</dependencies>
负载均衡配置
我们可以使用 application.yaml 文件配置我们的组件。该注释用于激活负载均衡器功能,并通过参数定义配置类。@LoadBalancerClientsdefaultConfiguration
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = BookClientConfiguration.class)
@LoadBalancerClients(defaultConfiguration = LoadBalancerConfiguration.class)
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}
配置类定义了一个 bean 类型,并允许我们设置要使用的特定平衡算法。在下面的示例中,我们使用算法。此算法根据分配给每个节点的权重来选择服务。ServiceInstanceListSupplierweighted
@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 启动:3.2.1
- 春云:2023.0.0
- 爪哇 17
要在 JUnit 测试中使用 Hoverfly,我们必须包含以下依赖项:
<dependencies>
<!-- Hoverfly -->
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我们将使用算法在客户端应用程序中配置负载均衡器。这意味着它将始终首选之前选择的实例(如果可用)。您可以使用如下所示的 configuration 类来实现该行为:withSameInstancePreference
@Configuration
public class LoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withSameInstancePreference()
.build(context);
}
}
我们希望独立于外部环境测试 Client 端组件。为此,我们通过将属性设置为 .然后,我们在端口 8091 和 8092 上静态定义两个作者服务实例:eureka.client.enabledfalse
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 文件中配置的端口,我们将参数设置为 的值 . 我们还用 进行注释,以将 Hoverfly 集成到运行环境中。@SpringBootTest
webEnvironment
SpringBootTest.WebEnvironment.DEFINED_PORT@ExtendWith(HoverflyExtension.class)
使用 Hoverfly 域特定语言,我们模拟了服务器应用程序的两个实例,公开了端点 .我们通过方法为两者设置不同的延迟。在客户端上,我们定义了一个终端节点,该终端节点通过负载均衡器转发调用并将其定向到 REST 服务的一个实例或另一个实例。/authors/getInstanceLB
endDelay
/library/getAuthorServiceInstanceLBgetInstanceLB
我们将在 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 毫秒的延迟。您可以通过在两个延迟之间设置较低的范围来更改值,以查看结果如何变化。