负载均衡算法实战:从轮询到一致性哈希
发布日期: 2026/07/22 阅读总量: 1

真实场景:一个雪崩事故

2023年双11,我们一个微服务集群有4台节点:2台8核16G,2台4核8G。用的Nginx默认轮询,结果4核节点CPU冲到95%,8核节点只有30%。凌晨3点,4核节点OOM,请求全部转发到8核节点,8核节点扛不住也挂了。全站挂了12分钟。

事后复盘:轮询算法不考虑节点权重,导致资源不均。换加权轮询后,同样流量下CPU峰值降到60%。

四种负载均衡算法原理与实现

环境:PHP 8.3.0,单进程模拟,无外部依赖。服务器配置:Intel Xeon E5-2680 v4 @ 2.40GHz,16核32线程,64GB RAM。

1. 轮询(Round Robin)

最简单的算法:按顺序把请求分给每个节点,循环往复。

class RoundRobinBalancer {
    private array $servers;
    private int $current = 0;

    public function __construct(array $servers) {
        $this->servers = array_values($servers);
    }

    public function next(): string {
        $server = $this->servers[$this->current];
        $this->current = ($this->current + 1) % count($this->servers);
        return $server;
    }
}

// 使用
$balancer = new RoundRobinBalancer(['192.168.1.1:8080', '192.168.1.2:8080', '192.168.1.3:8080']);
for ($i = 0; $i < 10; $i++) {
    echo $balancer->next() . PHP_EOL;
}

输出:轮流返回三个IP,无权重。

2. 加权轮询(Weighted Round Robin)

给每个节点分配权重,权重高的节点被选中的概率大。实现用平滑加权轮询(Nginx算法),避免短时间内连续请求同一节点。

class WeightedRoundRobinBalancer {
    private array $servers; // ['ip' => weight]
    private array $currentWeights;

    public function __construct(array $servers) {
        $this->servers = $servers;
        $this->currentWeights = array_fill_keys(array_keys($servers), 0);
    }

    public function next(): string {
        $total = 0;
        $best = null;
        $bestWeight = -1;

        foreach ($this->servers as $ip => $weight) {
            $this->currentWeights[$ip] += $weight;
            $total += $weight;
            if ($this->currentWeights[$ip] > $bestWeight) {
                $bestWeight = $this->currentWeights[$ip];
                $best = $ip;
            }
        }

        $this->currentWeights[$best] -= $total;
        return $best;
    }
}

// 使用:权重3:2:1
$balancer = new WeightedRoundRobinBalancer([
    '192.168.1.1:8080' => 3,
    '192.168.1.2:8080' => 2,
    '192.168.1.3:8080' => 1,
]);
for ($i = 0; $i < 12; $i++) {
    echo $balancer->next() . PHP_EOL;
}

输出:1.1出现6次,1.2出现4次,1.3出现2次,分布均匀。

3. 哈希(Hash)

对请求的某个标识(如用户ID、IP)做哈希,映射到节点。保证同一标识的请求始终落在同一节点,适合会话保持。

class HashBalancer {
    private array $servers;

    public function __construct(array $servers) {
        $this->servers = $servers;
    }

    public function getServer(string $key): string {
        $hash = crc32($key);
        $index = $hash % count($this->servers);
        return $this->servers[$index];
    }
}

// 使用
$balancer = new HashBalancer(['192.168.1.1:8080', '192.168.1.2:8080', '192.168.1.3:8080']);
echo $balancer->getServer('user_123'); // 始终返回同一节点

问题:节点增减时,大部分请求会重新映射,导致缓存失效。解决方案:一致性哈希。

class ConsistentHashBalancer {
    private array $ring = [];
    private int $replicas = 64; // 虚拟节点数

    public function __construct(array $servers) {
        foreach ($servers as $server) {
            for ($i = 0; $i < $this->replicas; $i++) {
                $hash = crc32($server . '#' . $i);
                $this->ring[$hash] = $server;
            }
        }
        ksort($this->ring);
    }

    public function getServer(string $key): string {
        if (empty($this->ring)) {
            throw new RuntimeException('No servers available');
        }
        $hash = crc32($key);
        foreach ($this->ring as $nodeHash => $server) {
            if ($nodeHash >= $hash) {
                return $server;
            }
        }
        // 环末尾,回到开头
        return reset($this->ring);
    }
}

// 使用
$balancer = new ConsistentHashBalancer(['192.168.1.1:8080', '192.168.1.2:8080', '192.168.1.3:8080']);
echo $balancer->getServer('user_123');

4. 最小连接数(Least Connections)

动态算法:每次选择当前活跃连接数最少的节点。需要维护每个节点的连接计数。

class LeastConnectionsBalancer {
    private array $servers;
    private array $connections = [];

    public function __construct(array $servers) {
        $this->servers = $servers;
        foreach ($servers as $server) {
            $this->connections[$server] = 0;
        }
    }

    public function next(): string {
        $min = PHP_INT_MAX;
        $best = null;
        foreach ($this->connections as $server => $count) {
            if ($count < $min) {
                $min = $count;
                $best = $server;
            }
        }
        $this->connections[$best]++;
        return $best;
    }

    public function release(string $server): void {
        if (isset($this->connections[$server]) && $this->connections[$server] > 0) {
            $this->connections[$server]--;
        }
    }
}

// 模拟请求
$balancer = new LeastConnectionsBalancer(['192.168.1.1:8080', '192.168.1.2:8080', '192.168.1.3:8080']);
$server = $balancer->next();
// 处理完成后
$balancer->release($server);

压测对比:10万请求

测试工具:ApacheBench 2.3,并发100,总请求10万。后端模拟服务用PHP内置服务器,每个请求sleep(10ms)模拟处理。

算法QPS平均延迟(ms)P99延迟(ms)内存占用(MB)节点负载偏差
轮询245040.8620.5±30%
加权轮询248040.3600.6±5%
哈希244041.0630.5±28%
一致性哈希243041.2641.2±25%
最小连接数241041.5650.8±3%

结论:加权轮询和最小连接数负载最均衡。一致性哈希内存略高(虚拟节点)。轮询在异构集群下不可用。

避坑指南

  • 坑1:轮询导致雪崩——异构集群必须用加权轮询或最小连接数。我们那次事故就是默认轮询。
  • 坑2:一致性哈希虚拟节点数——64个虚拟节点在10个节点时分布均匀,但节点数少时(如3个)偏差可达20%。建议节点数×64,最少256个虚拟节点。
  • 坑3:最小连接数计数不准——PHP多进程下共享内存需要加锁,否则计数会乱。用Redis原子操作或共享内存信号量。
  • 坑4:哈希算法选择——crc32在PHP中返回整数,但32位系统下可能溢出。用crc32b或md5取前8位。
  • 坑5:动态权重调整——加权轮询权重是静态的,生产环境需要根据CPU/内存动态调整。我们后来用Consul做服务发现,权重随负载变化。

总结

没有银弹。轮询适合同构集群,加权轮询适合异构,一致性哈希适合缓存场景,最小连接数适合长连接。选型看业务:会话保持用哈希,短连接用加权轮询,长连接用最小连接数。