1. 引言
在微服务架构中,一个服务往往会有多个实例同时对外提供能力。请求该打到哪一台机器上?这就是负载均衡要解决的问题。
Spring Cloud Netflix 体系中的 Ribbon 是一个客户端负载均衡组件:它运行在服务消费者一侧,从注册中心(如 Eureka、Nacos)拉取服务实例列表,再按照某种策略挑选出一个实例发起调用。Ribbon 内置了多种负载均衡策略(IRule 的实现类),不同策略适用于不同业务场景。
本文围绕 Ribbon 的七种常用负载均衡策略展开,讲清每种策略的核心思想、源码实现、适用场景与注意事项,并给出选型建议与自定义策略的写法。
2. Ribbon 负载均衡的整体结构
Ribbon 的负载均衡逻辑可以抽象为三层:
- ServerList:负责获取服务实例列表(可来自注册中心,也可静态配置)。
- ServerListFilter:对实例列表做过滤(如只保留同机房实例)。
- IRule:从过滤后的实例列表中,按照策略选出一个实例。
其中 IRule 就是本文的主角,它的核心接口只有一个方法:
java
public interface IRule {
Server choose(Object key);
void setLoadBalancer(ILoadBalancer lb);
ILoadBalancer getLoadBalancer();
}
choose 方法返回一个 Server 对象,代表本次请求要访问的实例。Ribbon 默认使用的策略是 ZoneAvoidanceRule。
#mermaid-svg-pFgLWBMjFpLByfPa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pFgLWBMjFpLByfPa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pFgLWBMjFpLByfPa .error-icon{fill:#552222;}#mermaid-svg-pFgLWBMjFpLByfPa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pFgLWBMjFpLByfPa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pFgLWBMjFpLByfPa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pFgLWBMjFpLByfPa .marker.cross{stroke:#333333;}#mermaid-svg-pFgLWBMjFpLByfPa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pFgLWBMjFpLByfPa p{margin:0;}#mermaid-svg-pFgLWBMjFpLByfPa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pFgLWBMjFpLByfPa .cluster-label text{fill:#333;}#mermaid-svg-pFgLWBMjFpLByfPa .cluster-label span{color:#333;}#mermaid-svg-pFgLWBMjFpLByfPa .cluster-label span p{background-color:transparent;}#mermaid-svg-pFgLWBMjFpLByfPa .label text,#mermaid-svg-pFgLWBMjFpLByfPa span{fill:#333;color:#333;}#mermaid-svg-pFgLWBMjFpLByfPa .node rect,#mermaid-svg-pFgLWBMjFpLByfPa .node circle,#mermaid-svg-pFgLWBMjFpLByfPa .node ellipse,#mermaid-svg-pFgLWBMjFpLByfPa .node polygon,#mermaid-svg-pFgLWBMjFpLByfPa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pFgLWBMjFpLByfPa .rough-node .label text,#mermaid-svg-pFgLWBMjFpLByfPa .node .label text,#mermaid-svg-pFgLWBMjFpLByfPa .image-shape .label,#mermaid-svg-pFgLWBMjFpLByfPa .icon-shape .label{text-anchor:middle;}#mermaid-svg-pFgLWBMjFpLByfPa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pFgLWBMjFpLByfPa .rough-node .label,#mermaid-svg-pFgLWBMjFpLByfPa .node .label,#mermaid-svg-pFgLWBMjFpLByfPa .image-shape .label,#mermaid-svg-pFgLWBMjFpLByfPa .icon-shape .label{text-align:center;}#mermaid-svg-pFgLWBMjFpLByfPa .node.clickable{cursor:pointer;}#mermaid-svg-pFgLWBMjFpLByfPa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pFgLWBMjFpLByfPa .arrowheadPath{fill:#333333;}#mermaid-svg-pFgLWBMjFpLByfPa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pFgLWBMjFpLByfPa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pFgLWBMjFpLByfPa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pFgLWBMjFpLByfPa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pFgLWBMjFpLByfPa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pFgLWBMjFpLByfPa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pFgLWBMjFpLByfPa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pFgLWBMjFpLByfPa .cluster text{fill:#333;}#mermaid-svg-pFgLWBMjFpLByfPa .cluster span{color:#333;}#mermaid-svg-pFgLWBMjFpLByfPa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pFgLWBMjFpLByfPa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pFgLWBMjFpLByfPa rect.text{fill:none;stroke-width:0;}#mermaid-svg-pFgLWBMjFpLByfPa .icon-shape,#mermaid-svg-pFgLWBMjFpLByfPa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pFgLWBMjFpLByfPa .icon-shape p,#mermaid-svg-pFgLWBMjFpLByfPa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pFgLWBMjFpLByfPa .icon-shape .label rect,#mermaid-svg-pFgLWBMjFpLByfPa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pFgLWBMjFpLByfPa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pFgLWBMjFpLByfPa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pFgLWBMjFpLByfPa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务消费者发起调用
LoadBalancerInterceptor 拦截
ILoadBalancer 获取实例列表
ServerListFilter 过滤实例
IRule.choose()
返回目标 Server
发起 HTTP 请求
3. 七种负载均衡策略详解
3.1 RoundRobinRule:轮询策略
核心思想:按顺序依次选择实例,第 1 个请求给实例 A,第 2 个给实例 B,第 3 个给实例 C,第 4 个又回到 A,如此循环。
源码要点 :内部维护一个 AtomicInteger nextServerCyclicCounter,每次通过 CAS 自增并取模得到下标。
java
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return server;
}
server = null;
}
return server;
}
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next)) {
return next;
}
}
}
适用场景:实例性能基本一致、请求耗时差异不大的无状态服务,是最简单也最常用的策略之一。
注意点:轮询不考虑实例的负载与响应速度,若某台机器性能较弱,仍会被均匀分配请求。
3.2 RandomRule:随机策略
核心思想:从可用实例列表中随机挑选一个。
源码要点 :使用 ThreadLocalRandom 生成随机下标,并做存活校验。
java
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return 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) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return server;
}
server = null;
Thread.yield();
}
return server;
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
适用场景:实例数量较多、希望避免请求集中到固定顺序的实例上;也常用于压测时打散流量。
注意点:随机策略在实例数量较少时,分布可能不够均匀,存在短时间内的倾斜。
3.3 RetryRule:重试策略
核心思想 :内部持有一个默认的 RoundRobinRule,先按轮询选实例;如果选出的实例不可用或请求失败,则在设定的时间阈值内不断重试,直到成功或超时。
源码要点:
java
public class RetryRule extends AbstractLoadBalancerRule {
IRule subRule = new RoundRobinRule();
long maxRetryMillis = 500;
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis());
while (!Thread.interrupted()) {
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
}
return answer;
}
}
适用场景:对可用性要求较高、允许少量重试开销的场景。
注意点:重试会放大下游压力,务必配合超时与熔断使用,避免雪崩。
3.4 WeightedResponseTimeRule:响应时间加权策略
核心思想 :根据每个实例的平均响应时间 动态计算权重,响应越快的实例权重越高,被选中的概率越大。它是 RoundRobinRule 的子类,启动初期统计信息不足时会退化为轮询。
源码要点 :后台定时任务 DynamicServerWeightTask 每 30 秒重新计算一次权重。
java
class ServerWeight {
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
List<Server> allServers = lb.getAllServers();
int serverCount = allServers.size();
if (serverCount == 0) {
return;
}
List<ServerStats> serverStatsList = new ArrayList<>(serverCount);
double totalResponseTime = 0;
for (Server server : allServers) {
ServerStats ss = stats.get(server);
if (ss == null) {
ss = new ServerStats();
stats.put(server, ss);
}
totalResponseTime += ss.getResponseTimeAvg();
serverStatsList.add(ss);
}
double weightSoFar = 0;
for (int i = 0; i < serverCount; i++) {
double avg = serverStatsList.get(i).getResponseTimeAvg();
// 响应时间越短,权重越大
double weight = totalResponseTime - avg;
weightSoFar += weight;
weights.put(allServers.get(i), weightSoFar);
}
weights.put(null, weightSoFar);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
}
适用场景:实例性能存在差异(如机器配置不同、部分实例负载较高),希望把更多流量分给快实例。
注意点:权重依赖历史统计,冷启动阶段效果不明显;统计窗口内的抖动可能造成权重波动。
3.5 BestAvailableRule:最小并发策略
核心思想 :遍历所有可用实例,跳过被熔断(isCircuitBreakerTripped)的实例,选出当前并发请求数最小的那一个。
源码要点:
java
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server : serverList) {
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
适用场景:请求耗时差异较大、希望按实时压力分配流量的场景。
注意点:每次选择都要遍历全部实例,实例数量很多时有一定开销;且它只关注并发数,不关注响应时间。
3.6 AvailabilityFilteringRule:可用性过滤策略
核心思想 :先按轮询选出一个实例,再校验其是否满足可用性条件------是否处于熔断状态 、当前并发连接数是否超过阈值。不满足则继续轮询下一个,最多尝试若干次。
源码要点:
java
public Server choose(Object key) {
int count = 0;
Server server = roundRobinRule.choose(key);
while (count++ <= 10) {
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
return super.choose(key);
}
其中 AvailabilityPredicate 的判断逻辑为:
java
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
private boolean shouldSkipServer(ServerStats stats) {
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
适用场景:希望自动规避故障实例与过载实例,是生产环境中比较稳妥的选择。
注意点 :过滤条件依赖 LoadBalancerStats 的统计准确性;并发阈值需要结合实例能力调优。
3.7 ZoneAvoidanceRule:区域感知策略(默认)
核心思想 :在区域(Zone)维度做负载均衡。先根据区域的整体健康状况(实例数、平均负载、故障率等)排除掉表现差的区域,再在剩余区域中结合可用性过滤与轮询选择实例。它是 Ribbon 的默认策略。
源码要点 :它组合了 ZoneAvoidancePredicate 与 AvailabilityPredicate,并通过 CompositePredicate 做组合过滤。
java
public class ZoneAvoidanceRule extends PredicateBasedRule {
private static final Random random = new Random();
private CompositePredicate compositePredicate;
public ZoneAvoidanceRule() {
super();
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, null);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
private CompositePredicate createCompositePredicate(
ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
return CompositePredicate.withPredicates(p1, p2)
.addFallbackPredicate(p2)
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
}
适用场景:多机房、多可用区部署,希望优先在同区域调用以降低延迟,同时能自动避开故障区域。
注意点 :需要实例正确上报 zone 信息(Eureka 中通过 eureka.instance.metadata-map.zone 配置),否则区域感知退化为普通轮询。
4. 七种策略对比
| 策略 | 核心依据 | 是否感知负载 | 是否感知响应时间 | 典型场景 |
|---|---|---|---|---|
| RoundRobinRule | 顺序轮询 | 否 | 否 | 同构实例、无状态服务 |
| RandomRule | 随机 | 否 | 否 | 实例多、打散流量 |
| RetryRule | 轮询 + 重试 | 否 | 否 | 高可用要求场景 |
| WeightedResponseTimeRule | 响应时间加权 | 部分 | 是 | 实例性能不均 |
| BestAvailableRule | 最小并发数 | 是 | 否 | 请求耗时差异大 |
| AvailabilityFilteringRule | 可用性过滤 + 轮询 | 是 | 否 | 规避故障与过载实例 |
| ZoneAvoidanceRule | 区域健康度 + 可用性 | 是 | 否 | 多机房、多可用区部署 |
5. 如何配置与自定义策略
5.1 通过配置文件指定策略
yaml
# 针对某个服务单独配置
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 连接超时与读取超时
ConnectTimeout: 1000
ReadTimeout: 3000
# 重试次数
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
5.2 通过 Java 配置类指定策略
java
@Configuration
public class RibbonConfig {
@Bean
public IRule ribbonRule() {
// 使用响应时间加权策略
return new WeightedResponseTimeRule();
}
}
注意:该配置类不要 放在 @ComponentScan 能扫描到的包下,否则会变成全局策略;应放在独立包中,并通过 @RibbonClient(name = "user-service", configuration = RibbonConfig.class) 指定。
5.3 自定义负载均衡策略
实现 AbstractLoadBalancerRule,重写 choose 方法即可。下面是一个「按版本号灰度」的简单示例:
java
public class GrayRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = lb.getAllServers();
List<Server> grayServers = new ArrayList<>();
for (Server server : servers) {
if (!server.isAlive()) {
continue;
}
// 从实例元数据中读取版本标记
String version = server.getMetaInfo()
.getAppName();
if ("gray".equals(version)) {
grayServers.add(server);
}
}
if (grayServers.isEmpty()) {
return new RoundRobinRule().choose(lb, key);
}
int index = ThreadLocalRandom.current().nextInt(grayServers.size());
return grayServers.get(index);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// 可在此读取自定义配置
}
}
6. 选型建议与注意事项
- 默认优先 :没有特殊诉求时,直接使用默认的
ZoneAvoidanceRule,它在多区域部署下表现稳定。 - 实例同构 :
RoundRobinRule足够,简单可预测。 - 实例异构 :优先考虑
WeightedResponseTimeRule或BestAvailableRule。 - 高可用优先 :
AvailabilityFilteringRule能自动规避熔断与过载实例。 - 重试要克制 :
RetryRule与 Ribbon 自身的重试参数叠加时容易造成请求放大,建议配合 Hystrix / Sentinel 的熔断降级。 - 统计依赖 :
WeightedResponseTimeRule、BestAvailableRule、AvailabilityFilteringRule都依赖LoadBalancerStats,需保证统计数据的准确性。 - 版本演进:Ribbon 已进入维护模式,新项目可考虑 Spring Cloud LoadBalancer,但策略思想是相通的。
7. 总结
Ribbon 的七种负载均衡策略本质上是在「简单」与「智能」之间做取舍:轮询与随机最简单,响应时间加权与最小并发更贴近真实负载,可用性过滤与区域感知则进一步考虑了故障与拓扑。理解每种策略的源码实现与适用边界,才能在实际项目中做出合理选择,而不是盲目照搬默认配置。