Spring Cloud LoadBalancer 教程

轮询策略(RoundRobinLoadBalancer)

在 Spring Cloud LoadBalancer 2025 版本中,轮询策略(RoundRobinLoadBalancer) 是默认的负载均衡算法,其核心逻辑是按照固定顺序依次将请求分发到可用的服务实例,适用于服务实例性能相近、负载较为均衡的场景。

 

核心原理

轮询策略的核心思想是“顺序循环”:维护一个计数器(或指针),每次请求到达时,计数器递增并取模(对可用实例数量取模),得到当前应选择的实例索引,确保请求在所有可用实例间均匀分配。

例如:若有 3 个服务实例(A、B、C),则请求顺序为:A → B → C → A → B → C → ...

 

适用场景

轮询策略作为 Spring Cloud LoadBalancer 的默认负载均衡策略,可用于以下场景:

  • 服务实例性能差异小、负载相对均衡

轮询策略的核心假设是 “所有服务实例的处理能力与资源配置一致”,因此在实例性能同质化的集群中,能天然适配负载分配需求。

例如电商系统中的 “商品详情服务”,所有实例均部署在相同规格的服务器上(如 4C8G 云服务器),且服务逻辑无特殊资源依赖(如无本地缓存、无硬件加速需求),每个实例处理单个请求的响应时间、CPU 占用率差异小于 10%。此时轮询策略无需额外计算权重或负载,即可让每个实例接收相近数量的请求,避免因 “性能评估偏差” 导致的分配失衡。

对于完全无状态的服务(如用户登录校验服务、数据格式转换服务),实例间无需共享会话或数据,仅需根据请求量横向扩展实例数量。例如某 API 网关后端挂载 5 个相同配置的 “权限校验服务” 实例,轮询策略会按顺序将请求依次分发至 5 个实例,每个实例的请求量理论占比均为 20%,且实例扩容 / 缩容时(如从 5 个增至 8 个),策略会自动基于新的实例数量调整轮询范围,无需人工干预。

  • 希望请求在所有实例间均匀分配,避免某一实例过载

当业务对 “实例负载均衡度” 要求较高,且不希望因算法偏向性导致某一实例长期过载时,轮询策略的 “绝对均匀性” 成为核心优势。

例如金融系统中的 “账单查询服务”,每日 9:00-18:00 期间请求量稳定在 1000 QPS,且无突发流量峰值。此时若使用 “随机策略” 可能因概率偏差导致某一实例短时间接收大量请求(如 10 个实例中某实例 1 分钟内接收 120 次请求,另一实例仅接收 80 次),而轮询策略能确保每个实例的请求量偏差控制在 “±1 次 / 分钟” 内,避免因局部过载导致的响应延迟或超时。

  • 简单场景下的快速集成(无需复杂配置)

轮询策略作为 Spring Cloud LoadBalancer 的默认策略,无需额外引入依赖、无需编写自定义代码,仅需基础配置即可生效,特别适合快速迭代、轻量级架构的业务场景。

在业务初期(如 MVP 阶段),开发团队需快速搭建微服务架构并验证核心流程,无需在负载均衡策略上投入过多精力。例如某创业公司的 “用户注册服务”,仅部署 2 个实例用于测试,此时直接使用默认的轮询策略,无需配置权重、健康检查阈值等参数,即可实现基本的负载均衡功能,减少开发与调试时间。

 

局限性

在适应场景介绍了轮训策略的优点,它也存在如下缺点:

未考虑实例负载差异

轮训负载均衡策略完全平均分配请求,未考虑实例硬件配置差异。性能弱的实例(如低 CPU、低内存)会因接收同等数量请求而过载,性能强的实例则资源闲置。

为了解决该问题,后面进化出了加权轮询策略,它通过为实例分配不同权重,实现了请求的“按需分配”,核心逻辑如下:

  • 权重与性能挂钩:为性能强的实例设置更高权重(如 CPU 核数多的实例权重设为 5),性能弱的实例设置低权重(如 CPU 核数少的实例权重设为 1)。

  • 请求分配按权重比例:权重决定实例接收请求的概率,权重为 5 的实例接收的请求量约为权重 1 实例的 5 倍,实现资源利用与负载压力的平衡。

  • 兼容基础策略:当所有实例权重设置相同时,加权轮询会退化为基础轮询,保证了策略的兼容性和过渡性。

无会话亲和性

由于轮训负载均衡策略无法固定客户端与实例的对应关系(用户多次请求可能被分配到不同的机器上执行)。对于需要维持会话状态的业务(如登录状态、购物车数据),可能导致请求路由到不同实例,引发业务异常。

为了解决轮询策略中“无会话亲和性” 的问题(即无法保证同一客户端请求始终路由到同一实例),常见方案如下:

  • 基于客户端标识的固定路由(最常用)

通过提取客户端的唯一标识(如 IP 地址、用户 ID、设备 ID 等),计算哈希值并与服务实例列表绑定,确保同一标识的请求始终路由到同一实例。该方案实现简单,无需修改服务端。如果客户端标识分布不均,可能导致某些实例负载失衡(需结合权重调整)。

  • 会话共享(彻底消除会话依赖)

将会话状态从服务实例中剥离,存储到独立的共享存储中(如 Redis、Memcached),使客户端请求可路由到任意实例,实例通过共享存储获取会话信息。该方案彻底解耦会话与实例的绑定,支持实例动态扩缩容,适合分布式系统。但是,增加缓存依赖,需考虑缓存一致性和性能开销。

  • 框架自带的会话亲和性配置

部分负载均衡工具或反向代理支持开箱即用的会话亲和性(Sticky Session)配置。例如,Nginx 可以通过ip_hash指令基于客户端 IP 绑定实例,或cookie指令基于自定义 Cookie 绑定:

