Skip to content

Latest commit

 

History

History
2752 lines (2215 loc) · 92.7 KB

File metadata and controls

2752 lines (2215 loc) · 92.7 KB

Ribbon深度解析:客户端负载均衡的算法与源码剖析

文章标签: #java #springcloud #ribbon #负载均衡 #feign #源码分析 #面试 #netflix

目录


引言:客户端负载均衡的本质

在微服务架构中,单个服务通常部署多个实例以实现高可用和水平扩展。当消费方调用提供方时,如何将请求合理地分发到这些实例上,是客户端负载均衡(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的定位

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不是简单的"轮询器",它是一个完整的客户端负载均衡框架,包含服务列表管理、健康检查、负载均衡策略、重试机制等多个模块。


理论基础:负载均衡算法与分布式系统

1. 负载均衡的分类

负载均衡分类体系:

负载均衡
├── 按层级
│   ├── 四层负载均衡(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)

2. 经典负载均衡算法详解

算法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)
- 适合缓存场景

3. 健康检查机制

Ribbon的健康检查模型:

┌─────────────────────────────────────────────────────────────┐
│                    健康检查架构                               │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  IPing (接口)                                                 │
│    │                                                          │
│    ├── DummyPing (默认,总是返回true)                          │
│    │                                                          │
│    ├── PingUrl (HTTP GET探测)                                 │
│    │   └── 发送HTTP请求到/health端点                           │
│    │                                                          │
│    ├── PingConstant (固定返回值)                               │
│    │                                                          │
│    └── NIWSDiscoveryPing (结合Eureka状态)                     │
│        └── 使用Eureka的InstanceStatus判断是否可用               │
│                                                               │
│  ILoadBalancer                                                │
│    │                                                          │
│    ├── 维护"所有实例"列表 (allServers)                         │
│    └── 维护"可用实例"列表 (upServers)                          │
│                                                               │
│  定时任务:默认每10秒执行一次Ping                               │
│  结果:更新upServers列表,负载均衡从upServers中选择               │
│                                                               │
└─────────────────────────────────────────────────────────────┘

4. 负载均衡与CAP定理

客户端负载均衡的CAP权衡:

Consistency(一致性):
- 所有客户端看到相同的服务列表
- 需要实时同步注册表状态
- 代价:增加网络开销和延迟

Availability(可用性):
- 即使注册中心不可用,仍能基于本地缓存调用
- 代价:可能调用到已下线的实例

Partition Tolerance(分区容错):
- 网络分区时仍能做出负载均衡决策
- 客户端模式天然具有分区容错性

Ribbon的选择:
- 优先保证可用性(本地缓存)
- 容忍短暂不一致(缓存刷新间隔30秒)
- 通过重试和熔断处理不一致导致的失败

演进史:从Netflix Ribbon到Spring Cloud LoadBalancer

第一阶段:Netflix Ribbon诞生(2013-2015)

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微服务标准组件

第二阶段:功能完善期(2015-2018)

关键版本演进:

Ribbon 2.0(2015):
- 引入ILoadBalancer新接口
- 支持响应式编程(初步)
- 改进的缓存和刷新机制

Spring Cloud Edgware(2017):
- 支持配置文件定义负载均衡策略
- 改进的重试机制(与Spring Retry集成)
- 更好的指标监控支持

Spring Cloud Finchley(2018):
- 与Spring Boot 2.0兼容
- 响应式支持增强
- 改进的 health check 机制

第三阶段:维护模式与替代方案(2018-2020)

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

第四阶段:当前状态(2021-2026)

Ribbon当前使用场景:

1. 存量系统维护:
   - 大量基于spring-cloud-netflix的项目
   - 迁移成本较高
   
2. 学习价值:
   - 经典的客户端负载均衡实现
   - 算法实现清晰,适合学习
   
3. 特定需求:
   - Spring Cloud LoadBalancer功能不够丰富时
   - 需要WeightedResponseTimeRule等高级策略

替代方案对比:

Spring Cloud LoadBalancer:
- 优点:官方支持、响应式、与Spring生态深度集成
- 缺点:策略较少(目前仅RoundRobin和Random)
- 适用:新项目、响应式应用

Nacos:
- 优点:支持权重、灰度、多种策略
- 缺点:阿里生态绑定较深
- 适用:使用Nacos注册中心的项目

