背景
当前我们的微服务架构基于Spring Cloud Alibaba体系,通过定制NacosRule实现了跨集群访问和灰度发布功能。但随着Spring Cloud与Nacos版本升级,官方已弃用Ribbon转向LoadBalancer,这要求我们完成以下技术升级:
- 负载均衡机制迁移:将原有Ribbon规则适配到LoadBalancer
- 功能继承保障:保持跨集群路由和灰度能力
- 技术风险控制:深入理解底层机制以提升问题排查效率
技术选型对比
特性 | Ribbon | LoadBalancer |
---|---|---|
维护状态 | 停止更新 | 官方维护 |
响应式支持 | 不支持 | 原生支持 |
配置灵活性 | XML/注解 | 全Java配置 |
扩展性 | 中等 | 高 |
服务发现集成 | 需要适配 | 深度整合 |
负载均衡
什么是负载均衡?简单来说,负载均衡就是将网络流量(负载)分摊到不同的网络服务器(可以平均分配,也可以不平均),系统就可以实现服务的水平横向扩展。
服务端负载均衡
服务器端负载均衡指的是存放在服务器端的负载均衡器,例如 Nginx、HAProxy、F5 等。
客户端负载均衡
客户端负载均衡指的是嵌套在客户端的负载均衡器,例如 Ribbon、Loadbalancer。
Spring cloud loadbalancer
在介绍loadbalancer之前,如果让你来设计一个负载均衡组件,你会怎么设计?
我们可能需要考虑以下这几个问题:
- 如何获取服务器列表?
- 服务器列表发生变更如何监听同步?
- 如何将客户端请求进行拦截然后选择服务器进行转发?
- 如何将负载进行分摊?
带着这些问题,我们来深入了解Spring Cloud LoadBalancer的实现机制。
服务器列表获取
ServiceInstanceListSupplier
public interface ServiceInstanceListSupplier
extends Supplier<Flux<List<ServiceInstance>>> {
// 服务id
String getServiceId();
// 构造器
static ServiceInstanceListSupplierBuilder builder() {
return new ServiceInstanceListSupplierBuilder();
}
//用于创建一个 固定的 ServiceInstanceListSupplier(实例列表不会变)。
//允许从 Spring Environment 读取配置来构造 FixedServiceInstanceListSupplier。
static FixedServiceInstanceListSupplier.Builder fixed(Environment environment) {
return new FixedServiceInstanceListSupplier.Builder(environment);
}
//直接返回 serviceId 对应的 FixedServiceInstanceListSupplier。
static FixedServiceInstanceListSupplier.SimpleBuilder fixed(String serviceId) {
return new FixedServiceInstanceListSupplier.SimpleBuilder(serviceId);
}
//实现 ServiceInstanceListSupplier,用于返回一个固定的实例列表,不会动态更新。
//适用于测试环境或者静态服务发现场景。
class FixedServiceInstanceListSupplier implements ServiceInstanceListSupplier {
private final String serviceId;
private List<ServiceInstance> instances;
@Deprecated
public static Builder with(Environment env) {
return new Builder(env);
}
//构造 FixedServiceInstanceListSupplier,接受服务 ID 和实例列表。
private FixedServiceInstanceListSupplier(String serviceId,
List<ServiceInstance> instances) {
this.serviceId = serviceId;
this.instances = instances;
}
//返回当前 FixedServiceInstanceListSupplier 所管理的 serviceId。
@Override
public String getServiceId() {
return serviceId;
}
//返回固定的实例列表,不会随 Nacos 或 Eureka 变化。
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.just(instances);
}
}
该接口提供符合条件的实例列表,并提供了 builder 方法返回 ServiceInstanceListSupplierBuilder 实例用来构造 ServiceInstanceListSupplier
同时FixedServiceInstanceListSupplier
,它返回固定的服务实例列表。它主要用于
- ** 测试负载均衡逻辑**(在不依赖 Nacos/Eureka 的情况下提供固定实例)。**
- 静态配置服务实例(比如在某些特殊场景下,不使用注册中心,而是固定 IP+端口)。
示例
List<ServiceInstance> instances = List.of(
new DefaultServiceInstance("id1", "my-service", "127.0.0.1", 8080, false),
new DefaultServiceInstance("id2", "my-service", "127.0.0.2", 8081, false)
);
ServiceInstanceListSupplier supplier = ServiceInstanceListSupplier.fixed("my-service")
.withInstances(instances)
.build();
// 获取实例列表
supplier.get().subscribe(list -> list.forEach(instance ->
System.out.println(instance.getHost() + ":" + instance.getPort())
));
整个 ServiceInstanceListSupplier 的实现类都是 rx式 编程风格,但核心逻辑不难看懂,下面就贴出几个实现类简单了解下
DiscoveryClientServiceInstanceListSupplier
...
// 普通mvc项目获取获取实例列表
public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate,
Environment environment) {
this.serviceId = environment.getProperty(PROPERTY_NAME);
resolveTimeout(environment);
this.serviceInstances = Flux
.defer(() -> Mono.fromCallable(() -> delegate.getInstances(serviceId)))
.timeout(timeout, Flux.defer(() -> {
logTimeout();
return Flux.just(new ArrayList<>());
}), Schedulers.boundedElastic()).onErrorResume(error -> {
logException(error);
return Flux.just(new ArrayList<>());
});
}
// webflux项目获取获取实例列表
public DiscoveryClientServiceInstanceListSupplier(ReactiveDiscoveryClient delegate,
Environment environment) {
...
}
...
主要逻辑在构造方法中,等价于 this.serviceInstances = discoveryClient.getInstances(serviceId),不难理解:从注册中心拉去实例列表
DelegatingServiceInstanceListSupplier
public abstract class DelegatingServiceInstanceListSupplier
implements ServiceInstanceListSupplier, InitializingBean, DisposableBean {
protected final ServiceInstanceListSupplier delegate;
public DelegatingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
Assert.notNull(delegate, "delegate may not be null");
this.delegate = delegate;
}
...
装饰层,内嵌一个代理对象,一般就是 DiscoveryClientServiceInstanceListSupplier 来获取实例列表,装饰逻辑就是过滤对应的列表
其下面有n个实现子类,暂不贴代码了
主要功能为:
- ZonePreferenceServiceInstanceListSupplier:区域优先选择,优先选择与当前客户端在相同
zone
(可用区)的实例,提高访问效率,降低跨区域流量消耗。 - CachingServiceInstanceListSupplier:缓存
ServiceInstanceListSupplier
的返回结果,减少注册中心查询次数,提升性能。 - SameInstancePreferenceServiceInstanceListSupplier:尽量路由到之前选中的实例,减少服务间的切换,提高请求一致性。
- HealthCheckServiceInstanceListSupplier:在负载均衡前,过滤掉不健康的服务实例,确保请求不会路由到故障实例。
选择实例以及转发
ReactorLoadBalancer
public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> {
/**
* Choose the next server based on the load balancing algorithm.
* @param request - an input request
* @return - mono of response
*/
@SuppressWarnings("rawtypes")
Mono<Response<T>> choose(Request request);
default Mono<Response<T>> choose() {
return choose(REQUEST);
}
}
该接口从 ServiceInstanceListSupplier 返回的实例中选择最终目标,其中也分为普通mvc项目和webflux项目,spring-cloud-loadbalancer 默认只提供了两个实现:
- RandomLoadBalancer:随机选择
- RoundRobinLoadBalancer:轮询
如果不明白这两区别,可以看文章最后的部分
服务装配
LoadBalancerClientConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {
private static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 193827465;
//实例选择规则:默认轮询
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
//WebFlux 环境下的默认 ServiceInstanceListSupplier 配置
@Configuration(proxyBeanMethods = false)
@ConditionalOnReactiveDiscoveryEnabled
@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER)
public static class ReactiveSupportConfiguration {
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations",
havingValue = "default", matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient()
.withCaching().build(context);
}
...
}
//普通 web 环境下的配置
@Configuration(proxyBeanMethods = false)
@ConditionalOnBlockingDiscoveryEnabled
@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER + 1)
public static class BlockingSupportConfiguration {
...
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations",
havingValue = "health-check")
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient()
.withHealthChecks().build(context);
}
...
}
}
现在回过头来看每个 LoadBalancerClient 容器实例下默认注册的配置类 LoadBalancerClientConfiguration,如代码所示:
默认的实例选择规则是 轮询
对应的实例列表获取规则取决于 spring.cloud.loadbalancer.configurations 属性配置
LoadBalancerAutoConfiguration
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients
@EnableConfigurationProperties(LoadBalancerProperties.class)
@AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class,
LoadBalancerBeanPostProcessorAutoConfiguration.class,
ReactiveLoadBalancerAutoConfiguration.class })
public class LoadBalancerAutoConfiguration {
private final ObjectProvider<List<LoadBalancerClientSpecification>> configurations;
public LoadBalancerAutoConfiguration(
ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
this.configurations = configurations;
}
@Bean
@ConditionalOnMissingBean
public LoadBalancerZoneConfig zoneConfig(Environment environment) {
return new LoadBalancerZoneConfig(
environment.getProperty("spring.cloud.loadbalancer.zone"));
}
@ConditionalOnMissingBean
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory();
clientFactory.setConfigurations(
this.configurations.getIfAvailable(Collections::emptyList));
return clientFactory;
}
}
等上述所有实例加载完,最后整体装配
执行最终实例
BlockingLoadBalancerClient
public class BlockingLoadBalancerClient implements LoadBalancerClient {
...
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
ServiceInstance serviceInstance = choose(serviceId);
if (serviceInstance == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
return execute(serviceId, serviceInstance, request);
}
//执行request请求
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
try {
return request.apply(serviceInstance);
}
catch (IOException iOException) {
throw iOException;
}
catch (Exception exception) {
ReflectionUtils.rethrowRuntimeException(exception);
}
return null;
}
@Override
public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
@Override
public ServiceInstance choose(String serviceId) {
//获取 serviceId 容器中的 ReactiveLoadBalancer 实例
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
.getInstance(serviceId);
if (loadBalancer == null) {
return null;
}
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
.block();
if (loadBalancerResponse == null) {
return null;
}
return loadBalancerResponse.getServer();
}
}
最后回到 BlockingLoadBalancerClient#execute 逻辑(容器中默认装配的 LoadBalancerClient):
逻辑无非就是选择最终的实例来执行请求
底层逻辑就是从隔离好的 容器 中获取对应的 ServiceInstanceListSupplier 和 ReactiveLoadBalancer 来选择实例
实践指南
配置类
- 注册中心维度,相当于每个client端隔离一份配置,都走自己定义的逻辑,通过@LoadBalancerClients注解整合到一份配置类中,具体配置一内部类的形式维护,缺点就是,client端规则发生变化时,需要修改对应配置类
- 通用策略配置,每种策略配置隔离一份,通过@LoadBalancerClients注解整合到一份配置类中,client端选择对应的配置即可,缺点就是,组合会很多,比较复杂,但是好处时,如果发生变更的话,只需要更改@LoadBalancerClients的属性值,拓展性也比较好
注册中心维度
@LoadBalancerClients({
@LoadBalancerClient(value = "eureka-client-1", configuration = ClietConfig.EurekaClient1Config.class)
, @LoadBalancerClient(value = "eureka-client-2", configuration = ClietConfig.EurekaClient2Config.class)
})
public class ClietConfig {
//如果这里面的规则发生变化,就得改这里面的东西
static class EurekaClient1Config {
// ...
}
//如果这里面的规则发生变化,就得改这里面的东西
static class EurekaClient2Config {
// ...
}
}
通用策略维度
@LoadBalancerClients({
@LoadBalancerClient(value = "eureka-client-1", configuration = LoadBalanceConfig.RandomLoadBalancerConfig.class)
, @LoadBalancerClient(value = "eureka-client-2", configuration = LoadBalanceConfig.ZonePerferServiceListConfig.class)
})
public class LoadBalanceConfig {
//这些策略不用更改,一般时通用
static class RandomLoadBalancerConfig {
// ...
}
//这些策略不用更改,一般时通用
static class HintServiceListConfig {
// ...
}
// ...
}
灰度切入
目前根据loadbalancer提供的类来看,可以实现灰度切入的有两个地方,分别为:
- ReactiveLoadBalancer:选择实例
- ServiceInstanceListSupplier:过滤实例
严格意义上来说,我的理解是灰度功能是来选择示例的,并不是过滤实例的,所以我可能更倾向于通过ReactiveLoadBalancer来实现,实现他的choose方法;
但是这里有个坑是啥?
如果你想在ReactiveLoadBalancer层面实现基于请求头的路由决策,你需要在调用choose方法时传递一些上下文信息。Spring Cloud LoadBalancer中的Request接口可以携带这些信息。然而,Request对象需要在调用choose之前构建,并且Spring Cloud LoadBalancer并没有提供一个内置的方式来根据传入的HTTP请求构建这个Request对象。
不过这个可以通过上下文来传递下来,需要自己来做一些封装
本文暂时不细讲灰度的实现,后续有时间的话,我专门出一篇文章来说
自定义负载策略
通过"选择实例以及转发"章节的介绍,我们可以发现,loadbalancer主要提供了两个默认策略:
- RandomLoadBalancer:随机选择
- RoundRobinLoadBalancer:轮询
同时,他们又是被 @ConditionalOnMissingBean修饰,所以,如果我们想自定义自己的策略规则,我们直接通过 @Configuration和@Bean,注入自己的策略就行,以下是一个简单示例
@Configuration
@LoadBalancerClients(defaultConfiguration = MyBalancerConfiguration.class)
public class MyBalancerConfiguration {
/**
* 自定义负载均衡器
*/
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new MyLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
其中MyLoadBalancer是我们自定义的规则
@Slf4j
public class MyLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final String serviceId;
//服务列表
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public MyLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> supplier, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = supplier;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
/**
* 进行路由选择
*/
...
}
}
拓展
ReactiveLoadBalancer 和ServiceInstanceListSupplier 区别
概念区分
组件名称 | 作用 |
---|---|
ServiceInstanceListSupplier | 负责提供某个服务的所有可用实例列表(获取并缓存服务实例列表)。 |
ReactiveLoadBalancer | 负责基于负载均衡策略,从 ServiceInstanceListSupplier 提供的实例列表中选择一个合适的实例。 |
简单来说:
ServiceInstanceListSupplier
负责提供候选实例列表。ReactiveLoadBalancer
负责从这些候选实例中挑选一个最终的实例。
核心职责
ServiceInstanceListSupplier
-
主要作用:提供目标服务的可用
ServiceInstance
列表。 -
底层实现:它会通过
DiscoveryClient
(如 Nacos、Eureka 等)获取服务实例列表,并可能会对实例进行缓存、筛选、排序等操作。 -
接口定义:
public interface ServiceInstanceListSupplier { Flux<List<ServiceInstance>> get(); }
- 该接口返回一个
Flux<List<ServiceInstance>>
,代表着它是响应式的,能够动态推送服务实例列表更新(比如当 Nacos 服务实例变更时)。 - 其默认实现
DiscoveryClientServiceInstanceListSupplier
会基于DiscoveryClient
获取实例信息。
- 该接口返回一个
-
可以自定义过滤、排序规则:
-
例如,你可以扩展
ServiceInstanceListSupplier
,让它优先返回健康检查通过的实例,或者特定版本的实例。 -
代码示例:
@Component public class MyCustomInstanceSupplier implements ServiceInstanceListSupplier { private final DiscoveryClient discoveryClient; public MyCustomInstanceSupplier(DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } @Override public Flux<List<ServiceInstance>> get() { return Flux.defer(() -> { List<ServiceInstance> instances = discoveryClient.getInstances("my-service"); // 自定义筛选逻辑,比如过滤掉某些状态的实例 List<ServiceInstance> filteredInstances = instances.stream() .filter(instance -> instance.getMetadata().get("status").equals("UP")) .collect(Collectors.toList()); return Flux.just(filteredInstances); }); } }
-
ReactiveLoadBalancer
-
主要作用:根据某种负载均衡算法,从
ServiceInstanceListSupplier
提供的实例中挑选一个。 -
底层实现:它的核心方法是:
public interface ReactiveLoadBalancer<T> { Mono<Response<T>> choose(Request request); }
choose(Request request)
: 选择一个具体的ServiceInstance
。- 其中
T
通常是ServiceInstance
,也可以是Response<ServiceInstance>
(包含 metadata)。
-
默认实现
RoundRobinLoadBalancer
:基于轮询算法选择实例。RandomLoadBalancer
:随机选择一个实例。CachingServiceInstanceListSupplier
:基于缓存提高性能,避免频繁查询服务列表。
-
示例:自定义
ReactiveLoadBalancer
-
例如,你可以自定义负载均衡算法,优先选择 CPU 负载最低的实例:
@Component public class MyCustomLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> { private final ServiceInstanceListSupplier supplier; public MyCustomLoadBalancer(ServiceInstanceListSupplier supplier) { this.supplier = supplier; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { return supplier.get() .map(instances -> { // 选择 CPU 负载最低的实例 ServiceInstance selectedInstance = instances.stream() .min(Comparator.comparing(instance -> getCpuLoad(instance))) .orElse(null); return new DefaultResponse(selectedInstance); }); } private double getCpuLoad(ServiceInstance instance) { // 这里假设实例 metadata 里包含 cpu_load return Double.parseDouble(instance.getMetadata().getOrDefault("cpu_load", "100")); } }
-
完整负载均衡流程
- 客户端发起请求
- 例如,Spring Cloud Gateway 或 RestTemplate 发起对
my-service
的调用。
- 例如,Spring Cloud Gateway 或 RestTemplate 发起对
- 获取可用实例列表
ServiceInstanceListSupplier.get()
从注册中心(Nacos、Eureka)获取my-service
的所有实例。
- 选择负载均衡策略
ReactiveLoadBalancer.choose(request)
使用RoundRobinLoadBalancer
或自定义策略,选择一个实例。
- 最终返回一个实例
ReactiveLoadBalancer
选出具体的ServiceInstance
,并返回给WebClient
或RestTemplate
进行请求。
示意图:
请求 -> ServiceInstanceListSupplier 获取实例列表 -> ReactiveLoadBalancer 选择实例 -> 返回实例给调用方
总结
组件 | 主要作用 | 关键方法 | 关系 |
---|---|---|---|
ServiceInstanceListSupplier | 提供某个服务的可用实例列表 | Flux<List<ServiceInstance>> get() | 负责获取候选服务实例 |
ReactiveLoadBalancer | 依据负载均衡策略选择具体实例 | Mono<Response<ServiceInstance>> choose(Request request) | 负责选择最终的服务实例 |
ServiceInstanceListSupplier
负责从服务发现组件(如 Nacos)获取所有可用实例,可能还会做缓存、排序、过滤。ReactiveLoadBalancer
负责根据负载均衡策略(轮询、随机、自定义策略)从ServiceInstanceListSupplier
获取的实例中挑选一个最终的实例。ReactiveLoadBalancer
依赖ServiceInstanceListSupplier
,它无法直接从 Nacos 之类的服务注册中心获取实例,而是必须由ServiceInstanceListSupplier
提供候选实例。