负载均衡策略详解:从轮询到一致性哈希的实战指南

全面解析负载均衡的核心算法与实现策略,涵盖轮询、加权轮询、最少连接、一致性哈希等算法原理,深入讲解L4/L7负载均衡、健康检查与会话保持的实战技巧。

引言

负载均衡是分布式系统的核心组件,负责将流量分发到多个后端实例。合理的负载均衡策略能够提升系统吞吐量、降低延迟、增强可用性。

本文将系统介绍各种负载均衡算法的原理与实现。

负载均衡算法对比

算法原理适用场景优点缺点
轮询依次分配无状态服务简单、公平不考虑负载差异
加权轮询按权重分配异构服务器灵活配置仍需手动调优
最少连接选择连接最少长连接服务自适应状态开销
一致性哈希哈希环映射缓存、有状态服务节点变更影响小实现复杂
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同一用户路由到同一后端

关键原则:

  • 健康检查:及时剔除故障节点
  • 监控指标:观察各后端负载分布
  • 动态调整:根据实际负载调整权重

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页