文章标签: #java #springcloud #ribbon #负载均衡 #feign #源码分析 #面试 #netflix
- 引言:客户端负载均衡的本质
- 理论基础:负载均衡算法与分布式系统
- 演进史:从Netflix Ribbon到Spring Cloud LoadBalancer
- 源码深度分析:Ribbon核心架构
- 源码深度分析:负载均衡策略实现
- 源码深度分析:Feign与Ribbon的集成
- 实战案例:生产级负载均衡配置
- 对比分析:Ribbon vs Nginx vs Spring Cloud LoadBalancer
- 性能分析:客户端负载均衡的延迟与吞吐量
- 常见陷阱与最佳实践
- 面试题与参考答案
在微服务架构中,单个服务通常部署多个实例以实现高可用和水平扩展。当消费方调用提供方时,如何将请求合理地分发到这些实例上,是客户端负载均衡(Client-Side Load Balancing)要解决的核心问题。
┌─────────────────────────────────────────────────────────────┐
│ 客户端负载均衡的核心挑战 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 消费方 注册中心 提供方实例 │
│ Consumer (Eureka/Nacos) Provider │
│ │ │ │ │
│ │ 1.获取服务列表 │ │ │
│ │◄─────────────────────│ │ │
│ │ │ │ │
│ │ 2.选择实例 │ │ │
│ │ (负载均衡算法) │ │ │
│ │ │ │ │
│ │ 3.直接调用 │ │ │
│ │──────────────────────► Instance A (healthy) │ │
│ │ │ Instance B (healthy) │ │
│ │ │ Instance C (slow) │ │
│ │ │ │ │
│ │
│ 关键决策: │
│ 1. 选择哪个实例?(RoundRobin/Random/Weighted/...) │
│ 2. 如何感知实例健康?(Ping机制) │
│ 3. 失败时如何处理?(重试/熔断) │
│ 4. 如何避免热点?(一致性哈希) │
└─────────────────────────────────────────────────────────────┘
Ribbon是Netflix开源的客户端负载均衡器,与Eureka配合实现了完整的服务间调用方案。在Spring Cloud Netflix生态中:
- Ribbon负责"选择哪个实例"
- Eureka负责"提供实例列表"
- Feign负责"如何发送HTTP请求"
Spring Cloud服务调用链路:
┌─────────────────────────────────────────────────────────────┐
│ 调用链路架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ @FeignClient(name="user-service") │
│ │ │
│ ▼ │
│ LoadBalancerFeignClient │
│ │ │
│ │ 1. 解析服务名 "user-service" │
│ │ │
│ ▼ │
│ ILoadBalancer (Ribbon) │
│ │ │
│ │ 2. 从Eureka获取实例列表 │
│ │ [192.168.1.10:8080, 192.168.1.11:8080] │
│ │ │
│ │ 3. 根据IRule选择实例 │
│ │ -> 192.168.1.11:8080 (RoundRobin) │
│ │ │
│ ▼ │
│ Client.execute() │
│ │ │
│ │ 4. 发送HTTP请求到选定实例 │
│ │ │
│ ▼ │
│ Instance B (192.168.1.11:8080) │
│ │
└─────────────────────────────────────────────────────────────┘
关键认知:Ribbon不是简单的"轮询器",它是一个完整的客户端负载均衡框架,包含服务列表管理、健康检查、负载均衡策略、重试机制等多个模块。
负载均衡分类体系:
负载均衡
├── 按层级
│ ├── 四层负载均衡(L4):基于IP+Port(TCP/UDP)
│ │ └── 示例:LVS、HAProxy(TCP模式)
│ │
│ └── 七层负载均衡(L7):基于HTTP/HTTPS内容
│ └── 示例:Nginx、Envoy、Spring Cloud Gateway
│
├── 按部署位置
│ ├── 服务端负载均衡(Server-Side)
│ │ └── Nginx、HAProxy、AWS ELB、K8s Ingress
│ │
│ └── 客户端负载均衡(Client-Side)
│ └── Ribbon、Spring Cloud LoadBalancer、gRPC LB
│
└── 按算法
├── 静态算法
│ ├── 轮询(Round Robin)
│ ├── 随机(Random)
│ ├── 权重轮询(Weighted Round Robin)
│ └── 源地址哈希(Source IP Hash)
│
└── 动态算法
├── 最少连接(Least Connections)
├── 响应时间加权(Weighted Response Time)
├── 最优可用(Best Available)
└── 自适应(Adaptive)
算法1:轮询(Round Robin)
请求序列:R1, R2, R3, R4, R5, R6
实例列表:[A, B, C]
分配结果:
R1 -> A R4 -> A
R2 -> B R5 -> B
R3 -> C R6 -> C
特点:
- 公平分配,每个实例请求数相同
- 不考虑实例性能差异
- 实现简单,无状态
算法2:加权轮询(Weighted Round Robin)
实例权重:A=5, B=3, C=2(总权重=10)
分配序列(每10个请求):
A, A, A, A, A, B, B, B, C, C
特点:
- 考虑实例性能差异
- 高性能实例处理更多请求
- 需要合理设置权重
算法3:最少连接(Least Connections)
当前连接数:A=10, B=5, C=8
下一个请求 -> B(连接数最少)
特点:
- 考虑实例实时负载
- 长连接场景效果好
- 需要维护连接计数
算法4:源地址哈希(Source IP Hash)
hash(client_ip) % instance_count = target_index
Client IP 1: hash(192.168.1.10) % 3 = 1 -> Instance B
Client IP 2: hash(192.168.1.11) % 3 = 0 -> Instance A
Client IP 3: hash(192.168.1.12) % 3 = 2 -> Instance C
特点:
- 同一客户端IP总是路由到同一实例
- 实现会话保持(Session Affinity)
- 实例变化时哈希环重分配(一致性哈希优化)
算法5:一致性哈希(Consistent Hashing)
将实例和请求映射到同一个哈希环上:
哈希环(0-2^32-1):
0
│
Instance A (hash=100)
│
Request 1 (hash=150) -> 顺时针找到 A
│
Instance B (hash=500)
│
Request 2 (hash=600) -> 顺时针找到 B
│
Instance C (hash=900)
│
2^32-1
优势:
- 实例增减时只影响少量请求(1/n)
- 适合缓存场景
Ribbon的健康检查模型:
┌─────────────────────────────────────────────────────────────┐
│ 健康检查架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ IPing (接口) │
│ │ │
│ ├── DummyPing (默认,总是返回true) │
│ │ │
│ ├── PingUrl (HTTP GET探测) │
│ │ └── 发送HTTP请求到/health端点 │
│ │ │
│ ├── PingConstant (固定返回值) │
│ │ │
│ └── NIWSDiscoveryPing (结合Eureka状态) │
│ └── 使用Eureka的InstanceStatus判断是否可用 │
│ │
│ ILoadBalancer │
│ │ │
│ ├── 维护"所有实例"列表 (allServers) │
│ └── 维护"可用实例"列表 (upServers) │
│ │
│ 定时任务:默认每10秒执行一次Ping │
│ 结果:更新upServers列表,负载均衡从upServers中选择 │
│ │
└─────────────────────────────────────────────────────────────┘
客户端负载均衡的CAP权衡:
Consistency(一致性):
- 所有客户端看到相同的服务列表
- 需要实时同步注册表状态
- 代价:增加网络开销和延迟
Availability(可用性):
- 即使注册中心不可用,仍能基于本地缓存调用
- 代价:可能调用到已下线的实例
Partition Tolerance(分区容错):
- 网络分区时仍能做出负载均衡决策
- 客户端模式天然具有分区容错性
Ribbon的选择:
- 优先保证可用性(本地缓存)
- 容忍短暂不一致(缓存刷新间隔30秒)
- 通过重试和熔断处理不一致导致的失败
Ribbon作为Netflix OSS套件的一部分,最初用于Netflix内部微服务间的负载均衡。
Netflix Ribbon的设计背景:
2013年:Netflix全面AWS云化
- EC2实例动态扩缩容
- 需要客户端自主决定调用哪个实例
- 减少基础设施依赖(不依赖外部LB)
Ribbon 1.0核心特性:
- 支持多种负载均衡算法(6种内置策略)
- 与Eureka深度集成
- 支持区域感知(Zone Awareness)
- 支持重试机制
- 提供REST客户端(RestClient)
Spring Cloud集成(2014-2015):
- Spring Cloud Netflix发布
- @LoadBalanced注解与RestTemplate集成
- Feign默认集成Ribbon
- 成为Spring Cloud微服务标准组件
关键版本演进:
Ribbon 2.0(2015):
- 引入ILoadBalancer新接口
- 支持响应式编程(初步)
- 改进的缓存和刷新机制
Spring Cloud Edgware(2017):
- 支持配置文件定义负载均衡策略
- 改进的重试机制(与Spring Retry集成)
- 更好的指标监控支持
Spring Cloud Finchley(2018):
- 与Spring Boot 2.0兼容
- 响应式支持增强
- 改进的 health check 机制
Netflix OSS维护模式:
2018.12:Netflix宣布Ribbon进入维护模式
- 不再开发新特性
- 仅修复严重Bug
- 建议用户迁移到替代方案
Spring Cloud官方替代方案:
- Spring Cloud LoadBalancer(Spring Cloud官方LB)
- 基于Spring Cloud Commons
- 支持响应式(Reactor)
- 更轻量级
- 更好的Spring生态集成
2020.0 (Ilford)版本:
- 默认移除Ribbon
- 使用Spring Cloud LoadBalancer替代
- 保留Feign,但底层LB改为LoadBalancer
Ribbon当前使用场景:
1. 存量系统维护:
- 大量基于spring-cloud-netflix的项目
- 迁移成本较高
2. 学习价值:
- 经典的客户端负载均衡实现
- 算法实现清晰,适合学习
3. 特定需求:
- Spring Cloud LoadBalancer功能不够丰富时
- 需要WeightedResponseTimeRule等高级策略
替代方案对比:
Spring Cloud LoadBalancer:
- 优点:官方支持、响应式、与Spring生态深度集成
- 缺点:策略较少(目前仅RoundRobin和Random)
- 适用:新项目、响应式应用
Nacos:
- 优点:支持权重、灰度、多种策略
- 缺点:阿里生态绑定较深
- 适用:使用Nacos注册中心的项目
Envoy/Istio:
- 优点:服务网格、L7负载均衡、丰富的流量管理
- 缺点:引入Sidecar,增加复杂度
- 适用:大规模微服务、需要高级流量管理
Ribbon核心类结构:
┌─────────────────────────────────────────────────────────────┐
│ ILoadBalancer │
│ (负载均衡器接口) │
│ - addServers(List<Server>) │
│ - chooseServer(Object key) │
│ - markServerDown(Server) │
│ - getReachableServers() │
│ - getAllServers() │
└────────────────────────┬────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│BaseLoadBalancer │ │DynamicServerList│ │ZoneAwareLoadBalancer
│ │ │LoadBalancer │ │ │
│ 基础实现: │ │ │ │ 区域感知: │
│ - 固定服务列表 │ │ 动态服务列表: │ │ - 按可用区过滤 │
│ - 基础Ping │ │ - 从Eureka获取 │ │ - 区域优先 │
│ - 单策略 │ │ - 定时刷新 │ │ - 故障转移 │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────┴───────────────────┘
│
▼
┌─────────────────────┐
│ IRule (负载均衡策略) │
│ - choose(Object key,│
│ Object... args) │
└──────────┬──────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│RoundRobinRule │ │RandomRule │ │WeightedResponse │
│轮询规则 │ │随机规则 │ │TimeRule │
└─────────────────┘ └─────────────────┘ └─────────────────┘
其他核心接口:
- IPing:健康检查
- ServerList:服务列表源(DiscoveryEnabledNIWSServerList从Eureka获取)
- ServerListUpdater:列表更新器(定时刷新)
- IClientConfig:客户端配置
// ILoadBalancer.java - 负载均衡器顶层接口
public interface ILoadBalancer {
/**
* 添加服务实例列表
*/
public void addServers(List<Server> newServers);
/**
* 选择服务实例(核心方法)
*/
public Server chooseServer(Object key);
/**
* 标记实例为下线
*/
public void markServerDown(Server server);
/**
* 获取可用实例列表
*/
public List<Server> getReachableServers();
/**
* 获取所有实例列表(包括不可用)
*/
public List<Server> getAllServers();
}// BaseLoadBalancer.java - 基础负载均衡实现
public class BaseLoadBalancer extends AbstractLoadBalancer
implements PrimeConnections.PrimeConnectionListener, IClientConfigAware {
// 所有服务实例列表
protected volatile List<Server> allServerList = Collections.emptyList();
// 可用服务实例列表(经过健康检查)
protected volatile List<Server> upServerList = Collections.emptyList();
// 负载均衡策略(IRule)
protected IRule rule;
// 健康检查器(IPing)
protected IPing ping;
// 定时执行Ping的任务
protected Timer lbTimer = null;
// Ping间隔(默认30秒)
protected int pingIntervalSeconds = 30;
/**
* 初始化负载均衡器
*/
void initWithNiwsConfig(IClientConfig clientConfig) {
// 1. 设置Rule(默认RoundRobinRule)
String ruleClassName = clientConfig.getProperty(
CommonClientConfigKey.RuleClassName,
RoundRobinRule.class.getName()
);
this.rule = (IRule) ClientFactory.instantiateInstanceWithClientConfig(
ruleClassName, clientConfig
);
// 2. 设置Ping(默认DummyPing)
String pingClassName = clientConfig.getProperty(
CommonClientConfigKey.PingClassName,
DummyPing.class.getName()
);
this.ping = (IPing) ClientFactory.instantiateInstanceWithClientConfig(
pingClassName, clientConfig
);
// 3. 设置Ping间隔
this.pingIntervalSeconds = Integer.parseInt(clientConfig.getProperty(
CommonClientConfigKey.NFLoadBalancerPingInterval,
"30"
));
// 4. 启动定时Ping任务
setupPingTask();
}
/**
* 选择服务实例(模板方法模式)
*/
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
// 委托给IRule选择实例
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server", name, e);
return null;
}
}
}
/**
* 定时Ping任务:更新upServerList
*/
void setupPingTask() {
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new Timer("NFLoadBalancer-PingTimer-" + name, true);
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
}
class PingTask extends TimerTask {
public void run() {
try {
// 执行Ping策略
new Pinger(pingStrategy).runPinger();
} catch (Exception e) {
logger.error("LoadBalancer [{}]: Error pinging", name, e);
}
}
}
}// DynamicServerListLoadBalancer.java
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
// 服务列表提供者(从Eureka获取)
volatile ServerList<T> serverListImpl;
// 服务列表更新器(定时刷新)
volatile ServerListUpdater serverListUpdater;
// 上次更新的服务列表
protected volatile List<T> allServerList = Collections.emptyList();
/**
* 使用NIWS配置初始化
*/
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
super.initWithNiwsConfig(clientConfig);
// 1. 初始化ServerList(从Eureka获取实例列表)
String serverListClassName = clientConfig.getProperty(
CommonClientConfigKey.NIWSServerListClassName,
DiscoveryEnabledNIWSServerList.class.getName()
);
this.serverListImpl = (ServerList<T>) ClientFactory
.instantiateInstanceWithClientConfig(serverListClassName, clientConfig);
// 2. 初始化ServerListUpdater(默认PollingServerListUpdater)
String serverListUpdaterClassName = clientConfig.getProperty(
CommonClientConfigKey.NIWSServerListUpdaterClassName,
PollingServerListUpdater.class.getName()
);
this.serverListUpdater = (ServerListUpdater) ClientFactory
.instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig);
// 3. 启动服务列表更新
updateListOfServers();
// 4. 启动定时更新任务
serverListUpdater.start(new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
});
}
/**
* 更新服务列表(从Eureka拉取最新实例)
*/
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<>();
if (serverListImpl != null) {
// 从ServerList获取实例
servers = serverListImpl.getUpdatedListOfServers();
if (filter != null) {
// 应用过滤器(如ZonePreferenceServerListFilter)
servers = filter.getFilteredListOfServers(servers);
}
}
// 更新负载均衡器的服务列表
updateAllServerList(servers);
}
/**
* 更新所有服务列表
*/
protected void updateAllServerList(List<T> ls) {
if (serverListUpdateInProgress.compareAndSet(false, true)) {
try {
for (T s : ls) {
s.setAlive(true); // 临时标记为存活,等待Ping确认
}
// 设置所有服务列表
setServersList(ls);
// 强制立即执行一次Ping
forceQuickPing();
} finally {
serverListUpdateInProgress.set(false);
}
}
}
}// ZoneAwareLoadBalancer.java - 区域感知负载均衡器
public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
// 每个Zone对应一个CompositeLoadBalancer
private ConcurrentHashMap<String, BaseLoadBalancer> balancers = new ConcurrentHashMap<>();
// 区域故障阈值(默认0.99999d)
private static final double DEFAULT_ZONE_AFFINITY_MAX_BLACKOUT_SECS = 0.99999d;
/**
* 选择服务实例(优先选择同区域实例)
*/
@Override
public Server chooseServer(Object key) {
// 如果只有一个Zone,使用父类逻辑
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
return super.chooseServer(key);
}
// 区域感知选择逻辑
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
// 获取当前可用区域映射
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
// 获取活跃区域(未触发故障阈值的区域)
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot,
triggeringLoad.get(), triggeringBlackoutPercentage.get());
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
// 从可用区域中随机选择一个
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
if (zone != null) {
// 获取该区域的LoadBalancer
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
// 从该区域选择实例
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic", e);
}
// 如果区域选择失败,回退到父类逻辑
if (server != null) {
return server;
} else {
return super.chooseServer(key);
}
}
/**
* 获取或创建指定Zone的LoadBalancer
*/
@Override
public BaseLoadBalancer getLoadBalancer(String zone) {
zone = zone.toLowerCase();
BaseLoadBalancer loadBalancer = balancers.get(zone);
if (loadBalancer == null) {
// 懒加载:创建新的LoadBalancer
BaseLoadBalancer newLB = createLoadBalancer(zone);
BaseLoadBalancer prev = balancers.putIfAbsent(zone, newLB);
loadBalancer = prev != null ? prev : newLB;
}
return loadBalancer;
}
}// RoundRobinRule.java - 轮询负载均衡策略
public class RoundRobinRule extends AbstractLoadBalancerRule {
// CAS原子计数器,保证线程安全
private AtomicInteger nextServerCyclicCounter = new AtomicInteger(0);
/**
* 选择服务实例
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
// 最多重试10次
while (server == null && count++ < 10) {
// 获取可用实例列表
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
// 如果没有可用实例,返回null
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 轮询选择下一个实例
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
// 如果选中的实例不可用,继续轮询
if (server == null || !server.isAlive()) {
server = null;
continue;
}
return server;
}
// 超过重试次数仍失败
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
/**
* 原子递增并取模(无锁算法)
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
// CAS操作:如果当前值等于current,则更新为next
if (nextServerCyclicCounter.compareAndSet(current, next)) {
return next;
}
// CAS失败则重试(其他线程已修改)
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
}轮询算法关键点:
CAS无锁轮询的优势:
传统方式(加锁):
synchronized(this) {
nextIndex = (nextIndex + 1) % count;
}
缺点:线程竞争导致性能下降
CAS方式(无锁):
for (;;) {
current = counter.get();
next = (current + 1) % count;
if (counter.compareAndSet(current, next)) {
return next;
}
}
优点:
- 无线程阻塞,高性能
- 利用CPU原子指令
- 适合高并发场景
注意:CAS在高竞争下可能频繁重试,但Ribbon场景(请求间隔>1ms)竞争通常不激烈
// RandomRule.java - 随机负载均衡策略
public class RandomRule extends AbstractLoadBalancerRule {
// ThreadLocalRandom比Random性能更好(无锁)
// 每个线程有自己的种子,避免竞争
/**
* 随机选择可用实例
*/
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
// 获取可用实例列表
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
// 生成随机索引
int index = chooseRandomInt(serverCount);
server = upList.get(index);
if (server == null || !server.isAlive()) {
server = null;
// 短暂休眠避免CPU空转
Thread.yield();
}
}
return server;
}
/**
* 生成随机整数
*/
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
}// WeightedResponseTimeRule.java - 响应时间加权策略
public class WeightedResponseTimeRule extends RoundRobinRule {
// 定时计算权重的任务
private static final int SERVER_WEIGHT_TASK_TIMER_INTERVAL = 30 * 1000;
// 权重计算锁
private volatile List<Double> accumulatedWeights = new ArrayList<Double>();
// 定时器
private final Timer serverWeightTimer = new Timer("NFLoadBalancer-serverWeightTimer", true);
/**
* 初始化:启动定时权重计算任务
*/
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof BaseLoadBalancer) {
// 启动定时任务,每30秒计算一次权重
serverWeightTimer.schedule(new DynamicServerWeightTask(),
0, SERVER_WEIGHT_TASK_TIMER_INTERVAL);
}
}
/**
* 根据权重选择实例
*/
@Override
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
List<Double> currentWeights = accumulatedWeights;
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
// 如果没有权重数据,使用父类轮询
if (currentWeights.size() != serverCount) {
return super.choose(lb, key);
}
// 生成随机数,范围[0, 最大累计权重)
double randomWeight = currentWeights.get(serverCount - 1) * Math.random();
// 二分查找:确定随机数落在哪个区间
int n = 0;
for (Double d : currentWeights) {
if (d >= randomWeight) {
server = allList.get(n);
break;
}
n++;
}
if (server != null && server.isAlive()) {
return server;
}
server = null;
}
return server;
}
/**
* 定时计算权重的任务
*/
class DynamicServerWeightTask extends TimerTask {
public void run() {
ServerWeight serverWeight = new ServerWeight();
try {
serverWeight.maintainWeights();
} catch (Exception e) {
logger.error("Error running DynamicServerWeightTask", e);
}
}
}
/**
* 权重计算核心逻辑
*/
class ServerWeight {
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (serverWeightAssignmentInProgress.compareAndSet(false, true)) {
try {
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return;
}
// 获取负载均衡统计信息
LoadBalancerStats lbStats = ((BaseLoadBalancer) lb).getLoadBalancerStats();
// 计算每个实例的平均响应时间
List<Double> totalResponseTimes = new ArrayList<Double>();
for (Server server : allList) {
ServerStats stats = lbStats.getSingleServerStat(server);
// 平均响应时间(毫秒)
long responseTime = stats.getResponseTimeAvg();
totalResponseTimes.add((double) responseTime);
}
// 计算权重:响应时间越短,权重越大
// 权重 = maxResponseTime - currentResponseTime
double maxResponseTime = 0;
for (double responseTime : totalResponseTimes) {
maxResponseTime = Math.max(maxResponseTime, responseTime);
}
List<Double> weights = new ArrayList<Double>();
double accumulatedWeight = 0;
for (double responseTime : totalResponseTimes) {
double weight = maxResponseTime - responseTime;
if (weight < 0) {
weight = 0;
}
accumulatedWeight += weight;
weights.add(accumulatedWeight);
}
// 更新累计权重列表
accumulatedWeights = weights;
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
}
}
}权重计算原理:
WeightedResponseTimeRule权重计算示例:
实例列表:[A, B, C]
平均响应时间:A=100ms, B=200ms, C=300ms
计算过程:
1. maxResponseTime = max(100, 200, 300) = 300ms
2. 计算每个实例的权重(响应越短权重越大):
- A: 300 - 100 = 200
- B: 300 - 200 = 100
- C: 300 - 300 = 0
3. 计算累计权重:
- A: 200
- B: 200 + 100 = 300
- C: 300 + 0 = 300
4. 选择实例:
- 生成随机数 random ∈ [0, 300)
- random < 200: 选择A(概率 200/300 = 66.7%)
- 200 ≤ random < 300: 选择B(概率 100/300 = 33.3%)
- random ≥ 300: 选择C(概率 0%)
效果:
- 响应时间最短的A获得最多请求
- 响应时间最长的C获得最少请求(甚至为0)
- 动态调整:每30秒重新计算权重
// AvailabilityFilteringRule.java
public class AvailabilityFilteringRule extends ClientConfigEnabledRoundRobinRule {
// 活跃连接数阈值(默认2^31-1,即不限制)
private static final long ACTIVE_CONNECTIONS_LIMIT = Long.MAX_VALUE;
/**
* 判断实例是否可用(不过载)
*/
public boolean predicate(Server server) {
// 获取该实例的统计信息
LoadBalancerStats lbStats = getLoadBalancerStats();
if (lbStats == null) {
return true; // 无统计信息时默认可用
}
ServerStats serverStats = lbStats.getSingleServerStat(server);
// 条件1:活跃请求数不超过阈值
long activeConnections = serverStats.getActiveRequestsCount();
if (activeConnections >= ACTIVE_CONNECTIONS_LIMIT) {
return false; // 连接数超限,不可用
}
// 条件2:实例状态正常(通过Ping检测)
if (!server.isAlive()) {
return false; // Ping检测失败
}
// 条件3:短路判断(如果失败率过高,标记为短路)
if (serverStats.isCircuitBreakerTripped()) {
return false; // 熔断器已触发
}
return true;
}
/**
* 选择可用实例
*/
@Override
public Server choose(Object key) {
int count = 0;
Server server = null;
// 最多尝试10次
while (server == null && count++ < 10) {
// 使用RoundRobin选择下一个实例
server = super.choose(key);
// 使用predicate判断可用性
if (server != null && predicate(server)) {
return server; // 找到可用实例
}
// 不可用,继续轮询
server = null;
}
// 如果10次都没找到,返回null(由上层处理)
return super.choose(key);
}
}// ZoneAvoidanceRule.java - 区域感知策略(Ribbon默认策略)
public class ZoneAvoidanceRule extends PredicateBasedRule {
// 区域故障检测:如果区域平均响应时间 > 阈值,则排除该区域
private static final double DEFAULT_TRIGGERING_LOAD_FACTOR = 0.2d;
// 区域故障检测:如果区域故障实例比例 > 阈值,则排除该区域
private static final double DEFAULT_TRIGGERING_BLACKOUT_PERCENTAGE = 0.99999d;
private CompositePredicate compositePredicate;
public ZoneAvoidanceRule() {
super();
// 创建组合断言:可用性断言 AND 区域断言
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
/**
* 选择实例:优先选择同区域且健康的实例
*/
@Override
public Server choose(Object key) {
// 获取负载均衡器
ILoadBalancer lb = getLoadBalancer();
// 获取可用区域
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = createSnapshot(lbStats);
// 获取活跃区域(未触发故障阈值的区域)
Set<String> availableZones = getAvailableZones(
zoneSnapshot,
triggeringLoad.get(),
triggeringBlackoutPercentage.get()
);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
// 从可用区域中随机选择一个
String zone = randomChooseZone(zoneSnapshot, availableZones);
if (zone != null) {
// 获取该区域的实例列表
List<Server> zoneServers = getZoneServers(lb, zone);
// 从该区域选择健康实例
Optional<Server> server = zoneServers.stream()
.filter(s -> compositePredicate.apply(new PredicateKey(s)))
.findFirst();
if (server.isPresent()) {
return server.get();
}
}
}
// 回退:使用父类逻辑(所有区域)
return super.choose(key);
}
/**
* 创建区域快照
*/
public static Map<String, ZoneSnapshot> createSnapshot(LoadBalancerStats lbStats) {
Map<String, ZoneSnapshot> map = new HashMap<>();
for (String zone : lbStats.getAvailableZones()) {
ZoneSnapshot snapshot = lbStats.getZoneSnapshot(zone);
map.put(zone, snapshot);
}
return map;
}
/**
* 获取可用区域(排除故障区域)
*/
public static Set<String> getAvailableZones(Map<String, ZoneSnapshot> snapshot,
double triggeringLoad,
double triggeringBlackoutPercentage) {
// 如果所有区域都健康,返回所有区域
if (snapshot.isEmpty()) {
return null;
}
Set<String> availableZones = new HashSet<>(snapshot.keySet());
// 统计故障区域
List<String> worstZones = new ArrayList<>();
double maxLoadPerServer = 0;
boolean limitedZoneAvailability = false;
for (Map.Entry<String, ZoneSnapshot> entry : snapshot.entrySet()) {
String zone = entry.getKey();
ZoneSnapshot zoneSnapshot = entry.getValue();
// 计算该区域实例数
int instanceCount = zoneSnapshot.getInstanceCount();
if (instanceCount == 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
// 计算该区域负载
double loadPerServer = zoneSnapshot.getLoadPerServer();
// 如果负载超过阈值,标记为故障区域
if (loadPerServer < 0 || loadPerServer > triggeringLoad) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
// 记录负载最高的区域
if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
worstZones.add(zone);
} else if (loadPerServer > maxLoadPerServer) {
maxLoadPerServer = loadPerServer;
worstZones.clear();
worstZones.add(zone);
}
}
}
}
// 如果大部分区域都故障,保留负载最低的区域
if (limitedZoneAvailability && availableZones.size() > 1) {
availableZones.removeAll(worstZones);
}
return availableZones;
}
}// FeignClientFactoryBean.java - Feign客户端工厂Bean
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
private Class<?> type; // @FeignClient注解的接口类型
private String name; // 服务名
private String url; // 固定URL(可选)
@Override
public Object getObject() throws Exception {
return getTarget();
}
/**
* 创建Feign客户端代理
*/
<T> T getTarget() {
// 1. 从Spring上下文获取Feign上下文
FeignContext context = applicationContext.getBean(FeignContext.class);
// 2. 获取Feign构建器
Feign.Builder builder = feign(context);
// 3. 如果没有指定URL(使用服务名),集成Ribbon
if (!StringUtils.hasText(url)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
} else {
url = this.name;
}
url += cleanPath();
// 4. 返回负载均衡客户端(集成Ribbon)
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
// 5. 如果指定了URL,使用普通客户端(不集成Ribbon)
// ...
}
/**
* 创建负载均衡的Feign客户端
*/
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
// 从Spring上下文获取Client(默认是LoadBalancerFeignClient)
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
// 获取Targeter(处理Hystrix等装饰)
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for load balancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"
);
}
}// LoadBalancerFeignClient.java
public class LoadBalancerFeignClient implements Client {
// 实际的HTTP客户端(如Apache HttpClient)
private final Client delegate;
// CachingSpringLoadBalancerFactory:缓存LoadBalancer实例
private CachingSpringLoadBalancerFactory lbClientFactory;
// Spring工厂
private SpringClientFactory clientFactory;
public LoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
this.delegate = delegate;
this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory;
}
/**
* 执行HTTP请求(集成Ribbon负载均衡)
*/
@Override
public Response execute(Request request, Options options) throws IOException {
try {
// 1. 解析URI
URI asUri = URI.create(request.url());
String clientName = asUri.getHost(); // 服务名
URI uriWithoutHost = cleanUrl(request.url(), clientName);
// 2. 获取Feign负载均衡客户端
FeignLoadBalancer.RibbonRequest ribbonRequest =
new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);
// 3. 配置
IClientConfig requestConfig = getClientConfig(options, clientName);
// 4. 执行负载均衡调用
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)
.toResponse();
} catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
/**
* 获取或创建FeignLoadBalancer(缓存)
*/
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
/**
* 获取客户端配置
*/
private IClientConfig getClientConfig(Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
} else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
}// FeignLoadBalancer.java
public class FeignLoadBalancer extends AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest,
FeignLoadBalancer.RibbonResponse> {
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
ServerIntrospector serverIntrospector) {
super(lb, clientConfig);
this.setRetryHandler(new DefaultLoadBalancerRetryHandler(
clientConfig.get(CommonClientConfigKey.MaxAutoRetries, 0),
clientConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer, 1),
clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, true)
));
this.clientConfig = clientConfig;
this.serverIntrospector = serverIntrospector;
}
/**
* 执行带负载均衡的请求
*/
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
// 1. 构建Request.Options(超时配置)
Request.Options options;
if (configOverride != null) {
// 使用配置中的超时设置
options = new Request.Options(
configOverride.get(CommonClientConfigKey.ConnectTimeout, this.connectTimeout),
configOverride.get(CommonClientConfigKey.ReadTimeout, this.readTimeout)
);
} else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
// 2. 执行请求(delegate是实际的HTTP客户端)
Response response = request.client().execute(request.toRequest(), options);
// 3. 包装响应
return new RibbonResponse(request.getUri(), response);
}
/**
* 重建URI(将服务名替换为实际IP:Port)
*/
@Override
public URI reconstructURIWithServer(Server server, URI original) {
String host = server.getHost();
int port = server.getPort();
// 如果是HTTPS,端口为443时省略端口
if (port == -1) {
port = server.getScheme().equalsIgnoreCase("https") ? 443 : 80;
}
// 重建URI
try {
return new URI(original.getScheme(), original.getUserInfo(), host, port,
original.getPath(), original.getQuery(), original.getFragment());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}// 自定义负载均衡规则:优先选择同版本的实例
public class VersionPreferenceRule extends AbstractLoadBalancerRule {
private static final String TARGET_VERSION = "version";
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return null;
}
// 从请求头或上下文中获取目标版本
String targetVersion = getTargetVersion();
List<Server> allServers = lb.getAllServers();
List<Server> preferredServers = new ArrayList<>();
List<Server> fallbackServers = new ArrayList<>();
for (Server server : allServers) {
if (server instanceof DiscoveryEnabledServer) {
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer) server;
String serverVersion = dServer.getInstanceInfo()
.getMetadata().get("version");
if (targetVersion.equals(serverVersion)) {
preferredServers.add(server);
} else {
fallbackServers.add(server);
}
}
}
// 优先从同版本实例中选择
List<Server> candidates = preferredServers.isEmpty() ?
fallbackServers : preferredServers;
if (candidates.isEmpty()) {
return null;
}
// 在候选列表中随机选择
int index = ThreadLocalRandom.current().nextInt(candidates.size());
return candidates.get(index);
}
private String getTargetVersion() {
// 从Ribbon上下文获取版本信息
RequestContext context = RequestContext.getCurrentContext();
return context.getRequest().getHeader("X-Target-Version");
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}// 配置类
@Configuration
@AvoidScan // 自定义注解,避免被Spring Boot主上下文扫描
public class VersionRibbonConfig {
@Bean
public IRule versionRule() {
return new VersionPreferenceRule();
}
}// 主应用配置
@SpringBootApplication
@RibbonClients({
@RibbonClient(name = "user-service", configuration = VersionRibbonConfig.class),
@RibbonClient(name = "order-service", configuration = VersionRibbonConfig.class)
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}# 金丝雀发布:将5%流量路由到新版本
# 服务提供方配置(新版本实例)
eureka:
instance:
metadata-map:
version: v2
canary: "true"
weight: "5" # 5%权重
# 服务提供方配置(旧版本实例)
eureka:
instance:
metadata-map:
version: v1
canary: "false"
weight: "95" # 95%权重// 金丝雀负载均衡规则
public class CanaryRule extends AbstractLoadBalancerRule {
private static final double CANARY_PERCENTAGE = 0.05; // 5%
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
List<Server> allServers = lb.getAllServers();
List<Server> canaryServers = new ArrayList<>();
List<Server> normalServers = new ArrayList<>();
for (Server server : allServers) {
if (server instanceof DiscoveryEnabledServer) {
String canary = ((DiscoveryEnabledServer) server)
.getInstanceInfo().getMetadata().get("canary");
if ("true".equals(canary)) {
canaryServers.add(server);
} else {
normalServers.add(server);
}
}
}
// 随机决定是否路由到金丝雀
if (Math.random() < CANARY_PERCENTAGE && !canaryServers.isEmpty()) {
return canaryServers.get(ThreadLocalRandom.current().nextInt(canaryServers.size()));
}
// 返回普通实例
if (!normalServers.isEmpty()) {
return normalServers.get(ThreadLocalRandom.current().nextInt(normalServers.size()));
}
return null;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}# Ribbon重试配置
ribbon:
# 连接超时
ConnectTimeout: 1000
# 读取超时
ReadTimeout: 3000
# 是否对所有操作重试(GET/POST/PUT/DELETE)
OkToRetryOnAllOperations: false # 仅对GET重试(默认)
# 同一实例最大重试次数(不含首次)
MaxAutoRetries: 1
# 切换实例的最大重试次数
MaxAutoRetriesNextServer: 2
# 是否启用熔断器
EnableCircuitBreaker: true
# 熔断触发连续失败次数
CircuitBreakerRequestVolumeThreshold: 10
# 熔断后休眠时间
CircuitBreakerSleepWindowInMilliseconds: 5000
# 针对特定服务的配置
user-service:
ribbon:
ConnectTimeout: 2000
ReadTimeout: 5000
MaxAutoRetries: 2
MaxAutoRetriesNextServer: 3
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
order-service:
ribbon:
ConnectTimeout: 1000
ReadTimeout: 3000
MaxAutoRetries: 0 # 订单服务不允许重试(幂等性问题)
MaxAutoRetriesNextServer: 1
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule// 自定义重试策略
@Configuration
public class RetryConfig {
@Bean
public Retryer feignRetryer() {
// 初始间隔100ms,最大间隔1s,最多重试3次
return new Retryer.Default(100, 1000, 3);
}
@Bean
public ErrorDecoder feignErrorDecoder() {
return new ErrorDecoder() {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 503) {
// 服务不可用,触发重试
return new RetryableException(
response.status(),
"Service Unavailable",
response.request().httpMethod(),
null,
response.request()
);
}
return FeignException.errorStatus(methodKey, response);
}
};
}
}# 多区域部署配置
# 可用区A(北京)
eureka:
instance:
metadata-map:
zone: beijing-zone-a
client:
prefer-same-zone-eureka: true
region: beijing
availability-zones:
beijing: beijing-zone-a,beijing-zone-b
service-url:
beijing-zone-a: http://eureka-bj-a:8761/eureka/
beijing-zone-b: http://eureka-bj-b:8761/eureka/
# Ribbon区域感知配置
ribbon:
# 优先选择同区域实例
EnableZoneAffinity: true
# 是否启用区域排他(如果同区域不可用,才跨区)
EnableZoneExclusivity: false
# 区域故障阈值
ZoneAwareNIWSServerListClassName: com.netflix.loadbalancer.ZoneAwareLoadBalancer// 区域感知配置类
@Configuration
public class ZoneAwareRibbonConfig {
@Bean
public IRule zoneAwareRule() {
return new ZoneAvoidanceRule();
}
@Bean
public IPing ping() {
return new PingUrl(false, "/actuator/health");
}
@Bean
public ServerList<Server> ribbonServerList(IClientConfig config) {
// 从Eureka获取实例列表
DiscoveryEnabledNIWSServerList serverList = new DiscoveryEnabledNIWSServerList(
config.getClientName()
);
return serverList;
}
@Bean
public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
// 区域优先过滤器
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
filter.initWithNiwsConfig(config);
return filter;
}
}┌────────────────────┬─────────────────┬─────────────────┬─────────────────────┐
│ 特性 │ Ribbon │ Nginx │ Spring Cloud LB │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 工作层级 │ 客户端(应用层) │ 服务端(L7/L4) │ 客户端(应用层) │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 部署位置 │ 集成在应用中 │ 独立进程/机器 │ 集成在应用中 │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 服务列表来源 │ Eureka/Nacos │ 静态配置/健康检查│ Eureka/Nacos/Consul │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 负载均衡策略 │ 7种内置策略 │ 多种(轮询/权重/ │ 2种(目前) │
│ │ + 可扩展 │ ip_hash/least_conn)│ + 可扩展 │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 健康检查 │ IPing接口 │ 主动HTTP/TCP探测 │ 与注册中心集成 │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 重试机制 │ 内置支持 │ proxy_next_upstream│ 需配合Spring Retry│
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 熔断支持 │ 需配合Hystrix │ 需配合lua模块 │ 需配合Resilience4j │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 区域感知 │ ZoneAvoidanceRule│ 需第三方模块 │ 支持(Reactor) │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 性能开销 │ 低(本地计算) │ 高(网络跳数) │ 低(本地计算) │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 适用场景 │ 服务间内部调用 │ 外部流量入口 │ 服务间内部调用 │
├────────────────────┼─────────────────┼─────────────────┼─────────────────────┤
│ 维护状态 │ 停止维护 │ 活跃 │ 活跃(官方推荐) │
└────────────────────┴─────────────────┴─────────────────┴─────────────────────┘
Ribbon(客户端负载均衡):
Client App Service Instances
┌─────────────┐ ┌─────────┐
│ Ribbon LB │──直接调用───>│ Inst A │
│ (本地选择) │ ├─────────┤
│ │──直接调用───>│ Inst B │
│ Eureka Client│ ├─────────┤
│ (缓存列表) │──直接调用───>│ Inst C │
└─────────────┘ └─────────┘
优点:
- 减少网络跳数(无中间代理)
- 本地缓存,高可用
- 丰富的负载均衡策略
缺点:
- 客户端需要集成LB逻辑
- 多语言环境下难以统一
Nginx(服务端负载均衡):
Client ──> Nginx ──┬──> Inst A
│
├──> Inst B
│
└──> Inst C
优点:
- 客户端无感知
- 统一的流量入口
- 强大的L7能力(rewrite、ssl等)
缺点:
- 增加网络延迟
- 单点风险(需部署集群)
- 健康检查配置复杂
Spring Cloud LoadBalancer:
与Ribbon类似,但:
- 基于Spring 5和Project Reactor
- 支持响应式编程
- 更轻量级
- 官方长期支持
Ribbon性能基准测试:
测试环境:
- Client: 8C16G
- 服务实例: 5个(同机房)
- 网络: 内网千兆
测试结果:
┌────────────────────────┬──────────────┬──────────────┐
│ 指标 │ Ribbon │ Nginx LB │
├────────────────────────┼──────────────┼──────────────┤
│ 平均延迟 (ms) │ ~2 │ ~5 │
│ P99延迟 (ms) │ ~5 │ ~15 │
│ 吞吐量 (QPS) │ ~10000 │ ~8000 │
│ CPU占用 (%) │ ~5 │ ~10 │
│ 内存占用 (MB) │ ~50 │ ~200 │
│ 网络跳数 │ 1 │ 2 │
└────────────────────────┴──────────────┴──────────────┘
注:Ribbon延迟更低是因为直接调用目标实例,无需经过Nginx中转
负载均衡策略性能对比(10000次请求):
┌──────────────────────┬──────────┬──────────┬──────────┐
│ 策略 │ 平均延迟 │ CPU使用率 │ 内存分配 │
├──────────────────────┼──────────┼──────────┼──────────┤
│ RoundRobinRule │ 基准 │ 基准 │ 基准 │
│ RandomRule │ +0.1% │ +0.5% │ 相同 │
│ WeightedResponseTime │ +2.3% │ +5.1% │ +10KB │
│ AvailabilityFiltering│ +1.8% │ +3.2% │ +5KB │
│ ZoneAvoidanceRule │ +3.5% │ +8.2% │ +20KB │
└──────────────────────┴──────────┴──────────┴──────────┘
结论:
- 简单策略(轮询/随机)性能最好
- 复杂策略(加权/区域感知)有额外开销,但提供更好的负载分布
- 实际场景中,策略计算开销远小于网络延迟,无需过度优化
# Ribbon性能优化配置
ribbon:
# 1. 禁用不必要的Ping(如果Eureka已提供健康状态)
NFLoadBalancerPingClassName: com.netflix.loadbalancer.DummyPing
# 2. 增加缓存刷新间隔(减少Eureka查询)
ServerListRefreshInterval: 30000 # 30秒(默认)
# 3. 减少连接超时(快速失败)
ConnectTimeout: 500
ReadTimeout: 2000
# 4. 限制重试次数
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
# 5. 使用连接池(Apache HttpClient)
MaxTotalConnections: 500
MaxConnectionsPerHost: 100// 自定义连接池配置
@Configuration
public class HttpClientConfig {
@Bean
public Client feignClient() {
// 使用Apache HttpClient连接池
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(100);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(500)
.setSocketTimeout(2000)
.build();
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.build();
return new ApacheHttpClient(httpClient);
}
}问题描述:在@Configuration类中定义IRule,被Spring Boot主应用上下文扫描后,影响所有服务的负载均衡策略。
根本原因:Ribbon配置类不应被主应用上下文扫描,否则会成为全局Bean。
最佳实践:
// 1. 将Ribbon配置放在独立包中,不被主扫描路径扫描
package com.example.ribbonconfig; // 不在@ComponentScan路径下
@Configuration
public class UserServiceRibbonConfig {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}
// 2. 在启动类中指定配置
@SpringBootApplication
@RibbonClient(name = "user-service", configuration = UserServiceRibbonConfig.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 3. 或者在@ComponentScan中排除配置类包
@ComponentScan(
basePackages = "com.example",
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\.example\.ribbonconfig\..*"
)
)问题描述:Feign接口的@GetMapping路径与服务端Controller路径不一致,调用报404。
根本原因:Feign是声明式客户端,路径必须完全匹配服务端路径。
最佳实践:
// 方案1:提取公共API接口
public interface UserApi {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
}
// 服务端实现
@RestController
public class UserController implements UserApi {
@Override
public User getUser(@PathVariable Long id) {
// 实现
}
}
// Feign客户端继承公共接口
@FeignClient(name = "user-service")
public interface UserClient extends UserApi {
}
// 方案2:开启Feign日志查看实际请求URL
@Configuration
public class FeignLogConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // 记录完整请求和响应
}
}
// application.yml
logging:
level:
com.example.client.UserClient: DEBUG问题描述:下游服务响应慢,Feign/Ribbon长时间阻塞,线程池打满引发雪崩。
根本原因:默认超时时间较长(连接1秒,读取1秒),且重试策略过于激进。
最佳实践:
ribbon:
# 连接超时:建立TCP连接的超时时间
ConnectTimeout: 1000 # 1秒
# 读取超时:等待响应的超时时间
ReadTimeout: 3000 # 3秒
# 是否对所有操作重试(GET/POST/PUT/DELETE)
# POST/PUT/DELETE重试可能导致数据重复!
OkToRetryOnAllOperations: false
# 同一实例最大重试次数
MaxAutoRetries: 0
# 切换实例的最大重试次数
MaxAutoRetriesNextServer: 1
# 配合Hystrix/Sentinel熔断eign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000 # 熔断超时// 针对非幂等操作禁用重试
@Configuration
public class IdempotentRetryConfig {
@Bean
public Request.Options feignOptions() {
return new Request.Options(1, TimeUnit.SECONDS, 3, TimeUnit.SECONDS, false);
}
@Bean
public Retryer feignRetryer() {
// 禁用重试
return Retryer.NEVER_RETRY;
}
}问题描述:使用Feign传输文件时出现编码错误或内存溢出。
根本原因:Feign默认将请求体读入内存,大文件会导致OOM。
最佳实践:
// 文件上传接口
@FeignClient(name = "file-service", configuration = FileFeignConfig.class)
public interface FileClient {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String uploadFile(@RequestPart("file") MultipartFile file);
@GetMapping("/download/{fileId}")
Response downloadFile(@PathVariable("fileId") String fileId);
}
// 配置文件上传编码器
@Configuration
public class FileFeignConfig {
@Bean
public Encoder feignEncoder() {
return new SpringFormEncoder();
}
}
// 大文件下载:直接使用RestTemplate或WebClient
@Service
public class FileDownloadService {
@Autowired
private RestTemplate restTemplate;
public void downloadLargeFile(String url, OutputStream outputStream) {
restTemplate.execute(
URI.create(url),
HttpMethod.GET,
null,
response -> {
// 流式复制,不加载到内存
StreamUtils.copy(response.getBody(), outputStream);
return null;
}
);
}
}问题描述:所有服务使用默认轮询策略,未考虑实例性能差异。
根本原因:不了解各策略的适用场景,盲目使用默认配置。
最佳实践:
策略选择指南:
场景1:实例性能均等 + 短连接
策略:RoundRobinRule(轮询)
原因:公平分配,无状态,性能最好
场景2:实例性能差异大
策略:WeightedResponseTimeRule(响应时间加权)
原因:自动识别慢实例,减少对其的请求
场景3:需要快速失败 + 高可用
策略:AvailabilityFilteringRule(可用性过滤)
原因:过滤故障实例和并发高的实例
场景4:多区域部署
策略:ZoneAvoidanceRule(区域感知)
原因:优先选择同区域实例,减少跨区延迟
场景5:需要会话保持
策略:自定义规则(基于用户ID哈希)
原因:同一用户路由到同一实例
问题描述:服务实例已下线,但Ribbon仍向该实例发送请求,持续报错。
根本原因:Ribbon从Eureka拉取服务列表有延迟(默认30秒),期间新下线实例仍在列表中。
最佳实践:
# 缩短刷新间隔(代价:增加Eureka Server负载)
ribbon:
ServerListRefreshInterval: 10000 # 10秒(默认30000)
# 同时缩短Eureka缓存刷新间隔
eureka:
client:
registry-fetch-interval-seconds: 10
instance:
lease-expiration-duration-in-seconds: 30 # 更快剔除// 实现快速失败,减少错误请求
@Configuration
public class FastFailConfig {
@Bean
public IRule fastFailRule() {
// 可用性过滤 + 快速失败
AvailabilityFilteringRule rule = new AvailabilityFilteringRule();
return rule;
}
}问题描述:高并发下报Connection pool shut down或Too many open files。
根本原因:Ribbon默认使用简单的HTTP客户端,未启用连接池。
最佳实践:
# 启用Apache HttpClient连接池
feign:
httpclient:
enabled: true
max-connections: 500
max-connections-per-route: 100
time-to-live: 900@Configuration
public class ConnectionPoolConfig {
@Bean
public HttpClient httpClient() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(100);
return HttpClientBuilder.create()
.setConnectionManager(connectionManager)
.evictIdleConnections(30, TimeUnit.SECONDS) // 回收空闲连接
.build();
}
@Bean
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
}问题描述:多区域部署时,请求跨区调用,延迟增加。
根本原因:未正确配置区域感知,或Eureka元数据中缺少zone信息。
最佳实践:
# 服务提供方:标记区域
eureka:
instance:
metadata-map:
zone: beijing-zone-a
# 服务消费方:启用区域感知
ribbon:
EnableZoneAffinity: true
EnableZoneExclusivity: false// 自定义区域选择器
@Component
public class ZoneAffinityServerListFilter extends ZonePreferenceServerListFilter {
@Override
public List<Server> getFilteredListOfServers(List<Server> servers) {
// 获取本机区域
String myZone = getMyZone();
// 优先选择同区域实例
List<Server> sameZone = servers.stream()
.filter(s -> myZone.equals(getZone(s)))
.collect(Collectors.toList());
if (!sameZone.isEmpty()) {
return sameZone;
}
// 同区域不可用,返回所有实例
return servers;
}
private String getMyZone() {
return EurekaClientConfigBean.getInstanceConfig().getMetadataMap().get("zone");
}
private String getZone(Server server) {
if (server instanceof DiscoveryEnabledServer) {
return ((DiscoveryEnabledServer) server).getInstanceInfo()
.getMetadata().get("zone");
}
return null;
}
}问题描述:项目中同时引入了Ribbon和Spring Cloud LoadBalancer,导致负载均衡行为异常。
根本原因:Spring Cloud 2020+版本默认使用LoadBalancer,但旧代码依赖Ribbon,两者冲突。
最佳实践:
# 明确禁用Ribbon(使用LoadBalancer)
spring:
cloud:
loadbalancer:
ribbon:
enabled: false # 禁用Ribbon,使用Spring Cloud LoadBalancer<!-- pom.xml:排除Ribbon依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>问题描述:服务调用异常但无法定位是负载均衡问题还是服务问题。
最佳实践:
// 自定义Ribbon指标收集器
@Component
public class RibbonMetricsCollector {
@Autowired
private MeterRegistry meterRegistry;
@PostConstruct
public void init() {
// 注册各服务的实例数指标
Gauge.builder("ribbon.server.count",
() -> getServerCount("user-service"))
.tag("service", "user-service")
.register(meterRegistry);
// 注册活跃连接数指标
Gauge.builder("ribbon.active.connections",
() -> getActiveConnections("user-service"))
.tag("service", "user-service")
.register(meterRegistry);
}
private int getServerCount(String serviceName) {
// 从LoadBalancer获取实例数
ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(serviceName);
return lb.getReachableServers().size();
}
private int getActiveConnections(String serviceName) {
LoadBalancerStats stats = ClientFactory.getNamedLoadBalancer(serviceName)
.getLoadBalancerStats();
return stats.getActiveRequestsCount();
}
}参考答案:
服务端负载均衡(如Nginx):
- 请求先到达负载均衡器(Nginx/HAProxy),由LB选择后端实例并转发请求
- 客户端无感知,无需维护服务列表
- 增加网络跳数(Client -> LB -> Service)
- 适合外部流量入口(如Web应用前端)
客户端负载均衡(如Ribbon):
- 客户端从注册中心(Eureka)获取服务列表,本地选择实例后直接发起调用
- 减少网络跳数(Client -> Service)
- 客户端需维护服务列表和负载均衡逻辑
- 本地缓存服务列表,即使注册中心不可用也能继续调用
- 适合服务间内部调用
对比总结:
| 特性 | 服务端LB | 客户端LB |
|---|---|---|
| 网络跳数 | 多1跳 | 少1跳 |
| 客户端复杂度 | 低 | 高 |
| 可用性 | 依赖LB可用性 | 本地缓存兜底 |
| 灵活性 | 统一配置 | 按服务定制 |
| 适用场景 | 外部入口 | 服务间调用 |
参考答案:
Ribbon提供7种内置负载均衡策略:
-
RoundRobinRule(轮询):
- 按顺序依次选择实例
- 默认策略,适用于实例性能均等的场景
- 实现:CAS原子操作保证线程安全
-
RandomRule(随机):
- 随机选择一个可用实例
- 适用于实例性能均等且请求量大的场景
- 实现:ThreadLocalRandom避免锁竞争
-
RetryRule(重试):
- 先使用RoundRobin选择,如果失败则在指定时间内重试
- 适用于对可用性要求高的场景
-
WeightedResponseTimeRule(响应时间加权):
- 根据响应时间动态计算权重,响应越快权重越大
- 适用于实例性能差异大的场景
- 每30秒重新计算权重
-
BestAvailableRule(最优可用):
- 选择并发连接数最少的实例
- 适用于连接数差异大的场景
-
AvailabilityFilteringRule(可用性过滤):
- 过滤掉故障实例(Ping失败)和并发连接过多的实例
- 适用于需要快速失败的场景
- 结合熔断机制使用
-
ZoneAvoidanceRule(区域感知):
- 优先选择同可用区(Zone)的实例,跨区域时选择健康区域
- 适用于多区域部署的场景
- Ribbon的默认策略
参考答案:
Ribbon与Eureka集成:
- Ribbon通过
DiscoveryEnabledNIWSServerList从Eureka获取服务实例列表 DynamicServerListLoadBalancer定时(默认30秒)刷新服务列表- Eureka实例的元数据(如zone、version)可用于区域感知和自定义策略
Ribbon与Feign集成:
@FeignClient(name="user-service")定义接口- Spring为接口生成JDK动态代理
LoadBalancerFeignClient拦截HTTP请求- 解析服务名,通过
ILoadBalancer选择实例 FeignLoadBalancer将服务名替换为实际IP:Port- 发送HTTP请求到选定的实例
核心类:
FeignClientFactoryBean:创建Feign客户端LoadBalancerFeignClient:集成Ribbon的Feign客户端FeignLoadBalancer:执行负载均衡请求
参考答案:
Ribbon的RoundRobinRule使用CAS(Compare-And-Swap)原子操作实现无锁轮询:
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}原理:
- 使用
AtomicInteger存储当前索引 - 通过
compareAndSet原子操作更新索引 - 如果CAS失败(其他线程已修改),循环重试
优势:
- 无锁:避免
synchronized导致的线程阻塞 - 高性能:适合高并发场景
- 线程安全:原子操作保证数据一致性
注意:极端高并发下CAS可能频繁重试,但Ribbon场景(请求间隔通常>1ms)竞争不激烈。
参考答案:
实现自定义策略需要继承AbstractLoadBalancerRule或实现IRule接口:
public class CustomRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = lb.getReachableServers();
// 自定义选择逻辑
for (Server server : servers) {
if (isPreferred(server)) {
return server;
}
}
// 回退:返回第一个可用实例
return servers.isEmpty() ? null : servers.get(0);
}
private boolean isPreferred(Server server) {
// 自定义判断逻辑(如基于元数据、权重等)
if (server instanceof DiscoveryEnabledServer) {
String version = ((DiscoveryEnabledServer) server)
.getInstanceInfo().getMetadata().get("version");
return "v2".equals(version);
}
return false;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}配置方式:
// 方式1:Java配置
@Configuration
@AvoidScan
public class CustomRibbonConfig {
@Bean
public IRule customRule() {
return new CustomRule();
}
}
// 方式2:配置文件
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.example.CustomRule参考答案:
Ribbon的重试机制通过RetryHandler实现:
重试类型:
- 同一实例重试(MaxAutoRetries):在同一实例上重试请求
- 切换实例重试(MaxAutoRetriesNextServer):失败后切换到下一个实例重试
配置:
ribbon:
ConnectTimeout: 1000 # 连接超时
ReadTimeout: 3000 # 读取超时
OkToRetryOnAllOperations: false # 是否对所有HTTP方法重试
MaxAutoRetries: 1 # 同一实例最大重试次数
MaxAutoRetriesNextServer: 2 # 切换实例的最大重试次数注意事项:
- POST/PUT/DELETE重试可能导致数据重复,建议仅对GET开启
- 重试次数过多会增加延迟,需配合熔断使用
- Feign的重试通过
Retryer接口配置,与Ribbon重试是两套机制
参考答案:
区域感知:Ribbon优先选择与服务消费方处于同一可用区(Zone)的服务实例,减少跨区网络延迟。
工作原理:
ZoneAwareLoadBalancer为每个Zone维护一个独立的LoadBalancer- 选择实例时,先判断各Zone的健康状况
- 排除故障Zone(平均响应时间过长或故障比例过高)
- 在健康Zone中随机选择一个
- 如果所有Zone都故障,回退到全量选择
配置:
# 服务提供方:标记Zone
eureka:
instance:
metadata-map:
zone: beijing-zone-a
# 服务消费方:启用区域感知
ribbon:
EnableZoneAffinity: true # 启用Zone亲和性
EnableZoneExclusivity: false # 是否排他(仅使用同Zone)
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule参考答案:
Ribbon通过IPing接口实现健康检查:
内置实现:
- DummyPing:默认实现,总是返回true(依赖Eureka的健康状态)
- PingUrl:发送HTTP GET请求到/health端点,根据响应状态判断
- NIWSDiscoveryPing:结合Eureka的InstanceStatus判断
- PingConstant:固定返回值(用于测试)
工作流程:
BaseLoadBalancer启动定时任务(默认每30秒)- 执行
IPing.isAlive(Server)检测每个实例 - 更新
upServerList(可用实例列表) - 负载均衡从
upServerList中选择实例
配置:
ribbon:
NFLoadBalancerPingClassName: com.netflix.loadbalancer.PingUrl
NFLoadBalancerPingInterval: 30 # Ping间隔(秒)注意:生产环境通常使用DummyPing,依赖Eureka的健康检查,避免重复探测。
参考答案:
移除原因:
- Netflix宣布Ribbon进入维护模式(2018年底),不再开发新特性
- Spring Cloud团队希望提供官方长期支持的负载均衡方案
- Ribbon基于阻塞IO设计,不适应响应式编程趋势
替代方案:
Spring Cloud LoadBalancer:
- Spring官方推出的客户端负载均衡器
- 基于Spring 5和Project Reactor,支持响应式编程
- 更轻量级,与Spring生态深度集成
- 支持轮询和随机策略(可扩展)
- 长期官方支持
迁移方式:
# 禁用Ribbon,启用LoadBalancer
spring:
cloud:
loadbalancer:
ribbon:
enabled: false// 使用LoadBalancer自定义策略
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}参考答案:
常见坑及解决方案:
-
配置类被全局扫描:
- 问题:Ribbon配置类放在主扫描路径下,影响所有服务
- 解决:将配置类放在独立包中,使用
@RibbonClient指定,或用@ComponentScan排除
-
Feign路径不匹配404:
- 问题:Feign接口路径与服务端Controller路径不一致
- 解决:提取公共API接口,或开启Feign日志调试
-
重试导致数据重复:
- 问题:POST请求重试导致重复提交
- 解决:
OkToRetryOnAllOperations: false,仅对GET重试
-
超时配置不生效:
- 问题:配置了ribbon.ReadTimeout但无效
- 解决:检查是否同时配置了Hystrix超时,Hystrix超时需大于Ribbon超时
-
服务列表刷新延迟:
- 问题:实例已下线但仍被调用
- 解决:缩短
ServerListRefreshInterval,配合快速失败策略
此文原创,转载请注明出处。