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;
}

我曾经踩过一个坑:连接数少不代表处理能力强。有的请求是长连接,占着茅坑不拉屎;有的请求是短连接,瞬间就完事。所以最少连接算法更适合处理时间差异大的场景,比如数据库连接池。

避坑指南:我曾经在生产环境里直接用最少连接算法做HTTP反向代理,结果发现有些请求是WebSocket长连接,导致连接数一直不释放,新请求全挤到其他机器上。后来加了超时断开机制才解决。

一致性哈希(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?因为太少会导致分布不均,太多又浪费内存。这个数字是我在压测时试出来的,你可以根据实际机器数量调整。

注意:一致性哈希并不能保证绝对的负载均衡。如果服务器数量少,或者虚拟节点分配不合理,还是会出现倾斜。我曾经见过一个案例,3台机器用了50个虚拟节点,结果有一台机器承担了60%的流量。后来把虚拟节点调到200,才基本均匀。

三种算法对比

算法 核心思想 优点 缺点 典型场景
轮询 轮流分配 实现简单,无状态 不考虑负载差异 同配置服务器集群
最少连接 动态感知负载 能应对请求处理时间不均 需要维护连接状态 数据库连接池、长连接服务
一致性哈希 环状映射 节点增减影响小 实现复杂,可能分布不均 缓存集群、分布式存储

核心逻辑流程图

下面这张图展示了三种算法的决策路径。我画的时候尽量简化了,方便你一眼看出区别。

收到新请求 轮询算法 取模得到服务器索引 最少连接算法 遍历比较连接数 一致性哈希 在哈希环上查找 转发到目标服务器 处理完成

实际项目中的选择建议

说实话,没有银弹。我在不同项目里用过不同的组合:

  • 静态页面集群:轮询就够了。因为每个请求的处理时间几乎一样,没必要搞复杂。
  • API网关:最少连接 + 超时熔断。因为不同API的响应时间差异很大,有的查数据库,有的读缓存。
  • 分布式缓存:一致性哈希。这是必须的,否则加一台机器,整个缓存就废了。
我的一个小技巧:在C语言实现中,一致性哈希的虚拟节点可以用数组存储,然后排序。查找时用二分查找,时间复杂度O(log n)。我试过用红黑树,但实际性能提升不大,反而增加了代码复杂度。

嗯,今天就聊到这里。这三种算法你可以在自己的项目里试试,先从轮询开始,慢慢过渡到更复杂的方案。记住,没有最好的算法,只有最合适的。

公众号:蓝海资料掘金营,微信deep3321