目录

Spring-Cloud-LoadBalancer-原理与实践

Spring Cloud LoadBalancer 原理与实践

背景

当前我们的微服务架构基于Spring Cloud Alibaba体系,通过定制NacosRule实现了跨集群访问和灰度发布功能。但随着Spring Cloud与Nacos版本升级,官方已弃用Ribbon转向LoadBalancer,这要求我们完成以下技术升级:

  1. 负载均衡机制迁移:将原有Ribbon规则适配到LoadBalancer
  2. 功能继承保障:保持跨集群路由和灰度能力
  3. 技术风险控制:深入理解底层机制以提升问题排查效率 技术选型对比
    特性RibbonLoadBalancer
    维护状态停止更新官方维护
    响应式支持不支持原生支持
    配置灵活性XML/注解全Java配置
    扩展性中等
    服务发现集成需要适配深度整合

负载均衡

什么是负载均衡?简单来说,负载均衡就是将网络流量(负载)分摊到不同的网络服务器(可以平均分配,也可以不平均),系统就可以实现服务的水平横向扩展。

服务端负载均衡

服务器端负载均衡指的是存放在服务器端的负载均衡器,例如 Nginx、HAProxy、F5 等。 https://i-blog.csdnimg.cn/direct/0fec7de438c9442cab0c0aca92c73da8.png#pic_center

客户端负载均衡

客户端负载均衡指的是嵌套在客户端的负载均衡器,例如 Ribbon、Loadbalancer。 https://i-blog.csdnimg.cn/direct/9a51576af25a496bbc3e5bf6e7a12f30.png#pic_center

Spring cloud loadbalancer

在介绍loadbalancer之前,如果让你来设计一个负载均衡组件,你会怎么设计? 我们可能需要考虑以下这几个问题:

  • 如何获取服务器列表?
  • 服务器列表发生变更如何监听同步?
  • 如何将客户端请求进行拦截然后选择服务器进行转发?
  • 如何将负载进行分摊? 带着这些问题,我们来深入了解Spring Cloud LoadBalancer的实现机制。

服务器列表获取

