Spring Cloud负载均衡
Spring Cloud 里的负载均衡,解决的是:
一个服务有多个实例时,本次请求应该发给哪个实例。
阅读入口:先回答“候选实例从哪里来”
按“Discovery 提供候选 → Supplier 过滤 → 算法选择 → 请求发送 → 失败反馈”阅读。先掌握 LoadBalancer 主线,再把 Ribbon 当作存量实现对照。
核心原理:实例供应链与选择策略
LoadBalancer 不负责注册实例,也不负责发送业务请求;它消费 ServiceInstance 供应链,依次完成缓存、健康过滤、区域/版本过滤和算法选择。Ribbon 是 Netflix 时代的旧实现,Spring Cloud LoadBalancer 是当前主线。
flowchart TD
A["DiscoveryClient实例列表"] --> B["ServiceInstanceListSupplier"]
B --> C["缓存与健康过滤"]
C --> D["区域/版本/灰度过滤"]
D --> E["RoundRobin/Random/权重"]
E --> F["返回一个目标实例"]
F --> G["Feign/Gateway/RestClient发起请求"]本页负责零基础概念、常用算法、配置和基础排查。需要继续追踪命名子容器、Supplier装饰链、缓存TTL、RoundRobin源码、严格灰度、Subset、健康探测、重试避让、失败窗口和生产Runbook,请继续阅读:
例如注册中心里有 3 个用户服务实例:
user-service
192.168.1.10:8081
192.168.1.11:8081
192.168.1.12:8081调用方写的是:
http://user-service/users/1001负载均衡器要把服务名转换成某一个真实地址:
http://192.168.1.11:8081/users/1001先分清几个角色
| 角色 | 负责什么 | 不负责什么 |
|---|---|---|
| Nacos / Eureka / Consul | 保存服务名和实例列表 | 不负责每次请求选哪个实例 |
| Spring Cloud LoadBalancer | 从实例列表里选择一个实例 | 不负责服务注册 |
| Ribbon | 老版本客户端负载均衡组件 | 新项目不推荐继续选型 |
| OpenFeign | 把接口调用转换成 HTTP 请求 | 本身不是注册中心 |
| Gateway | 入口路由和过滤 | 不应该承载复杂业务 |
| Nginx / SLB / Kubernetes Service | 服务端负载均衡 | 不理解 Spring Cloud 服务元数据 |
注册中心和负载均衡经常被混在一起说,但它们职责不同:
flowchart TD
A["调用方"] --> B["注册中心"]
B --> C["返回 user-service 实例列表"]
C --> D["Spring Cloud LoadBalancer"]
D --> E["选择一个实例"]
E --> F["HTTP Client 发请求"]两代方案:Ribbon 和 Spring Cloud LoadBalancer
Ribbon
Ribbon 是 Netflix 体系里的客户端负载均衡组件,老 Spring Cloud 项目中很常见。
经典组合:
Eureka + Ribbon + Feign + Hystrix + ZuulRibbon 的职责是:从 Eureka 拿到实例列表后,在调用方本地选择一个实例。
Spring Cloud LoadBalancer
Spring Cloud LoadBalancer 是新项目更推荐的负载均衡方案。
常见组合:
Nacos / Eureka + Spring Cloud LoadBalancer + OpenFeign + Gateway + Sentinel现在理解 Spring Cloud 负载均衡时,应重点理解 Spring Cloud LoadBalancer;遇到历史项目时,再理解 Ribbon。
| 对比项 | Ribbon | Spring Cloud LoadBalancer |
|---|---|---|
| 所属体系 | Netflix OSS | Spring Cloud 官方 |
| 项目状态 | 老项目维护常见 | 新项目推荐 |
| 调用模型 | 阻塞模型为主 | 支持阻塞和响应式场景 |
| 常见集成 | RestTemplate、Feign | RestTemplate、WebClient、Feign、Gateway |
| 扩展方式 | IRule 等 Ribbon 规则 | ReactorServiceInstanceLoadBalancer、ServiceInstanceListSupplier 等 |
客户端负载均衡和服务端负载均衡
客户端负载均衡
Spring Cloud 微服务内部调用常见的是客户端负载均衡。
flowchart TD
A["订单服务"] --> B["获取 user-service 实例列表"]
B --> C["订单服务本地选择实例"]
C --> D["调用用户服务实例"]特点:
- 调用方自己选择实例。
- 链路短,不必每次经过统一转发层。
- 可以结合服务元数据做灰度、同机房优先、权重路由。
- 调用方需要感知服务发现和负载均衡能力。
服务端负载均衡
Nginx、云负载均衡、Kubernetes Service 更接近服务端负载均衡。
flowchart TD
A["调用方"] --> B["Nginx / SLB / Kubernetes Service"]
B --> C["服务端维护实例1、2、3的后端池"]
C --> D["为本次请求选择一个实例"]
D --> E["转发请求并返回响应"]特点:
- 调用方只访问统一入口。
- 入口层负责转发。
- 对调用方简单。
- 入口层可能成为瓶颈或额外跳点。
生产架构里两种方式经常同时存在:
外部流量 -> Nginx / Ingress / Gateway
微服务内部调用 -> OpenFeign + Spring Cloud LoadBalancer负载均衡完整流程
以 OpenFeign 调用为例:
flowchart TD
A["业务代码调用 UserClient"] --> B["Feign 生成 HTTP 请求"]
B --> C["目标服务名 user-service"]
C --> D["DiscoveryClient 获取实例列表"]
D --> E["ServiceInstanceListSupplier 供应实例"]
E --> F["LoadBalancer 选择实例"]
F --> G["替换为真实 IP:Port"]
G --> H["HTTP Client 发送请求"]这里的关键点是:服务名本身不能直接发网络请求,必须解析成真实实例。
负载均衡选择实例的输入从哪里来
负载均衡不是凭空随机选。它依赖一组“候选实例”,这些实例来自注册中心和调用方本地缓存,再经过过滤和排序后才进入选择算法。
flowchart TD
A["注册中心实例列表"] --> B["调用方本地缓存"]
B --> C["ServiceInstanceListSupplier"]
C --> D["按健康状态过滤"]
D --> E["按命名空间/分组/集群过滤"]
E --> F["按版本/灰度/机房过滤"]
F --> G["负载均衡算法选择"]
G --> H["得到最终实例"]可以把一次选择拆成三步:
| 步骤 | 做什么 | 常见问题 |
|---|---|---|
| 获取实例 | 从注册中心或本地缓存拿服务实例 | 服务没注册、命名空间不一致 |
| 过滤实例 | 按健康、版本、区域、元数据筛选 | 过滤规则太严导致无实例 |
| 选择实例 | 轮询、随机、权重、同机房优先 | 选到慢实例、负载不均 |
这也是为什么“注册中心有实例”不代表“调用方一定能调用到”。调用方可能没刷新本地缓存,可能和服务提供者不在同一个 namespace/group,也可能被灰度规则过滤掉。
为什么会选到旧实例
服务下线不是所有调用方瞬间感知。注册中心剔除、客户端缓存刷新、连接池断开都存在时间窗口。
flowchart TD
A["实例准备下线"] --> B["停止接收新流量"]
B --> C["注册中心标记下线"]
C --> D["调用方刷新实例列表"]
D --> E["连接池旧连接关闭"]如果没有优雅下线,可能出现:
- 实例已经停止服务,但注册中心还没剔除。
- 注册中心已剔除,但调用方本地缓存还没刷新。
- 调用方实例列表已刷新,但 HTTP 连接池里还有旧连接。
- Kubernetes Pod 正在终止,但流量仍然打进来。
生产处理:
| 场景 | 建议 |
|---|---|
| Java 服务下线 | 先摘注册,再等待一段时间,最后关闭进程 |
| K8s 发布 | 配置 readinessProbe、preStop、terminationGracePeriod |
| Feign 调用 | 配置连接超时、读取超时、熔断 |
| 注册中心 | 调整健康检查和实例心跳,不要过大也不要过小 |
为什么会选到慢实例
普通轮询或随机只关心“实例在不在线”,不一定知道“实例现在快不快”。
慢实例常见原因:
| 原因 | 表现 | 处理 |
|---|---|---|
| 该实例 Full GC | 偶发长耗时 | 查 GC 日志,临时摘流 |
| 该实例线程池满 | 请求排队 | 查线程池和 Tomcat 指标 |
| 该实例连接的 DB 慢 | 只有部分实例慢 | 查数据源、慢 SQL、网络 |
| 该实例部署在跨机房 | 延迟高 | 同机房优先 |
| 该实例版本有 bug | 错误率高 | 灰度回滚、版本过滤 |
负载均衡可以配合权重、区域、健康检查和熔断降低慢实例影响,但不能替代实例自身的健康监控。服务治理的关键是:发现慢实例后要能摘流、降权、熔断或回滚。
如何验证负载均衡是否按预期工作
排查负载不均时不要只看“我感觉某台流量高”。建议收集这些证据:
| 证据 | 说明 |
|---|---|
| 注册中心实例列表 | 确认调用方理论上能看到哪些实例 |
| 调用方本地日志 | 打印选中的 instanceId、IP、port、metadata |
| 网关或 Feign 指标 | 看每个实例的 QPS、错误率、耗时 |
| traceId | 串起一次请求到底去了哪个实例 |
| 实例自身指标 | CPU、线程池、连接池、GC、DB 慢 SQL |
可以在低风险环境临时给 Feign 拦截器或 HTTP Client 日志增加目标实例信息:
traceId=abc123 service=user-service instance=192.168.1.11:8081 version=v1 cost=82ms线上不要长期打印过多明细日志,否则日志量会很大。更好的方式是把实例维度打到 Micrometer 指标或链路追踪标签中。
常见负载均衡算法
轮询
轮询是最容易理解的策略。
第 1 次 -> 实例 1
第 2 次 -> 实例 2
第 3 次 -> 实例 3
第 4 次 -> 实例 1适合场景:
- 实例配置接近。
- 请求耗时接近。
- 没有灰度和权重要求。
缺点:
- 不知道实例真实压力。
- 实例配置不同时可能不公平。
- 慢实例仍然会被分到请求。
随机
随机策略每次随机选择一个实例。
适合实例规模较大、实例能力接近的场景。请求量足够大时,随机分布会趋近均匀。
缺点是短时间内可能不均衡。
权重
权重策略会按实例权重分配请求。
实例 A weight=5
实例 B weight=3
实例 C weight=2大致表示 A 承担 50% 流量,B 承担 30%,C 承担 20%。
适合场景:
- 机器配置不同。
- 灰度发布。
- 新版本先承接少量流量。
权重通常来自注册中心元数据或服务治理平台。
同机房优先
如果系统跨机房部署,上海服务优先调用上海实例,北京服务优先调用北京实例。
flowchart TD
A["上海订单服务发起调用"] --> B["按zone过滤上海用户实例"]
B --> C["有匹配则优先上海实例"]
C --> D["没有匹配则按策略跨区或失败"]内置Zone Preference通常在本zone为空时回退全部实例;严格地域合规场景必须自定义为空即失败的过滤策略,不能依赖默认偏好。
这样可以降低网络延迟和跨机房流量成本。
灰度/版本优先
实例可以带元数据:
metadata:
version: v2
gray: true调用方或网关可以根据用户、请求头、租户、版本选择实例。
例如:
X-Version: v2 -> 只调用 version=v2 的实例
普通请求 -> 调用 version=v1 的实例Spring Cloud LoadBalancer 内部到底怎么工作
很多同学只记住“LoadBalancer 选一个实例”,但面试和排查更常问的是:实例列表从哪里来、什么时候缓存、为什么注册中心有实例但调用方选不到、怎么扩展。
Spring Cloud LoadBalancer 可以拆成两层理解:
| 层次 | 核心对象 | 负责什么 |
|---|---|---|
| 实例供应层 | ServiceInstanceListSupplier | 按服务名拿候选实例,并可做缓存、健康过滤、区域过滤、元数据过滤 |
| 实例选择层 | ReactorServiceInstanceLoadBalancer | 从候选实例中按算法选择一个 ServiceInstance |
一次选择过程可以这样理解:
flowchart TD
A["调用方访问user-service"] --> B["Factory取得该服务负载均衡上下文"]
B --> C["Supplier从Discovery或缓存取候选"]
C --> D["按健康、区域、版本和权重处理"]
D --> E["空列表返回无实例<br/>非空列表进入选择算法"]
E --> F["返回一个ServiceInstance"]
F --> G["HTTP Client重写真实IP和端口"]这里有两个非常关键的结论:
- 注册中心有实例,只代表“注册中心视角存在实例”,不代表“调用方当前候选列表里一定有实例”。
- 负载均衡不是直接修改你的业务代码,而是在 HTTP 请求发送前把
http://user-service/...解析成http://ip:port/...。
ServiceInstance 里通常有什么
ServiceInstance 可以理解为一个服务实例描述对象,核心字段包括:
| 字段 | 例子 | 用途 |
|---|---|---|
serviceId | user-service | 服务名 |
host | 192.168.1.11 | 真实主机地址 |
port | 8081 | 真实端口 |
secure | false | 是否 HTTPS |
metadata | version=v2, zone=shanghai | 灰度、同机房、权重、租户路由 |
instanceId | user-service-192.168.1.11-8081 | 日志和排查定位 |
生产排查时,建议日志或 trace 标签里带上 serviceId、instanceId、host、port、version、zone。否则你只能看到“调用 user-service 慢”,却不知道到底慢在哪台机器。
为什么注册中心有实例,LoadBalancer 还是选不到
常见原因不是“框架坏了”,而是实例进入负载均衡前被过滤掉了。
| 原因 | 现象 | 排查点 |
|---|---|---|
| 服务名不一致 | No instances available for xxx | @FeignClient、Gateway lb://、注册名是否一致 |
| namespace/group 不一致 | 注册中心控制台有实例,调用方看不到 | Nacos namespace、group、cluster 配置 |
| 实例健康状态异常 | 注册中心显示不健康或权重为 0 | 健康检查、心跳、metadata |
| 灰度规则过滤为空 | 普通用户能访问,灰度用户不能访问 | 版本 header、metadata、路由规则 |
| 本地缓存未刷新 | 新实例上线后短时间没流量 | 客户端缓存刷新周期、注册中心推送 |
| 权重/区域规则配置错误 | 所有请求都打到少数实例 | 权重值、zone、clusterName |
所以排查时不要只问“注册中心有没有实例”,还要问:
调用方当前拿到的候选实例列表是什么?
候选列表经过哪些过滤?
最终选择算法选中了谁?
HTTP Client 实际请求的 IP:Port 是什么?Ribbon 和 LoadBalancer 的扩展点对比
老项目可能还在用 Ribbon,新项目更常用 Spring Cloud LoadBalancer。两者都属于“客户端负载均衡”,但扩展点不同。
| 能力 | Ribbon | Spring Cloud LoadBalancer |
|---|---|---|
| 实例来源 | ServerList、Eureka 集成 | DiscoveryClientServiceInstanceListSupplier |
| 选择规则 | IRule | ReactorServiceInstanceLoadBalancer |
| 健康判断 | IPing、Server 状态 | 注册中心健康状态、Supplier 过滤 |
| 缓存刷新 | Ribbon 自身缓存和 Eureka 客户端 | DiscoveryClient / Supplier / Caching Supplier |
| 灰度扩展 | 自定义 IRule | 自定义 ServiceInstanceListSupplier 或 LoadBalancer |
| 适合项目 | 历史 Netflix 技术栈 | 新 Spring Cloud 技术栈 |
面试回答时不要只说“Ribbon 被 LoadBalancer 替代了”。更准确的回答是:
Ribbon 和 Spring Cloud LoadBalancer 解决的问题相同,都是客户端从多个服务实例中选择一个实例。Ribbon 是 Netflix OSS 老组件,扩展点主要是 IRule、IPing、ServerList;Spring Cloud LoadBalancer 是 Spring Cloud 官方方案,核心扩展点是 ServiceInstanceListSupplier 和 ReactorServiceInstanceLoadBalancer。现在新项目优先用 LoadBalancer,老项目遇到 Ribbon 要能读懂 IRule 和缓存刷新逻辑。自定义同机房和灰度路由 Demo
下面这个 Demo 展示的是生产项目常见思路:优先调用同机房实例,再按请求头选择版本。代码重点不是让你背 API,而是理解“过滤候选实例”这个位置最适合放灰度和区域规则。
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class ZoneVersionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ServiceInstanceListSupplier supplier;
private final AtomicInteger position = new AtomicInteger();
private final String currentZone;
public ZoneVersionLoadBalancer(ServiceInstanceListSupplier supplier, String currentZone) {
this.supplier = supplier;
this.currentZone = currentZone;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return supplier.get(request).next().map(instances -> {
List<ServiceInstance> candidates = filterSameZone(instances);
if (candidates.isEmpty()) {
candidates = instances;
}
if (candidates.isEmpty()) {
return new EmptyResponse();
}
int index = Math.abs(position.getAndIncrement());
return new DefaultResponse(candidates.get(index % candidates.size()));
});
}
private List<ServiceInstance> filterSameZone(List<ServiceInstance> instances) {
return instances.stream()
.filter(instance -> currentZone.equals(instance.getMetadata().get("zone")))
.toList();
}
}如果你要做“按 header 选择 v2 版本”,实际项目中通常会把请求上下文放进 Request,再从候选实例的 metadata.version 里筛选。一定要考虑兜底策略:
| 策略 | 结果 | 适合场景 |
|---|---|---|
| 灰度实例为空直接失败 | 能暴露配置错误,但用户请求失败 | 内部测试环境 |
| 灰度实例为空回退稳定版本 | 可用性更好,但可能掩盖灰度配置问题 | 普通线上灰度 |
| 灰度实例异常自动摘流 | 降低故障影响 | 配合治理平台和指标 |
超时、重试、熔断与负载均衡的边界
负载均衡只回答“选谁”,不回答“等多久、失败怎么办、能不能再试一次”。
flowchart TD
A["Feign / Gateway 发起调用"] --> B["LoadBalancer 选择实例"]
B --> C["HTTP Client申请连接并建连"]
C --> D["按连接、读取或正常响应分类"]
D --> E["记录目标实例、耗时和结果"]
E --> F["成功进入解码<br/>失败按熔断、幂等和预算处理"]连接失败、读取超时和成功响应是互斥结果,图中纵向合并是为了窄屏阅读。每类结果的准确语义、不解决的问题和写请求边界见下表。
几个边界要分清:
| 能力 | 解决什么 | 不解决什么 |
|---|---|---|
| 负载均衡 | 本次请求发给哪个实例 | 下游慢、重复写、线程耗尽 |
| 连接超时 | 连不上目标实例时快速失败 | 下游业务执行慢 |
| 读取超时 | 下游长时间不返回时释放调用线程 | 下游是否已经执行成功 |
| 重试 | 偶发网络错误或临时实例故障 | 非幂等写操作的重复提交 |
| 熔断 | 下游持续异常时快速失败 | 修复下游自身问题 |
| 限流 | 防止流量超过系统容量 | 保证每个请求都成功 |
特别注意:超时不等于下游一定失败。例如订单服务调用库存服务扣减库存,订单服务读超时了,但库存服务可能已经扣减成功。如果这时订单服务直接重试,又没有幂等号,就可能重复扣库存。
商业项目建议:
| 场景 | 建议 |
|---|---|
| 查询用户、字典、配置 | 可以短超时 + 少量重试 + 缓存兜底 |
| 扣库存、支付、发货 | 必须有幂等键;超时后查询状态或走补偿,不要盲目重试 |
| 外部医院接口采集 | 超时后记录失败批次,异步补偿,避免采集线程被拖死 |
| 资产同步到 ES | 主库事务先成功,ES 失败进入重试表或 MQ 补偿 |
RestTemplate 使用负载均衡
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}调用:
@Service
public class UserQueryService {
private final RestTemplate restTemplate;
public UserQueryService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getUser(Long userId) {
return restTemplate.getForObject(
"http://user-service/users/" + userId,
String.class
);
}
}@LoadBalanced 的作用是:让 RestTemplate 识别 user-service 这种服务名,并通过 LoadBalancer 选择实例。
如果没有 @LoadBalanced,RestTemplate 会把 user-service 当普通域名解析,通常会失败。
OpenFeign 使用负载均衡
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/users/{id}")
UserDTO getUser(@PathVariable("id") Long id);
}Feign 调用链路:
flowchart TD
A["UserClient.getUser"] --> B["Feign 代理"]
B --> C["服务名 user-service"]
C --> D["LoadBalancer 获取实例并选择"]
D --> E["发送 HTTP 请求"]Feign 让代码看起来像本地接口,但底层仍然要走服务发现、负载均衡、HTTP 请求和响应解码。
Gateway 使用负载均衡
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service
predicates:
- Path=/users/**lb://user-service 的含义是:
lb:走 LoadBalancer。user-service:目标服务名。- Gateway 会从注册中心找实例,再选择一个实例转发。
flowchart TD
A["外部请求 /users/1001"] --> B["Gateway"]
B --> C["匹配 user-route"]
C --> D["识别 lb://user-service"]
D --> E["LoadBalancer 选择实例"]
E --> F["转发到用户服务"]自定义负载均衡思路
真实项目可能需要:
- 同机房优先。
- 根据版本灰度。
- 根据权重分流。
- 排除不健康实例。
- 优先调用低延迟实例。
自定义策略时不要只看“怎么写代码”,要先明确业务规则。
伪代码:
public ServiceInstance choose(List<ServiceInstance> instances, RequestContext context) {
List<ServiceInstance> sameZone = instances.stream()
.filter(instance -> sameZone(instance, context))
.toList();
List<ServiceInstance> candidates = sameZone.isEmpty() ? instances : sameZone;
return weightedRandom(candidates);
}核心思路:
- 从注册中心拿到候选实例。
- 根据元数据过滤实例。
- 根据权重或算法选择实例。
- 选不到时要有降级策略。
为什么负载均衡不能解决所有问题
负载均衡只解决“请求分给谁”,不能解决所有稳定性问题。
| 问题 | 负载均衡能否解决 | 需要什么 |
|---|---|---|
| 单个实例压力大 | 部分能 | 扩容、权重、限流 |
| 下游所有实例都慢 | 不能 | 超时、熔断、降级、排查下游 |
| 请求量远超系统容量 | 不能 | 限流、削峰、扩容 |
| 写操作重复提交 | 不能 | 幂等、唯一键、状态机 |
| 调用链路太长 | 不能 | 架构优化、批量接口、缓存 |
| 注册中心数据延迟 | 不能完全解决 | 健康检查、超时、重试、熔断 |
常见线上问题
某个实例已经挂了,为什么还会被调用
可能原因:
- 注册中心还没剔除实例。
- 调用方本地缓存还没刷新。
- 健康检查不准确。
- 实例进程活着,但业务线程池已经满了。
- 网络到某个实例不通。
所以服务调用还必须配置超时和熔断,不能只依赖注册中心。
为什么流量没有平均分布
可能原因:
- 请求量太小,随机或轮询短时间不均衡。
- 实例权重不同。
- 部分请求固定 key,被路由到固定实例。
- 长连接或连接池复用导致看起来不均衡。
- 某些实例被过滤。
- 网关层和服务内部都有负载均衡,观察口径不一致。
增加实例后为什么没流量
排查:
- 新实例是否注册成功。
- 注册中心是否显示健康。
- 调用方是否拉到新实例。
- 命名空间、分组、集群是否一致。
- 网关或调用方是否有版本/灰度过滤规则。
商业项目里的负载均衡参数怎么配
负载均衡不是只选算法,还要和超时、连接池、重试、熔断、实例下线一起设计。很多线上事故不是“算法错了”,而是这些参数互相打架。
以订单服务调用资产服务为例:
order-service -> asset-service调用方要同时关心:
| 配置 | 解决什么 | 配错后果 |
|---|---|---|
| 连接超时 | TCP 连接建不起来时快速失败 | 太长会占住调用线程,太短会误伤跨机房请求 |
| 读取超时 | 下游迟迟不返回时释放线程 | 太长会拖垮上游,太短会造成大量不确定超时 |
| 连接池最大连接数 | 控制到下游的并发连接 | 太小会排队,太大可能压垮下游 |
| 每路由连接数 | 控制到单个实例的连接数 | 单实例被打爆或连接不够用 |
| 重试次数 | 偶发网络失败恢复 | 写操作重复提交,流量被放大 |
| 熔断阈值 | 下游持续异常时快速失败 | 太敏感误熔断,太迟钝拖垮上游 |
| 实例缓存刷新 | 感知扩容、下线、健康变化 | 刷新慢会继续调用旧实例 |
Feign 超时配置 Demo
不同 Spring Cloud 版本和 HTTP Client 选择会有差异,下面给出通用思路:连接超时要短,读取超时要结合业务耗时;写接口更要谨慎重试。
spring:
cloud:
openfeign:
client:
config:
asset-service:
connectTimeout: 1000
readTimeout: 3000
loggerLevel: basic含义:
connectTimeout=1000:1 秒内连不上资产服务实例就快速失败。readTimeout=3000:请求发出去后 3 秒内没有响应就读超时。loggerLevel=basic:记录基础请求日志,生产不要长期开 full,避免日志过大和敏感信息泄露。
为什么不能所有接口都配 30 秒?因为上游 Tomcat 或业务线程会一直等下游,流量一上来线程池很快被占满,后续正常请求也进不来。
连接池为什么会影响负载均衡
负载均衡选择的是实例,但真正发请求的是 HTTP Client。HTTP Client 通常会复用连接池。
flowchart TD
A["Feign 调用"] --> B["LoadBalancer 选择实例"]
B --> C["HTTP Client 获取连接"]
C --> D["有空闲连接则复用<br/>没有则等待或新建"]
D --> E["应用连接池等待和连接超时"]
E --> F["取得连接后发送请求"]
F --> G["超时则失败<br/>成功则读取并归还连接"]如果连接池太小,即使负载均衡选到了健康实例,请求也可能卡在“等连接”。这时你看到的现象是 Feign 慢、下游 QPS 不高、调用方线程堆积。根因不是注册中心,也不是负载均衡算法,而是 HTTP Client 连接池容量不足或连接没有及时释放。
生产排查要同时看:
- 每个下游服务的连接池最大连接数。
- 每个实例的连接数分布。
- 等连接耗时。
- 下游响应耗时。
- 调用方线程池和 Tomcat 线程状态。
重试风暴:为什么扩容后仍然会慢
面试和线上都很常见一个问题:下游慢了,上游加了重试,短时间看似成功率提高,随后整个链路更慢。
假设入口 1000 QPS,订单服务调用资产服务,失败后最多重试 2 次。
原始请求:1000 QPS
第一次重试:最多再 1000 QPS
第二次重试:最多再 1000 QPS
资产服务理论最大承压:3000 QPS流程图:
flowchart TD
A["下游 asset-service 变慢"] --> B["调用方 read timeout"]
B --> C["触发重试"]
C --> D["下游收到更多请求"]
D --> E["线程池和连接池更拥塞"]
E --> F["更多请求超时"]
F --> G["重试预算失控后形成雪崩"]这就是重试风暴。重试本来是为了解决偶发失败,但下游整体变慢时,重试会放大流量。
商业建议:
| 场景 | 重试建议 |
|---|---|
| 查询字典、查询配置 | 可以短超时 + 少量重试 + 本地缓存兜底 |
| 查询资产详情 | 可重试,但要限制次数和总耗时 |
| 创建订单、扣库存、支付 | 不要盲目重试,必须有幂等键、状态查询或补偿 |
| 医院接口采集 | 超时后记录失败批次,异步补偿,避免阻塞采集线程 |
重试必须配合:
- 总超时时间。
- 退避策略。
- 熔断。
- 限流。
- 幂等设计。
- 失败补偿。
权重、灰度和同机房优先怎么落地
负载均衡算法不应该只停留在“轮询或随机”。商业项目常常需要更细的路由规则。
权重路由
实例元数据:
metadata:
weight: 20常见场景:
- 新机器配置更高,给更高权重。
- 新版本灰度,只给 5% 流量。
- 某实例观察到 CPU 高,临时降权。
注意:权重不是权限控制。权重只影响概率,不能保证某类请求一定不打到某实例。如果必须严格隔离,要用版本、租户、机房等明确过滤条件。
灰度路由
灰度发布常见方式:
请求头 X-Version: v2 -> 调用 metadata.version=v2 的实例
普通请求 -> 调用 metadata.version=v1 的实例流程:
flowchart TD
A["请求进入"] --> B["读取 Header 或用户灰度规则"]
B --> C["确定目标是v2灰度或v1稳定版"]
C --> D["按metadata.version过滤候选"]
D --> E["空列表执行失败或回退<br/>非空列表进入负载均衡选择"]候选为空与非空是互斥结果。图中纵向合并用于窄屏阅读,真正实现时必须先判断列表,再选择“失败、回退或继续选址”。
灰度一定要明确兜底:
| 灰度实例为空怎么办 | 优点 | 风险 |
|---|---|---|
| 直接失败 | 能暴露配置错误 | 用户失败 |
| 回退稳定版本 | 可用性好 | 可能掩盖灰度配置错误 |
| 按业务开关决定 | 灵活 | 规则复杂,需要监控 |
同机房优先
跨机房系统里,同机房优先可以降低延迟和专线成本。
上海 order-service -> 优先上海 asset-service
北京 order-service -> 优先北京 asset-service但同机房优先也要有降级策略:如果本机房资产服务全挂,是否允许跨机房调用?这要按业务要求决定。支付、库存、核心写链路可能宁愿失败也不跨不一致区域;查询类接口通常可以跨机房兜底。
优雅下线全过程
服务发布时最容易出现“Pod 已经准备停止,但调用方还在打流量”。这不是一个组件能单独解决的问题,而是注册中心、K8s、HTTP 连接池、业务线程共同参与。
推荐顺序:
flowchart TD
A["发布系统准备停止实例"] --> B["Readiness 置为不可用"]
B --> C["从注册中心摘除或标记下线"]
C --> D["等待调用方刷新本地实例缓存"]
D --> E["停止接收新请求"]
E --> F["等待存量请求处理完"]
F --> G["关闭消费者、定时任务、连接池"]
G --> H["进程退出"]如果直接 kill 进程,会出现:
- 注册中心还没来得及剔除,调用方继续选择它。
- Gateway 或 Feign 本地缓存里还有旧实例。
- HTTP 连接池里还有旧连接。
- 正在处理的请求被中断。
- MQ 消费、定时任务、批处理任务没收尾。
Kubernetes 场景要关注:
| 配置 | 作用 |
|---|---|
readinessProbe | 控制是否接收流量 |
preStop | 退出前执行摘流、等待或清理脚本 |
terminationGracePeriodSeconds | 给应用优雅关闭时间 |
| Spring Boot graceful shutdown | 等待 Web 请求处理完成再关闭 |
服务调用故障排查决策树
flowchart TD
A["服务调用失败"] --> B["先区分Gateway入口或服务内部"]
B --> C["保存状态码、Feign异常和traceId"]
C --> D["核对路由、serviceId与候选实例"]
D --> E["核对网络、连接池与下游是否收到"]
E --> F["核对下游线程、数据库和依赖"]
F --> G["核对重试、熔断、限流与降级"]
G --> H["按根因修复并验证恢复"]404、503、504、无实例、连接超时、读取超时和5xx的详细分流保留在本节说明与生产排查Runbook中。排查顺序应沿证据链推进,不要同时修改算法、超时和重试导致无法归因。
排查顺序一定要从“错误类型”出发,不要一上来就改负载均衡算法。
商业落地建议
| 建议 | 原因 |
|---|---|
| 新项目优先 Spring Cloud LoadBalancer | Ribbon 是老方案 |
| 服务内部调用用服务名,不写死 IP | 方便扩容、下线、迁移 |
| 所有远程调用都设置超时 | 防止线程无限等待 |
| 核心链路配熔断和限流 | 防止下游拖垮上游 |
| 写接口不要盲目重试 | 防止重复下单、重复扣款 |
| 灰度发布依赖元数据 | 版本、权重、区域都应可治理 |
| 监控实例调用量和失败率 | 发现负载不均和故障实例 |
| 发布要做优雅下线 | 避免旧实例在停止窗口继续接流量 |
| 重试必须配合幂等和熔断 | 避免下游慢时形成重试风暴 |
| 日志和 trace 打出目标实例 | 才能定位到底哪台实例慢 |
独立面试入口
标准回答、追问和精确源码跳转已统一迁移到LoadBalancer独立面试题。知识页保留完整原理、Demo、商业场景和排查步骤,避免把背诵答案与课程正文混在一起。
和服务调用链路的关系
负载均衡是 Spring Cloud 服务调用链路 中的一环。
完整链路是:
Feign / Gateway 发起请求
-> 根据服务名找实例列表
-> LoadBalancer 选择实例
-> HTTP Client 发请求
-> 下游处理
-> 熔断、限流、降级兜底如果只理解负载均衡,不理解注册中心、Feign、Gateway、熔断和链路追踪,仍然无法完整排查微服务调用问题。
小结
Spring Cloud 负载均衡的核心是:调用方根据服务名拿到实例列表,再在本地选择一个实例发起请求。老项目常见 Ribbon,新项目推荐 Spring Cloud LoadBalancer。负载均衡解决的是“选哪个实例”,但不能替代超时、重试、熔断、限流、降级和幂等设计。
