💡 Spring Cloud LoadBalancer 实现自定义负载均衡策略(基于服务元数据筛选)
在微服务架构中,我们常常希望对服务实例进行更精细的路由控制,例如:
- 灰度发布:不同环境访问不同版本
- 操作系统差异:Windows/Mac/Linux 客户端访问对应的服务
- 多机房、多网段:按区域划分访问
本文将基于 Spring Cloud LoadBalancer 实现一个自定义策略,按服务实例 metadata 中的 env
(环境)和 os
(系统)字段进行智能路由。
🧩 配置目的和背景
我们假设服务实例在注册到 Nacos 时会带上 metadata 元信息:
# application.yml
spring:
cloud:
nacos:
discovery:
metadata:
env: ${spring.profiles.active}
os: ${os.name}
📌 上述配置的作用:
env
: 当前应用运行环境(dev/test/prod)os
: 当前操作系统(如 macOS、Windows)
这样注册到 Nacos 的服务实例就带上了标签,可以作为路由依据。
⚙️ 自定义负载均衡策略核心代码
1️⃣ 负载均衡器实现类 CustomFilteredLoadBalancer.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
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.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 自定义负载均衡器,实现 ReactorServiceInstanceLoadBalancer 接口。
* 功能:
* - 根据服务ID获取服务实例列表(由 ServiceInstanceListSupplier 提供)
* - 过滤出满足当前环境(env)和操作系统(os)条件的实例
* - 返回符合条件的实例封装成 Response,否则返回空响应
*/
@Slf4j
public class CustomFilteredLoadBalancer implements ReactorServiceInstanceLoadBalancer {
final AtomicInteger position;
private final String serviceId;
// 延迟获取 ServiceInstanceListSupplier,避免循环依赖
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String currentEnv; // 当前环境,如 "dev"
private final String currentOs; // 当前操作系统,如 "mac"
public CustomFilteredLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
String currentEnv,
String currentOs) {
this(serviceInstanceListSupplierProvider, serviceId, currentEnv, currentOs,new Random().nextInt(1000));
}
public CustomFilteredLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
String currentEnv,
String currentOs,
int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.currentEnv = currentEnv.toLowerCase();
this.currentOs = currentOs.toLowerCase();
this.position = new AtomicInteger(seedPosition);
}
/**
* 选择符合条件的服务实例
*
* @param request 当前请求对象(通常不使用)
* @return Mono<Response<ServiceInstance>> 响应封装选中的实例或空实例
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(this::getInstanceResponse);
}
/**
* 根据 env 和 os 过滤服务实例列表
*
* @param serviceInstances 服务实例列表
* @return Response<ServiceInstance> 包含选中实例或 EmptyResponse
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("没有可供服务的服务器: " + this.serviceId);
return new EmptyResponse();
}
log.info("原始实例数量:" + serviceInstances.size());
// 自定义扩展过滤逻辑
List<ServiceInstance> instances = serviceInstances.stream()
.filter(instance -> {
String instanceEnv = instance.getMetadata().getOrDefault("env", "").toLowerCase();
boolean envMatch = instanceEnv.equals(currentEnv);
if (!envMatch) {
log.debug("过滤实例 [" + instance.getUri() + "],环境不匹配,实例env=" + instanceEnv + ", 当前env=" + currentEnv);
}
return envMatch;
})
.filter(instance -> {
String instanceOs = instance.getMetadata().getOrDefault("os", "").toLowerCase();
boolean osMatch;
if (currentOs.contains("mac")) {
osMatch = instanceOs.contains("mac");
} else if (currentOs.contains("windows")) {
osMatch = instanceOs.contains("windows");
} else {
osMatch = instanceOs.equals(currentOs);
}
if (!osMatch) {
log.debug("过滤实例 [" + instance.getUri() + "],系统不匹配,实例os=" + instanceOs + ", 当前os=" + currentOs);
}
return osMatch;
})
.collect(Collectors.toList());
log.info("过滤后实例数量:" + instances.size());
if (instances.isEmpty()) {
log.warn("没有符合条件的实例,返回空实例");
return new EmptyResponse();
}
// 如果只有一个实例,则直接返回
if (instances.size() == 1) {
return new DefaultResponse(instances.get(0));
}
// 如果有多个实例,可以采用随机或轮询
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = instances.get(pos % instances.size());
log.info("选中实例:" + instance.getUri());
return new DefaultResponse(instance);
}
}
此处过滤后的服务实例选择逻辑:
- 如果只有一个实例,则直接返回
- 如果有多个实例,可以采用随机或轮询 ,这里我采用了
org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer
官方的轮循负载均衡器里面的源码
这个自定义的负载均衡策略过滤逻辑可以用一句话概括:
根据当前环境变量(env)和操作系统类型(os),从服务实例中筛选出最匹配的实例来使用。
👇 简单白话解释:
当你访问某个服务时,这个策略会先从注册中心获取到所有可用的服务实例,然后一步步筛选:
先看环境变量(
env
)是否匹配
比如你本地运行的是 dev 环境,那就只选出那些注册时标记了 env=dev 的实例。再看操作系统(
os
)是否匹配
继续在第一步筛出来的基础上,再选出和你当前电脑或服务器系统一致的实例。你用的是
Mac
,就只选出标记为os=mac
的实例;是
Windows
,就选os=windows
;是
Linux
,就严格匹配os=linux
。
最终选中一个实例返回
如果匹配到多个符合的实例,就默认选第一个(你也可以改成随机、轮询等方式);
如果一个都没有匹配上,就返回空结果,意味着没有服务可用。
📌 这个策略适合什么场景?
多套部署环境并存(如
dev/test/prod
);某些服务只适用于特定操作系统(比如调用原生接口或依赖特定底层库);
希望本地测试时访问本地服务实例,线上访问线上服务实例。
2️⃣ 配置类 CustomLoadBalancerConfiguration.java
/**
* 自定义负载均衡器配置类,用于将自定义实现注册为 Spring Bean。
* 系统会通过该配置在启动时注入我们的自定义负载均衡逻辑。
*/
public class CustomLoadBalancerConfiguration {
@Bean
public ReactorLoadBalancer<?> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
// 从环境中获取当前调用的服务名(被调用方)
String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
// 获取当前环境配置(如 dev、test、prod)
String currentEnv = SpringUtils.getActiveProfile();
// 获取当前操作系统名(如 Windows、Mac OS X)
String currentOs = System.getProperty("os.name", "unknown");
return new CustomFilteredLoadBalancer(
loadBalancerClientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
serviceId,
currentEnv,
currentOs);
}
}
3️⃣ 启动类中启用配置 Application.java
/**
* 启动类
* 使用 @LoadBalancerClients 注册我们的自定义负载均衡策略配置
*/
@EnableFeignClients
@SpringBootApplication
@LoadBalancerClients(defaultConfiguration = {CustomLoadBalancerConfiguration.class})
public class GsApplication {
public static void main(String[] args) {
SpringApplication.run(GsApplication.class, args);
}
}
📋 核心逻辑解析
步骤 | 动作 | 说明 |
---|---|---|
1 | 注入元数据 | 在服务注册时注入 env 和 os 字段 |
2 | 实现 LoadBalancer | 自定义 choose 方法,按需过滤实例 |
3 | 注册 Bean | 使用 ReactorLoadBalancer 接口注册负载均衡器 |
4 | 全局启用 | 启动类中用 @LoadBalancerClients 激活策略 |
✅ 优点
- ✔️ 支持灰度环境隔离(按环境路由)
- ✔️ 支持客户端系统识别(按 OS 分流)
- ✔️ 可扩展为版本控制、机房标签等智能路由
❗ 注意事项
- 💡 元数据必须在注册服务时注入
- 💡 所有调用方都必须配置正确的
spring.profiles.active
和os.name
- 💡 默认只取第一个匹配的实例,如需其他策略请自行修改
📝 小结与建议
该方案通过 Spring Cloud LoadBalancer 提供的 SPI 接口扩展机制,灵活实现了按元数据(环境+系统)进行服务调用的策略。非常适用于以下场景:
- 多环境隔离(dev/test/prod)
- 灰度发布和分阶段上线
- 跨平台客户端(Windows/Mac/Linux)路由控制