Envoy/Istio:
- 优点:服务网格、L7负载均衡、丰富的流量管理
- 缺点:引入Sidecar,增加复杂度
- 适用:大规模微服务、需要高级流量管理

源码深度分析:Ribbon核心架构

1. 整体架构与核心接口

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:客户端配置

2. ILoadBalancer接口与BaseLoadBalancer

// 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);
            }
        }
    }
}

3. DynamicServerListLoadBalancer:动态服务列表

// 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);
            }
        }
    }
}

4. ZoneAwareLoadBalancer:区域感知负载均衡

// 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;
    }
}

源码深度分析:负载均衡策略实现

1. RoundRobinRule:轮询策略

// 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)竞争通常不激烈

2. RandomRule:随机策略

// 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);
    }
}

3. WeightedResponseTimeRule:响应时间加权策略

// 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秒重新计算权重

4. AvailabilityFilteringRule:可用性过滤策略

// 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);
    }
}

5. ZoneAvoidanceRule:区域感知策略

// 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;
    }
}

源码深度分析:Feign与Ribbon的集成

1. Feign客户端初始化流程

// 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?"
        );
    }
}

2. LoadBalancerFeignClient:Ribbon与Feign的桥梁

// 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;
    }
}

3. FeignLoadBalancer:执行负载均衡请求

// 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);
        }
    }
}

实战案例:生产级负载均衡配置

案例1:自定义负载均衡策略(基于版本号)

// 自定义负载均衡规则:优先选择同版本的实例
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);
    }
}

案例2:金丝雀发布配置

# 金丝雀发布:将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) {
    }
}

案例3:重试与熔断配置

# 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);
            }
        };
    }
}

案例4:多区域部署与区域感知

# 多区域部署配置

# 可用区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 vs Nginx vs Spring Cloud LoadBalancer

核心特性对比

┌────────────────────┬─────────────────┬─────────────────┬─────────────────────┐
│      特性          │     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
- 支持响应式编程
- 更轻量级
- 官方长期支持

性能分析:客户端负载均衡的延迟与吞吐量

1. 性能基准测试

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中转

2. 各策略性能对比

负载均衡策略性能对比(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   │
└──────────────────────┴──────────┴──────────┴──────────┘

结论:
- 简单策略(轮询/随机)性能最好
- 复杂策略(加权/区域感知)有额外开销,但提供更好的负载分布
- 实际场景中,策略计算开销远小于网络延迟,无需过度优化

3. 优化建议

# 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);
    }
}

常见陷阱与最佳实践

陷阱1:Ribbon配置类被Spring扫描导致全局生效

问题描述:在@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\..*"
    )
)

陷阱2:Feign接口路径写错导致404

问题描述: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

陷阱3:未配置超时和重试导致级联故障

问题描述:下游服务响应慢,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;
    }
}

陷阱4:Feign文件上传/下载处理不当

问题描述:使用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;
            }
        );
    }
}

陷阱5:负载均衡策略选择不当

问题描述:所有服务使用默认轮询策略,未考虑实例性能差异。

根本原因:不了解各策略的适用场景,盲目使用默认配置。

最佳实践

策略选择指南:

场景1:实例性能均等 + 短连接
策略:RoundRobinRule(轮询)
原因:公平分配,无状态,性能最好

场景2:实例性能差异大
策略:WeightedResponseTimeRule(响应时间加权)
原因:自动识别慢实例,减少对其的请求

场景3:需要快速失败 + 高可用
策略:AvailabilityFilteringRule(可用性过滤)
原因:过滤故障实例和并发高的实例

场景4:多区域部署
策略:ZoneAvoidanceRule(区域感知)
原因:优先选择同区域实例,减少跨区延迟

场景5:需要会话保持
策略:自定义规则(基于用户ID哈希)
原因:同一用户路由到同一实例

陷阱6:忽略服务列表刷新延迟

问题描述:服务实例已下线,但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;
    }
}

陷阱7:未配置连接池导致连接耗尽

问题描述:高并发下报Connection pool shut downToo 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);
    }
}

陷阱8:未处理ZoneAwareLoadBalancer的区域配置

问题描述:多区域部署时,请求跨区调用,延迟增加。

