第 28 章:负载均衡
学习目标
- 掌握 4 种负载均衡算法
- 学会 Nginx / Spring Cloud LoadBalancer 实战
- 配置会话保持与一致性哈希
- 避免负载不均、热点实例的坑
一、负载均衡算法
java
// 1. 轮询(Round Robin)
public class RoundRobin {
private AtomicInteger index = new AtomicInteger(0);
public ServiceInstance choose(List<ServiceInstance> instances) {
return instances.get(index.getAndIncrement() % instances.size());
}
}
// 2. 随机(Random)
public class Random {
public ServiceInstance choose(List<ServiceInstance> instances) {
return instances.get(ThreadLocalRandom.current().nextInt(instances.size()));
}
}
// 3. 最小连接(Least Connections)
public class LeastConnections {
public ServiceInstance choose(List<ServiceInstance> instances) {
return instances.stream()
.min(Comparator.comparingInt(i -> connectionCount.get(i.getId())))
.orElseThrow();
}
}
// 4. 一致性哈希(Consistent Hash)
public class ConsistentHash {
private final TreeMap<Long, ServiceInstance> ring = new TreeMap<>();
public ServiceInstance choose(String key) {
long hash = hash(key);
SortedMap<Long, ServiceInstance> tailMap = ring.tailMap(hash);
return tailMap.isEmpty() ? ring.firstEntry().getValue() : tailMap.get(tailMap.firstKey());
}
}text
算法对比
├── 轮询:均匀,不考虑实例性能
├── 随机:均匀,小集群偏差
├── 最小连接:考虑负载,适合长连接
└── 一致性哈希:缓存命中率,适合会话保持⚠️ 坑 1:所有请求都被哈希到一个实例 = 热点。加虚拟节点,让分布更均匀。
二、Nginx 负载均衡
nginx
upstream order-service {
# 1. 轮询(默认)
server order1:8080;
server order2:8080;
server order3:8080;
# 2. 加权轮询
server order1:8080 weight=3; # 3 倍流量
server order2:8080 weight=2;
server order3:8080 weight=1;
# 3. ip_hash(会话保持)
ip_hash;
server order1:8080;
server order2:8080;
# 4. least_conn
least_conn;
server order1:8080;
server order2:8080;
}
server {
listen 80;
location /api/order {
proxy_pass http://order-service;
}
}nginx
# 健康检查
upstream order-service {
server order1:8080 max_fails=3 fail_timeout=30s;
server order2:8080;
server order3:8080 backup; # 备份
}三、Spring Cloud LoadBalancer
yaml
spring:
cloud:
loadbalancer:
ribbon:
enabled: false # 禁用 Ribbon(JDK 17 已移除)
configurations:
round-robin # 轮询java
// 1. 自定义负载均衡
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment env, LoadBalancerClientFactory factory) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
// 2. 调用
@Resource
private LoadBalancerClient loadBalancerClient;
public String callOrder() {
ServiceInstance instance = loadBalancerClient.choose("order-service");
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/api/...";
return restTemplate.getForObject(url, String.class);
}四、Feign 集成
java
// 自动负载均衡
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/api/order/{id}")
Order getOrder(@PathVariable Long id);
}yaml
# 切换负载均衡策略
spring:
cloud:
loadbalancer:
client:
name: order-service
configurations: random五、加权负载均衡
java
// 实例元数据
eureka:
instance:
metadata-map:
weight: 100 # 权重 100java
// 自定义权重负载均衡
public class WeightLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return supplier.get().next().map(instances -> {
// 总权重
int totalWeight = instances.stream()
.mapToInt(i -> Integer.parseInt(i.getMetadata().getOrDefault("weight", "100")))
.sum();
// 随机
int random = ThreadLocalRandom.current().nextInt(totalWeight);
int current = 0;
for (ServiceInstance instance : instances) {
int weight = Integer.parseInt(instance.getMetadata().getOrDefault("weight", "100"));
current += weight;
if (random < current) {
return new DefaultResponse(instance);
}
}
return new DefaultResponse(instances.get(0));
});
}
}⚠️ 坑 2:权重分发在 client,Nginx 一层再做一次 → 权重叠加,流量不平衡。只在一层做权重。
六、负载均衡 + 限流
java
// 实例级限流:每个实例 100 QPS
// 3 个实例 → 总 300 QPS
// 集群限流:总 QPS 100(Nacos/Lua 统一管理)
public class ClusterRateLimiter {
public boolean tryAcquire(String service, int permits) {
String lua = "return redis.call('INCRBY', KEYS[1], " + permits + ")";
Long count = redisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), List.of("rl:" + service));
return count <= maxCount;
}
}七、负载均衡 + 健康检查
yaml
# 1. 实例不健康自动剔除
eureka:
instance:
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30
# 2. 主动健康检查
health-check:
enabled: true
interval: 5000
timeout: 3000
unhealthy-threshold: 3java
// 自定义健康检查
@Bean
public HealthIndicator customHealth() {
return () -> checkDatabase() && checkRedis()
? Health.up().build()
: Health.down().build();
}八、负载均衡 + 重试
java
// 失败时换实例重试
@Retryable(retryFor = Exception.class, maxAttempts = 3)
public String callOrder(Long orderId) {
return orderClient.getOrder(orderId);
}yaml
# Spring Cloud Retry
spring:
cloud:
loadbalancer:
retry:
enabled: true
max-retries: 3
retry-on-all-exceptions: true⚠️ 坑 3:重试导致请求放大,1 个请求变 3 个,后端压力 3 倍。重试 + 熔断配合。
九、负载均衡策略对比
| 策略 | 适用场景 |
|---|---|
| 轮询 | 集群性能一致 |
| 加权轮询 | 集群性能不一致 |
| 最小连接 | 长连接服务 |
| 一致性哈希 | 缓存、会话 |
| 随机 | 简单场景 |
十、压测与监控
promql
# 各实例请求数
sum(rate(http_server_requests_seconds_count[1m])) by (instance)
# 各实例 P99 延迟
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[1m])) by (instance, le))
# 各实例错误率
sum(rate(http_server_requests_seconds_count{status=~"5.."}[1m])) by (instance) /
sum(rate(http_server_requests_seconds_count[1m])) by (instance)text
观察指标
├── 请求分布:各实例均匀 ±10%
├── 错误率:各实例 < 0.1%
└── 延迟:各实例 P99 < 100ms本章小结
| 工具 | 适用 |
|---|---|
| Nginx | 入口负载均衡 |
| Spring Cloud LB | 微服务间 |
| 自研 | 特殊策略 |
| 关键点 | 建议 |
|---|---|
| 权重 | 一层做 |
| 健康检查 | 5-10s |
| 重试 | + 熔断 |
| 监控 | 各实例均衡 |
动手练习
- Nginx 轮询:配置 3 个后端,观察流量分布
- 加权负载:用权重 metadata,验证高权重实例流量大
- 一致性哈希:写一个一致性哈希 LB,验证缓存命中率
- 健康检查:故意停一个实例,观察流量转移
下一章:第 29 章:数据一致性 →