计数不等于衡量
轮询让每个后端拿到相同数量的请求,但只有每个请求成本相同时才公平。当一个端点触发报表、另一个只返回缓存记录时,相同的请求数会产生完全不同的 CPU 压力。这种表现看起来像容量问题,实质是分布问题。
常见算法
轮询简单可预测,但对成本视而不见;加权轮询把已知的容量差异编码进去,可权重会过期;最少连接能适应慢后端,前提是连接数能代表工作量;最少响应时间对延迟敏感,但测量噪声大;两次随机选择(power of two choices)随机挑两个后端再选较闲的一个,成本低且效果稳定。例如,同时承载读写端点的网关,用两次随机选择往往比最少连接更均衡,因为写请求占用连接的时间远长于读请求。
一致性哈希的取舍
一致性哈希让同一个 key 在实例增减时仍落在同一后端,这对缓存和分片状态至关重要。代价是均衡性:虚拟节点太少时,某个后端可能承担过高比例。增加虚拟节点能平滑分布,但会提高内存与查找开销。
如何选择
从与请求成本匹配的最简单算法开始,观察每个后端的真实负载而不只是请求数。每次容量变化后重新校准权重,并压测负载均衡器本身:算法配置失误往往在后端饱和之前就耗尽了连接池。
代码示例
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;
}
}