这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 服务调用负责把“逻辑服务名”变成一次具体请求。它包括客户端发现、负载均衡、超时、重试、熔断、隔离、重试预算和可观测性。调用链路设计不当,会放大故障。
18.1 调用方式
| 方式 | 契约 | 特点 |
|---|---|---|
| RestTemplate / RestClient | URL / HTTP | 简单直接 |
| WebClient | HTTP / Reactive | 非阻塞 |
| OpenFeign | 接口注解 | 声明式,常见 |
| gRPC | Protobuf | 高性能、强契约 |
| Dubbo | 接口 | RPC 生态完整 |
选择依据:
- 是否需要强契约;
- 是否跨语言;
- 是否浏览器可访问;
- 性能要求;
- 团队技术栈;
- 现有治理能力。
18.2 Spring Cloud LoadBalancer
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
RestClient 使用服务名:
@Configuration
public class HttpClientConfig {
@Bean
@LoadBalanced
RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
调用:
@Service
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://inventory-service").build();
}
public InventoryView get(Long skuId) {
return restClient.get()
.uri("/api/inventories/{id}", skuId)
.retrieve()
.body(InventoryView.class);
}
}
http://inventory-service 会由 LoadBalancer 解析成具体实例。
18.3 OpenFeign
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用:
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {
}
客户端:
@FeignClient(name = "inventory-service", path = "/api/inventories")
public interface InventoryFeignClient {
@GetMapping("/{id}")
InventoryView get(@PathVariable("id") Long id);
@PutMapping("/{id}/reserve")
void reserve(@PathVariable("id") Long id, @RequestBody ReserveRequest request);
}
Feign 将接口代理成 HTTP 调用,并集成注册中心、负载均衡和编解码器。
18.4 负载均衡策略
常见策略:
| 策略 | 特点 |
|---|---|
| Round Robin | 轮询,默认常见 |
| Random | 随机 |
| Weighted | 按权重 |
| Zone Aware | 同机房优先 |
| Least Requests | 少请求优先 |
| Consistent Hash | 相同 key 到相同实例 |
选择:
- 普通无状态服务:轮询;
- 机器配置差异大:权重;
- 多机房:同机房优先;
- 本地缓存:一致性哈希;
- 请求耗时差异大:最少请求。
负载均衡不能解决所有热点,业务 key 设计仍关键。
18.5 超时
必须为每层设置超时:
spring:
cloud:
openfeign:
client:
config:
inventory-service:
connect-timeout: 500
read-timeout: 2000
超时层次:
网关超时
> 服务间调用超时
> 数据库 / 缓存超时
原则:
- 连接超时通常小于读超时;
- 下游超时不能超过上游允许时间;
- 不同接口不同超时;
- 超时后记录目标实例;
- 超时配置可观测。
没有超时等于把下游故障放大成线程池耗尽。
18.6 重试
可重试条件:
- 请求幂等;
- 错误类型可恢复;
- 下游仍有余量;
- 重试次数有限;
- 有退避;
- 有重试预算。
示例:
GET /products/{id} 可重试
POST /orders 不应盲目重试,除非有幂等键
PUT /configs/{id} 全量更新可按契约重试
退避:
100ms
200ms
400ms
jitter
重试风暴:
上游 100 请求
-> 每个重试 3 次
-> 下游瞬间 400 请求
控制方式:
- 每实例重试上限;
- 重试预算比例;
- 熔断优先;
- 只重试连接失败或明确可重试状态;
- 限制并发。
18.7 线程池隔离
调用不同下游使用独立线程池:
Order Service
|-- pay-pool
|-- inventory-pool
|-- user-pool
好处:
- 一个下游慢不会耗尽全部线程;
- 故障范围可控;
- 可单独配置队列和超时;
- 便于监控。
缺点:
- 线程数增加;
- 配置复杂;
- 需要监控每个池;
- 可能掩盖容量问题。
关键指标:
active
queue size
rejected
task latency
downstream latency
18.8 熔断与降级
熔断器状态:
CLOSED
-> 失败率超过阈值
OPEN
-> 冷却时间后
HALF_OPEN
-> 放少量请求探测
-> 成功恢复 CLOSED / 失败回到 OPEN
降级方式:
- 返回缓存;
- 返回默认值;
- 功能关闭;
- 异步补偿;
- 提示用户稍后重试。
熔断不是让错误消失,而是防止故障扩散。核心交易链路的降级必须符合业务规则。
18.9 Hedging 与单飞
Hedging:同时向多个实例发请求,取最先成功结果。
优点:降低尾延迟。
代价:
- 下游流量倍增;
- 副作用请求需幂等;
- 浪费资源;
- 排查复杂。
适合:
- 只读请求;
- P999 极敏感;
- 下游余量充足;
- 有严格预算。
singleflight:同一 key 并发请求只放一个回源,其他等待结果。适合热点缓存重建。
18.10 可观测性
每次调用记录:
traceId
caller service
target service
target instance
method / path
status
latency
timeout
retry count
error type
指标:
client_request_seconds
client_request_total
client_timeout_total
client_retry_total
client_circuit_open
pool_active
pool_queue
pool_rejected
告警:
- 错误率突增;
- P99 超阈值;
- 超时增加;
- 熔断打开;
- 线程池拒绝;
- 某实例失败率异常。
本章小结
服务调用不只是发 HTTP 请求,而是服务发现、负载均衡、超时、重试、熔断、隔离和观测的组合。生产调用方必须明确幂等性、重试预算和降级语义;每层超时要逐级收敛;指标要能定位到目标实例,否则故障排查仍靠猜。
思考题
- 为什么每个下游调用必须有超时?
- 哪些请求可以重试?
- 线程池隔离有什么代价?
- Hedging 适合什么场景?
- 如何定位调用到了哪个坏实例?