看负载均衡不能只看“怎么分发的”要看“服务列表从哪来”、“如何保证线程安全”、“如何感知服务健康状态”以及“算法的数学模型”。你可以把负载均衡想象成一家火爆餐厅的“领位员”。客户端你就是去吃饭的顾客。服务实例服务器就是餐厅里的桌子。负载均衡器Ribbon/SCLB就是站在门口负责安排桌子的领位员。你的问题其实就是这个领位员到底按什么规矩来安排桌子在 Spring Cloud 体系中Ribbon是上一代的霸主虽然进入维护模式但原理是通用的基石Spring Cloud LoadBalancer (SCLB)是新一代的响应式标准。第一层核心架构——谁在控制流量在代码跑起来之前必须脑子里有一张图。负载均衡的核心不仅仅是算法而是服务发现与规则执行的解耦。1. 核心组件模型无论是 Ribbon 还是 SCLB都遵循这个架构模式ServerList服务列表源负责从注册中心Nacos/Eureka拉取实例列表。ServerListFilter过滤器负责剔除不健康的实例比如熔断器认为挂掉的节点。IRule / LoadBalancer策略核心负责根据算法从过滤后的列表中挑一个。2. 客户端负载均衡 vs 服务端负载均衡服务端Nginx/F5客户端发请求给 NginxNginx 挑一个后端转发。客户端Ribbon/SCLB客户端自己维护一份服务列表缓存自己算哈希、自己选 IP然后直接发起 HTTP 请求。底层优势去中心化没有单点瓶颈。底层代价每个客户端都要消耗内存存列表且列表更新有延迟最终一致性。第二层轮询算法Round Robin—— 源码级的“原子”博弈轮询是最基础的算法但在高并发下如何保证“不重复、不遗漏、线程安全”是门学问。最老实的领位员规矩不管来的是谁也不管桌子大小就按顺序来。1号桌 → 2号桌 → 3号桌 → 回到1号桌……优点绝对公平谁也不偏袒。缺点死脑筋。如果3号桌是个瘸腿桌子机器性能差或者3号桌正在擦桌子正在处理耗时任务领位员还是把人往那领结果就是3号桌那边怨声载道1、2号桌却很闲。代码里这就是那个AtomicInteger在那不停地1然后% 总数。1. 数学原理假设服务列表为 S[S0,S1,...,Sn−1]S[S0​,S1​,...,Sn−1​] 第 ii 次请求的目标服务器 StargetStarget​ 为StargetS(i(modn))Starget​S(i(modn))​2. Ribbon 源码深度剖析 (RoundRobinRule)Ribbon 的实现非常经典它利用了AtomicInteger的 CASCompare-And-Swap机制来实现无锁的高性能计数。核心代码逻辑还原自 Ribbon 源码public class RoundRobinRule extends AbstractLoadBalancerRule { // 核心原子整数保证多线程并发下的自增安全 private AtomicInteger nextServerCyclicCounter; public RoundRobinRule() { this.nextServerCyclicCounter new AtomicInteger(0); } Override public Server choose(Object key) { // 1. 获取负载均衡器包含服务列表 ILoadBalancer lb getLoadBalancer(); // 2. 获取所有服务包括不健康的后续会过滤 ListServer allServers lb.getAllServers(); if (allServers null || allServers.isEmpty()) return null; // 3. 核心算法CAS 自增 取模 // 这是一个死循环直到 CAS 成功为止 int current nextServerCyclicCounter.get(); int next (current 1) % allServers.size(); // compareAndSet: 如果当前值还是 current就更新为 next返回 true // 如果中间被别的线程改了compareAndSet 返回 false循环重试 while (!nextServerCyclicCounter.compareAndSet(current, next)) { current nextServerCyclicCounter.get(); next (current 1) % allServers.size(); } // 4. 返回对应索引的服务器 return allServers.get(next); } }洞察线程安全使用AtomicInteger避免了synchronized的重量级锁利用 CPU 的 CAS 指令实现乐观锁。缺陷如果服务列表动态变化比如扩缩容单纯的取模会导致数据倾斜或请求抖动。例如从 3 台扩容到 4 台原本在 S2S2​ 的请求可能全部跳到 S0S0​ 导致缓存失效或连接断开。3.自定义算法造人写一个类继承AbstractLoadBalancerRule这是IRule的默认实现省事。定规矩重写choose方法把你的业务逻辑写进去。上岗告诉 Ribbon 别用默认的用你写的这个。场景假设你有两台机器A 是超级服务器权重 8B 是普通服务器权重 2。你希望 80% 的流量给 A20% 给 B。第一步造人写代码我们需要创建一个类继承AbstractLoadBalancerRule。import com.netflix.loadbalancer.*; import java.util.ArrayList; import java.util.List; import java.util.Random; // 1. 继承基类省去很多麻烦 public class CustomWeightedRule extends AbstractLoadBalancerRule { private Random random new Random(); Override public Server choose(Object key) { // 2. 获取负载均衡器 ILoadBalancer lb getLoadBalancer(); // 3. 获取所有服务实例列表这里获取的是所有已知的包括不健康的通常我们需要过滤 // 为了演示简单我们直接获取所有服务器。实际生产中建议用 lb.getReachableServers() ListServer allServers lb.getAllServers(); if (allServers null || allServers.isEmpty()) { return null; } // --- 核心逻辑开始 --- // 假设我们通过某种方式比如从配置中心或元数据获取每台机器的权重 // 这里为了演示我们手动模拟权重列表 ListServer weightedServers new ArrayList(); // 模拟把权重高的服务器在列表里多放几次 // 比如 A(权重8), B(权重2) - 列表变成 [A, A, A, A, A, A, A, A, B, B] // 这样随机选的时候选到 A 的概率自然就是 80% for (Server server : allServers) { // 假设我们从元数据里读取权重默认给 1 int weight 1; // 实际代码可能是: server.getMetadata().get(weight) // 这里简单演示如果是 192.168.1.100权重设为 5其他设为 1 if (192.168.1.100.equals(server.getHost())) { weight 5; } for (int i 0; i weight; i) { weightedServers.add(server); } } // 4. 从扩充后的列表中随机选一个 if (weightedServers.isEmpty()) { return null; } // 随机一个索引 int index random.nextInt(weightedServers.size()); Server chosen weightedServers.get(index); System.out.println(选中了服务器: chosen.getHost() (权重策略)); return chosen; // --- 核心逻辑结束 --- } // 这个方法一般不需要动它是用来初始化配置的 Override public void initWithNiwsConfig(IClientConfig clientConfig) { // 可以在这里读取配置文件里的参数 } }上面的代码用了一个最笨但最有效的办法叫“加权随机”。我想让 A 被选中的概率是 80%那我就在列表里放 8 个 A想让 B 是 20%就放 2 个 B。然后在这个大列表里随机抽一个抽到 A 的概率自然就是 80% 了。第二步上岗配置生效代码写好了怎么让 Ribbon 知道用它有两种方式推荐方式一。方式一在代码里配置推荐针对特定服务如果你只想对user-service这个服务用这个策略可以在配置类里这么写import com.netflix.loadbalancer.IRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class RibbonConfig { // 注意Bean 的名字最好和服务名有关或者直接返回 IRule 类型 Bean public IRule myCustomRule() { return new CustomWeightedRule(); } }然后在启动类或者FeignClient里指定这个配置// 指定配置类 FeignClient(name user-service, configuration RibbonConfig.class) public interface UserClient { // ... }方式二在配置文件里写全局生效如果你想让所有服务都用这个策略可以在application.yml里写user-service: # 服务名 ribbon: NFLoadBalancerRuleClassName: com.yourpackage.CustomWeightedRule # 你的类的全限定名虽然代码很简单但实际用的时候要注意这几个点线程安全choose方法会被高并发调用所以你的类里不要定义可变的成员变量除非加锁或用原子类。上面的Random是线程安全的但如果你定义个int count来计数那就得小心了。空指针保护一定要判断allServers是否为空。如果注册中心挂了或者没有可用实例你的代码不能崩要返回null让上层去处理重试。元数据来源上面的代码是硬编码 IP 来演示权重。实际项目中权重通常存在注册中心比如 Nacos 的元数据 Metadata 里。你需要通过server.getMetadata().get(weight)来动态获取。4.Ribbon的权重从Nacos读取Nacos 的 SDK 内部其实已经实现了加权随机算法。它会根据你在 Nacos 控制台设置的权重比如 A 是 10B 是 1在客户端内部维护一个加权列表。所以我们不需要在 Java 代码里去手动计算权重、写随机算法。我们只需要“截胡”Ribbon 的选路过程直接告诉它“别你自己瞎选了直接问 Nacos 要一个实例Nacos 给谁你就用谁。”第一步自定义规则类核心代码我们需要写一个类继承AbstractLoadBalancerRule但在choose方法里我们不写算法而是直接调用 Nacos 的 API。package com.example.config; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.cloud.nacos.ribbon.NacosServer; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.Server; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; Slf4j public class NacosWeightedRule extends AbstractLoadBalancerRule { Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; Override public void initWithNiwsConfig(IClientConfig iClientConfig) { // 初始化配置一般留空即可 } Override public Server choose(Object key) { try { // 1. 获取当前要调用的服务名称比如 user-service BaseLoadBalancer loadBalancer (BaseLoadBalancer) this.getLoadBalancer(); String serviceName loadBalancer.getName(); // 2. 获取 Nacos 的命名服务 NamingService namingService nacosDiscoveryProperties.namingServiceInstance(); // 3. 【核心关键】 // 调用 Nacos 自带的 selectOneHealthyInstance // 这个方法内部已经根据你在 Nacos 控制台配置的权重算好了选哪一个 // 它返回的 Instance 就是已经加权过的结果 Instance instance namingService.selectOneHealthyInstance(serviceName); if (instance null) { log.warn(Nacos 没有返回健康的实例: {}, serviceName); return null; } log.info(Ribbon 选择了 Nacos 推荐的实例: {}:{}, instance.getIp(), instance.getPort()); // 4. 将 Nacos 的 Instance 包装成 Ribbon 的 Server 对象返回 return new NacosServer(instance); } catch (NacosException e) { log.error(调用 Nacos 服务发现接口异常, e); return null; } } }我们完全绕过了 Ribbon 自带的RoundRobinRule轮询或RandomRule随机。我们直接调用了namingService.selectOneHealthyInstance(serviceName)。Nacos SDK 的魔法这个 SDK 方法内部已经读取了服务列表的weight字段并且维护了一个加权随机算法。它每次调用都会根据权重概率返回一个实例。所以我们的自定义规则只需要做一个“传话筒”把 Nacos 选好的实例交给 Ribbon 即可。第二步配置生效代码写好了现在要告诉 Ribbon 启用它。上面写了为了完整性再写一遍方式一通过配置文件推荐简单在你的application.yml中针对特定的服务假设叫user-service进行配置user-service: # 这里的服务名必须和 FeignClient(name...) 或 RestTemplate 调用名一致 ribbon: # 指定使用我们刚才写的类 NFLoadBalancerRuleClassName: com.example.config.NacosWeightedRule方式二通过 Java 配置类如果你更喜欢用代码配置或者需要针对不同的服务用不同的规则import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.context.annotation.Configuration; Configuration // 指定针对 user-service 服务使用 RibbonConfig 的配置 RibbonClient(name user-service, configuration RibbonConfig.class) public class RibbonConfig { // 在配置类里定义 Bean或者直接 new 都可以 // 只要 Ribbon 能扫描到这个配置类即可 }第三步去 Nacos 控制台设置权重代码和配置都好了最后一步是去 Nacos 控制台验证。太简单、不说了第四步验证效果启动你的消费者服务疯狂发送请求。观察控制台日志我在代码里加了log.infoRibbon 选择了 Nacos 推荐的实例: 192.168.1.100:8080 Ribbon 选择了 Nacos 推荐的实例: 192.168.1.100:8080 Ribbon 选择了 Nacos 推荐的实例: 192.168.1.100:8080 Ribbon 选择了 Nacos 推荐的实例: 192.168.1.101:8081 Ribbon 选择了 Nacos 推荐的实例: 192.168.1.100:8080 ...第三层加权响应时间算法WeightedResponseTimeRule—— 动态反馈系统这是 Ribbon 中最“智能”的算法。它不是静态配置权重而是根据实时响应时间动态计算权重。最精明的领位员规矩手里拿着个小本本记录每张桌子的上菜速度。如果1号桌上菜巨快领位员就会把70%的新客人都往1号桌领。如果3号桌总是上菜慢领位员就很少往那领除非别的桌都满了。优点谁行谁上。系统会自动把压力给到性能最好的机器整体效率最高。代码里这就是那个定时任务在后台算平均分分高的权重就大。1. 数学原理这是一个反比加权模型。假设服务器 SiSi​ 的平均响应时间为 RTiRTi​ 则其权重 WiWi​ 为Wi1RTiWi​RTi​1​为了便于选择我们计算累积权重CWiCWi​ CWi∑k0iWkCWi​k0∑i​Wk​选择过程就是生成一个 [0,CWtotal][0,CWtotal​] 之间的随机数 RR 找到第一个满足 CWi≥RCWi​≥R 的服务器。2. 源码逻辑深度剖析这个算法由两部分组成定时计算任务随机选择逻辑。A. 动态权重计算后台守护线程Ribbon 会启动一个DynamicServerWeightTask定时任务默认每 30 秒或者根据流量动态调整。// 伪代码还原 class ServerWeight { public void maintainWeights() { ListServer servers allServerList; ListDouble weights new ArrayList(); double totalWeight 0; for (Server server : servers) { // 1. 获取该服务器的平均响应时间 (RT) // Ribbon 内部维护了一个滑动窗口来统计 RT double responseTime stats.getSingleServerStat(server).getResponseTimeAvg(); // 2. 计算权重响应时间越短权重越大 // 如果 RT 为 0给一个默认大值 double weight (responseTime 0) ? 1.0 : 1.0 / responseTime; totalWeight weight; weights.add(totalWeight); // 存储累积权重 } // 3. 更新全局的权重列表volatile 保证可见性 accumulatedWeights weights; } }B. 请求选择逻辑Override public Server choose(ILoadBalancer lb, Object key) { // 1. 检查权重是否初始化刚开始没有统计数据退化为轮询 if (maxTotalWeight 0.001d) { return super.choose(lb, key); // 降级为轮询 } // 2. 生成随机数 [0, maxTotalWeight) double randomWeight random.nextDouble() * maxTotalWeight; // 3. 线性查找因为 accumulatedWeights 是有序的 int n 0; for (Double weight : accumulatedWeights) { if (weight randomWeight) { return upList.get(n); // 命中 } n; } return null; }反馈机制这实际上是一个简单的负反馈控制系统。某台机器慢了 - 权重降低 - 流量减少 - 机器负载降低 - 响应变快 - 权重回升。平滑性权重的更新是异步的不会在请求路径上计算保证了接口的低延迟。第四层Spring Cloud LoadBalancer (SCLB) —— 响应式时代的进化Spring Cloud LoadBalancer 是 Spring 官方推出的替代品旨在解决 Ribbon 的阻塞模型问题并更好地支持 ReactorWebFlux。1. 核心架构变化Ribbon基于ILoadBalancer接口阻塞式。SCLB基于ReactorLoadBalancerServiceInstance接口返回MonoServiceInstance响应式流。2. 核心代码实现轮询SCLB 的实现更加函数式它利用了AtomicInteger和ServiceInstanceListSupplier。// 简化的 SCLB 轮询实现逻辑 public class RoundRobinLoadBalancer implements ReactorLoadBalancerServiceInstance { private final AtomicInteger position; private final String serviceId; private final ServiceInstanceListSupplier supplier; Override public MonoServiceInstance choose(Request request) { // 1. 获取服务列表从注册中心 return supplier.get().next() .map(instances - { // 2. 过滤空列表 if (instances.isEmpty()) return null; // 3. 核心算法位置自增 取模 int pos this.position.incrementAndGet() Integer.MAX_VALUE; // 保证正数 ServiceInstance instance instances.get(pos % instances.size()); return instance; }); } }懒加载与流式处理SCLB 只有在真正需要发起请求时subscribe()时才会去获取服务列表避免了 Ribbon 那种定期轮询更新列表的资源浪费。扩展性SCLB 的Request上下文更加丰富可以传递ReactiveLoadBalancer.ClientFilter中的各种元数据方便做更复杂的灰度路由。第五层一致性哈希Consistent Hashing—— 解决“缓存击穿”的终极武器虽然你主要问了轮询和加权但作为ai时代的程序员必须知道在有状态服务比如 Redis 客户端分片、RPC 会话保持场景下轮询是灾难。认死理的领位员解决“缓存”问题场景假设你是个老顾客你每次来都习惯坐靠窗的位置而且你的茶水都提前泡好放在那了。普通领位员轮询不管你今天按顺序把你安排到了大厅。你到了发现没茶水还得重新泡麻烦死了。一致性哈希领位员他记得你的特征比如你的脸或者你的会员号。只要靠窗的桌子还在他永远把你安排在那。只有当靠窗的桌子坏了节点宕机他才会把你安排到离靠窗最近的那张桌子。优点老客户体验好。你的“茶水”缓存数据不用重新弄连接也不用重新建。1. 痛点轮询算法中如果节点数 NN 变为 N1N1 映射关系变为 i(modN1)i(modN1) 导致几乎所有的请求都重新映射缓存命中率归零。2. 数学原理利用哈希环。将节点 IP 和 请求参数如 UserID都通过哈希函数如 MD5映射到一个 232232 的圆环上。请求顺时针寻找遇到的第一个节点。增减节点只会影响环上逆时针方向相邻节点的数据其他节点不受影响。3. 虚拟节点Virtual Node为了防止节点太少导致数据倾斜比如 3 台机器在环上分布不均通常给每台物理机器生成 100-1000 个虚拟节点。代码思路伪代码SortedMapLong, Server circle new TreeMap(); // 哈希环 // 初始化添加物理节点及其虚拟节点 for (Server server : servers) { for (int i 0; i 100; i) { // 100个虚拟节点 long hash hashFunction.apply(server.getIp() # i); circle.put(hash, server); } } // 选择算法 public Server choose(String requestKey) { long hash hashFunction.apply(requestKey); // 找到环上第一个大于等于 hash 的节点 Map.EntryLong, Server entry circle.ceilingEntry(hash); if (entry null) { // 如果到了环尾回到环头 entry circle.firstEntry(); } return entry.getValue(); }算法/框架底层原理适用场景缺点轮询 (Ribbon/SCLB)AtomicIntegerCAS 自增 取模无状态服务机器性能均等无法感知机器负载扩容有抖动加权响应时间 (Ribbon)滑动窗口统计 RT 反比加权 随机选择机器性能不均或对延迟敏感权重更新有延迟初期有冷启动问题随机 (RandomRule)java.util.Random简单的无状态服务分配不如轮询均匀一致性哈希哈希环 虚拟节点RPC 调用、Redis 分片、会话保持实现复杂环的管理开销大最小连接数维护活跃连接计数器长连接服务如 Dubbo, DB 连接池需要实时统计连接数有锁竞争总结一下Ribbon/SCLB就是那个领位员。轮询排队坐最公平但不管机器快慢。加权谁快给谁最智能但计算稍微麻烦点。一致性哈希熟人熟座最省资源适合那种“不能换人”的场景。最后的建议如果是HTTP 微服务Spring Cloud默认用SCLB 轮询即可简单高效。如果是RPC 框架Dubbo/Feign强烈建议使用一致性哈希或最小活跃数以减少网络抖动和长连接建立开销。如果后端机器性能差异巨大比如新老机器混部必须上加权算法。