负载均衡轮询(Round - Robin)策略是一种按照顺序依次将客户端请求分配给后端服务器的方法。假设有服务器 A、B、C,第一个请求会被分配到服务器 A,第二个请求分配到服务器 B,第三个请求分配到服务器 C,然后再从服务器 A 开始下一轮分配,如此循环往复。它是一种简单且公平的负载分配策略,能确保每个服务器都能获得相对均等的请求处理机会。
如下图:
轮询策略严格按照顺序将请求分配给服务器,每个服务器被分配请求的顺序是固定且循环的。这种公平的分配方式保证了所有服务器在一段时间内处理的请求数量大致相同,避免了某些服务器过度使用或闲置的情况,尤其适用于服务器性能相近的场景。
例如,在一个由相同配置的 Web 服务器组成的集群中,轮询策略能够均匀地将用户对网页的访问请求分配到各个服务器,使每个服务器的负载水平基本保持一致。
由于请求分配的顺序是固定的,所以其行为具有高度的可预测性。这对于系统的维护和监控非常有利,管理员可以很容易地预估每个服务器的负载情况,并且根据服务器的处理能力和请求流量来提前规划资源配置。
例如,如果知道每台服务器在单位时间内能够处理 100 个请求,通过轮询策略,当有 300 个请求到来时,管理员可以准确地预测出每台服务器将处理 100 个请求。
在 application.properties 中,使用如下配置:
user.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
将在名为 user 的客户端上开启轮训策略。
Ribbon 轮询策略通过 com.netflix.loadbalancer.RoundRobinRule 类实现,主要看 choose 方法:
public Server choose(ILoadBalancer lb, Object key) { // 如果传入的负载均衡器为null,则记录警告日志并返回null, // 表示无法进行服务器选择。 if (lb == null) { log.warn("no load balancer"); return null; } else { Server server = null; int count = 0; // 循环选择服务器 // 使用一个无限循环来不断尝试选择服务器,直到找到合适的服务器或者达到最大尝试次数 while(true) { // 检查server是否为null且尝试次数count小于 10。如果满足条件,则进行以下操作: if (server == null && count++ < 10) { // 获取可达服务器列表(reachableServers)和所有服务器列表(allServers) List<Server> reachableServers = lb.getReachableServers(); List<Server> allServers = lb.getAllServers(); // 计算可达服务器数量(upCount)和总服务器数量(serverCount) int upCount = reachableServers.size(); int serverCount = allServers.size(); // 如果可达服务器数量和总服务器数量都不为 0,则通过incrementAndGetModulo(serverCount) // 方法获取下一个服务器索引,并从所有服务器列表中获取对应的服务器。 if (upCount != 0 && serverCount != 0) { int nextServerIndex = this.incrementAndGetModulo(serverCount); server = (Server)allServers.get(nextServerIndex); // 如果获取的服务器为null,则调用Thread.yield()让出 CPU 时间片,然后继续循环尝试 if (server == null) { Thread.yield(); } else { // 如果服务器不为null,则检查服务器是否活着(isAlive()) // 且准备好服务(isReadyToServe())。 if (server.isAlive() && server.isReadyToServe()) { // 如果是,则返回该服务器; return server; } // 如果不是,则将server置为null,继续循环尝试 server = null; } continue; } log.warn("No up servers available from load balancer: " + lb); return null; } // 如果尝试次数count大于等于 10,则记录警告日志,并返回当前的server(可能为null) if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } } }
继续看看 this.incrementAndGetModulo(serverCount) 方法:
// modulo 是所有服务器的数量,即 serverCount // int serverCount = allServers.size(); private int incrementAndGetModulo(int modulo) { int current; int next; do { // nextServerCyclicCounter 是 AtomicInteger 类型成员变量 current = this.nextServerCyclicCounter.get(); // 计算下一个值 next,通过将当前值加一,并对modulo取模得到。 // 这样可以确保生成的值始终在 0 到 modulo-1 的范围内 next = (current + 1) % modulo; // 原子操作: // 使用 AtomicInteger 的 compareAndSet 方法确保了操作的原子性。 // 通过不断尝试更新,直到成功为止,可以确保最终得到的结果是正确的,并且不会出现丢失更新的情况。 } while(!this.nextServerCyclicCounter.compareAndSet(current, next)); return next; }
注意:在多线程环境下,AtomicInteger.compareAndSet 可以避免多个线程同时更新计数器时出现数据不一致的问题。例如,如果没有使用原子操作,多个线程可能同时读取到相同的当前值,计算出相同的下一个值,并尝试更新计数器,导致结果不可预测。
简单易理解和实现:轮询策略的逻辑清晰明了,实现起来相对简单。无论是通过硬件设备还是软件算法实现,都不需要复杂的配置和大量的计算资源。在代码层面,通常只需要维护一个指向当前服务器的指针,每次请求后将指针向后移动一位即可。
负载均衡效果好(服务器性能相近时):当后端服务器的性能差异不大时,轮询策略能够有效地将负载平均分配到各个服务器上,充分利用服务器资源,提高系统的整体性能和稳定性。
无法适应服务器性能差异:如果后端服务器的性能不一致,轮询策略可能会导致性能较差的服务器成为系统的瓶颈。例如,有两台服务器,一台处理速度是另一台的两倍,按照轮询策略分配请求,性能差的服务器可能会因为无法及时处理分配到的请求而出现响应延迟,甚至可能导致请求堆积和系统故障。
缺乏灵活性:轮询策略是一种相对固定的分配方式,不能根据服务器的实时负载、响应时间等动态因素进行调整。即使某个服务器出现故障或者负载过高,它仍然会按照固定的顺序分配请求,可能会影响系统的整体性能和可用性。
服务器性能相同或相近的场景:适用于服务器硬件配置、软件环境以及处理能力基本相同的服务器集群。例如,在一个数据中心中,新部署的一批相同型号的服务器用于提供相同的服务,如运行多个相同配置的数据库副本或者 Web 应用服务器,轮询策略可以很好地将请求均匀分配到这些服务器上,保证系统的高效运行。
对请求处理顺序没有严格要求的服务:对于那些不依赖于特定服务器处理顺序的服务,轮询策略是一个很好的选择。比如,在一个提供文件下载或者图片浏览服务的服务器集群中,每个请求之间相对独立,不需要在同一台服务器上进行连续处理,轮询策略可以有效地分配请求负载,提高服务的响应速度和用户体验。