Nacos 上的负载均衡

服务多级存储模型

在之前我们讲解 Nacos 的数据模型可以了解,Nacos 实例 Key 由如下三元组唯一确定,命名空间,分组,服务名

1
2
3
4
5
6
7
8
Namespace(命名空间)
├─ group(服务分组)
│ ├─ service(服务名)
│ │ ├─ instance 1(实例1:IP+端口)
│ │ ├─ instance 2(实例2:IP+端口)
│ │ └─ ...
│ └─ other service(其他服务)
└─ other group(其他分组)

在之前,我们的服务模型都是按照两层划分的,一个服务就是一个业务的微服务,一个服务下可以有多个实例

不过随着我们的业务规模越来越大,为了增加稳定性和容灾性,我们会将一个实例部署在多个机房,所以 Nacos 引入了地域 (Zone) 的概念,把同一个服务的多个实例部署到不同地域的机房中 (鸡蛋分开不同的篮子放) ;又把在同一个地域的机房的多个服务实例称为集群 (Cluster) 。比如,杭州机房的 2 个用户服务 user-service 称为杭州 user-service 集群。

这样,把一在同一个机房的多个实例称为一个集群,三层的服务多级模型就出现了

一个服务可以分多个集群部署,一个服务器可以部署多个服务(但是一般不会这么做,一般一台服务器就只部署一个服务实例),一个集群可以有多个实例;

image-20250720123815502

因此,在 Nacos 的服务分级模型中,

  • 第一级是微服务 (如订单服务) ;
  • 第二级是集群 (如北京订单服务集群、上海订单服务集群等) ;
  • 第三集是实例 (如杭州服务集群的 8081 端口实例、8082 端口实例等) 。

所以就会产生服务跨集群调用的问题

微服务之间的远程调用要优先调用同一个地域的集群中的实例,因为访问同一个集群中的服务距离较短、速度比较快、延迟比较低 (例如北京集群的订单服务调用北京集群的用户服务) 。而跨地域集群地域距离远、速度慢、延迟高 (例如北京集群的订单服务调用上海集群的用户服务) 。

那为什么 Nacos 还要增加一层 集群 的概念呢?

首先就是为了避免跨地域集群的远程调用发生,让微服务之间的远程调用尽可能地发生在同一个地域集群中,保证访问的高速低延迟。其次就是为了当同地域的集群服务不可用时,可以跨地域集群访问,保证服务的高可用,提升系统的容灾能力。

服务多级存储模型的实例演示

首先,我们要进行这样的一个配置,这些配置中,都可以修改 Nacos 的服务存储模型中的内容,我感觉还不止这些,但是我就知道这些了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 命名空间配置 - 创建自定义命名空间(需要先在Nacos控制台创建)
spring.cloud.nacos.discovery.namespace=dev-namespace

# 分组配置 - 将服务归类到指定分组
spring.cloud.nacos.discovery.group=DEV_GROUP

# 集群名称配置 - 指定服务所属的集群
spring.cloud.nacos.discovery.cluster-name=HZ

# 服务实例元数据配置 - 可以添加自定义元数据
spring.cloud.nacos.discovery.metadata.version=1.0
spring.cloud.nacos.discovery.metadata.env=dev
spring.cloud.nacos.discovery.metadata.region=hangzhou

# 注册服务的权重,用于负载均衡时的权重策略
spring.cloud.nacos.discovery.weight=2

# 是否启用Nacos Watch,用于监听服务列表变化
spring.cloud.nacos.discovery.watch.enabled=true

# 是否将IP注册到Nacos而不是主机名
spring.cloud.nacos.discovery.ip-type=IPv4
spring.cloud.nacos.discovery.register-enabled=true

然后在控制台就可以看到集群的变化,可以再Nacos中看到部署的服务实例

image-20250721095833185

点击“详情”:可以看到集群和部署的实例

image-20250721095849037

Nacos 负载均衡的原理详解

首先,Nacos 增加集群这一层是为了确保微服务间远程调用尽可能地在同一地域的集群中发生,避免跨集群的远程调用。这种调用情况必然涉及到一些全新的负载均衡的内容,我们来写一些东西测试 Nacos 默认的负载均衡策略

为每个端口的实例都写一个配置文件,实现两个实例在杭州,一个实例在沈阳的这种情况,方便验证,然后像这样修改

image-20250721104217607

验证一下配置成功

image-20250721105321569
  • 提供者实例:端口8087
  • 消费者实例:
    • 杭州集群:端口8088(权重1)和8089(权重2)
    • 沈阳集群:端口8090(权重1)

我们修改了控制器,然后进行了服务提供端向消费端进行了十次调用

image-20250721110919234

发现 Nacos 会以集群下先作为轮询,然后集群内采取轮询,有人说 Nacos 默认的不是权重随机策略吗,怎么会这样,其实在Spring Cloud Alibaba早期版本中,Nacos确实默认使用的是基于权重的随机负载均衡,但在Spring Cloud 2020版本后,默认使用的是Spring Cloud LoadBalancer,它的默认策略是RoundRobinLoadBalancer(轮询)

