Spring Cloud LoadBalancer 的原理是什么?
简化版
Spring Cloud LoadBalancer 是客户端负载均衡器:先按服务名从 ServiceInstanceListSupplier 获得实例列表,再由 ReactorLoadBalancer 选择一个实例,最后把逻辑服务名转换成真实地址发起请求。它当前替代旧 Ribbon,默认常用轮询策略,但健康检查、缓存、权重和重试需要按场景配置。
详细版
服务端负载均衡由 Nginx 等中间层选择实例,客户端负载均衡则由调用方自己获取列表并选择。Spring Cloud 可为带负载均衡能力的 RestClient、WebClient 或 OpenFeign 调用解析 http://inventory-service/...,选择一个 ServiceInstance 后重建实际请求 URI。
负载均衡只决定“这次选谁”,不会自动保证调用成功。失败重试可能再次选实例,但需要额外重试支持,并且只适合幂等操作;实例列表缓存能降低注册中心压力,却会引入短暂陈旧数据。
完整版教学
一、调用链如何工作
服务名 -> ServiceInstanceListSupplier -> 实例列表
-> ReactorLoadBalancer
-> 选中 ServiceInstance
-> 重建 host:port 后发送
Supplier 可以从 DiscoveryClient 获取实例,也可以组合缓存、健康检查、区域或提示过滤。LoadBalancer 只面向整理后的候选集合执行选择算法。
| 组件 | 职责 | 典型风险 |
|---|---|---|
| ServiceInstanceListSupplier | 提供候选实例列表 | 列表陈旧、过滤条件错误 |
| ServiceInstance | 描述单个实例 | 元数据不准确会影响权重或区域选择 |
| ReactorLoadBalancer | 从候选列表中选一个实例 | 策略不适合流量特征 |
| LoadBalancer Cache | 缓存实例列表降低发现压力 | 实例下线后存在短暂陈旧窗口 |
| Retry 集成 | 失败后可能换实例再试 | 放大流量,非幂等请求有副作用 |
LoadBalancer 只决定「这一次请求选谁」,不保证被选实例一定成功,也不替代超时、熔断、限流和业务幂等。
二、常见选择策略
默认轮询让请求依次落到候选实例;随机策略按随机结果选择。真实系统还可能按权重、区域、版本或请求提示筛选,但权重必须来自可信实例元数据,且要考虑列表变更后的分布抖动。
例如 inventory-service 有 3 个实例 A、B、C,轮询选择序列可能是 A、B、C、A、B、C。若 C 的响应从 30ms 退化到 800ms,轮询仍会继续把三分之一请求发给 C;这时单靠选择算法不够,还需要超时、熔断、健康检查或权重调整一起工作。
轮询不等于负载一定均匀:慢实例会积压更多并发请求。若需要基于实时负载选择,必须有可用指标和防抖策略,否则控制信息本身可能滞后。
三、缓存与健康检查
每次请求都访问注册中心会放大延迟和故障,因此实例列表通常在客户端缓存。代价是实例下线后,在缓存刷新前仍可能被选中,所以调用侧仍要设置超时和容错。
如果缓存 TTL 是 35 秒,一个实例在第 1 秒宕机,而注册中心在第 5 秒摘除,某些客户端仍可能在下一次刷新前持有旧列表。这个窗口内失败应该由连接超时、快速失败、有限重试和熔断处理,而不是期待负载均衡器拥有实时全知视角。
LoadBalancer 健康检查适合实例列表不是由具备健康机制的注册中心提供等场景。注册中心已经维护健康状态时,再对每个实例高频主动探测可能重复施压,是否启用要结合发现实现。
四、重试不是负载均衡算法
重试会放大请求量。GET 等幂等读取在连接失败时可尝试其他实例,创建订单、扣款等非幂等操作若无幂等键,重试可能造成重复执行。
一次调用预算 1000ms
-> 第一次实例 A 超时 300ms
-> 退避 50ms
-> 第二次实例 B 超时 300ms
-> 剩余预算不足时停止重试并快速失败
需要同时限制单次超时、总调用预算、重试次数和退避时间。若所有实例都在故障,盲目换实例只会形成重试风暴,应由熔断、限流和隔离共同保护。
五、与 Ribbon 的版本边界
Ribbon 是旧 Spring Cloud Netflix 体系中的客户端负载均衡实现,已经不再是现代 Spring Cloud 的默认方案。当前回答应以 Spring Cloud LoadBalancer 的响应式抽象为主,维护旧系统时再说明 Ribbon 的 IRule、ServerList 等模型。
迁移不能只替换类名,还要检查自定义规则、重试配置、缓存和指标行为是否等价。例如旧 Ribbon 项目可能有自定义 IRule、ServerListFilter 或重试策略,迁移到 Spring Cloud LoadBalancer 时要逐项映射到 Supplier、LoadBalancer 策略和调用客户端配置,不能只改依赖。
六、面试回答的层次
这道题不要只答「轮询」。更完整的顺序是:
- 先说明客户端负载均衡与服务端负载均衡的区别;
- 再讲 Supplier 从注册中心或缓存里拿候选实例;
- 然后讲 ReactorLoadBalancer 按策略选一个 ServiceInstance;
- 接着讲请求 URI 被改写成真实 host 和 port;
- 最后补充缓存陈旧、重试放大、Ribbon 版本边界这些风险点。
七、常见误区与追问
- 误区:负载均衡能保证请求一定成功。 它只选择目标实例,请求是否成功还受网络、下游状态、超时和业务错误影响。
- 误区:轮询就一定公平。 轮询按次数分配,不按处理耗时和实时负载分配,慢实例仍可能积压请求。
- 误区:每次请求都实时查注册中心更准确。 这样会增加延迟和注册中心压力,主流方式是实例列表缓存加刷新。
- 误区:重试属于负载均衡的天然能力。 重试需要额外配置和预算控制,并且只适合幂等或具备幂等保护的请求。
- 追问:Spring Cloud LoadBalancer 和 Ribbon 什么关系? 它是现代 Spring Cloud 中替代 Ribbon 的客户端负载均衡方案,旧项目里的 Ribbon 概念不能直接当成当前默认答案。
- 追问:实例列表为空怎么办? 调用应快速失败并返回明确错误,同时通过注册中心状态、健康检查和监控排查实例是否未注册或全部不健康。
八、加强记忆
Spring Cloud LoadBalancer 的主线是「拿列表、选实例、改地址」:ServiceInstanceListSupplier 提供候选实例,ReactorLoadBalancer 执行选择策略,客户端把逻辑服务名改写为真实 host 和 port。缓存降低注册中心压力但带来陈旧窗口,重试可能换实例但会放大流量;它替代的是 Ribbon 这一类客户端负载均衡实现,不替代超时、熔断、健康治理和幂等设计。