轮询策略:最基础但容易踩坑的入门款
轮询是负载均衡里「最眼熟」的策略——把请求按顺序轮流发给每台服务器,比如第1个请求给Server A,第2个给Server B,第3个再回到Server A。它的优点像「白开水」:实现简单、无额外依赖,适合所有服务器配置完全一致的场景(比如容器化部署的同规格Pod)。

但你大概率踩过它的坑:如果Server A是8核16G,Server B是2核4G,纯轮询会让Server B连续接收请求,最终因为处理慢导致超时,反而拖垮整个集群。
来个Python的「纯轮询」极简实现,感受下它的「直白」:
class RoundRobinBalancer:
def __init__(self, servers):
self.servers = servers
self.index = 0 # 当前轮询到的位置
def select(self):
server = self.servers[self.index]
self.index = (self.index + 1) % len(self.servers)
return server
# 使用示例
servers = ["192.168.1.100:8080", "192.168.1.101:8080"]
balancer = RoundRobinBalancer(servers)
print(balancer.select()) # 输出192.168.1.100:8080
print(balancer.select()) # 输出192.168.1.101:8080
改进版:加权轮询——给性能好的服务器加「权重buff」。比如Server A权重3、Server B权重1,那么每4个请求里,A接收3次、B接收1次。Nginx的upstream
模块默认支持加权轮询,配置长这样:
upstream backend {
server 192.168.1.100 weight=3; # 权重3
server 192.168.1.101 weight=1; # 权重1
}
加权随机:给服务器加「权重buff」的灵活方案
轮询是「按顺序来」,加权随机是「按概率来」——让权重高的服务器有更高概率被选中。比如Server A权重3、Server B权重1,那么每次请求有75%概率发给A,25%发给B。
它比轮询更灵活吗?举个例子:如果服务器性能波动大(比如偶尔有临时任务占用资源),加权随机能「随机避开」暂时忙碌的服务器,而轮询会「硬塞」请求。
用Python实现加权随机的核心逻辑(本质是「把权重转化为概率池」):
import random
class WeightedRandomBalancer:
def __init__(self, servers_with_weight):
# servers_with_weight格式:[(server_addr, weight), ...]
self.server_pool = []
for server, weight in servers_with_weight:
self.server_pool.extend([server] * weight) # 按权重扩展列表
def select(self):
return random.choice(self.server_pool)
# 使用示例:Server A权重3,Server B权重1
balancer = WeightedRandomBalancer([("192.168.1.100", 3), ("192.168.1.101", 1)])
print(balancer.select()) # 75%概率输出192.168.1.100
适用场景:服务器配置差异大、性能波动频繁的场景(比如混合部署了物理机和云主机的集群)。
最少连接数:盯着「忙碌程度」的智能分配
轮询和加权随机都「不看当前状态」——哪怕Server A已经有1000个连接,它们还是会继续发请求。而最少连接数策略是「盯着服务器的忙碌程度」:选当前活跃连接数最少的服务器。
这招特别适合长连接场景(比如WebSocket聊天服务、IoT设备推送)——长连接会占用服务器资源更久,用最少连接数能避免「忙的服务器更忙」。
用Go写个「最少连接数」的简化实现(核心是用sync.Mutex
保护连接数统计):
package main
import (
"fmt"
"sync"
)
// Server 记录服务器地址和当前连接数
type Server struct {
Addr string
ConnCount int
mu sync.Mutex
}
// IncrConn 增加连接数(线程安全)
func (s *Server) IncrConn() {
s.mu.Lock()
defer s.mu.Unlock()
s.ConnCount++
}
// DecrConn 减少连接数(线程安全)
func (s *Server) DecrConn() {
s.mu.Lock()
defer s.mu.Unlock()
s.ConnCount--
}
// LeastConnBalancer 最少连接数负载均衡器
type LeastConnBalancer struct {
servers []*Server
}
// NewLeastConnBalancer 初始化负载均衡器
func NewLeastConnBalancer(servers []*Server) *LeastConnBalancer {
return &LeastConnBalancer{servers: servers}
}
// Select 选择连接数最少的服务器
func (b *LeastConnBalancer) Select() *Server {
var leastServer *Server
minConn := int(^uint(0) >> 1) // 初始化为最大int值
for _, s := range b.servers {
s.mu.Lock()
currentConn := s.ConnCount
s.mu.Unlock()
if currentConn < minConn {
minConn = currentConn
leastServer = s
}
}
return leastServer
}
func main() {
servers := []*Server{
{Addr: "192.168.1.100", ConnCount: 50},
{Addr: "192.168.1.101", ConnCount: 10}, // 连接数最少
{Addr: "192.168.1.102", ConnCount: 80},
}
balancer := NewLeastConnBalancer(servers)
selected := balancer.Select()
fmt.Printf("选中服务器:%s(当前连接数:%d)
", selected.Addr, selected.ConnCount)
}
运行结果:肯定会选中192.168.1.101
——它的连接数最少。
IP哈希:让用户「粘」在固定服务器的方案
有些场景需要「会话保持」——比如用户在电商网站加了购物车,刷新页面后不能丢失状态。这时候IP哈希策略能让同一用户的请求始终发给同一台服务器。
原理很简单:对用户的IP地址取哈希值,然后「模」服务器数量,得到的结果就是要分配的服务器索引。比如:
– 用户IP是192.168.1.10
,哈希值是12345
– 服务器数量是3,12345 % 3 = 0
→ 发给第0台服务器
用Python实现IP哈希的核心逻辑:
def ip_hash(server_list, client_ip):
# 对IP进行哈希(简化版:将IP转为整数)
ip_int = int(''.join(client_ip.split('.')))
index = ip_int % len(server_list)
return server_list[index]
# 使用示例
servers = ["192.168.1.100", "192.168.1.101", "192.168.1.102"]
client_ip = "192.168.0.5"
print(ip_hash(servers, client_ip)) # 每次调用都返回同一台服务器
注意踩坑:如果用户用了代理(比如VPN、CDN),IP会变成代理服务器的IP——这会导致所有用同一代理的用户都被分配到同一台服务器。解决办法是用「Cookie哈希」或「Session ID哈希」代替IP哈希。
一致性哈希:解决服务器上下线的「雪崩」问题
前面的策略都有个致命问题:服务器增减会导致「哈希重构」。比如原来有3台服务器,用户A的IP哈希到第0台;如果新增1台服务器(变成4台),用户A的哈希结果会变成ip_int %4
,大概率分配到其他服务器——这会导致用户会话丢失、缓存失效(比如Redis的缓存雪崩)。
一致性哈希策略能解决这个问题:把服务器和请求都映射到一个「环形哈希空间」(比如0~2^32-1的整数环),然后请求找「顺时针最近的服务器」。
举个例子:
1. 把Server A、B、C分别映射到环上的位置100、200、300。
2. 用户请求的哈希位置是150 → 顺时针找最近的是Server B。
3. 如果新增Server D到250的位置 → 用户请求150还是找Server B,只有150~250之间的请求会转到Server D——影响范围极小。
用Go的github.com/golang/groupcache/consistenthash
库实现一致性哈希(这是Go生态里最常用的一致性哈希库):
package main
import (
"fmt"
"github.com/golang/groupcache/consistenthash"
)
func main() {
// 创建一致性哈希环(虚拟节点数设为10,增加分布均匀性)
ring := consistenthash.New(10, nil)
// 添加服务器节点
servers := []string{"192.168.1.100", "192.168.1.101", "192.168.1.102"}
ring.Add(servers...)
// 模拟请求哈希(比如用户ID的哈希值)
requestKey := "user_123"
selectedServer := ring.Get(requestKey)
fmt.Printf("请求%s被分配到:%s
", requestKey, selectedServer)
// 新增服务器节点
ring.Add("192.168.1.103")
newSelectedServer := ring.Get(requestKey)
fmt.Printf("新增服务器后,请求%s被分配到:%s
", requestKey, newSelectedServer)
}
关键参数:虚拟节点数(比如10)——虚拟节点越多,哈希分布越均匀,但计算成本也越高。一般建议设为5~20。
策略选择决策表:1分钟选对方案
最后给你一张「作弊表」,不用再翻前面的内容就能快速选策略:
策略类型 | 核心逻辑 | 适用场景 | 缺点 |
---|---|---|---|
轮询 | 按顺序轮流分配 | 服务器配置完全一致 | 不看状态,易压垮慢服务器 |
加权轮询 | 按权重顺序分配 | 服务器配置差异大 | 权重需要人工调整 |
加权随机 | 按权重概率分配 | 服务器性能波动大 | 偶尔会「随机到忙的服务器」 |
最少连接数 | 选连接最少的服务器 | 长连接/高并发场景(WebSocket) | 需要统计连接数,有性能开销 |
IP哈希 | 按IP固定分配 | 需要会话保持的场景(电商) | 代理IP会导致分配不均 |
一致性哈希 | 环形空间找最近服务器 | 服务器频繁增减的场景(云集群) | 实现复杂,需维护哈希环 |
最后提醒:负载均衡的「隐藏技巧」
- 健康检查不能少:不管用什么策略,都要定期检查服务器是否存活(比如用TCP端口探测、HTTP接口返回200)。Nginx的
health_check
模块、K8s的livenessProbe
都是干这个的。 - 动态调整权重:比如用Prometheus采集服务器的CPU利用率,当CPU超过80%时,自动降低权重(比如从3降到1)——这能避免服务器被压垮。
- 混合策略:比如「加权随机+最少连接数」——平时用加权随机,当某台服务器的连接数超过阈值时,暂时排除它。
负载均衡不是「选最复杂的策略」,而是「选最适合当前场景的策略」。比如小公司的初期集群,用加权轮询就够了;大公司的高并发集群,才需要上一致性哈希+动态权重调整。
希望这篇指南能帮你少踩坑,把负载均衡从「玄学」变成「可控的工程问题」~
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/302