而我们常用的策略是权重随机策略,比 Ribbon 的要复杂一些,简单讲一下

  1. 权重设置:每个服务实例可以设置一个权重值(weight),这个权重值表示该实例在负载均衡中被选中的概率。权重值越大,被选中的概率就越高。在 Nacos 中,服务实例的权重可以通过 Nacos 控制台或者相关配置进行设置 。例如,有三个服务消费者端口实例 A、B、C,分别设置权重为 5、3、2,权重总和为 5 + 3 + 2 = 10。
  2. 随机选择:在进行负载均衡决策时,Nacos 首先会根据各个服务实例的权重,计算出每个实例被选中的概率范围。以上述例子为例,实例 A 的概率范围是 [0, 5),实例 B 的概率范围是 [5, 8),实例 C 的概率范围是 [8, 10) 。然后,Nacos 会生成一个 0 到权重总和之间的随机数,根据随机数落在哪个概率范围,来决定选择哪个服务实例。比如生成的随机数是 3,落在了实例 A 的概率范围内,那么这次调用就会选择实例 A 。

假设存在以下服务消费者实例及其权重配置:

服务消费者实例 IP 端口 权重
169.254.27.53 8088 3
169.254.27.53 8091 2
169.254.27.53 8092 5
  1. 计算权重总和:3 + 2 + 5 = 10。
  2. 确定每个实例的概率范围:
    • 实例(169.254.27.53:8088):[0, 3)
    • 实例(169.254.27.53:8091):[3, 5)
    • 实例(169.254.27.53:8092):[5, 10)
  3. 生成一个 0 到 10 之间的随机数,比如随机数是 7,落在了实例(169.254.27.53:8092)的概率范围内,那么此次请求就会被路由到该实例上。

要启用Nacos的权重随机负载均衡和集群亲和性,需要进行以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Nacos权重负载均衡配置类
* 配置使用Nacos自己的负载均衡器,支持权重随机和同集群优先
*/
@Configuration
@LoadBalancerClients(defaultConfiguration = NacosWeightedBalanceConfig.class)
public class NacosWeightedBalanceConfig {

/**
* 创建NacosLoadBalancer实例
* 这个负载均衡器会优先选择同集群下的实例,并按照权重进行随机选择
*/
@Bean
public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory,
NacosDiscoveryProperties nacosDiscoveryProperties) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name, nacosDiscoveryProperties);
}
}

修改application.properties,添加配置

1
2
3
4
5
6
7
# 添加以下配置
# 设置负载均衡器类型为Nacos
spring.cloud.loadbalancer.nacos.enabled=true
# 启用同集群优先策略
spring.cloud.nacos.discovery.cluster-name=HZ
# 启用Nacos权重
spring.cloud.nacos.discovery.weight=1

我们把其中一个实例的权重降低,来测试一下

image-20250721111548608

修改配置后,再次访问 http://localhost:8087/provider/call-consumer/25,观察结果:调整的太低了,没访问出来))

实际上,Nacos 作为服务注册中心时,本身并不直接提供负载均衡的核心实现,而是通过集成 Spring Cloud LoadBalancer 或 Ribbon 等组件来实现负载均衡功能。不过,Nacos 提供了服务发现能力,为负载均衡提供了服务列表数据支持。

Nacos 的负载均衡—NacosIRule 源码讲解

讲了如上内容,都是为了 Nacos 的负载均衡做准备,现在就开始讲解 Nacos 的负载均衡和它和 Ribbon 的联系与区别

Nacos 其实同样的使用的是 Ribbon 的规则,不过 Nacos 实现了自己的类。继承自 AbstractLoadBalancerRule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.netflix.client.IClientConfigAware;

// 定义负载均衡规则的基础结构
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
// 负载均衡器
private ILoadBalancer lb;

// 设置负载均衡器实例,允许外部向当前规则注入负载均衡器
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}

// 获取当前关联的负载均衡器实例,提供了访问负载均衡器的途径
public ILoadBalancer getLoadBalancer() {
return this.lb;
}
}

这个类封装了对ILoadBalancer的管理(设置和获取),实现了两个接口:IRuleIClientConfigAware,表明它具备负载均衡规则和客户端配置感知的能力,这两个接口的内容之前在 Ribbon 时候详细讲过,没看的的可以去看,在这里只是简单说一下

1
2
3
4
5
6
7
8
9
package com.netflix.loadbalancer;

public interface IRule {
Server choose(Object var1);

void setLoadBalancer(ILoadBalancer var1);

ILoadBalancer getLoadBalancer();
}

