SpringCloud(二):远程调用、负载均衡与容错
SpringCloud(二):远程调用、负载均衡与容错
导语:微服务之间"怎么调"决定了系统的延迟与稳定性。本篇讲透 OpenFeign 的声明式调用与底层原理(动态代理 + Contract + 编解码 + 负载均衡)、客户端 vs 服务端负载均衡、Feign 首次调用慢与超时重试配置,再落到 Sentinel 的整合与核心概念,并回答"超时/重试/熔断怎么配合才不出事故",共 12 题。(熔断与限流的原理见《架构 / 高可用(一)》,本篇侧重 Spring Cloud 中的落地方式。)
一、声明式调用 OpenFeign
1. 什么是 OpenFeign?它的优点是什么?如何使用?
答: OpenFeign 是 Spring Cloud 的声明式 HTTP 客户端——用"接口 + 注解"描述远程调用,框架自动生成实现(动态代理),底层整合了负载均衡与熔断。
优点:
- 声明式:面向接口编程,调用远程就像调用本地方法,代码极简;
- 内置客户端负载均衡(现为 Spring Cloud LoadBalancer);
- 与 Sentinel / Resilience4J 集成实现熔断降级;
- 支持编解码扩展、日志、超时、压缩、拦截器。
@EnableFeignClients // ① 启动类开启
public class Application { ... }
@FeignClient(name = "order-service", path = "/order", fallbackFactory = OrderClientFallback.class)
public interface OrderClient { // ② 声明接口
@GetMapping("/{id}")
OrderDTO getOrder(@PathVariable("id") Long id);
}
@Autowired private OrderClient orderClient; // ③ 直接注入使用关键点:
name/value是服务名(用于服务发现 + 负载均衡),不是域名;path是统一前缀。契约优先:把接口抽成独立模块(api模块),提供方实现、消费方依赖,可以避免"两端注解不一致"。
2. OpenFeign 的底层原理是什么?
答: 一条链:FactoryBean 注册 → 动态代理 → Contract 解析 → 模板构建 → 负载均衡 → 编解码。
① @EnableFeignClients → 扫描 @FeignClient → 为每个接口注册 FeignClientFactoryBean
② 注入时 getObject():
· 收集配置(Encoder/Decoder/Contract/Logger/Client/Retryer)
· ReflectiveFeign.newInstance() → 用 JDK 动态代理生成实现类
· 每个方法对应一个 MethodHandler(默认 SyncMethodHandler)
③ 调用时:
· SpringMvcContract 把接口上的 Spring MVC 注解解析为 MethodMetadata
· RequestTemplate.Factory 构建请求模板(URL、Header、Body)
· Encoder 编码(如 SpringEncoder 走 HttpMessageConverter)
· Client 执行请求 ——【默认 FeignBlockingLoadBalancerClient】
→ 从 LoadBalancer 拿一个 ServiceInstance 改写 URL → 真正发 HTTP
· Decoder 解码响应(如 ResponseEntityDecoder)
④ 异常/日志:ErrorDecoder、Logger(NONE/BASIC/HEADERS/FULL)两个高频追问:
- 客户端类型:默认是
Client.Default(基于HttpURLConnection,无连接池)→ 生产建议换 Apache HttpClient / OkHttp; - 契约(Contract):Spring Cloud 用
SpringMvcContract,所以能识别@GetMapping/@PathVariable等 Spring MVC 注解(这也是它和原生 Feign 的区别);若用原生注解(@RequestLine)需显式改成FeignContract。
3. Ribbon / Spring Cloud LoadBalancer 的原理是什么?什么是客户端负载均衡?
答: 客户端负载均衡指"负载均衡逻辑在调用方进程内"——调用方从注册中心拿到实例列表,自己选一个发起请求。
Ribbon 的核心组件:
| 组件 | 职责 |
|---|---|
ServerList | 获取实例列表(配合注册中心) |
ServerListFilter / ServerListUpdater | 过滤与更新列表 |
IRule | 选择策略(choose()),决定"选哪个 Server" |
IPing | 探测实例可用性(Ribbon 默认 10s) |
ILoadBalancer | 组装上述组件,对外提供 chooseServer() |
IClientConfig | 超时、重试等配置 |
Spring Cloud LoadBalancer(Ribbon 的替代):
- 核心接口:
ReactorLoadBalancer<T>→choose()返回Mono<T>(响应式,与 WebFlux/Gateway 统一); - 数据来源:
ServiceInstanceListSupplier(委托DiscoveryClient,可用 Caffeine 缓存); - 默认实现:
RoundRobinLoadBalancer(轮询);Nacos 场景可用NacosLoadBalancer(支持权重 + 同集群优先)。
Ribbon 的默认策略是
ZoneAvoidanceRule(区域感知 + 轮询),Spring Cloud LoadBalancer 的默认是轮询——这个差异是高频追问点。
4. 什么是服务端负载均衡?和客户端负载均衡有什么区别?
答:
| 服务端(集中式)LB | 客户端(进程内)LB | |
|---|---|---|
| 实现 | Nginx / F5 / LVS / 云 SLB | Ribbon、Spring Cloud LoadBalancer、gRPC LB |
| 位置 | 独立中间层,位于调用方与提供方之间 | 集成在调用方进程内 |
| 优点 | 对业务零侵入、可跨语言、集中管控 | 少一跳(延迟更低)、灵活(可自定义策略、能感知服务元数据) |
| 缺点 | 多一次转发、可能成为瓶颈/单点(需自身高可用) | 需客户端集成(语言相关)、策略分散在各服务 |
微服务中的典型组合:
外部流量:客户端 → DNS/GSLB → 网关/LB(服务端 LB)→ 服务
服务间调用:服务 A(客户端 LB:从注册中心取列表自选)→ 服务 B加分点:能说出"客户端 LB 依赖注册中心,因此注册中心故障时靠本地缓存列表继续工作"(见《SpringCloud(一)》第 12 题),体现组件间的关联理解。
5. Feign 第一次调用为什么很慢?如何优化?
答: 首次调用慢是多个初始化动作叠加的结果:
| 原因 | 说明 |
|---|---|
| 负载均衡/Feign 上下文懒加载 | LoadBalancerClient、ServiceInstanceListSupplier、Ribbon 的 ILoadBalancer 等在首次调用才创建 |
| 代理与契约解析 | 首次调用才生成动态代理、解析 Contract、构建 MethodMetadata |
| 连接建立成本 | 首次 TCP 三次握手 +(HTTPS)TLS 握手 + 连接池为空 |
| 注册表首次拉取 | 首次需要从注册中心拉取实例列表 |
优化手段:
# ① 饥饿加载(Ribbon 时代)
ribbon:
eager-load:
enabled: true
clients: order-service,user-service
# ② Spring Cloud LoadBalancer 的饥饿加载(新版)
spring:
cloud:
loadbalancer:
eager-load:
clients: order-service
cache:
enabled: true
openfeign:
lazy-attributes-resolution: false # 提前解析 @FeignClient 属性③ 换带连接池的客户端(Apache HttpClient / OkHttp),并预热连接
④ 应用启动后主动调用一次"预热"(ApplicationRunner 中做,注意别强依赖下游可用)
⑤ 部署时用就绪探针(readiness)保证流量只在预热完成后进入必须提醒的坑:"启动预热"不能变成"启动强依赖下游"——如果下游不可用导致预热失败、应用起不来,就变成了"下游故障引发大面积不可用"。预热应容错 + 超时(见《高可用(一)》)。
6. Feign 如何做性能优化、超时与重试配置?
答:
(1)性能优化
spring:
cloud:
openfeign:
httpclient:
enabled: true # 用 Apache HttpClient(带连接池)
compression:
request.enabled: true
response.enabled: true
client:
config:
default:
loggerLevel: basic # 生产别用 full(会打印完整 body,性能与安全风险)· 换 HttpClient/OkHttp → 连接池复用,避免每次新建连接
· 开启 GZIP 压缩(大 body 场景收益明显)
· 合理设置连接池大小(与下游并发能力匹配,不是越大越好)
· 收窄日志级别,避免打印大对象
· 用"接口聚合"减少调用次数(一次批量查询替代 N 次单查)(2)超时与重试
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 1000 # 连接超时
readTimeout: 3000 # 读超时(业务处理时间)
order-service:
readTimeout: 5000 # 按服务精细配置必须注意的三件事:
- Feign 自身默认
Retryer.NEVER_RETRY(不重试),但负载均衡层(Ribbon/LoadBalancer)可能重试——两层叠加会导致"重试次数被放大",必须统一口径(推荐只在一层重试,通常放在上层业务或用 Resilience4J/Sentinel); - 重试只对"幂等"操作安全:查询可重试,创建订单/扣款重试必须有幂等键(《分布式(三)》幂等);
- 超时预算:Feign 的超时必须 ≤ 上游留给你的时间,避免"上游已超时、你还在等下游"(见《高可用(一)》第 9 题)。
7. Feign 如何整合熔断降级?
答: 分两条路线(注意版本与坐标变化):
路线 A:Sentinel(Spring Cloud Alibaba)
feign:
sentinel:
enabled: true # 开启 Sentinel 对 Feign 的支持@FeignClient(name = "order-service", fallbackFactory = OrderClientFallbackFactory.class)
public interface OrderClient { ... }
@Component
public class OrderClientFallbackFactory implements FallbackFactory<OrderClient> {
@Override public OrderClient create(Throwable cause) {
return id -> { // 兜底实现
log.warn("order-service 降级, cause={}", cause.getMessage());
return OrderDTO.empty();
};
}
}路线 B:Spring Cloud CircuitBreaker(Resilience4J / Sentinel 作为实现)
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true # 2020+ 的官方方式(替代已移除的 Hystrix 支持)要点与坑:
fallbackvsfallbackFactory:fallback只能兜底异常、拿不到原因;fallbackFactory能拿到Throwable,便于区分"限流/熔断/超时"——生产推荐后者;- 降级方法必须是"无副作用 + 明确语义":返回空对象、缓存快照或错误码,不要返回"看起来成功"的假数据导致业务误判;
feign.sentinel.enabled与circuitbreaker.enabled不要重复开启(会导致重复代理/顺序问题);- 熔断与降级的原理与阈值设定见《高可用(一)》第 6~10 题。
8. Sentinel 的核心概念有哪些?
答:
| 概念 | 说明 |
|---|---|
| 资源(Resource) | 被保护的逻辑单元:一个 URL、一个方法、一个 Feign 调用;用 @SentinelResource 或适配器自动定义 |
| 规则(Rule) | 流控、熔断降级、系统保护、热点参数、授权规则;支持从 Nacos/Apollo 动态加载 |
| 统计 | 基于 滑动窗口 LeapArray 实时统计 QPS、RT、异常数、并发线程数 |
| 流控维度 | 按 QPS 或并发线程数;流控模式:直接 / 关联 / 链路;流控效果:快速失败 / Warm Up(预热)/ 排队等待 |
| 熔断策略 | 慢调用比例 / 异常比例 / 异常数(三种) |
| 系统保护 | 按 Load / CPU / 入口 QPS / 并发线程数 做整体保护,自适应限流 |
@SentinelResource(value = "queryOrder", blockHandler = "blocked", fallback = "fallback")
public OrderDTO query(Long id) { ... }加分点:Sentinel 的"系统自适应保护"是它区别于普通限流器的关键——它能根据 Load1 / CPU 使用率动态调整入口流量,避免"限流阈值是静态配置、跟不上实际负载"的问题(原理与算法详见《高可用(一)》)。
9. Sentinel 和 Hystrix 有什么区别?
答: 核心区别在隔离方式与能力范围(详见《高可用(一)》第 7 题):
| 维度 | Sentinel | Hystrix |
|---|---|---|
| 隔离策略 | 信号量 / 并发线程数控制(轻量) | 线程池隔离为主(开销大) |
| 线程模型 | 复用调用线程,无线程切换开销 | 每个依赖一个线程池,双层线程模型 |
| 能力范围 | 限流 + 熔断降级 + 系统保护 + 热点参数 | 主要是熔断 + 隔离 + 降级 |
| 规则配置 | Dashboard 动态推送 + 配置中心 | 偏向硬编码/配置文件 |
| 生态与维护 | 活跃(Spring Cloud Alibaba 默认) | 已停止维护,Spring Cloud 2020+ 已移除 |
结论:Hystrix 已退出历史舞台,新项目用 Sentinel(国内主流)或 Resilience4J(Spring Cloud 官方轻量方案,函数式 + 装饰器风格)。
10. 负载均衡策略有哪些?如何自定义?
答:
| 策略 | 语义 | 适用 |
|---|---|---|
| 轮询(Round Robin) | 依次分配 | 实例性能均匀(Spring Cloud LoadBalancer 默认) |
| 随机(Random) | 随机选 | 实例多、想避免热点 |
| 加权(Weighted) | 按权重分配(Nacos 支持权重 + 同集群优先) | 灰度发布、机器规格不一致(最实用) |
| 响应时间加权 | 响应快的分配更多 | 实例性能不均 |
| 最少连接/最小活跃 | 选当前负载最低的 | 请求耗时差异大 |
| 一致性哈希 | 相同 key 固定落到同一实例 | 需要会话粘性、本地缓存命中率 |
自定义(Spring Cloud LoadBalancer):
public class MyLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return supplier.get().next()
.map(list -> new DefaultResponse(pickByGray(list))); // 自定义灰/权重规则
}
}灰度发布的落地方式:注册中心元数据(version/lane)+ 自定义 LoadBalancer 或 网关按 Header 路由——前者管服务间调用,后者管入口流量(见《SpringCloud(三)》)。
11. RPC 和 REST 在微服务中怎么选?
答:
| 维度 | REST(HTTP + JSON) | RPC(Dubbo / gRPC,二进制) |
|---|---|---|
| 性能 | 一般(文本、连接管理需优化) | 高(二进制、长连接、多路复用) |
| 可读性与调试 | 好(可直接 curl/浏览器) | 差(需专门工具) |
| 跨语言 | 好(天生通用) | gRPC 好;Dubbo 主要 Java |
| 契约 | OpenAPI(弱契约) | 强契约(IDL/接口,编译期检查) |
| 生态 | Spring Cloud 全家桶围绕它 | 需要自己补网关/治理 |
| 适用 | 对外 API、跨语言、通用交互 | 内部高并发、低延迟、强契约的服务间调用 |
结论:
对外(网关到客户端) → REST(通用、易调试、跨端)
内部高频服务间调用 → RPC(gRPC / Dubbo,性能与契约更优)实际项目常混用:Spring Cloud 负责治理面(注册/配置/网关),内部关键链路用 Dubbo/gRPC 提升性能。
12. 服务间调用如何避免"雪崩 / 重试风暴"?
答: 这是"框架之上"的稳定性设计,需要限流 + 熔断 + 隔离 + 超时 + 重试组合(原理见《高可用(一)》),在 Spring Cloud 中的落点如下:
| 手段 | Spring Cloud 落点 |
|---|---|
| 超时 | Feign connectTimeout/readTimeout、RestTemplate/RestClient 超时、DB/Redis 超时;逐层递减(超时预算) |
| 重试 | 只在一层重试、指数退避、有上限、只对幂等操作;下游熔断打开时不重试 |
| 熔断 | Sentinel 慢调用比例/异常比例熔断,fallbackFactory 兜底 |
| 限流 | Sentinel 单机/集群限流、Gateway 入口限流(入口是限流的第一道闸) |
| 隔离 | 每个下游独立线程池/信号量、独立连接池(不同下游不共享资源) |
| 降级 | 兜底数据、开关控制、区分"可降级/不可降级"接口 |
| 调用链收敛 | 减少同步调用层数(串行调用改成并行/异步)、聚合接口、CQRS(见《高性能(二)》) |
最容易被忽略的一条:重试必须与熔断联动——否则下游已经挂了,重试还在成倍放大流量(重试次数 × 并发),会把"局部故障"升级为"全面雪崩"。
本章小结:Feign 的考点是"声明式外观 + 动态代理 + 负载均衡 + 编解码"这条链;稳定性考点是"超时 / 重试 / 熔断 / 隔离 / 限流"这五件套,且它们必须一起配——任何一项缺失都会让另外几项失效(例如有熔断无超时、有重试无熔断)。
