Ribbon 七种负载均衡策略详解:原理、源码与选型实践

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 的七种负载均衡策略本质上是在「简单」与「智能」之间做取舍:轮询与随机最简单,响应时间加权与最小并发更贴近真实负载,可用性过滤与区域感知则进一步考虑了故障与拓扑。理解每种策略的源码实现与适用边界,才能在实际项目中做出合理选择,而不是盲目照搬默认配置。

相关推荐
云和数据.ChenGuang2 小时前
再论深拷贝和浅拷贝
开发语言·人工智能·python·1024程序员节
总要学点什么3 小时前
第92篇:三维特效-圆(逐渐消逝)——从圆心开始,让一个洞把整片光啃到只剩外圈
gis·webgl·cesium·空间分析·1024程序员节
普通人65 小时前
CSS-box-sizing-使用指南
开发语言·前端·css·1024程序员节
女神下凡5 小时前
芯参谋(83):AliOS Things_驱动开发指南
arm开发·驱动开发·单片机·嵌入式硬件·设计规范·1024程序员节
鬼手点金5 小时前
Skills:领域能力的可组合单元
java·人工智能·笔记·自动化·1024程序员节
总要学点什么14 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
喵了几个咪16 小时前
AI 时代的危与机:当潮水退去,技术人如何自处
1024程序员节
LMT流氓兔19 小时前
第五单元——第十六课:给书店助手加上网页界面
java·开发语言·jvm·人工智能·oracle·langchain·1024程序员节
开开心心就好20 小时前
音频降噪剪辑工具,音量裁剪一键处理
运维·服务器·前端·spring·ocr·音视频·1024程序员节