在分布式系统、负载均衡器、RPC 框架中,常常需要在多个服务实例之间进行流量分配。如果实例的能力不同,就需要根据"权重"来调度请求。常见的负载算法有:
- 普通轮询 (Round Robin, RR):所有实例均匀分配请求;
- 加权随机 (Weighted Random):按权重比例随机选择实例;
- 加权轮询 (Weighted Round Robin, WRR):按照权重比例循环选择实例。
但是,传统的 WRR 有个缺点:容易出现高权重实例连续被选中的情况,短时间内分布不均衡。
为了解决这个问题,Nginx 提出了 平滑加权轮询(Smooth Weighted Round Robin, SWRR) 算法。
1. 为什么需要 SWRR?
举个例子:有三个实例,权重分别是 A=5, B=1, C=1。
传统 WRR 的可能调度序列:
A, A, A, A, A, B, C
A 连续五次,虽然长期比例正确,但短期内不平滑,可能导致抖动。
SWRR 的调度序列:
A, A, B, A, C, A, A
同样保证了比例 5:1:1,但分布更加均匀,用户体验更稳定。
2. SWRR 算法原理
SWRR 的核心思想是:
每个实例维护一个动态权重 currentWeight,每次调用时:
- 累加 :对所有实例执行
currentWeight += weight
- 选择 :选择
currentWeight
最大的实例作为本次结果; - 扣减 :对选中的实例执行
currentWeight -= totalWeight
(totalWeight
是所有权重之和)
这样不断迭代,currentWeight
会在一个"周期"内上下波动,保证:
- 长期调度比例符合权重;
- 短期分布尽量均匀;
- 周期结束时,
currentWeight
回到全 0,进入下一轮。
3. 示例推演(A=5, B=1, C=1)
初始状态:(A=0, B=0, C=0)
,总权重=7。
调用次序 | 累加后 (A,B,C) | 选中 | 扣减后 (A,B,C) |
---|---|---|---|
1 | (5,1,1) | A | (-2,1,1) |
2 | (3,2,2) | A | (-4,2,2) |
3 | (1,3,3) | B | (1,-4,3) |
4 | (6,-3,4) | A | (-1,-3,4) |
5 | (4,-2,5) | C | (4,-2,-2) |
6 | (9,-1,-1) | A | (2,-1,-1) |
7 | (7,0,0) | A | (0,0,0) |
初始: (0,0,0) 累加: (5,1,1)
选择 A → 减 7 → (-2,1,1) 累加: (3,2,2)
选择 A → 减 7 → (-4,2,2) 累加: (1,3,3)
选择 B → 减 7 → (1,-4,3) 累加: (6,-3,4)
选择 A → 减 7 → (-1,-3,4) 累加: (4,-2,5)
选择 C → 减 7 → (4,-2,-2) 累加: (9,-1,-1)
选择 A → 减 7 → (2,-1,-1) 累加: (7,0,0)
选择 A → 减 7 → (0,0,0)
可以看到:
- 7 次调用刚好符合权重比例 (5:1:1);
- 结束后状态回到
(0,0,0)
,进入下一个周期; - 周期性地产生 A, A, B, A, C, A, A 这样平滑的序列。
4. Java 实现
下面给出一个 Java 版的 SWRR 负载均衡器 ,它能够在多实例(ModelInstance)之间,按照各自权重 长期比例接近、短期分布尽量平滑 地分配请求。
核心处理流程:
-
每次选择前:对每个实例的 currentWeight += weight。
-
选出 currentWeight 最大的实例 best。
-
令 best.currentWeight -= totalWeight(totalWeight = Σ weight)。
-
下次选择重复 1~3。
-
这样就能得到"比例接近权重、且不突发"的平滑分布(如 5:1:1 会呈现 A,A,B,A,C,A,A 这种节奏)。
当 所有权重之和 ≤ 0 时,这里退化为 普通轮询(Round Robin)。
java
import per.mjn.route_rule.domain.entity.ModelInstance;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* 平滑加权轮询(Smooth Weighted Round Robin)
* 按模型粒度加锁,线程安全。
*/
public class WeightedRoundRobinLoadBalancer {
private static class Weighted {
int weight; // 固定权重
int currentWeight = 0; // 动态权重
ModelInstance instance;
}
/** 每个模型的状态 */
private final Map<String, List<Weighted>> stateMap = new ConcurrentHashMap<>();
/** 每个模型一把锁,避免全局阻塞 */
private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
/** 当所有权重 <= 0 时,退化为普通轮询 */
private final Map<String, AtomicInteger> rrCounters = new ConcurrentHashMap<>();
public ModelInstance choose(List<ModelInstance> instances, String modelKey) {
if (instances == null || instances.isEmpty()) return null;
final String key = String.valueOf(modelKey);
ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock());
lock.lock();
try {
instances.sort(Comparator.comparing(ModelInstance::getId));
// 初始化/刷新状态
List<Weighted> state = stateMap.compute(key, (k, old) -> {
if (old == null || old.size() != instances.size() || !equalsByWeight(old, instances)) {
return build(instances);
}
return old;
});
int totalWeight = state.stream().mapToInt(w -> w.weight).sum();
if (totalWeight <= 0) {
AtomicInteger c = rrCounters.computeIfAbsent(key, k -> new AtomicInteger(0));
int idx = Math.floorMod(c.getAndIncrement(), state.size());
return state.get(idx).instance;
}
Weighted best = null;
for (Weighted w : state) {
w.currentWeight += w.weight;
if (best == null || w.currentWeight > best.currentWeight) {
best = w;
}
}
best.currentWeight -= totalWeight;
return best.instance;
} finally {
lock.unlock();
}
}
private boolean equalsByWeight(List<Weighted> old, List<ModelInstance> instances) {
if (old.size() != instances.size()) return false;
for (int i = 0; i < old.size(); i++) {
if (!Objects.equals(old.get(i).instance.getId(), instances.get(i).getId())) return false;
if (old.get(i).weight != safeWeight(instances.get(i).getWeight())) return false;
}
return true;
}
private List<Weighted> build(List<ModelInstance> instances) {
List<Weighted> list = new ArrayList<>();
for (ModelInstance ins : instances) {
Weighted w = new Weighted();
w.weight = safeWeight(ins.getWeight());
w.instance = ins;
list.add(w);
}
return list;
}
private int safeWeight(Integer w) {
return (w == null || w < 0) ? 1 : w;
}
}
流程图如下:
是 否 否 是 是 否 开始: choose 方法 instances 是否为空? 返回 null 结束 按实例ID排序 保证稳定顺序 stateMap 是否存在 且与实例及权重一致? 重建状态: currentWeight 置零 并载入权重 读取旧状态 计算总权重: 所有权重之和 总权重 小于等于 0 ? 退化为普通轮询 计算 idx: 计数器的值对 N 取模 返回第 idx 个实例 遍历: 每项的 currentWeight 加上自身权重 选出 currentWeight 最大的实例 记为 best 将 best 的 currentWeight 减去 总权重 返回 best 实例
5. 特点与优势
- 平滑性:避免了高权重实例集中出现,调度更加均匀;
- 比例准确:长期来看,命中次数严格符合权重;
- 周期性:每次执行"权重和"次调用后,状态回到初始值,进入下一个周期;
- 适合高并发:通过按模型粒度加锁,避免全局瓶颈。
6. 总结
- 普通轮询:公平,但无法体现实例差异。
- 加权随机:简单,可能有抖动。
- 加权轮询:比例正确,但不够平滑。
- 平滑加权轮询 (SWRR):既保证比例,又让分布均匀,是生产环境负载均衡的常用算法(Nginx、Dubbo 等框架都在用)。
SWRR 看似简单,实则优雅。它通过"累加 + 扣总和"的小技巧,把"按比例"与"平滑性"完美结合,是一个非常值得学习的经典调度算法。如果需要一个既能体现实例权重,又能保持稳定性的算法,那么SWRR 不失为一种好的选择。