← 中文资源

简体中文版本

负载均衡算法及其失效场景

轮询、最少连接与一致性哈希的分流方式各不相同;选错算法会造成热点,看起来却像容量不足。

阅读英文原文

计数不等于衡量

轮询让每个后端拿到相同数量的请求,但只有每个请求成本相同时才公平。当一个端点触发报表、另一个只返回缓存记录时,相同的请求数会产生完全不同的 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;
  }
}