真实场景:一个雪崩事故
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) | 节点负载偏差 |
|---|---|---|---|---|---|
| 轮询 | 2450 | 40.8 | 62 | 0.5 | ±30% |
| 加权轮询 | 2480 | 40.3 | 60 | 0.6 | ±5% |
| 哈希 | 2440 | 41.0 | 63 | 0.5 | ±28% |
| 一致性哈希 | 2430 | 41.2 | 64 | 1.2 | ±25% |
| 最小连接数 | 2410 | 41.5 | 65 | 0.8 | ±3% |
结论:加权轮询和最小连接数负载最均衡。一致性哈希内存略高(虚拟节点)。轮询在异构集群下不可用。
避坑指南
- 坑1:轮询导致雪崩——异构集群必须用加权轮询或最小连接数。我们那次事故就是默认轮询。
- 坑2:一致性哈希虚拟节点数——64个虚拟节点在10个节点时分布均匀,但节点数少时(如3个)偏差可达20%。建议节点数×64,最少256个虚拟节点。
- 坑3:最小连接数计数不准——PHP多进程下共享内存需要加锁,否则计数会乱。用Redis原子操作或共享内存信号量。
- 坑4:哈希算法选择——crc32在PHP中返回整数,但32位系统下可能溢出。用crc32b或md5取前8位。
- 坑5:动态权重调整——加权轮询权重是静态的,生产环境需要根据CPU/内存动态调整。我们后来用Consul做服务发现,权重随负载变化。
总结
没有银弹。轮询适合同构集群,加权轮询适合异构,一致性哈希适合缓存场景,最小连接数适合长连接。选型看业务:会话保持用哈希,短连接用加权轮询,长连接用最小连接数。