根本原因:未正确配置区域感知,或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;
    }
}

陷阱9:Ribbon与Spring Cloud LoadBalancer混用导致冲突

问题描述:项目中同时引入了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>

陷阱10:未监控Ribbon的负载均衡指标

问题描述:服务调用异常但无法定位是负载均衡问题还是服务问题。

最佳实践

// 自定义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();
    }
}

面试题与参考答案

Q1:客户端负载均衡和服务端负载均衡有什么区别?

参考答案

服务端负载均衡(如Nginx)

  • 请求先到达负载均衡器(Nginx/HAProxy),由LB选择后端实例并转发请求
  • 客户端无感知,无需维护服务列表
  • 增加网络跳数(Client -> LB -> Service)
  • 适合外部流量入口(如Web应用前端)

客户端负载均衡(如Ribbon)

  • 客户端从注册中心(Eureka)获取服务列表,本地选择实例后直接发起调用
  • 减少网络跳数(Client -> Service)
  • 客户端需维护服务列表和负载均衡逻辑
  • 本地缓存服务列表,即使注册中心不可用也能继续调用
  • 适合服务间内部调用

对比总结

特性 服务端LB 客户端LB
网络跳数 多1跳 少1跳
客户端复杂度
可用性 依赖LB可用性 本地缓存兜底
灵活性 统一配置 按服务定制
适用场景 外部入口 服务间调用

Q2:Ribbon有哪些内置的负载均衡策略?各自的适用场景是什么?

参考答案

Ribbon提供7种内置负载均衡策略:

  1. RoundRobinRule(轮询)

    • 按顺序依次选择实例
    • 默认策略,适用于实例性能均等的场景
    • 实现:CAS原子操作保证线程安全
  2. RandomRule(随机)

    • 随机选择一个可用实例
    • 适用于实例性能均等且请求量大的场景
    • 实现:ThreadLocalRandom避免锁竞争
  3. RetryRule(重试)

    • 先使用RoundRobin选择,如果失败则在指定时间内重试
    • 适用于对可用性要求高的场景
  4. WeightedResponseTimeRule(响应时间加权)

    • 根据响应时间动态计算权重,响应越快权重越大
    • 适用于实例性能差异大的场景
    • 每30秒重新计算权重
  5. BestAvailableRule(最优可用)

    • 选择并发连接数最少的实例
    • 适用于连接数差异大的场景
  6. AvailabilityFilteringRule(可用性过滤)

    • 过滤掉故障实例(Ping失败)和并发连接过多的实例
    • 适用于需要快速失败的场景
    • 结合熔断机制使用
  7. ZoneAvoidanceRule(区域感知)

    • 优先选择同可用区(Zone)的实例,跨区域时选择健康区域
    • 适用于多区域部署的场景
    • Ribbon的默认策略

Q3:Ribbon是如何与Eureka和Feign集成的?

参考答案

Ribbon与Eureka集成

  1. Ribbon通过DiscoveryEnabledNIWSServerList从Eureka获取服务实例列表
  2. DynamicServerListLoadBalancer定时(默认30秒)刷新服务列表
  3. Eureka实例的元数据(如zone、version)可用于区域感知和自定义策略

Ribbon与Feign集成

  1. @FeignClient(name="user-service")定义接口
  2. Spring为接口生成JDK动态代理
  3. LoadBalancerFeignClient拦截HTTP请求
  4. 解析服务名,通过ILoadBalancer选择实例
  5. FeignLoadBalancer将服务名替换为实际IP:Port
  6. 发送HTTP请求到选定的实例

核心类

  • FeignClientFactoryBean:创建Feign客户端
  • LoadBalancerFeignClient:集成Ribbon的Feign客户端
  • FeignLoadBalancer:执行负载均衡请求

Q4:Ribbon的轮询算法是如何保证线程安全的?

参考答案

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;
    }
}

原理

  1. 使用AtomicInteger存储当前索引
  2. 通过compareAndSet原子操作更新索引
  3. 如果CAS失败(其他线程已修改),循环重试

优势

  • 无锁:避免synchronized导致的线程阻塞
  • 高性能:适合高并发场景
  • 线程安全:原子操作保证数据一致性

注意:极端高并发下CAS可能频繁重试,但Ribbon场景(请求间隔通常>1ms)竞争不激烈。