其中,`choose 方法就是负载均衡策略进行实例选择的方法接口,七种实现对应七种负载均衡的策略image-20250721101303075

还记得之前,我们从LoadBalancerInterceptor这个拦截器类,进入到其核心的 execute 方法,然后最后遭到 ReactorLoadBalancer这个负载均衡的控制器吗

image-20250721101603163

然后发现他有一个类 ReactorServiceInstanceLoadBalancer,会继承ReactorLoadBalancer,这就是负载均衡的实现组件的接口,规定从服务注册中心(如 Nacos、Eureka 等)获取可用的服务实例列表然后按照策略选择

image-20250721101748706

你再进入你就会发现,NacosLoadBalancer——Nacos 的负载均衡核心类,继承了这个,至此,一套从 Ribbon 和 Spring Cloud LoadBalance 完美过渡实现到 Nacos 的继承和类体系大概就是差不多了,它基于 Nacos 服务发现的负载均衡器实现,集成了 Spring Cloud LoadBalancer 的 ReactorServiceInstanceLoadBalancer 接口

管这个类叫 Nacos 负载均衡器 吧,这个类挺长的,只说其中核心的原理

一些属性,说一下省的我和大伙总忘了什么是什么,其中的Nacos发现配置属性前面已经说过这个自动配置类了

1
2
3
4
5
6
7
8
// 服务ID,标识当前负载均衡器对应的服务
private final String serviceId;

// 服务实例列表提供者,用于获取可用的服务实例
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

// Nacos发现配置属性,包含了Nacos相关的配置信息
private final NacosDiscoveryProperties nacosDiscoveryProperties;

其中有个挺好的东西,就是这个 IPV4 的正则,我自从发现了这个之后一直用的是这个))狠狠地偷

1
2
// IPv4地址正则表达式,用于验证IP地址是否为IPv4格式
private static final String IPV4_REGEX = "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}";

初始化方法就是构造 IPv6 的地址,然后根据IP类型过滤服务实例private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> instances),支持IPv4/IPv6双栈,根据当前服务IP类型选择合适的实例,这种 Nacos 的特性不多说,知道就行

核心类在这里,这也就是为什么上面的测试会产生这样的情况

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
52
53
/**
* 根据服务实例列表获取响应对象
* 实现了Nacos负载均衡的核心逻辑:集群优先 + 权重随机
*
* @param serviceInstances 服务实例列表
* @return 包含选中服务实例的响应对象
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
// 如果没有可用服务实例,返回空响应
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}

try {
// 获取当前服务的集群名称
String clusterName = this.nacosDiscoveryProperties.getClusterName();
List<ServiceInstance> instancesToChoose = serviceInstances;

// 如果配置了集群名称,则优先选择同一集群的服务实例
// 这是Nacos负载均衡的重要特性:集群优先策略
if (StringUtils.isNotBlank(clusterName)) {
List<ServiceInstance> sameClusterInstances = serviceInstances.stream()
.filter(serviceInstance -> {
// 从服务实例元数据中获取集群信息
String cluster = (String) serviceInstance.getMetadata().get("nacos.cluster");
return StringUtils.equals(cluster, clusterName);
})
.collect(Collectors.toList());

// 如果存在同一集群的实例,则只在这些实例中选择
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
}
} else {
// 没有配置集群名称,记录跨集群调用的警告
log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
this.serviceId, clusterName, serviceInstances);
}

// 根据IP类型过滤实例(IPv4/IPv6)
instancesToChoose = this.filterInstanceByIpType(instancesToChoose);

// 使用Nacos的负载均衡算法选择实例
// 核心算法是基于权重的随机选择,权重由Nacos控制台配置
ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);

return new DefaultResponse(instance);
} catch (Exception e) {
log.warn("NacosLoadBalancer error", e);
return null;
}
}
  • Nacos 的getHostByRandomWeight3方法实现了基于权重的随机选择,权重可以在 Nacos 控制台配置,权重越高的实例被选中的概率越大,这种算法既保证了负载分散,又能根据实例性能差异进行流量调控
  • 实现了 Spring Cloud LoadBalancer 的ReactorServiceInstanceLoadBalancer接口,通过ServiceInstanceListSupplier获取服务实例列表,与 Spring Cloud 生态无缝衔接,使用响应式编程(Mono)提高异步处理能力,适合高并发场景

其中有个 NacosLoadBalancerClientConfiguration 自动配置类,是 Spring Cloud Alibaba Nacos 负载均衡组件的核心配置类,主要用于配置 Nacos 集成 Spring Cloud LoadBalancer 所需的 Bean。`NacosDiscoveryProperties它被该配置以来。

它配置 Nacos 负载均衡器(NacosLoadBalancer),根据不同的服务发现模式(响应式 / 阻塞式)配置服务实例列表供应商,提供基于区域偏好的服务实例选择能力,其中,有个ReactiveSupportConfiguration是响应式服务发现配置,有个BlockingSupportConfiguration是阻塞式服务发现配置

image-20250721113430592
image-20250721113440923

而 Nacos 在服务发现和服务调用等位置需要使用负载均衡的地方,都是通过如下,间接引入到NacosLoadBalancerClientConfiguration 并且进行自动配置,引入负载均衡的配置和相关实现

1
this.nacosDiscoveryProperties = nacosDiscoveryProperties;