功能说明
该示例实现 nginx 风格的平滑加权轮询。每个节点有权重与当前分数,每次选择前把所有节点的当前分数加上各自权重,选出分数最高者,再让它减去总权重。这样权重为五、一、一的三节点不会连续五次都选中高权重节点,而是按比例均匀穿插,适合无状态服务的客户端负载均衡与灰度流量分配。
关键代码
add 校验权重为正整数并累加总权重;remove 按值查找并同步扣减总权重,未找到返回 false。next 在空集合时抛错,然后一次遍历完成加分与比较,选中节点后减去总权重,使所有分数长期保持有界。比较使用严格大于,因此分数相同时保留添加顺序,结果可预测。节点值按 Object.is 比较,支持字符串、对象引用等任意类型。
使用注意
权重只能是正整数,若业务使用百分比或小数权重,需要先放大为整数。节点运行状况不参与选择,失败节点必须先 remove,否则仍会按权重被选中。该实现不是并发安全的,多线程环境要加锁或使用每线程独立实例。动态修改权重需要删除后重新添加,且会重置该节点的当前分数,短期内分布可能偏离。
验证方式
添加权重为五、一、一的三节点,连续调用七次并统计各节点次数,断言比例正确且高权重节点不会连续出现;检查相同权重时按添加顺序轮转;remove 后总权重正确且不再被选中;空集合调用 next 抛错;权重为零、负数或小数时 add 抛 RangeError。
代码示例
interface WeightedNode<T> {
value: T;
weight: number;
current: number;
}
export class SmoothWeightedRoundRobin<T> {
private readonly nodes: WeightedNode<T>[] = [];
private totalWeight = 0;
add(value: T, weight: number): this {
if (!Number.isInteger(weight) || weight < 1) {
throw new RangeError("weight must be a positive integer");
}
this.nodes.push({ value, weight, current: 0 });
this.totalWeight += weight;
return this;
}
remove(value: T): boolean {
const index = this.nodes.findIndex((node) => Object.is(node.value, value));
if (index === -1) return false;
this.totalWeight -= this.nodes[index].weight;
this.nodes.splice(index, 1);
return true;
}
next(): T {
if (this.nodes.length === 0) throw new Error("no nodes available");
let best = this.nodes[0];
for (const node of this.nodes) {
node.current += node.weight;
if (node.current > best.current) best = node;
}
best.current -= this.totalWeight;
return best.value;
}
get size(): number {
return this.nodes.length;
}
}