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

本文深入解析Ribbon内置的七种负载均衡策略:轮询、随机、重试、响应时间加权、最小并发、可用性过滤和区域感知策略。涵盖各策略的核心原理、源码关键逻辑、适用场景及注意事项,并对比其性能与稳定性差异。同时介绍策略配置方式(配置文件/Java类)与自定义IRule实现方法,强调选型需结合实例同构性、高可用要求及统计准确性,最后指出Ribbon已进入维护期,新项目可考虑Spring Cloud LoadBalancer。

1. 引言

在微服务架构中,一个服务往往会有多个实例同时对外提供能力。请求该打到哪一台机器上?这就是负载均衡要解决的问题。

Spring Cloud Netflix 体系中的 Ribbon 是一个客户端负载均衡组件:它运行在服务消费者一侧,从注册中心(如 Eureka、Nacos)拉取服务实例列表,再按照某种策略挑选出一个实例发起调用。Ribbon 内置了多种负载均衡策略(IRule 的实现类),不同策略适用于不同业务场景。

本文围绕 Ribbon 的七种常用负载均衡策略展开,讲清每种策略的核心思想、源码实现、适用场景与注意事项,并给出选型建议与自定义策略的写法。

2. Ribbon 负载均衡的整体结构

Ribbon 的负载均衡逻辑可以抽象为三层:

  • ServerList:负责获取服务实例列表(可来自注册中心,也可静态配置)。
  • ServerListFilter:对实例列表做过滤(如只保留同机房实例)。
  • IRule:从过滤后的实例列表中,按照策略选出一个实例。

其中 IRule 就是本文的主角,它的核心接口只有一个方法:

public interface IRule {
    Server choose(Object key);
    void setLoadBalancer(ILoadBalancer lb);
    ILoadBalancer getLoadBalancer();
}

choose 方法返回一个 Server 对象,代表本次请求要访问的实例。Ribbon 默认使用的策略是 ZoneAvoidanceRule。

服务消费者发起调用

LoadBalancerInterceptor 拦截

ILoadBalancer 获取实例列表

ServerListFilter 过滤实例

IRule.choose()

返回目标 Server

发起 HTTP 请求

3. 七种负载均衡策略详解

3.1 RoundRobinRule:轮询策略

核心思想:按顺序依次选择实例,第 1 个请求给实例 A,第 2 个给实例 B,第 3 个给实例 C,第 4 个又回到 A,如此循环。

源码要点:内部维护一个 AtomicInteger nextServerCyclicCounter,每次通过 CAS 自增并取模得到下标。

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 生成随机下标,并做存活校验。

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,先按轮询选实例;如果选出的实例不可用或请求失败,则在设定的时间阈值内不断重试,直到成功或超时。

源码要点:

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 秒重新计算一次权重。

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)的实例,选出当前并发请求数最小的那一个。

源码要点:

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:可用性过滤策略

核心思想:先按轮询选出一个实例,再校验其是否满足可用性条件——是否处于熔断状态、当前并发连接数是否超过阈值。不满足则继续轮询下一个,最多尝试若干次。

源码要点:

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 的判断逻辑为:

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 做组合过滤。

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 通过配置文件指定策略

# 针对某个服务单独配置
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    # 连接超时与读取超时
    ConnectTimeout: 1000
    ReadTimeout: 3000
    # 重试次数
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 2

5.2 通过 Java 配置类指定策略

@Configuration
public class RibbonConfig {

    @Bean
    public IRule ribbonRule() {
        // 使用响应时间加权策略
        return new WeightedResponseTimeRule();
    }
}

注意:该配置类不要放在 @ComponentScan 能扫描到的包下,否则会变成全局策略;应放在独立包中,并通过 @RibbonClient(name = "user-service", configuration = RibbonConfig.class) 指定。

5.3 自定义负载均衡策略

实现 AbstractLoadBalancerRule,重写 choose 方法即可。下面是一个「按版本号灰度」的简单示例:

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值