负载均衡算法实践笔记

这次做高可用架构,调研了各种负载均衡算法,从简单的轮询到复杂的一致性哈希,各有各的适用场景。

适用于长连接场景,如 WebSoc

最简单的负载均衡算法,按顺序分发请求。

轮询算法

最简单的负载均衡算法,按顺序分发请求。

简单轮询

servers = ['server1', 'server2', 'server3']
current_index = 0

def get_server():
    global current_index
    server = servers[current_index]
    current_index = (current_index + 1) % len(servers)
    return server

加权轮询

如果服务器配置不同,可以分配不同权重:

servers = [
    {'name': 'server1', 'weight': 3},
    {'name': 'server2', 'weight': 2},
    {'name': 'server3', 'weight': 1}
]

def get_weighted_server():
    total_weight = sum(s['weight'] for s in servers)
    current_weight = 0

    for server in servers:
        current_weight += server['weight']
        if random.random() * total_weight <= current_weight:
            return server['name']

Nginx 的轮询配置:

upstream backend {
    server 192.168.1.1 weight=3;
    server 192.168.1.2 weight=2;
    server 192.168.1.3 weight=1;
}

最少连接算法

把请求分配给当前连接数最少的服务器。

import heapq

servers = {
    'server1': {'connections': 0},
    'server2': {'connections': 0},
    'server3': {'connections': 0}
}

def get_least_connections():
    # 使用堆找到连接数最少的服务器
    min_conn = min(servers.items(), key=lambda x: x[1]['connections'])
    return min_conn[0]

适用于长连接场景,如 WebSocket。

源地址哈希

根据客户端 IP 地址决定分发到哪个服务器。

import hashlib

servers = ['server1', 'server2', 'server3']

def get_server_by_ip(client_ip):
    hash_value = int(hashlib.md5(client_ip.encode()).hexdigest(), 16)
    index = hash_value % len(servers)
    return servers[index]

Nginx 配置:

upstream backend {
    ip_hash;
    server 192.168.1.1;
    server 192.168.1.2;
    server 192.168.1.3;
}

优势:同一客户端的请求会路由到同一服务器,利于会话保持

劣势:如果某台服务器宕机,会话会丢失

一致性哈希

解决服务器增删时,数据分布剧烈变化的问题。

基本原理

将服务器和请求映射到哈希环上,请求顺时针路由到下一个服务器。

graph TB subgraph 哈希环 A[哈希环0<br/>360度] B[服务器1<br/>位置30度] C[服务器2<br/>位置120度] D[服务器3<br/>位置240度] E[请求1<br/>位置40度] --> B F[请求2<br/>位置200度] --> D G[请求3<br/>位置100度] --> C A --> B A --> C A --> D end style A fill:#87CEEB style B fill:#90EE90 style C fill:#FFD700 style D fill:#FFB6C1

虚拟节点

为了数据分布更均匀,每个服务器对应多个虚拟节点。

import hashlib

servers = ['server1', 'server2', 'server3']
virtual_nodes = 100

ring = {}

# 为每个服务器创建虚拟节点
for server in servers:
    for i in range(virtual_nodes):
        virtual_key = f"{server}#{i}"
        hash_value = int(hashlib.md5(virtual_key.encode()).hexdigest(), 16)
        ring[hash_value] = server

def get_consistent_server(key):
    hash_value = int(hashlib.md5(key.encode()).hexdigest(), 16)
    sorted_ring = sorted(ring.keys())
    
    # 找到第一个大于等于 hash_value 的节点
    for node in sorted_ring:
        if node >= hash_value:
            return ring[node]
    
    # 如果没找到,返回第一个节点(环形)
    return ring[sorted_ring[0]]

优势

  • 服务器增删时,只会影响部分请求
  • 数据分布更均匀
  • 适合缓存、分布式存储场景

实践中的选择

场景一:无状态服务

对于无状态服务,如 REST API,轮询或最少连接就够了。

upstream backend {
    least_conn;
    server 192.168.1.1;
    server 192.168.1.2;
    server 192.168.1.3;
}

场景二:有状态服务

对于有状态服务,如 WebSocket,最少连接更合适。

场景三:需要会话保持

对于需要会话保持的服务,源地址哈希或 Cookie 哈希。

upstream backend {
    hash $cookie_session_id consistent;
    server 192.168.1.1;
    server 192.168.1.2;
    server 192.168.1.3;
}

场景四:缓存或分布式存储

一致性哈希是最佳选择,避免数据重新分布带来的缓存失效。

写在最后

负载均衡算法这东西,没有最好的,只有最适合的。

选型之前先问自己几个问题:

  • 服务是无状态还是有状态?
  • 需要会话保持吗?
  • 服务器配置是否相同?
  • 是否需要快速增删服务器?

回答清楚这些问题,选择就简单了。


这次调研下来,最大的收获是:不要追求"最优"算法,要追求"够用"的算法。轮询最简单,但它解决了很多问题。

版权声明: 本文首发于 指尖魔法屋-负载均衡算法实践笔记https://blog.thinkmoon.cn/post/51-load-balancing-algorithms-roundrobin-consistent-hashing/) 转载或引用必须申明原指尖魔法屋来源及源地址!