ServiceInstanceListSupplier public interface ServiceInstanceListSupplier extends Supplier» { // 服务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 instances; @Deprecated public static Builder with(Environment env) { return new Builder(env); } //构造 FixedServiceInstanceListSupplier,接受服务 ID 和实例列表。 private FixedServiceInstanceListSupplier(String serviceId, List instances) { this.serviceId = serviceId; this.instances = instances; } //返回当前 FixedServiceInstanceListSupplier 所管理的 serviceId。 @Override public String getServiceId() { return serviceId; } //返回固定的实例列表,不会随 Nacos 或 Eureka 变化。 @Override public Flux> get() { return Flux.just(instances); } } 该接口提供符合条件的实例列表,并提供了 builder 方法返回 ServiceInstanceListSupplierBuilder 实例用来构造 ServiceInstanceListSupplier 同时FixedServiceInstanceListSupplier,它返回固定的服务实例列表。它主要用于

  • ** 测试负载均衡逻辑**(在不依赖 Nacos/Eureka 的情况下提供固定实例)。**
  • 静态配置服务实例 (比如在某些特殊场景下,不使用注册中心,而是固定 IP+端口)。 示例 List 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 extends ReactiveLoadBalancer { /**

  • Choose the next server based on the load balancing algorithm.
  • @param request - an input request
  • @return - mono of response */ @SuppressWarnings(“rawtypes”) Mono> choose(Request request); default Mono> 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 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> configurations; public LoadBalancerAutoConfiguration( ObjectProvider> 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 execute(String serviceId, LoadBalancerRequest 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 execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest 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 loadBalancer = loadBalancerClientFactory .getInstance(serviceId); if (loadBalancer == null) { return null; } Response 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 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 serviceInstanceListSupplierProvider; public MyLoadBalancer(ObjectProvider supplier, String serviceId) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = supplier; } @Override public Mono> choose(Request request) { /*
  • 进行路由选择 */ … } }

拓展

ReactiveLoadBalancer 和ServiceInstanceListSupplier 区别

概念区分
组件名称作用
ServiceInstanceListSupplier负责提供某个服务的所有可用实例列表(获取并缓存服务实例列表)。
ReactiveLoadBalancer负责基于负载均衡策略,从 ServiceInstanceListSupplier
提供的实例列表中选择一个合适的实例。
简单来说:
  • ServiceInstanceListSupplier 负责提供候选实例列表
  • ReactiveLoadBalancer 负责从这些候选实例中挑选一个最终的实例
核心职责

ServiceInstanceListSupplier

  • 主要作用 :提供目标服务的可用 ServiceInstance 列表。
  • 底层实现 :它会通过 DiscoveryClient(如 Nacos、Eureka 等)获取服务实例列表,并可能会对实例进行缓存、筛选、排序等操作。
  • 接口定义 : public interface ServiceInstanceListSupplier { Flux> get(); }
  • 该接口返回一个 Flux>,代表着它是响应式的 ,能够动态推送 服务实例列表更新(比如当 Nacos 服务实例变更时)。
  • 其默认实现 DiscoveryClientServiceInstanceListSupplier 会基于 DiscoveryClient 获取实例信息。
  • 可以自定义过滤、排序规则
  • 例如,你可以扩展 ServiceInstanceListSupplier,让它优先返回健康检查通过的实例 ,或者特定版本的实例
  • 代码示例: @Component public class MyCustomInstanceSupplier implements ServiceInstanceListSupplier { private final DiscoveryClient discoveryClient; public MyCustomInstanceSupplier(DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } @Override public Flux> get() { return Flux.defer(() -> { List instances = discoveryClient.getInstances(“my-service”); // 自定义筛选逻辑,比如过滤掉某些状态的实例 List filteredInstances = instances.stream() .filter(instance -> instance.getMetadata().get(“status”).equals(“UP”)) .collect(Collectors.toList()); return Flux.just(filteredInstances); }); } }

ReactiveLoadBalancer

  • 主要作用 :根据某种负载均衡算法,从 ServiceInstanceListSupplier 提供的实例中挑选一个。
  • 底层实现 :它的核心方法是: public interface ReactiveLoadBalancer { Mono> choose(Request request); }
  • choose(Request request): 选择一个具体的 ServiceInstance
  • 其中 T 通常是 ServiceInstance,也可以是 Response(包含 metadata)。
  • 默认实现
  • RoundRobinLoadBalancer:基于轮询算法选择实例。
  • RandomLoadBalancer:随机选择一个实例。
  • CachingServiceInstanceListSupplier:基于缓存提高性能,避免频繁查询服务列表。
  • 示例:自定义ReactiveLoadBalancer
  • 例如,你可以自定义负载均衡算法,优先选择 CPU 负载最低的实例: @Component public class MyCustomLoadBalancer implements ReactiveLoadBalancer { private final ServiceInstanceListSupplier supplier; public MyCustomLoadBalancer(ServiceInstanceListSupplier supplier) { this.supplier = supplier; } @Override public Mono> 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”)); } }
完整负载均衡流程
  1. 客户端发起请求
  • 例如,Spring Cloud Gateway 或 RestTemplate 发起对 my-service 的调用。
  1. 获取可用实例列表
  • ServiceInstanceListSupplier.get() 从注册中心(Nacos、Eureka)获取 my-service 的所有实例。
  1. 选择负载均衡策略
  • ReactiveLoadBalancer.choose(request) 使用 RoundRobinLoadBalancer 或自定义策略,选择一个实例。
  1. 最终返回一个实例
  • ReactiveLoadBalancer 选出具体的 ServiceInstance,并返回给 WebClientRestTemplate 进行请求。 示意图: 请求 -> ServiceInstanceListSupplier 获取实例列表 -> ReactiveLoadBalancer 选择实例 -> 返回实例给调用方

总结
组件主要作用关键方法关系
ServiceInstanceListSupplier提供某个服务的可用实例列表
Flux> get()负责获取候选服务实例
ReactiveLoadBalancer依据负载均衡策略选择具体实例`Mono>
choose(Request request)`负责选择最终的服务实例
  • ServiceInstanceListSupplier 负责从服务发现组件(如 Nacos)获取所有可用实例,可能还会做缓存、排序、过滤
  • ReactiveLoadBalancer 负责根据负载均衡策略(轮询、随机、自定义策略)从 ServiceInstanceListSupplier 获取的实例中挑选一个最终的实例。
  • ReactiveLoadBalancer 依赖 ServiceInstanceListSupplier,它无法直接从 Nacos 之类的服务注册中心获取实例,而是必须由 ServiceInstanceListSupplier 提供候选实例。