87、负载均衡:轮询算法、最少连接算法、一致性哈希
说到负载均衡,我脑子里第一个蹦出来的场景,就是早年在一家电商公司做网关优化。那时候流量一大,单机扛不住,加机器吧,又不知道怎么分流量才合理。说白了,负载均衡就是解决「请求该发给谁」的问题。
今天咱们聊三种最经典的算法:轮询、最少连接、一致性哈希。这三种我都在生产环境里折腾过,各有各的脾气。你想想看,选错了算法,可能一半机器闲得发慌,另一半累到宕机。
轮询算法(Round Robin)
轮询是最朴素的算法。它把请求按顺序轮流发给每台服务器。就像打牌轮流发牌,谁也不多拿,谁也不少拿。
核心逻辑:维护一个计数器,每次请求过来,计数器加1,然后对服务器总数取模,得到目标服务器索引。
// 轮询算法核心代码
static int current_index = 0;
int get_next_server(int server_count) {
int server = current_index % server_count;
current_index++;
return server;
}
我在项目中遇到过一个问题:如果某台服务器挂了,轮询还是会继续往它那里发请求。所以实际使用时,必须配合健康检查。嗯,这里要注意,轮询不考虑服务器当前负载,如果机器配置不一样,弱的机器容易先崩。
最少连接算法(Least Connections)
最少连接算法就聪明一点了。它会实时统计每台服务器当前的活跃连接数,新请求来了,直接扔给连接数最少的那台。
说白了,这就是「能者多劳」的思路。你想想看,如果一台机器已经扛了100个连接,另一台才扛10个,新请求当然给后者。
// 最少连接算法核心逻辑
typedef struct {
int id;
int active_connections;
} Server;
Server* get_least_connections(Server* servers, int count) {
Server* best = &servers[0];
for (int i = 1; i < count; i++) {
if (servers[i].active_connections < best->active_connections) {
best = &servers[i];
}
}
return best;
}
我曾经踩过一个坑:连接数少不代表处理能力强。有的请求是长连接,占着茅坑不拉屎;有的请求是短连接,瞬间就完事。所以最少连接算法更适合处理时间差异大的场景,比如数据库连接池。
一致性哈希(Consistent Hashing)
一致性哈希就有点意思了。它不是为了均衡负载本身,而是为了解决「缓存失效」的问题。你想想看,如果后端服务器数量变了,普通哈希会导致大量缓存同时失效,所有请求瞬间打到数据库上——这就是所谓的「缓存雪崩」。
一致性哈希把服务器和请求都映射到一个环上。每个请求找它顺时针方向遇到的第一个服务器。这样,当增加或减少一台服务器时,只有环上相邻的一小部分请求需要重新分配。
// 一致性哈希简化实现(使用虚拟节点)
#define VIRTUAL_NODES 150
uint32_t hash(const char* key) {
// 实际项目中用 MurmurHash 或 CityHash
uint32_t h = 0;
while (*key) {
h = h * 31 + *key++;
}
return h;
}
int get_server(const char* key, uint32_t* ring, int ring_size) {
uint32_t h = hash(key);
// 二分查找第一个大于等于 h 的节点
int idx = binary_search(ring, ring_size, h);
return ring[idx % ring_size];
}
我个人习惯给每台物理服务器分配150个虚拟节点。为什么是150?因为太少会导致分布不均,太多又浪费内存。这个数字是我在压测时试出来的,你可以根据实际机器数量调整。
三种算法对比
| 算法 | 核心思想 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|
| 轮询 | 轮流分配 | 实现简单,无状态 | 不考虑负载差异 | 同配置服务器集群 |
| 最少连接 | 动态感知负载 | 能应对请求处理时间不均 | 需要维护连接状态 | 数据库连接池、长连接服务 |
| 一致性哈希 | 环状映射 | 节点增减影响小 | 实现复杂,可能分布不均 | 缓存集群、分布式存储 |
核心逻辑流程图
下面这张图展示了三种算法的决策路径。我画的时候尽量简化了,方便你一眼看出区别。
实际项目中的选择建议
说实话,没有银弹。我在不同项目里用过不同的组合:
- 静态页面集群:轮询就够了。因为每个请求的处理时间几乎一样,没必要搞复杂。
- API网关:最少连接 + 超时熔断。因为不同API的响应时间差异很大,有的查数据库,有的读缓存。
- 分布式缓存:一致性哈希。这是必须的,否则加一台机器,整个缓存就废了。
嗯,今天就聊到这里。这三种算法你可以在自己的项目里试试,先从轮询开始,慢慢过渡到更复杂的方案。记住,没有最好的算法,只有最合适的。