3. 服务调用与负载均衡

在微服务架构中,服务消费者调用服务提供者是最频繁的运行时行为。Spring Cloud 提供了两条主流路径:一是基于 RestTemplate 的显式 HTTP 调用,二是基于 OpenFeign 的声明式接口调用。两者最终都要经过客户端负载均衡器选择目标实例,因此理解“调用方式 + 负载均衡 + 超时重试”三者的协作关系,是掌握服务间通信的关键。

一、RestTemplate 与 OpenFeign 的定位差异

RestTemplate 是 Spring 提供的同步 HTTP 客户端,写法直观,适合快速验证和少量调用。要让其具备服务发现与负载均衡能力,需要配合 @LoadBalanced 注解,使请求 URL 中的服务名被解析为具体实例地址:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

// 调用时使用服务名而非 IP
String result = restTemplate.getForObject(
    "http://order-service/order/1001", String.class);

OpenFeign 则把 HTTP 调用抽象为 Java 接口,通过注解声明路径、方法和参数,由框架在运行时生成代理实现。它天然集成负载均衡、超时、重试和日志,代码更接近本地方法调用:

@FeignClient(name = "order-service")
public interface OrderClient {
    @GetMapping("/order/{id}")
    OrderDTO getOrder(@PathVariable("id") Long id);
}

选择建议:临时脚本、网关转发或需要精细控制请求对象时用 RestTemplate;业务服务之间长期、稳定的调用优先用 OpenFeign,因为接口即契约,便于统一治理。

二、负载均衡器:从 Ribbon 到 Spring Cloud LoadBalancer

早期 Spring Cloud 使用 Netflix Ribbon 作为客户端负载均衡器,其核心是 IRule 策略接口。Ribbon 已进入维护状态,当前版本推荐使用 Spring Cloud LoadBalancer。两者思路一致:从注册中心获取实例列表,按策略选出一个实例,再由调用方发起请求。

常见负载策略及其适用场景如下:

策略行为适用场景
轮询 RoundRobin按顺序依次选择实例实例性能相近的默认选择
随机 Random随机挑选实例压测、简单分散流量
加权 Weighted按权重比例分配机器配置不一致
最少连接/响应时间优先选择负载低或响应快的实例实例性能差异明显

Spring Cloud LoadBalancer 默认使用轮询策略。若需自定义,可实现 ReactorServiceInstanceLoadBalancer 或配置 LoadBalancerClientConfiguration。下面是一个按实例元数据中的 zone 做同区域优先的简化示例:

public class ZoneAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final ServiceInstanceListSupplier supplier;

    public ZoneAwareLoadBalancer(ServiceInstanceListSupplier supplier) {
        this.supplier = supplier;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplier.get().next().map(list -> {
            List<ServiceInstance> sameZone = list.stream()
                .filter(i -> "zone-a".equals(i.getMetadata().get("zone")))
                .collect(Collectors.toList());
            List<ServiceInstance> target = sameZone.isEmpty() ? list : sameZone;
            int idx = ThreadLocalRandom.current().nextInt(target.size());
            return new DefaultResponse(target.get(idx));
        });
    }
}

该策略优先把流量留在同可用区,降低跨区延迟;当同区无实例时回退到全量列表,保证可用性。

三、超时与重试:避免雪崩的关键参数

服务调用必须设置超时,否则慢实例会拖垮调用方线程池。OpenFeign 中通过 connectTimeout 和 readTimeout 控制连接与读取阶段:

feign:
  client:
    config:
      order-service:
        connectTimeout: 1000
        readTimeout: 2000
        loggerLevel: basic

重试需要谨慎:只有幂等操作才适合自动重试。Spring Cloud OpenFeign 默认关闭重试,可通过 Retryer 开启,并配合 MaxAutoRetries、MaxAutoRetriesNextServer 控制次数。例如“同一实例重试 1 次、换下一个实例再试 1 次”,总尝试次数为 4 次。若下游是非幂等的扣款接口,重试可能造成重复扣款,此时应改用业务幂等键或直接关闭重试。

超时时间应小于上游调用方的整体超时,重试次数不宜过多。建议为每个下游服务单独配置,避免全局一刀切。

四、调用链协作关系

下图展示一次服务调用从消费者到提供者的完整路径:调用方式决定请求如何发出,负载均衡器决定选哪个实例,超时与重试决定失败时如何收敛。

服务消费者 order-web RestTemplate @LoadBalanced OpenFeign @FeignClient 接口代理 LoadBalancer 轮询 / 随机 / 自定义 从注册中心取实例列表 order-service:8081 zone-a order-service:8082 zone-b order-service:8083 zone-a 超时 / 重试 connect 1s · read 2s 发起调用 声明式 / 编程式 选择实例

从图中可以看到,无论使用哪种调用方式,请求都会先进入负载均衡器,再由其从注册中心拉取的实例列表中按策略选出一个目标。超时与重试作用于整个调用过程,是保障稳定性的最后一道防线。

五、实践建议

  • 优先使用 OpenFeign 统一服务间调用,接口即契约,便于集中配置超时、重试和日志。
  • 为每个下游服务单独设置超时,读超时通常设为下游 P99 响应时间的 1.5~2 倍。
  • 重试只用于幂等查询;写操作通过业务幂等键或关闭重试来避免重复副作用。
  • 自定义负载策略时保留回退逻辑,避免因元数据缺失导致无实例可选。
  • 开启 Feign 日志时使用 basic 或 headers 级别,避免生产环境打印完整请求体。
验证负载均衡是否生效,可启动两个相同服务实例,连续调用接口并观察日志中的端口号是否交替出现;若始终命中同一实例,检查是否误用了固定 IP 或缓存了实例列表。