Q5:如何实现Ribbon的自定义负载均衡策略?

参考答案

实现自定义策略需要继承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

Q6:Ribbon的重试机制是如何工作的?如何配置?

参考答案

Ribbon的重试机制通过RetryHandler实现:

重试类型

  1. 同一实例重试(MaxAutoRetries):在同一实例上重试请求
  2. 切换实例重试(MaxAutoRetriesNextServer):失败后切换到下一个实例重试

配置

ribbon:
  ConnectTimeout: 1000       # 连接超时
  ReadTimeout: 3000          # 读取超时
  OkToRetryOnAllOperations: false  # 是否对所有HTTP方法重试
  MaxAutoRetries: 1          # 同一实例最大重试次数
  MaxAutoRetriesNextServer: 2  # 切换实例的最大重试次数

注意事项

  • POST/PUT/DELETE重试可能导致数据重复,建议仅对GET开启
  • 重试次数过多会增加延迟,需配合熔断使用
  • Feign的重试通过Retryer接口配置,与Ribbon重试是两套机制

Q7:什么是Ribbon的区域感知(Zone Awareness)?如何配置?

参考答案

区域感知:Ribbon优先选择与服务消费方处于同一可用区(Zone)的服务实例,减少跨区网络延迟。

工作原理

  1. ZoneAwareLoadBalancer为每个Zone维护一个独立的LoadBalancer
  2. 选择实例时,先判断各Zone的健康状况
  3. 排除故障Zone(平均响应时间过长或故障比例过高)
  4. 在健康Zone中随机选择一个
  5. 如果所有Zone都故障,回退到全量选择

配置

# 服务提供方:标记Zone
eureka:
  instance:
    metadata-map:
      zone: beijing-zone-a

# 服务消费方:启用区域感知
ribbon:
  EnableZoneAffinity: true      # 启用Zone亲和性
  EnableZoneExclusivity: false  # 是否排他(仅使用同Zone)
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

Q8:Ribbon的健康检查是如何工作的?

参考答案

Ribbon通过IPing接口实现健康检查:

内置实现

  1. DummyPing:默认实现,总是返回true(依赖Eureka的健康状态)
  2. PingUrl:发送HTTP GET请求到/health端点,根据响应状态判断
  3. NIWSDiscoveryPing:结合Eureka的InstanceStatus判断
  4. PingConstant:固定返回值(用于测试)

工作流程

  1. BaseLoadBalancer启动定时任务(默认每30秒)
  2. 执行IPing.isAlive(Server)检测每个实例
  3. 更新upServerList(可用实例列表)
  4. 负载均衡从upServerList中选择实例

配置

ribbon:
  NFLoadBalancerPingClassName: com.netflix.loadbalancer.PingUrl
  NFLoadBalancerPingInterval: 30  # Ping间隔(秒)

注意:生产环境通常使用DummyPing,依赖Eureka的健康检查,避免重复探测。

Q9:Spring Cloud 2020+版本为什么移除了Ribbon?替代方案是什么?

参考答案

移除原因

  1. Netflix宣布Ribbon进入维护模式(2018年底),不再开发新特性
  2. Spring Cloud团队希望提供官方长期支持的负载均衡方案
  3. 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);
}

Q10:Ribbon在实际项目中遇到过哪些坑?如何解决?

参考答案

常见坑及解决方案

  1. 配置类被全局扫描

    • 问题:Ribbon配置类放在主扫描路径下,影响所有服务
    • 解决:将配置类放在独立包中,使用@RibbonClient指定,或用@ComponentScan排除
  2. Feign路径不匹配404

    • 问题:Feign接口路径与服务端Controller路径不一致
    • 解决:提取公共API接口,或开启Feign日志调试
  3. 重试导致数据重复

    • 问题:POST请求重试导致重复提交
    • 解决:OkToRetryOnAllOperations: false,仅对GET重试
  4. 超时配置不生效

    • 问题:配置了ribbon.ReadTimeout但无效
    • 解决:检查是否同时配置了Hystrix超时,Hystrix超时需大于Ribbon超时
  5. 服务列表刷新延迟

    • 问题:实例已下线但仍被调用
    • 解决:缩短ServerListRefreshInterval,配合快速失败策略

此文原创,转载请注明出处。