引言
负载均衡是分布式系统的核心组件,负责将流量分发到多个后端实例。合理的负载均衡策略能够提升系统吞吐量、降低延迟、增强可用性。
本文将系统介绍各种负载均衡算法的原理与实现。
负载均衡算法对比
| 算法 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 轮询 | 依次分配 | 无状态服务 | 简单、公平 | 不考虑负载差异 |
| 加权轮询 | 按权重分配 | 异构服务器 | 灵活配置 | 仍需手动调优 |
| 最少连接 | 选择连接最少 | 长连接服务 | 自适应 | 状态开销 |
| 一致性哈希 | 哈希环映射 | 缓存、有状态服务 | 节点变更影响小 | 实现复杂 |
| IP哈希 | 源IP哈希 | 会话保持 | 简单 | 分布不均 |
轮询算法(Round Robin)
基础实现
type RoundRobinBalancer struct {
backends []string
current int
mu sync.Mutex
}
func NewRoundRobinBalancer(backends []string) *RoundRobinBalancer {
return &RoundRobinBalancer{
backends: backends,
current: 0,
}
}
func (b *RoundRobinBalancer) Next() string {
b.mu.Lock()
defer b.mu.Unlock()
backend := b.backends[b.current]
b.current = (b.current + 1) % len(b.backends)
return backend
}
// 使用示例
balancer := NewRoundRobinBalancer([]string{
"server1:8080",
"server2:8080",
"server3:8080",
})
for i := 0; i < 10; i++ {
fmt.Println(balancer.Next())
}
// 输出:server1, server2, server3, server1, server2, server3...
Nginx轮询配置
upstream backend_servers {
server backend1.example.com:8080;
server backend2.example.com:8080;
server backend3.example.com:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
加权轮询(Weighted Round Robin)
实现原理
type WeightedBackend struct {
Address string
Weight int
CurrentWeight int
EffectiveWeight int
}
type WeightedRoundRobinBalancer struct {
backends []*WeightedBackend
mu sync.Mutex
}
func NewWeightedRoundRobinBalancer(backends []*WeightedBackend) *WeightedRoundRobinBalancer {
for _, b := range backends {
b.EffectiveWeight = b.Weight
}
return &WeightedRoundRobinBalancer{backends: backends}
}
func (b *WeightedRoundRobinBalancer) Next() string {
b.mu.Lock()
defer b.mu.Unlock()
var best *WeightedBackend
totalWeight := 0
for _, backend := range b.backends {
if backend.EffectiveWeight <= 0 {
continue
}
// 增加当前权重
backend.CurrentWeight += backend.EffectiveWeight
totalWeight += backend.EffectiveWeight
// 选择当前权重最大的
if best == nil || backend.CurrentWeight > best.CurrentWeight {
best = backend
}
}
if best == nil {
return ""
}
// 被选中的减少总权重
best.CurrentWeight -= totalWeight
return best.Address
}
// 使用示例
balancer := NewWeightedRoundRobinBalancer([]*WeightedBackend{
{Address: "server1:8080", Weight: 5}, // 高配服务器
{Address: "server2:8080", Weight: 3}, // 中配服务器
{Address: "server3:8080", Weight: 1}, // 低配服务器
})
// 流量分配比例约为 5:3:1
Nginx加权轮询
upstream backend_servers {
server backend1.example.com:8080 weight=5; # 50%流量
server backend2.example.com:8080 weight=3; # 30%流量
server backend3.example.com:8080 weight=1; # 10%流量
# 备用服务器(仅在主服务器故障时使用)
server backend4.example.com:8080 backup;
}
最少连接(Least Connections)
实现原理
type BackendWithConnections struct {
Address string
Connections int64
}
type LeastConnectionsBalancer struct {
backends []*BackendWithConnections
mu sync.RWMutex
}
func NewLeastConnectionsBalancer(backends []*BackendWithConnections) *LeastConnectionsBalancer {
return &LeastConnectionsBalancer{backends: backends}
}
func (b *LeastConnectionsBalancer) Next() string {
b.mu.RLock()
defer b.mu.RUnlock()
var best *BackendWithConnections
for _, backend := range b.backends {
if best == nil || backend.Connections < best.Connections {
best = backend
}
}
if best != nil {
atomic.AddInt64(&best.Connections, 1)
return best.Address
}
return ""
}
func (b *LeastConnectionsBalancer) Release(address string) {
b.mu.RLock()
defer b.mu.RUnlock()
for _, backend := range b.backends {
if backend.Address == address {
atomic.AddInt64(&backend.Connections, -1)
return
}
}
}
// 使用示例
balancer := NewLeastConnectionsBalancer([]*BackendWithConnections{
{Address: "server1:8080", Connections: 0},
{Address: "server2:8080", Connections: 0},
})
// 请求处理
backend := balancer.Next()
defer balancer.Release(backend)
// 发送请求到选中的后端
resp, err := http.Get("http://" + backend + "/api")
Nginx最少连接
upstream backend_servers {
least_conn; # 使用最少连接算法
server backend1.example.com:8080;
server backend2.example.com:8080;
server backend3.example.com:8080;
}
一致性哈希(Consistent Hashing)
原理与实现
type ConsistentHashBalancer struct {
circle map[uint32]string // 哈希环
sortedHashes []uint32 // 排序的哈希值
replicas int // 虚拟节点数
mu sync.RWMutex
}
func NewConsistentHashBalancer(replicas int) *ConsistentHashBalancer {
return &ConsistentHashBalancer{
circle: make(map[uint32]string),
replicas: replicas,
}
}
func (b *ConsistentHashBalancer) AddBackend(address string) {
b.mu.Lock()
defer b.mu.Unlock()
// 为每个后端创建多个虚拟节点
for i := 0; i < b.replicas; i++ {
hash := b.hash(fmt.Sprintf("%s:%d", address, i))
b.circle[hash] = address
b.sortedHashes = append(b.sortedHashes, hash)
}
sort.Slice(b.sortedHashes, func(i, j int) bool {
return b.sortedHashes[i] < b.sortedHashes[j]
})
}
func (b *ConsistentHashBalancer) RemoveBackend(address string) {
b.mu.Lock()
defer b.mu.Unlock()
for i := 0; i < b.replicas; i++ {
hash := b.hash(fmt.Sprintf("%s:%d", address, i))
delete(b.circle, hash)
}
// 重建排序数组
b.sortedHashes = nil
for hash := range b.circle {
b.sortedHashes = append(b.sortedHashes, hash)
}
sort.Slice(b.sortedHashes, func(i, j int) bool {
return b.sortedHashes[i] < b.sortedHashes[j]
})
}
func (b *ConsistentHashBalancer) Get(key string) string {
b.mu.RLock()
defer b.mu.RUnlock()
if len(b.circle) == 0 {
return ""
}
hash := b.hash(key)
// 二分查找第一个大于等于hash的节点
idx := sort.Search(len(b.sortedHashes), func(i int) bool {
return b.sortedHashes[i] >= hash
})
// 环形结构:如果超出范围,回到起点
if idx >= len(b.sortedHashes) {
idx = 0
}
return b.circle[b.sortedHashes[idx]]
}
func (b *ConsistentHashBalancer) hash(key string) uint32 {
h := fnv.New32a()
h.Write([]byte(key))
return h.Sum32()
}
// 使用示例
balancer := NewConsistentHashBalancer(100) // 每个后端100个虚拟节点
balancer.AddBackend("server1:8080")
balancer.AddBackend("server2:8080")
balancer.AddBackend("server3:8080")
// 相同key总是路由到相同后端(会话保持)
backend1 := balancer.Get("user:123") // server2
backend2 := balancer.Get("user:123") // server2(相同)
// 添加新后端时,只有部分key会迁移
balancer.AddBackend("server4:8080")
backend3 := balancer.Get("user:123") // 可能仍是server2,或迁移到server4
一致性哈希的优势
传统哈希(hash(key) % n):
- 节点数变化时,几乎所有key都要迁移
- 例如:10个节点变11个节点,约90%的key迁移
一致性哈希:
- 节点数变化时,只有相邻节点的key迁移
- 例如:10个节点变11个节点,约10%的key迁移
- 适用场景:分布式缓存、有状态服务
健康检查机制
主动健康检查
type HealthChecker struct {
backends []string
healthy map[string]bool
mu sync.RWMutex
}
func NewHealthChecker(backends []string) *HealthChecker {
return &HealthChecker{
backends: backends,
healthy: make(map[string]bool),
}
}
func (h *HealthChecker) Start(interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
h.checkAll()
}
}()
}
func (h *HealthChecker) checkAll() {
for _, backend := range h.backends {
healthy := h.checkHealth(backend)
h.mu.Lock()
oldHealthy := h.healthy[backend]
h.healthy[backend] = healthy
h.mu.Unlock()
if oldHealthy != healthy {
if healthy {
log.Infof("Backend %s is now healthy", backend)
} else {
log.Warnf("Backend %s is now unhealthy", backend)
}
}
}
}
func (h *HealthChecker) checkHealth(backend string) bool {
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://%s/health", backend))
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
func (h *HealthChecker) IsHealthy(backend string) bool {
h.mu.RLock()
defer h.mu.RUnlock()
return h.healthy[backend]
}
func (h *HealthChecker) GetHealthyBackends() []string {
h.mu.RLock()
defer h.mu.RUnlock()
var healthy []string
for _, backend := range h.backends {
if h.healthy[backend] {
healthy = append(healthy, backend)
}
}
return healthy
}
Nginx健康检查
upstream backend_servers {
server backend1.example.com:8080 max_fails=3 fail_timeout=30s;
server backend2.example.com:8080 max_fails=3 fail_timeout=30s;
server backend3.example.com:8080 max_fails=3 fail_timeout=30s;
}
# max_fails: 失败次数阈值
# fail_timeout: 失败时间窗口和标记不可用时长
会话保持(Session Affinity)
IP哈希
type IPHashBalancer struct {
backends []string
}
func NewIPHashBalancer(backends []string) *IPHashBalancer {
return &IPHashBalancer{backends: backends}
}
func (b *IPHashBalancer) GetBackend(clientIP string) string {
hash := fnv.New32a()
hash.Write([]byte(clientIP))
idx := hash.Sum32() % uint32(len(b.backends))
return b.backends[idx]
}
Cookie会话保持
type CookieAffinityBalancer struct {
balancer LoadBalancer
cookieName string
}
func (b *CookieAffinityBalancer) GetBackend(r *http.Request, w http.ResponseWriter) string {
// 尝试从cookie获取后端
cookie, err := r.Cookie(b.cookieName)
if err == nil {
// 验证后端是否健康
if b.isHealthy(cookie.Value) {
return cookie.Value
}
}
// 选择新后端
backend := b.balancer.Next()
// 设置cookie
http.SetCookie(w, &http.Cookie{
Name: b.cookieName,
Value: backend,
Path: "/",
MaxAge: 3600,
HttpOnly: true,
})
return backend
}
总结
负载均衡策略选择指南:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 无状态Web服务 | 轮询/最少连接 | 简单、公平 |
| 异构服务器 | 加权轮询 | 按能力分配 |
| 长连接服务 | 最少连接 | 自适应负载 |
| 分布式缓存 | 一致性哈希 | 节点变更影响小 |
| 会话保持 | IP哈希/Cookie | 同一用户路由到同一后端 |
关键原则:
- 健康检查:及时剔除故障节点
- 监控指标:观察各后端负载分布
- 动态调整:根据实际负载调整权重
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。