LoadBalancer 入门教程 - 基础篇

大纲

前言

Ribbon 停更

Netflix Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项,如连接超时、失败重试等。简单的说,就是在配置文件中列出 Load Balancer(简称 LB)后面所有的机器,Ribbon 会自动地帮助用户基于某种规则(如简单轮询,随机连接等)去选择连接这些机器。开发者使用 Ribbon 很容易就可以实现自定义的负载均衡算法。截止 2024 年 1 月,Netflix Ribbon 一直处理维护模式,也就是处于停更状态,官方说明如下图所示:

Ribbon 替代方案

Spring Cloud Ribbon 是基于 Netflix Ribbon 的一款客户端负载均衡的工具。由于 Netflix Ribbon 的项目停更了,因此 Spring Cloud 官方给出了以下的替代方案。

LoadBalancer 的概述

LB 负载均衡(Load Balance)的作用就是将用户的请求平均分配到多个服务上,从而满足系统的高可用性(HA),常见的负载均衡软件有 LVS、Nginx 等。Spring Cloud LoadBalancer 是由 Spring Cloud 官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在 Spring Cloud Commons 模块中,可以用它来替换以前的 Ribbon 组件。相比较于 Ribbon 而言,Spring Cloud LoadBalancer 不仅能够支持 RestTemplate,还支持 WebClient(由 Spring WebFlux 中提供的功能,可以实现响应式异步请求)。更多介绍可参考 Spring Cloud 官网Spring Cloud LoadBalancer 官方文档

LoadBalancer 本地负载均衡客户端与 Nginx 服务端负载均衡的区别是什么?

  • Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx 来处理,然后由 Nginx 实现请求转发,即负载均衡是由服务端实现的。
  • LoadBalancer 是本地负载均衡,在调用微服务接口时候,会先从注册中心上获取服务列表,然后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用。

LoadBalancer 入门案例

代码下载

完整的案例代码可以从 这里 下载得到。

版本说明

组件版本说明
Spring Boot3.2.0
Spring Cloud2023.0.0
Consul1.15.4 作为注册中心

创建父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
</properties>

<dependencyManagement>
<dependencies>
<!--SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--SpringBoot Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

创建 Provider 工程

为了测试 LoadBalancer 的负载均衡功能,必须要有一个服务提供者,并且可以选择启动多个实例,在每个实例中需要有一个标识(例如端口)来识别每次的调用是到了不同的服务实例上。这里可以使用一份代码,采取改变端口号的方式启动多次,这样就能启动多个相同的服务实例。创建 Provider 的 Maven 工程后,由于需要将服务注册到 Consul,工程下的 pom.xml 文件需要引入 spring-cloud-starter-consul-discovery

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Consul

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

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

}

application.yml 文件中指定服务名称(provider-service)、注册中心地址与端口号,后面启动多个实例只需要修改这里的端口号即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8001

spring:
application:
name: provider-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true

创建用于测试的 Controller 类

1
2
3
4
5
6
7
8
9
@RestController
public class ProviderController {

@GetMapping("/provider/add")
public String add(Integer a, Integer b, HttpServletRequest request) {
return "From Port: " + request.getServerPort() + ", Result: " + (a + b);
}

}

创建 Consumer 工程

要使用 LoadBalancer,需要在 pom.xml 文件中引入依赖 spring-cloud-starter-loadbalancer。另外,由于需要从 Consul 获取服务列表,即作为 Consul 的客户端,还需要引入 spring-cloud-starter-consul-discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

创建启动主类,添加注解 @EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {

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

}

创建统一配置类,声明 RestTemplate 的 Bean,并且添加注解 @LoadBalanced,声明该 RestTemplate 需要使用客户端负载均衡的功能

1
2
3
4
5
6
7
8
9
10
@Configuration
public class RestTemplateConfig {

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

}

创建用于测试的 Controller 类,这里使用 RestTemplate 来调用第三方 Provider 服务的那个自定义 API

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class ConsumerController {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/consumer/add")
public String add(Integer a, Integer b, HttpServletRequest request) {
String result = restTemplate.getForObject("http://provider-service/provider/add?a=" + a + "&b=" + b, String.class);
return result;
}

}

application.yml 文件中配置端口号、注册中心地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8003

spring:
application:
name: consumer-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true

测试代码

  • (1) 启动 Consul 作为注册中心
  • (2) 更改 Provider 服务的端口号为 8001 与 8002 后,然后分别启动两个 Provider 服务,浏览器访问 http://127.0.0.1:8500,查看 Consul 的管理界面是否正常显示多个 Provider 服务
  • (3) 启动 Consumer 应用,浏览器访问 http://127.0.0.1:8003/add?a=10&b=5,若正常返回计算结果,则说明整个项目运行成功
  • (4) 提示:当存在多个服务提供者时,LoadBalancer 默认会使用轮询的方式访问服务,此外 LoadBalancer 对服务实例节点的增减也能动态感知到

LoadBalancer 负载均衡策略

内置的负载均衡策略

LoadBalancer 内置了两种负载均衡策略,分别是:

  • RoundRobinLoadBalancer:轮询策略,这是默认的负载均衡策略,它按顺序轮流选择服务实例。
  • RandomLoadBalancer:随机策略,它会随机选择一个服务实例。

切换负载均衡策略

LoadBalancer 默认使用的负载均衡策略是轮询策略,如果需要切换到随机策略,可以参考以下配置代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 定义随机策略
*/
@Configuration
public class RandomLoadBalancerConfiguration {

@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 使用 @LoadBalancerClient 注解声明某个服务(如 provider-service)使用随机策略,服务名称大小写敏感
*/
@Configuration
@LoadBalancerClient(value = "provider-service", configuration = RandomLoadBalancerConfiguration.class)
public class RestTemplateConfig {

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

}

代码下载

完整的案例代码可以从 这里 下载得到。

自定义负载均衡策略

在 Spring Cloud LoadBalancer 中,自定义负载均衡策略可以通过实现 ReactorServiceInstanceLoadBalancer 接口或扩展现有的负载均衡策略来实现。

LoadBalancer 进阶使用

获取注册服务列表

使用 LoadBalancer 后,若希望动态获取注册中心(如 Consul)里的服务列表,可以使用 DiscoveryClient 来实现,详细说明请看 这里

  • 案例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/discovery")
public class DiscoveryController {

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/service/list")
public List<String> serviceList() {
List<String> resultList = new ArrayList<>();

// 获取服务名称
List<String> services = discoveryClient.getServices();
services.stream().forEach(service -> {
// 获取服务实例
List<ServiceInstance> instances = discoveryClient.getInstances(service);
instances.stream().forEach(instance -> {
StringBuilder builder = new StringBuilder();
builder.append(instance.getInstanceId()).append(" ").append(instance.getHost()).append(" ")
.append(instance.getPort()).append(" ").append(instance.getUri());
resultList.add(builder.toString());
});
});

return resultList;
}

}
  • 调用接口返回的结果
1
2
3
4
5
6
7
[
"cloud-order-service-8011 thirdparty-register 8011 http://thirdparty-register:8011",
"cloud-payment-service-8001 thirdparty-register 8001 http://thirdparty-register:8001",
"cloud-payment-service-8002 thirdparty-register 8002 http://thirdparty-register:8002",
"cloud-payment-service-8003 thirdparty-register 8003 http://thirdparty-register:8003",
"consul 127.0.0.1 8300 http://127.0.0.1:8300"
]