在分布式系统设计和面试中,负载均衡是一个绕不开的话题。而加权轮询(Weighted Round Robin, WRR)作为一种经典且实用的负载均衡策略,经常出现在笔试题和面试环节中。本文将带你深入理解 WRR 算法的原理,并探讨几种常见的 Java 实现方式及其优缺点,助你轻松应对相关考题。
想象一下,你有几台服务器,但它们的处理能力(CPU、内存等)不一样。你希望性能强的服务器能多处理一些请求,性能弱的少处理一些,同时还要保证所有服务器都有机会处理请求,避免“旱的旱死,涝的涝死”。
加权轮询就是为了解决这个问题。它允许我们为每台服务器设置一个“权重”(Weight),权重越高的服务器,在一段时间内被分配到的请求比例就越高。
核心思想: 按服务器权重比例,周期性地、有序地将请求分配给服务器。
根据你提供的资料,其基本概念可以概括为:
优点:
缺点:
下面我们来看几种 Java 实现 WRR 的思路,从简单到优化。
这是最直观的一种想法:如果服务器 A 权重为 3,服务器 B 权重为 1,那我就创建一个包含 [A, A, A, B] 的列表,然后对这个列表进行普通的轮询(Round Robin)。
实现思路:
初始化: 在类加载时(或首次使用时),根据服务器 IP 和对应的权重,构建一个扩展列表。例如,{"A": 3, "B": 1} 会扩展成 ["A", "A", "A", "B"] (顺序可以不同,但数量要对)。
选择节点: 维护一个全局(或实例)的索引 index。每次请求时:
代码示例 (基于你改进后的 WeightedRoundRobinSimple.java):
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class WeightedRoundRobinSimple {
private static Integer index = 0;
private static Map mapNodes = new HashMap<>();
// 扩展后的服务器列表,只计算一次
private static final List nodes = new ArrayList<>();
static {
// 模拟服务器和权重
mapNodes.put("192.168.1.101", 1); // 权重 1
mapNodes.put("192.168.1.102", 3); // 权重 3
mapNodes.put("192.168.1.103", 2); // 权重 2
// 预计算扩展列表
Iterator> iterator = mapNodes.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry = iterator.next();
String key = entry.getKey();
for (int i = 0; i < entry.getValue(); i++){
nodes.add(key); // 添加 'weight' 次
}
}
// 结果可能是 [101, 102, 102, 102, 103, 103] (顺序取决于 Map 迭代顺序)
System.out.println("预计算的服务器列表:" + nodes);
}
public String selectNode(){
if (nodes.isEmpty()) {
return null;
}
String selectedNode = null;
synchronized (WeightedRoundRobinSimple.class) { // 使用类锁保证线程安全
if(index >= nodes.size()) {
index = 0; // 到达列表末尾,重置索引
}
selectedNode = nodes.get(index);
index++;
}
return selectedNode;
}
// main 方法用于测试 (省略,与你提供的一致)
}
评价:
优点: 实现简单,逻辑清晰,易于理解。改进后(预计算列表)性能比每次调用都重新生成列表要好得多。
缺点:
为了解决简单扩展法不够平滑和内存占用的问题,出现了一种更优化的算法,常被称为“平滑加权轮询”,Nginx 的 WRR 实现就采用了类似的思想。
核心思想:
每个服务器维护两个权重:
算法步骤 (基于你提供的 WeightedRoundRobin 类 和 图片逻辑):
为什么这样能工作?
代码示例 (基于你提供的 WeightedRoundRobin.java):
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class WeightedRoundRobin {
// 内部 Node 类表示服务器
public static class Node { // 设为 static 方便外部访问或保持原样内部类
private String serverName;
private final Integer weight; // 固定权重
private Integer currentWeight; // 当前权重
public Node(String serverName, Integer weight) {
this.serverName = serverName;
this.weight = weight;
this.currentWeight = 0; // Nginx 风格通常初始化为 0
// 你的例子似乎是隐式等于 weight 开始? 统一按 0 开始演示
}
// Getters and Setters... (省略)
public String getServerName() { return serverName; }
public Integer getWeight() { return weight; }
public Integer getCurrentWeight() { return currentWeight; }
public void setCurrentWeight(Integer currentWeight) { this.currentWeight = currentWeight; }
}
private final List servers;
private final int totalWeight;
public WeightedRoundRobin(List servers) {
this.servers = servers;
// 计算总权重
this.totalWeight = servers.stream().mapToInt(Node::getWeight).sum();
// 初始化 currentWeight (如果需要的话,这里设为0)
// this.servers.forEach(s -> s.setCurrentWeight(0)); // 显式初始化为 0
}
// 注意:这个方法需要是线程安全的,如果实例被多线程共享
public synchronized String getServer() { // 添加 synchronized 保证线程安全
if (servers.isEmpty()) {
return null;
}
// 1. 所有 currentWeight += weight
for (Node server : servers) {
server.setCurrentWeight(server.getCurrentWeight() + server.getWeight());
}
// 2. 找到 currentWeight 最大的服务器
Node bestServer = servers.stream()
.max(Comparator.comparingInt(Node::getCurrentWeight))
.orElse(null); // 处理空列表情况
if (bestServer == null) return null;
// 3. 选中的服务器 currentWeight -= totalWeight
bestServer.setCurrentWeight(bestServer.getCurrentWeight() - totalWeight);
return bestServer.getServerName();
}
public static void main(String[] args) {
// 初始化服务器列表
List serverNodes = Arrays.asList(
new Node("192.168.1.1", 1),
new Node("192.168.1.2", 2),
new Node("192.168.1.3", 3),
new Node("192.168.1.4", 4)
);
WeightedRoundRobin roundRobin = new WeightedRoundRobin(serverNodes);
// 模拟请求分发 (总权重 1+2+3+4 = 10)
System.out.println("平滑加权轮询测试:");
for (int i = 0; i < 10; i++) { // 进行一个总权重周期的测试
String server = roundRobin.getServer();
System.out.println("请求 " + (i + 1) + " 发送到: " + server);
// 打印当前权重状态,方便理解
System.out.print(" 当前权重状态: ");
serverNodes.forEach(n -> System.out.print(n.getServerName() + "={" + n.getCurrentWeight() + "} "));
System.out.println();
}
}
}
评价:
优点:
缺点:
加权轮询是负载均衡中的一个重要算法。理解其原理和不同实现方式的权衡对于系统设计和面试都非常有帮助。简单列表扩展法易于理解但有局限,而平滑加权轮询(类似 Nginx 的实现)则提供了更优的平滑性和内存效率。在面试中,能够清晰地阐述这两种方法并根据场景选择或比较,将展现出你扎实的基础和分析能力。