引言
负载均衡是分布式系统的核心组件,负责将流量分发到多个后端实例。合理的负载均衡策略能够提升系统吞吐量、降低延迟、增强可用性。
本文将系统介绍各种负载均衡算法的原理与实现。
负载均衡算法对比
| 算法 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 轮询 | 依次分配 | 无状态服务 | 简单、公平 | 不考虑负载差异 |
| 加权轮询 | 按权重分配 | 异构服务器 | 灵活配置 | 仍需手动调优 |
| 最少连接 | 选择连接最少 | 长连接服务 | 自适应 | 状态开销 |
| 一致性哈希 | 哈希环映射 | 缓存、有状态服务 | 节点变更影响小 | 实现复杂 |
| 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: 失败时间窗口和标记不可用时长
L4 vs L7 负载均衡深度对比
| 维度 | L4(传输层) | L7(应用层) |
|---|---|---|
| 决策依据 | IP + 端口 | URL、Header、Cookie、Body |
| 处理延迟 | 极低( us 级) | 较高(ms 级,需解析 HTTP) |
| SSL 终止 | 通常终止 | 可终止或透传(mTLS) |
| 典型实现 | LVS、HAProxy TCP、AWS NLB | Nginx、Envoy、AWS ALB |
| 适用场景 | 数据库、消息队列、游戏服务器 | Web API、微服务网关 |
实际架构中往往分层使用:边缘用 L7 做路由和 SSL 终止,内网服务间用 L4 做高效转发。
Envoy 配置实战示例
Envoy 作为云原生时代的事实标准代理,其配置体现了声明式负载均衡:
# envoy.yaml 核心片段
static_resources:
listeners:
- address:
socket_address: { address: 0.0.0.0, port_value: 80 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/api/v1/" }
route:
cluster: api_service
retry_policy:
retry_on: gateway-error
num_retries: 3
per_try_timeout: 2s
clusters:
- name: api_service
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN # LEAST_REQUEST / RING_HASH / MAGLEV
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 3
healthy_threshold: 2
http_health_check: { path: "/healthz" }
load_assignment:
cluster_name: api_service
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: 10.0.1.10, port_value: 8080 } } }
- endpoint: { address: { socket_address: { address: 10.0.1.11, port_value: 8080 } } }
Envoy 的 MAGLEV 一致性哈希算法在高并发场景下比标准一致性哈希有更好的均匀性,特别适合缓存类服务。
蓝绿发布与金丝雀发布中的负载均衡
负载均衡器是发布策略的核心执行组件:
蓝绿发布:负载均衡器配置两个等大的上游集群(蓝色=旧版,绿色=新版),发布时一次性切换流量。回滚仅需切回蓝色。
金丝雀发布:利用权重逐步放量,从 1% → 5% → 25% → 100%。每阶段观察错误率、延迟、业务指标,发现问题立即回滚。
# Nginx 金丝雀权重配置
upstream backend {
server blue-v1.example.com weight=95;
server green-v2.example.com weight=5;
}
流量镜像(Traffic Mirroring):将生产流量复制一份到新版集群,仅观察不响应,零风险验证。
服务网格中的智能路由
在 Istio / Linkerd 等服务网格中,负载均衡升级为"智能流量管理":
| 能力 | Istio 实现 | 典型用例 |
|---|---|---|
| 基于权重路由 | VirtualService | 金丝雀发布 |
| 基于 Header 路由 | match.headers | A/B 测试、内部版本 |
| 故障注入 | Fault | 混沌工程演练 |
| 熔断 | DestinationRule | 防止级联故障 |
| 超时与重试 | Route timeout | 弹性增强 |
# Istio DestinationRule:连接池与熔断
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: api-circuit-breaker
spec:
host: api-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 10
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
自动扩缩容与负载均衡联动
负载均衡器需与自动扩缩容(HPA / KEDA / Cluster Autoscaler)协同:
- 指标采集:LB 将 QPS、延迟、错误率暴露给 Prometheus
- 扩缩容决策:HPA 根据自定义指标(如请求队列长度)触发扩容
- 预热窗口:新 Pod 加入负载均衡池前,通过 readiness probe 确认健康
- 缩容优雅:ASM / CLB 先将 Pod 从 LB 摘除,再执行缩容,避免断流
云厂商的托管负载均衡(如 AWS ALB + Target Group)通常已内置与 ASG(Auto Scaling Group)的联动,实现"流量上涨 → 扩容 → 自动加入 LB → 流量分流"的全自动闭环。
会话保持(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 | 同一用户路由到同一后端 |
关键原则:
- 健康检查:及时剔除故障节点
- 监控指标:观察各后端负载分布
- 动态调整:根据实际负载调整权重
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。