Nacos:自定义负载均衡
一、问题及背景
核心问题
微服务架构下,多人协作调试同一服务时,本地启动的服务实例会与其他开发者的启动的服务实例共同注册到公共Nacos注册中心,导致Nacos默认的负载均衡策略(如轮询)将请求随机分发到不同实例上,出现以下问题:
- 调试混乱:自己的请求可能路由到其他开发者的本地服务,无法验证自身代码逻辑;
- 服务不稳定:请求在多个实例间切换,导致程序时好时坏;
- 协作效率低:需频繁沟通确认实例状态,排查问题困难。
应用背景
- 架构场景:Spring Cloud + Nacos 微服务架构,依赖Nacos实现服务发现与负载均衡;
- 协作场景:多开发者同时开发同一微服务模块,或依赖其他微服务(如基础服务、网关),需将本地实例注册到公共Nacos以保证依赖可用;
- 核心诉求:确保本地调试时,请求优先路由到本机实例,避免负载均衡到其他开发者的实例或无关服务器实例。
二、解决方案汇总
结合所有参考文档及行业实践,核心解决方案如下,覆盖从“完全隔离”到“灵活适配”的不同场景:
- 本地启动独立Nacos服务端(完全隔离);
- 自定义应用名称(服务名区分);
- Nacos多命名空间隔离(环境级隔离);
- 配置独立集群名称(集群级隔离);
- 自定义负载均衡策略(灵活适配,含IP优先、同网段优先、元数据区分等);
三、各方案具体操作步骤
方案1:本地启动独立Nacos服务端(完全隔离)
核心思路:每个开发者本地启动专属Nacos服务端,仅将自己的服务实例注册到本地Nacos,不与他人共享注册中心,彻底避免实例冲突。
操作步骤
下载Nacos服务端:
- 下载地址:nacos-server-2.3.2.zip(版本根据需求选择);
- 解压后,进入
bin目录,执行启动命令(单机模式):# Windows startup.cmd -m standalone # 单机模式启动(调试场景无需集群) # Linux/Mac sh startup.sh -m standalone
配置项目注册到本地Nacos:
在application.yml(或bootstrap.yml)中修改Nacos配置,指向本地地址:spring: cloud: nacos: discovery: server-addr: localhost:8848 # 本地Nacos地址(默认端口8848) namespace: demo-space-${spring.profiles.active} # 可选:按环境区分命名空间,避免本地多环境冲突 config: server-addr: localhost:8848 # 配置中心同样指向本地 namespace: demo-space-${spring.profiles.active} file-extension: yml # 配置文件格式 group: DEFAULT_GROUP # 配置分组 timeout: 3000 # 配置读取超时时间 application: name: demo-service # 无需修改服务名,因注册中心独立启动本地依赖服务:
若需依赖其他微服务(如网关、基础服务),可通过以下方式处理:- 本地启动依赖服务实例并注册到本地Nacos;
- 若依赖服务无需调试,可从公共Nacos导出配置,导入本地Nacos后启动服务镜像(如Docker容器)。
方案2:自定义应用名称(服务名区分)
核心思路:为每个开发者的本地实例分配唯一服务名(如原服务名+开发者姓名/标识),通过服务名精准路由,避免负载均衡到其他实例。
操作步骤
修改本地服务名称:
在application.yml中修改spring.application.name,添加开发者唯一标识:spring: application: name: demo-service-zhangsan # 原服务名(demo-service) + 开发者姓名/工号(确保唯一) profiles: active: dev # 开发环境标识拷贝Nacos配置文件:
在Nacos控制台中,复制原服务的配置文件(如demo-service-dev.yml),创建新配置文件(如demo-service-zhangsan-dev.yml),确保配置与原服务一致(如数据库连接、缓存配置等)。调整网关路由配置(关键步骤):
若使用网关(如Spring Cloud Gateway),需为新服务名添加路由规则,实现请求分流。建议通过配置中心动态管理路由,避免硬编码:spring: cloud: gateway: routes: # 原路由(其他开发者/服务器实例) - id: demo-service uri: lb://demo-service # 负载均衡到原服务名实例 predicates: - Path=/service/a/**,/service/b/** # 分配公共路径 filters: - StripPrefix=1 # 移除路径前缀(如将/service/a/xxx转为/a/xxx) # 新路由(当前开发者本地实例) - id: demo-service-zhangsan uri: lb://demo-service-zhangsan # 负载均衡到开发者专属服务名实例 predicates: - Path=/service/zhangsan/** # 开发者专属路径(建议包含标识) filters: - StripPrefix=1重启服务与网关:
启动本地服务(注册为新服务名),重启网关后,通过专属路径(如/service/zhangsan/xxx)访问,请求将仅路由到本地实例。
方案3:使用Nacos多命名空间隔离
核心思路:Nacos支持多命名空间(Namespace),每个开发者使用独立命名空间,实例仅在自身命名空间内可见,实现环境隔离。命名空间是Nacos提供的最高级隔离机制,适合中大型团队。
操作步骤
- 创建Nacos命名空间:
- 登录Nacos控制台(公共Nacos地址),进入「命名空间」→「新建命名空间」;
- 建议命名规范:
dev-{开发者标识}(如dev-zhangsan),描述填写开发者信息,获取命名空间ID(如xxx-xxx-xxx,后续配置需使用)。
- 配置项目关联命名空间:
在application.yml中添加namespace参数,指向步骤1创建的命名空间ID:spring: cloud: nacos: discovery: server-addr: nacos.neg.cc:8848 # 公共Nacos地址 namespace: xxx-xxx-xxx # 开发者专属命名空间ID(必填) config: server-addr: nacos.neg.cc:8848 namespace: xxx-xxx-xxx # 配置中心与注册中心使用同一命名空间 file-extension: yml application: name: demo-service-name # 服务名无需修改,命名空间已隔离实例 - 同步Nacos配置到命名空间:
- 方式1:在Nacos控制台手动复制原命名空间(如
dev)的配置文件到新命名空间; - 方式2:通过Nacos Open API批量复制配置(适合配置较多的场景)。
- 方式1:在Nacos控制台手动复制原命名空间(如
- 启动服务:
本地服务启动后,将自动注册到专属命名空间,仅能被同一命名空间的消费者调用。若需调用公共服务,可在消费者配置中指定公共服务的命名空间(如public)。
方案4:配置独立集群名称
核心思路:Nacos支持集群(Cluster)隔离,每个开发者配置专属集群名称,Nacos默认负载均衡策略会优先选择同集群实例,无同集群实例时才选择其他集群,适合需共享命名空间但需区分实例来源的场景。
操作步骤
- 修改集群配置:
在application.yml中指定集群名称(开发者唯一,建议规范:cluster-{开发者标识}):spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # Nacos服务地址 group: dev # 服务分组(保持与团队一致) cluster-name: cluster-zhangsan # 专属集群名称(确保唯一) loadbalancer.nacos.enabled: true # 启用Nacos负载均衡(关键:优先同集群) - 启动服务:
本地实例注册到Nacos后,会标记为cluster-zhangsan集群;当消费者调用服务时,Nacos负载均衡器会优先选择同集群实例,若无则自动路由到其他集群实例。 - 进阶配置(可选):
若需调整集群权重或负载均衡策略,可在Nacos控制台修改实例的集群配置,或通过配置文件指定:spring: cloud: nacos: discovery: weight: 100 # 实例权重(同集群内权重高的实例优先被调用)
方案5:自定义负载均衡策略(灵活适配)
核心思路:重写Nacos默认的负载均衡规则(如Ribbon、Spring Cloud LoadBalancer),通过「IP匹配」「同网段优先」「元数据区分」等逻辑,确保请求优先路由到本地实例。此方案是文档中提及最多、最灵活的方案,细分3种实现方式。
前置说明:核心配置位置界定,方案中的所有自定义负载均衡配置(规则实现、生效配置等)均不在网关中配置,核心配置位置为「服务消费者端」(即发起服务调用的微服务本身),网关仅需保持基础路由配置(无需额外修改),二者职责边界清晰:
- 执行主体:负载均衡的实例筛选逻辑由「服务消费者」执行,当消费者调用目标服务时,通过本地配置的自定义规则从注册中心(Nacos)获取的实例列表中筛选目标实例;
- 网关角色:网关仅负责「路由转发」,将客户端请求转发到目标服务名对应的实例集群(如
lb://demo-service),不参与具体的实例选择逻辑; - 特殊说明:网关层虽可配置简单负载均衡策略(如轮询、随机),但无法实现方案的「IP匹配」「同网段优先」「元数据区分」等自定义逻辑,不属于方案的配置范畴。
5.1 基于 IP 优先的负载均衡
目标:请求优先路由到与本机 IP 一致的实例,无则 fallback 到其他实例,确保本地调试优先命中本机服务。
5.1.1 Ribbon 实现方案
步骤 1:引入 Ribbon 依赖(若未集成)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.9.RELEASE</version> <!-- 版本需与Spring Cloud版本适配 -->
</dependency>步骤 2:编写自定义 Ribbon 规则
import com.alibaba.cloud.nacos.NacosServer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import org.springframework.util.CollectionUtils;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.stream.Collectors;
/**
* 基于IP优先的Ribbon负载均衡规则
* 优先选择与本机IP一致的实例
*/
public class SameIpRibbonRule extends RoundRobinRule {
// 本机IP集合(缓存避免重复计算)
private List<String> localIps;
/**
* 构造方法,初始化本机IP列表
*/
public SameIpRibbonRule() {
this.localIps = getLocalIps();
}
/**
* 选择目标服务实例
* @param lb 负载均衡器
* @param key 路由键
* @return 选中的服务实例
*/
@Override
public Server choose(ILoadBalancer lb, Object key) {
// 负载均衡器为空时,返回null
if (lb == null) {
return null;
}
// 获取所有健康实例
List<Server> healthyServers = lb.getReachableServers();
if (healthyServers.isEmpty()) {
return super.choose(lb, key); // 无健康实例时使用默认逻辑
}
// 筛选与本机IP一致的实例
List<Server> sameIpServers = healthyServers.stream()
.filter(server -> {
NacosServer nacosServer = (NacosServer) server;
// 检查实例IP是否在本机IP列表中
return localIps.contains(nacosServer.getHost());
})
.collect(Collectors.toList());
// 优先选择同IP实例,无则使用所有健康实例
List<Server> targetServers = CollectionUtils.isEmpty(sameIpServers) ? healthyServers : sameIpServers;
return targetServers.get(getNextIndex(targetServers.size()));
}
/**
* 获取本机所有非回环IPv4地址
* @return 本机IP列表
*/
private List<String> getLocalIps() {
try {
List<String> ips = new ArrayList<>();
// 遍历所有网络接口
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
// 遍历接口下的所有IP地址
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
// 过滤回环地址和IPv6地址
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
ips.add(addr.getHostAddress());
}
}
}
return ips;
} catch (SocketException e) {
throw new RuntimeException("获取本机IP失败", e);
}
}
/**
* 获取下一个轮询索引
* @param size 目标实例列表大小
* @return 轮询索引
*/
private int getNextIndex(int size) {
return super.incrementAndGetModulo(size);
}
}步骤 3:配置规则生效
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Ribbon客户端配置类
* 为demo-service服务指定IP优先规则
*/
@Configuration
@RibbonClient(name = "demo-service", configuration = SameIpRibbonConfig.class)
public class SameIpRibbonConfig {
/**
* 注册IP优先负载均衡规则
* @return IP优先规则实例
*/
@Bean
public SameIpRibbonRule sameIpRibbonRule() {
return new SameIpRibbonRule();
}
}5.1.2 Spring Cloud LoadBalancer 实现方案
步骤 1:引入 LoadBalancer 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<!-- 版本由Spring Cloud版本管理,无需手动指定 -->
</dependency>注意:排除Ribbon依赖避免冲突,在
application.yml中添加:spring: cloud: loadbalancer: ribbon: enabled: false # 禁用Ribbon,强制使用LoadBalancer
步骤 2:编写自定义 LoadBalancer
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 基于IP优先的Spring Cloud LoadBalancer实现
* 优先选择与本机IP一致的实例
*/
public class SameIpLoadBalancer implements ReactorServiceInstanceLoadBalancer {
// 目标服务的service-id
private final String serviceId;
// 轮询计数器(线程安全)
private final AtomicInteger position;
// 本机IP集合(缓存)
private final List<String> localIps;
// 服务实例列表提供者
private final ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider;
/**
* 构造方法
* @param serviceId 目标服务ID
* @param serviceInstanceListSupplierProvider 服务实例列表提供者
*/
public SameIpLoadBalancer(
String serviceId,
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
this.serviceId = serviceId;
this.position = new AtomicInteger(0);
this.localIps = getLocalIps(); // 初始化本机IP列表
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
/**
* 选择服务实例
* @param request 负载均衡请求
* @return 响应式的实例响应
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get(request).next()
.map(this::filterInstances);
}
/**
* 筛选同IP实例
* @param instances 候选实例列表
* @return 包含选中实例的响应
*/
private Response<ServiceInstance> filterInstances(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
}
// 筛选与本机IP一致的实例
List<ServiceInstance> sameIpInstances = instances.stream()
.filter(instance -> localIps.contains(instance.getHost()))
.collect(Collectors.toList());
// 确定目标实例列表(同IP优先,否则全量)
List<ServiceInstance> targetInstances = sameIpInstances.isEmpty() ? instances : sameIpInstances;
// 轮询选择实例
int index = position.incrementAndGet() % targetInstances.size();
return new DefaultResponse(targetInstances.get(index < 0 ? 0 : index));
}
/**
* 获取本机所有非回环IPv4地址
* @return 本机IP列表
*/
private List<String> getLocalIps() {
try {
List<String> ips = new ArrayList<>();
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
// 排除回环地址和IPv6
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
ips.add(addr.getHostAddress());
}
}
}
return ips;
} catch (SocketException e) {
throw new RuntimeException("获取本机IP失败", e);
}
}
}步骤 3:配置 LoadBalancer 生效
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* LoadBalancer客户端配置类
* 为demo-service服务指定IP优先负载均衡器
*/
@Configuration
@LoadBalancerClient(name = "demo-service", configuration = SameIpLoadBalancerConfig.class)
public class SameIpLoadBalancerConfig {
/**
* 注册IP优先负载均衡器
* @param serviceInstanceListSupplierProvider 服务实例列表提供者(自动注入)
* @return IP优先负载均衡器实例
*/
@Bean
public ReactorServiceInstanceLoadBalancer sameIpLoadBalancer(
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
return new SameIpLoadBalancer("demo-service", serviceInstanceListSupplierProvider);
}
}5.1.3 验证效果
- 本地启动服务(IP 为 192.168.1.100),在Nacos控制台确认实例IP正确;
- 调用
demo-service时,通过日志观察请求是否路由到192.168.1.100; - 关闭本地实例后,请求自动路由到其他健康实例。
5.2 基于同网段优先的负载均衡
目标:优先路由到同网段实例(如 192.168.1.x),其次同 IP,最后轮询其他网段实例,减少跨网段调用延迟。
5.2.1 Ribbon 实现方案
步骤 1:引入 Ribbon 依赖(若未集成)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.9.RELEASE</version> <!-- 版本需与Spring Cloud版本适配 -->
</dependency>步骤 2:编写自定义 Ribbon 规则
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServer;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import org.springframework.util.StringUtils;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.stream.Collectors;
/**
* 基于同网段优先的Ribbon负载均衡规则
* 优先级:同IP > 同网段 > 其他实例
*/
public class SameSegmentRibbonRule extends RoundRobinRule {
// Nacos配置属性(用于获取NamingService)
private final NacosDiscoveryProperties nacosProps;
// 本机IP
private String localIp;
// 本机网段(如192.168.1)
private String localSegment;
/**
* 构造方法,初始化本机IP和网段
* @param nacosProps Nacos配置属性(自动注入)
*/
public SameSegmentRibbonRule(NacosDiscoveryProperties nacosProps) {
this.nacosProps = nacosProps;
this.localIp = getLocalIp(); // 获取本机IP
// 提取网段(取IP中最后一个点之前的部分)
this.localSegment = StringUtils.hasText(localIp)
? localIp.substring(0, localIp.lastIndexOf("."))
: "";
}
/**
* 选择目标实例
* @param key 路由键
* @return 选中的实例
*/
@Override
public Server choose(Object key) {
// 获取负载均衡器(强转为BaseLoadBalancer以获取服务名)
BaseLoadBalancer lb = (BaseLoadBalancer) getLoadBalancer();
if (lb == null) {
return null;
}
try {
String serviceName = lb.getName(); // 目标服务名
// 获取Nacos命名服务实例
NamingService namingService = nacosProps.namingServiceInstance();
// 获取服务的所有实例
List<Instance> allInstances = namingService.getAllInstances(serviceName);
// 筛选健康实例
List<Instance> healthyInstances = allInstances.stream()
.filter(Instance::isHealthy)
.collect(Collectors.toList());
if (healthyInstances.isEmpty()) {
return super.choose(key); // 无健康实例时使用默认逻辑
}
// 1. 优先选择同IP实例
for (Instance instance : healthyInstances) {
if (instance.getIp().equals(localIp)) {
return new NacosServer(instance);
}
}
// 2. 其次选择同网段实例
for (Instance instance : healthyInstances) {
if (instance.getIp().startsWith(localSegment)) {
return new NacosServer(instance);
}
}
// 3. 最后使用默认轮询逻辑
return super.choose(key);
} catch (Exception e) {
// 异常时降级为默认轮询
return super.choose(key);
}
}
/**
* 获取本机非回环IPv4地址
* @return 本机IP(默认127.0.0.1)
*/
private String getLocalIp() {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
// 排除回环地址和IPv6
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
return addr.getHostAddress();
}
}
}
return "127.0.0.1"; // 无有效IP时返回回环地址
} catch (SocketException e) {
return null;
}
}
}步骤 3:配置规则生效
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Ribbon客户端配置类
* 为demo-service服务指定同网段优先规则
*/
@Configuration
@RibbonClient(name = "demo-service", configuration = SameSegmentRibbonConfig.class)
public class SameSegmentRibbonConfig {
/**
* 注册同网段优先规则(依赖Nacos配置属性)
* @param nacosProps Nacos配置属性(自动注入)
* @return 同网段优先规则实例
*/
@Bean
public SameSegmentRibbonRule sameSegmentRibbonRule(NacosDiscoveryProperties nacosProps) {
return new SameSegmentRibbonRule(nacosProps);
}
}5.2.2 Spring Cloud LoadBalancer 实现方案
步骤 1:引入 LoadBalancer 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<!-- 版本由Spring Cloud版本管理,无需手动指定 -->
</dependency>注意:排除Ribbon依赖避免冲突,在
application.yml中添加:spring: cloud: loadbalancer: ribbon: enabled: false # 禁用Ribbon,强制使用LoadBalancer
步骤 2:编写自定义 LoadBalancer
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 基于同网段优先的Spring Cloud LoadBalancer实现
* 优先级:同IP > 同网段 > 其他实例
*/
public class SameSegmentLoadBalancer implements ReactorServiceInstanceLoadBalancer {
// 目标服务的service-id
private final String serviceId;
// 轮询计数器
private final AtomicInteger position;
// 本机IP
private final String localIp;
// 本机网段(如192.168.1)
private final String localSegment;
// 服务实例列表提供者
private final ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider;
/**
* 构造方法
* @param serviceId 目标服务ID
* @param serviceInstanceListSupplierProvider 服务实例列表提供者
*/
public SameSegmentLoadBalancer(
String serviceId,
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
this.serviceId = serviceId;
this.position = new AtomicInteger(0);
this.localIp = getLocalIp(); // 初始化本机IP
// 提取网段
this.localSegment = localIp.contains(".")
? localIp.substring(0, localIp.lastIndexOf("."))
: "";
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
/**
* 选择服务实例
* @param request 负载均衡请求
* @return 响应式的实例响应
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get(request).next()
.map(this::filterInstances);
}
/**
* 按优先级筛选实例
* @param instances 候选实例列表
* @return 包含选中实例的响应
*/
private Response<ServiceInstance> filterInstances(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
}
// 1. 优先同IP实例
ServiceInstance sameIpInstance = instances.stream()
.filter(instance -> instance.getHost().equals(localIp))
.findFirst()
.orElse(null);
if (sameIpInstance != null) {
return new DefaultResponse(sameIpInstance);
}
// 2. 其次同网段实例
List<ServiceInstance> sameSegmentInstances = instances.stream()
.filter(instance -> instance.getHost().startsWith(localSegment))
.collect(Collectors.toList());
if (!sameSegmentInstances.isEmpty()) {
int index = position.incrementAndGet() % sameSegmentInstances.size();
return new DefaultResponse(sameSegmentInstances.get(index < 0 ? 0 : index));
}
// 3. 最后轮询所有实例
int index = position.incrementAndGet() % instances.size();
return new DefaultResponse(instances.get(index < 0 ? 0 : index));
}
/**
* 获取本机非回环IPv4地址
* @return 本机IP(默认127.0.0.1)
*/
private String getLocalIp() {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (!addr.isLoopbackAddress() && addr instanceof java.net.Inet4Address) {
return addr.getHostAddress();
}
}
}
return "127.0.0.1";
} catch (SocketException e) {
throw new RuntimeException("获取本机IP失败", e);
}
}
}步骤 3:配置 LoadBalancer 生效
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* LoadBalancer客户端配置类
* 为demo-service服务指定同网段优先负载均衡器
*/
@Configuration
@LoadBalancerClient(name = "demo-service", configuration = SameSegmentLoadBalancerConfig.class)
public class SameSegmentLoadBalancerConfig {
/**
* 注册同网段优先负载均衡器
* @param serviceInstanceListSupplierProvider 服务实例列表提供者(自动注入)
* @return 同网段优先负载均衡器实例
*/
@Bean
public ReactorServiceInstanceLoadBalancer sameSegmentLoadBalancer(
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
return new SameSegmentLoadBalancer("demo-service", serviceInstanceListSupplierProvider);
}
}5.2.3 验证效果
- 本地实例(192.168.1.100)、同网段实例(192.168.1.101)、跨网段实例(10.0.0.5)同时启动;
- 调用服务时,通过日志确认请求优先路由到192.168.1.100(同IP);
- 关闭本地实例后,请求路由到192.168.1.101(同网段);
- 关闭同网段实例后,请求路由到10.0.0.5(跨网段)。
5.3 基于元数据的负载均衡
目标:通过 Nacos 实例元数据(如 developer=zhangsan)区分开发者,请求优先路由到元数据匹配的实例,无匹配时降级到公共实例。
5.3.1 Ribbon 实现方案(适用于 Spring Cloud Hoxton 及以下版本)
步骤 1:引入 Ribbon 依赖(若未集成)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.9.RELEASE</version> <!-- 版本需与Spring Cloud版本适配 -->
</dependency>步骤 2:配置服务实例元数据
在本地服务的 application.yml 中添加元数据标识(开发者唯一):
spring:
cloud:
nacos:
discovery:
metadata:
developer: zhangsan # 开发者唯一标识(如姓名、工号)步骤 3:编写自定义 Ribbon 规则
继承 RoundRobinRule,按元数据筛选实例:
import com.alibaba.cloud.nacos.NacosServer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import org.springframework.beans.factory.annotation.Value;
import java.util.List;
import java.util.stream.Collectors;
/**
* 基于元数据的Ribbon负载均衡规则
* 优先选择元数据中developer与本地匹配的实例
*/
public class MetadataRibbonRule extends RoundRobinRule {
// 从配置文件读取本地开发者标识(避免硬编码,方便切换)
@Value("${spring.cloud.nacos.discovery.metadata.developer}")
private String localDeveloper;
// 元数据中用于区分开发者的键(固定值)
private static final String METADATA_KEY = "developer";
/**
* 选择目标服务实例
* @param lb 负载均衡器
* @param key 路由键(通常为请求参数)
* @return 选中的服务实例
*/
@Override
public Server choose(ILoadBalancer lb, Object key) {
// 负载均衡器为空时,返回null
if (lb == null) {
return null;
}
// 获取所有健康的服务实例
List<Server> healthyServers = lb.getReachableServers();
// 无健康实例时,使用父类(轮询)的默认逻辑
if (healthyServers.isEmpty()) {
return super.choose(lb, key);
}
// 筛选元数据中developer与本地匹配的实例
List<Server> targetServers = healthyServers.stream()
.filter(server -> {
// 将Server转换为NacosServer,以获取元数据
NacosServer nacosServer = (NacosServer) server;
// 比较元数据中的developer与本地标识
return localDeveloper.equals(nacosServer.getMetadata().get(METADATA_KEY));
})
.collect(Collectors.toList());
// 若无匹配的实例,筛选无developer元数据的公共实例
if (targetServers.isEmpty()) {
targetServers = healthyServers.stream()
.filter(server -> ((NacosServer) server).getMetadata().get(METADATA_KEY) == null)
.collect(Collectors.toList());
}
// 若仍无实例,使用父类逻辑;否则从目标实例中轮询选择
return targetServers.isEmpty()
? super.choose(lb, key)
: targetServers.get(getNextIndex(targetServers.size()));
}
/**
* 获取下一个轮询索引(复用父类的计数器)
* @param size 目标实例列表大小
* @return 轮询索引
*/
private int getNextIndex(int size) {
return super.incrementAndGetModulo(size);
}
}步骤 4:配置规则生效
创建配置类指定目标服务使用自定义规则:
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
/**
* Ribbon客户端配置类
* 为demo-service服务指定自定义负载均衡规则
*/
@Configuration
// name:目标服务的service-id;configuration:规则配置类
@RibbonClient(name = "demo-service", configuration = MetadataRibbonConfig.class)
public class MetadataRibbonConfig {
/**
* 注册自定义负载均衡规则
* @return 元数据匹配规则实例
*/
@Bean
public MetadataRibbonRule metadataRibbonRule() {
return new MetadataRibbonRule();
}
}5.3.2 Spring Cloud LoadBalancer 实现方案(适用于 Spring Cloud 2020.0.0+)
步骤 1:引入 LoadBalancer 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<!-- 版本由Spring Cloud版本管理,无需手动指定 -->
</dependency>注意:排除Ribbon依赖避免冲突,在
application.yml中添加:spring: cloud: loadbalancer: ribbon: enabled: false # 禁用Ribbon,强制使用LoadBalancer
步骤 2:配置服务实例元数据
在本地服务的 application.yml 中添加元数据标识(开发者唯一):
spring:
cloud:
nacos:
discovery:
metadata:
developer: zhangsan # 开发者唯一标识(如姓名、工号)步骤 3:编写自定义 LoadBalancer
实现 ReactorServiceInstanceLoadBalancer 接口:
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.beans.factory.annotation.Value;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 基于元数据的Spring Cloud LoadBalancer实现
* 优先选择元数据中developer与本地匹配的实例
*/
public class MetadataLoadBalancer implements ReactorServiceInstanceLoadBalancer {
// 目标服务的service-id
private final String serviceId;
// 轮询计数器(原子类保证线程安全)
private final AtomicInteger position;
// 本地开发者标识(从配置文件注入)
private final String localDeveloper;
// 元数据中用于区分开发者的键
private static final String METADATA_KEY = "developer";
// 服务实例列表提供者(从Spring容器注入)
private final ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider;
/**
* 构造方法(依赖注入)
* @param serviceId 目标服务ID
* @param localDeveloper 本地开发者标识
* @param serviceInstanceListSupplierProvider 服务实例列表提供者
*/
public MetadataLoadBalancer(
String serviceId,
@Value("${spring.cloud.nacos.discovery.metadata.developer}") String localDeveloper,
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
this.serviceId = serviceId;
this.position = new AtomicInteger(0);
this.localDeveloper = localDeveloper;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
/**
* 选择服务实例
* @param request 负载均衡请求(包含上下文信息)
* @return 封装选中实例的响应对象(响应式)
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// 获取服务实例列表提供者
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
// 从提供者获取实例列表,并处理筛选逻辑
return supplier.get(request).next()
.map(this::filterInstances);
}
/**
* 筛选符合条件的实例
* @param instances 候选实例列表
* @return 包含选中实例的响应
*/
private Response<ServiceInstance> filterInstances(List<ServiceInstance> instances) {
// 实例列表为空时,返回空响应
if (instances.isEmpty()) {
return new EmptyResponse();
}
// 筛选元数据中developer与本地匹配的实例
List<ServiceInstance> targetInstances = instances.stream()
.filter(instance -> localDeveloper.equals(instance.getMetadata().get(METADATA_KEY)))
.collect(Collectors.toList());
// 无匹配实例时,筛选无developer元数据的公共实例
if (targetInstances.isEmpty()) {
targetInstances = instances.stream()
.filter(instance -> instance.getMetadata().get(METADATA_KEY) == null)
.collect(Collectors.toList());
}
// 轮询选择实例(处理索引为负的情况)
int index = position.incrementAndGet() % targetInstances.size();
return new DefaultResponse(targetInstances.get(index < 0 ? 0 : index));
}
}步骤 4:配置 LoadBalancer 生效
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* LoadBalancer客户端配置类
* 为demo-service服务指定自定义负载均衡器
*/
@Configuration
// name:目标服务的service-id;configuration:负载均衡配置类
@LoadBalancerClient(name = "demo-service", configuration = MetadataLoadBalancerConfig.class)
public class MetadataLoadBalancerConfig {
/**
* 注册自定义负载均衡器
* @param serviceInstanceListSupplierProvider 服务实例列表提供者(自动注入)
* @return 元数据匹配负载均衡器实例
*/
@Bean
public ReactorServiceInstanceLoadBalancer metadataLoadBalancer(
ServiceInstanceListSupplier.Provider serviceInstanceListSupplierProvider) {
// 目标服务ID为demo-service,开发者标识从配置文件注入
return new MetadataLoadBalancer("demo-service", "zhangsan", serviceInstanceListSupplierProvider);
}
}5.3.3 验证效果
- 多开发者启动服务,配置不同
developer元数据(如zhangsan、lisi); - 在Nacos控制台查看实例元数据,确认标识正确;
- 调用
demo-service时,请求仅路由到developer=zhangsan的实例; - 关闭本地实例后,自动路由到无
developer元数据的公共实例。
5.4 扩展与注意事项
5.4.1 扩展场景
动态开关控制:通过 Nacos 配置中心添加开关(如
loadbalancer.same-ip.enabled=true),在规则中动态生效:@Value("${loadbalancer.same-ip.enabled:true}") private boolean sameIpEnabled; // 在 choose/filter 方法中判断 if (sameIpEnabled) { // 执行自定义逻辑(如IP优先) } else { // 执行默认逻辑(如轮询) }权重结合:筛选实例后,按 Nacos 实例权重调整选择概率。例如,为同网段实例添加权重系数,提高被选中概率:
// 简化示例:为同网段实例赋予更高权重 List<ServiceInstance> weightedInstances = new ArrayList<>(); for (ServiceInstance instance : sameSegmentInstances) { int weight = Integer.parseInt(instance.getMetadata().getOrDefault("weight", "1")); for (int i = 0; i < weight; i++) { weightedInstances.add(instance); // 权重越高,添加次数越多 } }
5.4.2 注意事项
- 版本兼容性:
- Spring Cloud Hoxton 及以下默认使用 Ribbon;
- Spring Cloud 2020.0.0+ 移除 Ribbon,默认使用 LoadBalancer;
- 二者不可共存,需根据版本选择实现方式。若需在新版本中使用Ribbon,需手动引入依赖并开启:
spring: cloud: loadbalancer: ribbon: enabled: true
- 性能优化:
- 缓存本机 IP/网段信息(如构造方法中初始化),避免每次请求重复计算;
- 配置实例列表刷新间隔:
# Ribbon配置 ribbon: ServerListRefreshInterval: 30000 # 30秒刷新一次实例列表 # LoadBalancer配置 spring: cloud: loadbalancer: cache: ttl: 30s # 缓存过期时间
- 日志监控:添加实例选择日志,便于排查问题:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger log = LoggerFactory.getLogger(SameIpRibbonRule.class); // 在选择实例后添加日志 log.info("选中实例: {}:{},筛选逻辑: 同IP优先", instance.getHost(), instance.getPort()); - 健康检查依赖:确保 Nacos 健康检查正常,通过
Instance::isHealthy过滤不健康实例,避免路由到已宕机的实例。
四、临时关闭健康检查(辅助)
核心思路:调试期间延长Nacos健康检查间隔或关闭健康检查,避免本地实例因调试暂停(如断点调试)被误剔除,仅作为辅助优化(不能解决实例冲突,需配合其他方案)。
操作步骤
- 修改Nacos健康检查配置:
在application.yml中调整Nacos心跳和健康检查参数:spring: cloud: nacos: discovery: server-addr: nacos.neg.cc:8848 heart-beat-interval: 30000 # 心跳间隔30秒(默认5秒,延长避免频繁检测) heart-beat-timeout: 60000 # 心跳超时60秒(默认15秒) ip-delete-timeout: 120000 # IP删除超时120秒(默认30秒,给足调试时间) - Nacos控制台调整(可选):
进入Nacos控制台→「服务管理」→「服务列表」→选择目标服务→「健康检查」,关闭该服务的健康检查(仅开发环境使用)。
风险提示:关闭健康检查可能导致请求路由到已宕机的实例,建议仅在本地调试时临时使用,生产环境必须启用健康检查。
五、方案总结与对比
| 方案 | 核心逻辑 | 实现复杂度 | 侵入性 | 协作成本 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|
| 本地独立Nacos | 注册中心完全隔离 | 中 | 低(仅配置修改) | 低(无需协作) | 本地依赖少、电脑配置充足的开发者 | 彻底隔离,无冲突;调试纯净 | 占用本地资源;需启动所有依赖服务;无法共享公共服务 |
| 自定义应用名称 | 服务名唯一区分 | 低-中 | 中(需修改服务名+网关) | 中(需协调网关路由) | 小团队、少服务、网关可控场景 | 配置简单;无需修改负载均衡逻辑 | 服务名冗余;多服务时网关路由维护复杂;无法复用公共配置 |
| Nacos多命名空间 | 命名空间隔离实例 | 低 | 低(仅配置修改) | 低(提前规划命名空间) | 中大型团队、多项目并行开发 | 隔离彻底;配置复用;不影响服务名和网关 | 需Nacos管理员分配命名空间;跨命名空间调用不便 |
| 独立集群名称 | 集群隔离实例 | 低 | 低(仅配置修改) | 低(提前规划集群名) | 需区分开发/测试/生产集群的场景 | 配置简单;支持集群级负载均衡 | 隔离性弱于命名空间;同集群内仍可能有多个实例冲突 |
| 自定义负载均衡 | 按IP/网段路由/元数据 | 高 | 中(仅消费者端配置) | 低(一次开发,全员复用) | 大多数协作场景(推荐) | 灵活适配多种需求;无需修改服务名/网关;支持共享公共服务 | 开发成本高;需维护负载均衡代码;依赖框架版本 |
优先级推荐
- 首选方案:自定义负载均衡策略(IP优先/元数据区分)→ 灵活、低协作成本、不影响现有架构;
- 次选方案:Nacos多命名空间 → 隔离彻底、配置简单,适合中大型团队;
- 备选方案:自定义应用名称 → 小团队快速落地,网关路由简单场景;
- 特殊场景:本地独立Nacos → 本地依赖少、追求纯净调试环境的开发者;
参考资料
致谢
- 公用nacos,实现只调用本机相应服务,不出现负载均衡到别人机器上
- 微服务研发时,多个人共同调试一个服务,在nacos会启动多个实例,导致请求服务接口时在你和别人之间来回轮询问题处理
- 若依微服务研发时,多个人共同调试一个服务,在nacos会启动多个实例,导致请求服务接口时在你和别人之间来回轮询问题处理
- 解决开发环境的服务冲突和实例乱窜
- Spring Cloud开发人员如何解决服务冲突和实例乱窜?
- Spring Cloud开发人员如何解决服务冲突和实例乱窜?(IP实现方案)
- SpringCloud+Nacos 对于多人调用公共服务
- nacos自定义负载均衡器(优先调用同IP服务)
- Nacos 自定义负载均衡,优先使用同IP服务(本地服务优先调用)
- 解决Springcloud Nacos 开发环境多服务在线乱入问题