upstream backend {
    ip_hash;  # 基于客户端IP哈希绑定实例
    server instance1:8080;
    server instance2:8080;
}

 

RoundRobinLoadBalancer  的新特性

相比旧版本(如 2023 版),2025 版本的 RoundRobinLoadBalancer 做了以下优化:

  • 响应式优先:基于 ReactorLoadBalancer 接口实现,原生支持响应式编程(如 WebFlux),避免阻塞操作。

  • 实例动态感知:实时感知服务实例的上下线(通过 ServiceInstanceListSupplier 动态获取实例列表),自动调整轮询范围。

  • 健康实例过滤:默认结合健康检查机制,仅对健康实例进行轮询(不健康实例自动排除)。

  • 线程安全:使用原子类(AtomicInteger)维护计数器,确保高并发场景下的轮询顺序正确性。

源码分析

RoundRobinLoadBalancer 是基于轮询(Round-Robin)算法去实现 ReactorServiceInstanceLoadBalancer 接口。而 ReactorServiceInstanceLoadBalancer 接口是 Spring Cloud LoadBalancer 中用于响应式负载均衡的核心接口,负责从服务实例列表中选择合适的实例,以支持非阻塞的服务调用。

源码如下:

package org.springframework.cloud.loadbalancer.core;

//...

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
	// 日志工具,用于记录警告等信息
	private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class);

	// 原子计数器,用于记录当前轮询位置,保证线程安全
	final AtomicInteger position;

	// 服务ID,标识当前负载均衡器对应的服务
	final String serviceId;

	// 单例提供者,用于获取服务实例列表供应商(ServiceInstanceListSupplier)
	// 采用 SingletonSupplier 确保实例化一次,避免重复创建
	private final SingletonSupplier<ServiceInstanceListSupplier> serviceInstanceListSingletonSupplier;

	/**
	 * 初始化轮询负载均衡器
	 * @param serviceInstanceListSupplierProvider 
     * 			服务实例列表供应商的提供者(ObjectProvider),用于获取可用的服务实例列表
	 * @param serviceId 服务ID,当前负载均衡器对应的服务名称
	 */
	public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
			String serviceId) {
		// 调用重载构造方法,使用随机数作为初始位置(0-999之间)
		this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
	}

	/**
	 * 初始化轮询负载均衡器(指定初始位置)
	 * @param serviceInstanceListSupplierProvider 服务实例列表供应商的提供者
	 * @param serviceId 服务ID
	 * @param seedPosition 轮询初始位置标记,用于指定计数器的初始值
	 */
	public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
			String serviceId, int seedPosition) {
		this.serviceId = serviceId;
		// 初始化服务实例列表供应商的单例提供者:
		// 若获取不到可用的 Supplier,默认使用 NoopServiceInstanceListSupplier(空实现)
		this.serviceInstanceListSingletonSupplier = SingletonSupplier
			.of(() -> serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new));
		// 初始化原子计数器,使用指定的初始位置
		this.position = new AtomicInteger(seedPosition);
	}

	@SuppressWarnings("rawtypes")
	@Override
	// 参考 Netflix Ocelli 的轮询实现:
	// https://github.com/Netflix/ocelli/blob/master/ocelli-core/
	// src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
	public Mono<Response<ServiceInstance>> choose(Request request) {
		// 从单例提供者中获取服务实例列表供应商
		ServiceInstanceListSupplier supplier = serviceInstanceListSingletonSupplier.obtain();
		// 调用供应商的get方法获取服务实例列表(响应式操作),并处理结果
		return supplier.get(request)
			.next() // 取流中的第一个元素(最新的实例列表)
            // 处理实例列表,返回选中的实例
			.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}

	/**
	 * 处理服务实例列表响应,返回负载均衡选择结果
	 * @param supplier 服务实例列表供应商
	 * @param serviceInstances 可用的服务实例列表
	 * @return 包含选中实例的响应对象
	 */
	private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
		// 从实例列表中选择一个实例
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		// 若供应商实现了 SelectedInstanceCallback 接口,通知其选中的实例(用于回调扩展)
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
	}

	/**
	 * 核心方法:根据轮询算法从实例列表中选择一个实例
	 * @param instances 可用的服务实例列表
	 * @return 包含选中实例的响应(或空响应)
	 */
	private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
		// 若实例列表为空,返回空响应并记录警告日志
		if (instances.isEmpty()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}

		// 若只有一个可用实例,直接返回该实例(无需轮询,优化性能)
		// 注:某些供应商可能已过滤实例,此时单实例场景无需移动计数器
		if (instances.size() == 1) {
			return new DefaultResponse(instances.get(0));
		}

		// 计算当前轮询位置:
		// 1. 计数器自增(保证线程安全)
		// 2. 与 Integer.MAX_VALUE 按位与,忽略符号位(避免负数,确保 pos 为非负整数)
		int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;

		// 取模计算选中的实例索引(pos % 实例数量),实现循环轮询
		ServiceInstance instance = instances.get(pos % instances.size());

		return new DefaultResponse(instance);
	}

}

源码的关键逻辑说明:

  • position:原子计数器,每次请求时通过 incrementAndGet() 自增,确保线程安全。并且与 Integer.MAX_VALUE 进行按位与操作,确保不是负数。

  • 索引计算:pos % instances.size() 保证索引在 [0, 实例数量 - 1] 范围内,实现循环轮询。

  • 实例列表来源:通过 ServiceInstanceListSupplier 获取,默认已集成健康检查过滤(仅返回健康实例)。

更多内容请查阅官方文档……

 

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号