Skip to content

Spring Cloud负载均衡

Spring Cloud 里的负载均衡,解决的是:

一个服务有多个实例时,本次请求应该发给哪个实例。

阅读入口:先回答“候选实例从哪里来”

按“Discovery 提供候选 → Supplier 过滤 → 算法选择 → 请求发送 → 失败反馈”阅读。先掌握 LoadBalancer 主线,再把 Ribbon 当作存量实现对照。

核心原理:实例供应链与选择策略

LoadBalancer 不负责注册实例,也不负责发送业务请求;它消费 ServiceInstance 供应链,依次完成缓存、健康过滤、区域/版本过滤和算法选择。Ribbon 是 Netflix 时代的旧实现,Spring Cloud LoadBalancer 是当前主线。

mermaid
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 个用户服务实例:

text
user-service
  192.168.1.10:8081
  192.168.1.11:8081
  192.168.1.12:8081

调用方写的是:

text
http://user-service/users/1001

负载均衡器要把服务名转换成某一个真实地址:

text
http://192.168.1.11:8081/users/1001

先分清几个角色

角色负责什么不负责什么
Nacos / Eureka / Consul保存服务名和实例列表不负责每次请求选哪个实例
Spring Cloud LoadBalancer从实例列表里选择一个实例不负责服务注册
Ribbon老版本客户端负载均衡组件新项目不推荐继续选型
OpenFeign把接口调用转换成 HTTP 请求本身不是注册中心
Gateway入口路由和过滤不应该承载复杂业务
Nginx / SLB / Kubernetes Service服务端负载均衡不理解 Spring Cloud 服务元数据

注册中心和负载均衡经常被混在一起说,但它们职责不同:

mermaid
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 项目中很常见。

经典组合:

text
Eureka + Ribbon + Feign + Hystrix + Zuul

Ribbon 的职责是:从 Eureka 拿到实例列表后,在调用方本地选择一个实例。

Spring Cloud LoadBalancer

Spring Cloud LoadBalancer 是新项目更推荐的负载均衡方案。

常见组合:

text
Nacos / Eureka + Spring Cloud LoadBalancer + OpenFeign + Gateway + Sentinel

现在理解 Spring Cloud 负载均衡时,应重点理解 Spring Cloud LoadBalancer;遇到历史项目时,再理解 Ribbon。

对比项RibbonSpring Cloud LoadBalancer
所属体系Netflix OSSSpring Cloud 官方
项目状态老项目维护常见新项目推荐
调用模型阻塞模型为主支持阻塞和响应式场景
常见集成RestTemplate、FeignRestTemplate、WebClient、Feign、Gateway
扩展方式IRule 等 Ribbon 规则ReactorServiceInstanceLoadBalancerServiceInstanceListSupplier

客户端负载均衡和服务端负载均衡

客户端负载均衡

Spring Cloud 微服务内部调用常见的是客户端负载均衡。

mermaid
flowchart TD
    A["订单服务"] --> B["获取 user-service 实例列表"]
    B --> C["订单服务本地选择实例"]
    C --> D["调用用户服务实例"]

特点:

  • 调用方自己选择实例。
  • 链路短,不必每次经过统一转发层。
  • 可以结合服务元数据做灰度、同机房优先、权重路由。
  • 调用方需要感知服务发现和负载均衡能力。

服务端负载均衡

Nginx、云负载均衡、Kubernetes Service 更接近服务端负载均衡。

mermaid
flowchart TD
    A["调用方"] --> B["Nginx / SLB / Kubernetes Service"]
    B --> C["服务端维护实例1、2、3的后端池"]
    C --> D["为本次请求选择一个实例"]
    D --> E["转发请求并返回响应"]

特点:

  • 调用方只访问统一入口。
  • 入口层负责转发。
  • 对调用方简单。
  • 入口层可能成为瓶颈或额外跳点。

生产架构里两种方式经常同时存在:

text
外部流量 -> Nginx / Ingress / Gateway
微服务内部调用 -> OpenFeign + Spring Cloud LoadBalancer

负载均衡完整流程

以 OpenFeign 调用为例:

mermaid
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 发送请求"]

这里的关键点是:服务名本身不能直接发网络请求,必须解析成真实实例。

负载均衡选择实例的输入从哪里来

负载均衡不是凭空随机选。它依赖一组“候选实例”,这些实例来自注册中心和调用方本地缓存,再经过过滤和排序后才进入选择算法。

mermaid
flowchart TD
    A["注册中心实例列表"] --> B["调用方本地缓存"]
    B --> C["ServiceInstanceListSupplier"]
    C --> D["按健康状态过滤"]
    D --> E["按命名空间/分组/集群过滤"]
    E --> F["按版本/灰度/机房过滤"]
    F --> G["负载均衡算法选择"]
    G --> H["得到最终实例"]

可以把一次选择拆成三步:

步骤做什么常见问题
获取实例从注册中心或本地缓存拿服务实例服务没注册、命名空间不一致
过滤实例按健康、版本、区域、元数据筛选过滤规则太严导致无实例
选择实例轮询、随机、权重、同机房优先选到慢实例、负载不均

这也是为什么“注册中心有实例”不代表“调用方一定能调用到”。调用方可能没刷新本地缓存,可能和服务提供者不在同一个 namespace/group,也可能被灰度规则过滤掉。

为什么会选到旧实例

服务下线不是所有调用方瞬间感知。注册中心剔除、客户端缓存刷新、连接池断开都存在时间窗口。

mermaid
flowchart TD
    A["实例准备下线"] --> B["停止接收新流量"]
    B --> C["注册中心标记下线"]
    C --> D["调用方刷新实例列表"]
    D --> E["连接池旧连接关闭"]

如果没有优雅下线,可能出现:

  1. 实例已经停止服务,但注册中心还没剔除。
  2. 注册中心已剔除,但调用方本地缓存还没刷新。
  3. 调用方实例列表已刷新,但 HTTP 连接池里还有旧连接。
  4. 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 日志增加目标实例信息:

text
traceId=abc123 service=user-service instance=192.168.1.11:8081 version=v1 cost=82ms

线上不要长期打印过多明细日志,否则日志量会很大。更好的方式是把实例维度打到 Micrometer 指标或链路追踪标签中。

常见负载均衡算法

轮询

轮询是最容易理解的策略。

text
第 1 次 -> 实例 1
第 2 次 -> 实例 2
第 3 次 -> 实例 3
第 4 次 -> 实例 1

适合场景:

  • 实例配置接近。
  • 请求耗时接近。
  • 没有灰度和权重要求。

缺点:

  • 不知道实例真实压力。
  • 实例配置不同时可能不公平。
  • 慢实例仍然会被分到请求。

随机

随机策略每次随机选择一个实例。

适合实例规模较大、实例能力接近的场景。请求量足够大时,随机分布会趋近均匀。

缺点是短时间内可能不均衡。

权重

权重策略会按实例权重分配请求。

text
实例 A weight=5
实例 B weight=3
实例 C weight=2

大致表示 A 承担 50% 流量,B 承担 30%,C 承担 20%。

适合场景:

  • 机器配置不同。
  • 灰度发布。
  • 新版本先承接少量流量。

权重通常来自注册中心元数据或服务治理平台。

同机房优先

如果系统跨机房部署,上海服务优先调用上海实例,北京服务优先调用北京实例。

mermaid
flowchart TD
    A["上海订单服务发起调用"] --> B["按zone过滤上海用户实例"]
    B --> C["有匹配则优先上海实例"]
    C --> D["没有匹配则按策略跨区或失败"]

内置Zone Preference通常在本zone为空时回退全部实例;严格地域合规场景必须自定义为空即失败的过滤策略,不能依赖默认偏好。

这样可以降低网络延迟和跨机房流量成本。

灰度/版本优先

实例可以带元数据:

yaml
metadata:
  version: v2
  gray: true

调用方或网关可以根据用户、请求头、租户、版本选择实例。

例如:

text
X-Version: v2 -> 只调用 version=v2 的实例
普通请求 -> 调用 version=v1 的实例

Spring Cloud LoadBalancer 内部到底怎么工作

很多同学只记住“LoadBalancer 选一个实例”,但面试和排查更常问的是:实例列表从哪里来、什么时候缓存、为什么注册中心有实例但调用方选不到、怎么扩展。

Spring Cloud LoadBalancer 可以拆成两层理解:

层次核心对象负责什么
实例供应层ServiceInstanceListSupplier按服务名拿候选实例,并可做缓存、健康过滤、区域过滤、元数据过滤
实例选择层ReactorServiceInstanceLoadBalancer从候选实例中按算法选择一个 ServiceInstance

一次选择过程可以这样理解:

mermaid
flowchart TD
    A["调用方访问user-service"] --> B["Factory取得该服务负载均衡上下文"]
    B --> C["Supplier从Discovery或缓存取候选"]
    C --> D["按健康、区域、版本和权重处理"]
    D --> E["空列表返回无实例<br/>非空列表进入选择算法"]
    E --> F["返回一个ServiceInstance"]
    F --> G["HTTP Client重写真实IP和端口"]

这里有两个非常关键的结论:

  1. 注册中心有实例,只代表“注册中心视角存在实例”,不代表“调用方当前候选列表里一定有实例”。
  2. 负载均衡不是直接修改你的业务代码,而是在 HTTP 请求发送前把 http://user-service/... 解析成 http://ip:port/...

ServiceInstance 里通常有什么

ServiceInstance 可以理解为一个服务实例描述对象,核心字段包括:

字段例子用途
serviceIduser-service服务名
host192.168.1.11真实主机地址
port8081真实端口
securefalse是否 HTTPS
metadataversion=v2, zone=shanghai灰度、同机房、权重、租户路由
instanceIduser-service-192.168.1.11-8081日志和排查定位

生产排查时,建议日志或 trace 标签里带上 serviceIdinstanceIdhostportversionzone。否则你只能看到“调用 user-service 慢”,却不知道到底慢在哪台机器。

为什么注册中心有实例,LoadBalancer 还是选不到

常见原因不是“框架坏了”,而是实例进入负载均衡前被过滤掉了。

原因现象排查点
服务名不一致No instances available for xxx@FeignClient、Gateway lb://、注册名是否一致
namespace/group 不一致注册中心控制台有实例,调用方看不到Nacos namespace、group、cluster 配置
实例健康状态异常注册中心显示不健康或权重为 0健康检查、心跳、metadata
灰度规则过滤为空普通用户能访问,灰度用户不能访问版本 header、metadata、路由规则
本地缓存未刷新新实例上线后短时间没流量客户端缓存刷新周期、注册中心推送
权重/区域规则配置错误所有请求都打到少数实例权重值、zone、clusterName

所以排查时不要只问“注册中心有没有实例”,还要问:

text
调用方当前拿到的候选实例列表是什么?
候选列表经过哪些过滤?
最终选择算法选中了谁?
HTTP Client 实际请求的 IP:Port 是什么?

Ribbon 和 LoadBalancer 的扩展点对比

老项目可能还在用 Ribbon,新项目更常用 Spring Cloud LoadBalancer。两者都属于“客户端负载均衡”,但扩展点不同。

能力RibbonSpring Cloud LoadBalancer
实例来源ServerList、Eureka 集成DiscoveryClientServiceInstanceListSupplier
选择规则IRuleReactorServiceInstanceLoadBalancer
健康判断IPing、Server 状态注册中心健康状态、Supplier 过滤
缓存刷新Ribbon 自身缓存和 Eureka 客户端DiscoveryClient / Supplier / Caching Supplier
灰度扩展自定义 IRule自定义 ServiceInstanceListSupplier 或 LoadBalancer
适合项目历史 Netflix 技术栈新 Spring Cloud 技术栈

面试回答时不要只说“Ribbon 被 LoadBalancer 替代了”。更准确的回答是:

text
Ribbon 和 Spring Cloud LoadBalancer 解决的问题相同,都是客户端从多个服务实例中选择一个实例。Ribbon 是 Netflix OSS 老组件,扩展点主要是 IRule、IPing、ServerList;Spring Cloud LoadBalancer 是 Spring Cloud 官方方案,核心扩展点是 ServiceInstanceListSupplier 和 ReactorServiceInstanceLoadBalancer。现在新项目优先用 LoadBalancer,老项目遇到 Ribbon 要能读懂 IRule 和缓存刷新逻辑。

自定义同机房和灰度路由 Demo

下面这个 Demo 展示的是生产项目常见思路:优先调用同机房实例,再按请求头选择版本。代码重点不是让你背 API,而是理解“过滤候选实例”这个位置最适合放灰度和区域规则。

java
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 里筛选。一定要考虑兜底策略:

策略结果适合场景
灰度实例为空直接失败能暴露配置错误,但用户请求失败内部测试环境
灰度实例为空回退稳定版本可用性更好,但可能掩盖灰度配置问题普通线上灰度
灰度实例异常自动摘流降低故障影响配合治理平台和指标

超时、重试、熔断与负载均衡的边界

负载均衡只回答“选谁”,不回答“等多久、失败怎么办、能不能再试一次”。

mermaid
flowchart TD
    A["Feign / Gateway 发起调用"] --> B["LoadBalancer 选择实例"]
    B --> C["HTTP Client申请连接并建连"]
    C --> D["按连接、读取或正常响应分类"]
    D --> E["记录目标实例、耗时和结果"]
    E --> F["成功进入解码<br/>失败按熔断、幂等和预算处理"]

连接失败、读取超时和成功响应是互斥结果,图中纵向合并是为了窄屏阅读。每类结果的准确语义、不解决的问题和写请求边界见下表。

几个边界要分清:

能力解决什么不解决什么
负载均衡本次请求发给哪个实例下游慢、重复写、线程耗尽
连接超时连不上目标实例时快速失败下游业务执行慢
读取超时下游长时间不返回时释放调用线程下游是否已经执行成功
重试偶发网络错误或临时实例故障非幂等写操作的重复提交
熔断下游持续异常时快速失败修复下游自身问题
限流防止流量超过系统容量保证每个请求都成功

特别注意:超时不等于下游一定失败。例如订单服务调用库存服务扣减库存,订单服务读超时了,但库存服务可能已经扣减成功。如果这时订单服务直接重试,又没有幂等号,就可能重复扣库存。

商业项目建议:

场景建议
查询用户、字典、配置可以短超时 + 少量重试 + 缓存兜底
扣库存、支付、发货必须有幂等键;超时后查询状态或走补偿,不要盲目重试
外部医院接口采集超时后记录失败批次,异步补偿,避免采集线程被拖死
资产同步到 ES主库事务先成功,ES 失败进入重试表或 MQ 补偿

RestTemplate 使用负载均衡

java
@Configuration
public class RestTemplateConfig {

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

调用:

java
@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 选择实例。

如果没有 @LoadBalancedRestTemplate 会把 user-service 当普通域名解析,通常会失败。

OpenFeign 使用负载均衡

java
@FeignClient("user-service")
public interface UserClient {

    @GetMapping("/users/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

Feign 调用链路:

mermaid
flowchart TD
    A["UserClient.getUser"] --> B["Feign 代理"]
    B --> C["服务名 user-service"]
    C --> D["LoadBalancer 获取实例并选择"]
    D --> E["发送 HTTP 请求"]

Feign 让代码看起来像本地接口,但底层仍然要走服务发现、负载均衡、HTTP 请求和响应解码。

Gateway 使用负载均衡

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: user-route
          uri: lb://user-service
          predicates:
            - Path=/users/**

lb://user-service 的含义是:

  • lb:走 LoadBalancer。
  • user-service:目标服务名。
  • Gateway 会从注册中心找实例,再选择一个实例转发。
mermaid
flowchart TD
    A["外部请求 /users/1001"] --> B["Gateway"]
    B --> C["匹配 user-route"]
    C --> D["识别 lb://user-service"]
    D --> E["LoadBalancer 选择实例"]
    E --> F["转发到用户服务"]

自定义负载均衡思路

真实项目可能需要:

  • 同机房优先。
  • 根据版本灰度。
  • 根据权重分流。
  • 排除不健康实例。
  • 优先调用低延迟实例。

自定义策略时不要只看“怎么写代码”,要先明确业务规则。

伪代码:

java
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);
}

核心思路:

  1. 从注册中心拿到候选实例。
  2. 根据元数据过滤实例。
  3. 根据权重或算法选择实例。
  4. 选不到时要有降级策略。

为什么负载均衡不能解决所有问题

负载均衡只解决“请求分给谁”,不能解决所有稳定性问题。

问题负载均衡能否解决需要什么
单个实例压力大部分能扩容、权重、限流
下游所有实例都慢不能超时、熔断、降级、排查下游
请求量远超系统容量不能限流、削峰、扩容
写操作重复提交不能幂等、唯一键、状态机
调用链路太长不能架构优化、批量接口、缓存
注册中心数据延迟不能完全解决健康检查、超时、重试、熔断

常见线上问题

某个实例已经挂了,为什么还会被调用

可能原因:

  • 注册中心还没剔除实例。
  • 调用方本地缓存还没刷新。
  • 健康检查不准确。
  • 实例进程活着,但业务线程池已经满了。
  • 网络到某个实例不通。

所以服务调用还必须配置超时和熔断,不能只依赖注册中心。

为什么流量没有平均分布

可能原因:

  • 请求量太小,随机或轮询短时间不均衡。
  • 实例权重不同。
  • 部分请求固定 key,被路由到固定实例。
  • 长连接或连接池复用导致看起来不均衡。
  • 某些实例被过滤。
  • 网关层和服务内部都有负载均衡,观察口径不一致。

增加实例后为什么没流量

排查:

  • 新实例是否注册成功。
  • 注册中心是否显示健康。
  • 调用方是否拉到新实例。
  • 命名空间、分组、集群是否一致。
  • 网关或调用方是否有版本/灰度过滤规则。

商业项目里的负载均衡参数怎么配

负载均衡不是只选算法,还要和超时、连接池、重试、熔断、实例下线一起设计。很多线上事故不是“算法错了”,而是这些参数互相打架。

以订单服务调用资产服务为例:

text
order-service -> asset-service

调用方要同时关心:

配置解决什么配错后果
连接超时TCP 连接建不起来时快速失败太长会占住调用线程,太短会误伤跨机房请求
读取超时下游迟迟不返回时释放线程太长会拖垮上游,太短会造成大量不确定超时
连接池最大连接数控制到下游的并发连接太小会排队,太大可能压垮下游
每路由连接数控制到单个实例的连接数单实例被打爆或连接不够用
重试次数偶发网络失败恢复写操作重复提交,流量被放大
熔断阈值下游持续异常时快速失败太敏感误熔断,太迟钝拖垮上游
实例缓存刷新感知扩容、下线、健康变化刷新慢会继续调用旧实例

Feign 超时配置 Demo

不同 Spring Cloud 版本和 HTTP Client 选择会有差异,下面给出通用思路:连接超时要短,读取超时要结合业务耗时;写接口更要谨慎重试。

yaml
spring:
  cloud:
    openfeign:
      client:
        config:
          asset-service:
            connectTimeout: 1000
            readTimeout: 3000
            loggerLevel: basic

含义:

  1. connectTimeout=1000:1 秒内连不上资产服务实例就快速失败。
  2. readTimeout=3000:请求发出去后 3 秒内没有响应就读超时。
  3. loggerLevel=basic:记录基础请求日志,生产不要长期开 full,避免日志过大和敏感信息泄露。

为什么不能所有接口都配 30 秒?因为上游 Tomcat 或业务线程会一直等下游,流量一上来线程池很快被占满,后续正常请求也进不来。

连接池为什么会影响负载均衡

负载均衡选择的是实例,但真正发请求的是 HTTP Client。HTTP Client 通常会复用连接池。

mermaid
flowchart TD
    A["Feign 调用"] --> B["LoadBalancer 选择实例"]
    B --> C["HTTP Client 获取连接"]
    C --> D["有空闲连接则复用<br/>没有则等待或新建"]
    D --> E["应用连接池等待和连接超时"]
    E --> F["取得连接后发送请求"]
    F --> G["超时则失败<br/>成功则读取并归还连接"]

如果连接池太小,即使负载均衡选到了健康实例,请求也可能卡在“等连接”。这时你看到的现象是 Feign 慢、下游 QPS 不高、调用方线程堆积。根因不是注册中心,也不是负载均衡算法,而是 HTTP Client 连接池容量不足或连接没有及时释放。

生产排查要同时看:

  1. 每个下游服务的连接池最大连接数。
  2. 每个实例的连接数分布。
  3. 等连接耗时。
  4. 下游响应耗时。
  5. 调用方线程池和 Tomcat 线程状态。

重试风暴:为什么扩容后仍然会慢

面试和线上都很常见一个问题:下游慢了,上游加了重试,短时间看似成功率提高,随后整个链路更慢。

假设入口 1000 QPS,订单服务调用资产服务,失败后最多重试 2 次。

text
原始请求:1000 QPS
第一次重试:最多再 1000 QPS
第二次重试:最多再 1000 QPS
资产服务理论最大承压:3000 QPS

流程图:

mermaid
flowchart TD
    A["下游 asset-service 变慢"] --> B["调用方 read timeout"]
    B --> C["触发重试"]
    C --> D["下游收到更多请求"]
    D --> E["线程池和连接池更拥塞"]
    E --> F["更多请求超时"]
    F --> G["重试预算失控后形成雪崩"]

这就是重试风暴。重试本来是为了解决偶发失败,但下游整体变慢时,重试会放大流量。

商业建议:

场景重试建议
查询字典、查询配置可以短超时 + 少量重试 + 本地缓存兜底
查询资产详情可重试,但要限制次数和总耗时
创建订单、扣库存、支付不要盲目重试,必须有幂等键、状态查询或补偿
医院接口采集超时后记录失败批次,异步补偿,避免阻塞采集线程

重试必须配合:

  1. 总超时时间。
  2. 退避策略。
  3. 熔断。
  4. 限流。
  5. 幂等设计。
  6. 失败补偿。

权重、灰度和同机房优先怎么落地

负载均衡算法不应该只停留在“轮询或随机”。商业项目常常需要更细的路由规则。

权重路由

实例元数据:

yaml
metadata:
  weight: 20

常见场景:

  1. 新机器配置更高,给更高权重。
  2. 新版本灰度,只给 5% 流量。
  3. 某实例观察到 CPU 高,临时降权。

注意:权重不是权限控制。权重只影响概率,不能保证某类请求一定不打到某实例。如果必须严格隔离,要用版本、租户、机房等明确过滤条件。

灰度路由

灰度发布常见方式:

text
请求头 X-Version: v2 -> 调用 metadata.version=v2 的实例
普通请求 -> 调用 metadata.version=v1 的实例

流程:

mermaid
flowchart TD
    A["请求进入"] --> B["读取 Header 或用户灰度规则"]
    B --> C["确定目标是v2灰度或v1稳定版"]
    C --> D["按metadata.version过滤候选"]
    D --> E["空列表执行失败或回退<br/>非空列表进入负载均衡选择"]

候选为空与非空是互斥结果。图中纵向合并用于窄屏阅读,真正实现时必须先判断列表,再选择“失败、回退或继续选址”。

灰度一定要明确兜底:

灰度实例为空怎么办优点风险
直接失败能暴露配置错误用户失败
回退稳定版本可用性好可能掩盖灰度配置错误
按业务开关决定灵活规则复杂,需要监控

同机房优先

跨机房系统里,同机房优先可以降低延迟和专线成本。

text
上海 order-service -> 优先上海 asset-service
北京 order-service -> 优先北京 asset-service

但同机房优先也要有降级策略:如果本机房资产服务全挂,是否允许跨机房调用?这要按业务要求决定。支付、库存、核心写链路可能宁愿失败也不跨不一致区域;查询类接口通常可以跨机房兜底。

优雅下线全过程

服务发布时最容易出现“Pod 已经准备停止,但调用方还在打流量”。这不是一个组件能单独解决的问题,而是注册中心、K8s、HTTP 连接池、业务线程共同参与。

推荐顺序:

mermaid
flowchart TD
    A["发布系统准备停止实例"] --> B["Readiness 置为不可用"]
    B --> C["从注册中心摘除或标记下线"]
    C --> D["等待调用方刷新本地实例缓存"]
    D --> E["停止接收新请求"]
    E --> F["等待存量请求处理完"]
    F --> G["关闭消费者、定时任务、连接池"]
    G --> H["进程退出"]

如果直接 kill 进程,会出现:

  1. 注册中心还没来得及剔除,调用方继续选择它。
  2. Gateway 或 Feign 本地缓存里还有旧实例。
  3. HTTP 连接池里还有旧连接。
  4. 正在处理的请求被中断。
  5. MQ 消费、定时任务、批处理任务没收尾。

Kubernetes 场景要关注:

配置作用
readinessProbe控制是否接收流量
preStop退出前执行摘流、等待或清理脚本
terminationGracePeriodSeconds给应用优雅关闭时间
Spring Boot graceful shutdown等待 Web 请求处理完成再关闭

服务调用故障排查决策树

mermaid
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 LoadBalancerRibbon 是老方案
服务内部调用用服务名,不写死 IP方便扩容、下线、迁移
所有远程调用都设置超时防止线程无限等待
核心链路配熔断和限流防止下游拖垮上游
写接口不要盲目重试防止重复下单、重复扣款
灰度发布依赖元数据版本、权重、区域都应可治理
监控实例调用量和失败率发现负载不均和故障实例
发布要做优雅下线避免旧实例在停止窗口继续接流量
重试必须配合幂等和熔断避免下游慢时形成重试风暴
日志和 trace 打出目标实例才能定位到底哪台实例慢

独立面试入口

标准回答、追问和精确源码跳转已统一迁移到LoadBalancer独立面试题。知识页保留完整原理、Demo、商业场景和排查步骤,避免把背诵答案与课程正文混在一起。

和服务调用链路的关系

负载均衡是 Spring Cloud 服务调用链路 中的一环。

完整链路是:

text
Feign / Gateway 发起请求
    -> 根据服务名找实例列表
    -> LoadBalancer 选择实例
    -> HTTP Client 发请求
    -> 下游处理
    -> 熔断、限流、降级兜底

如果只理解负载均衡,不理解注册中心、Feign、Gateway、熔断和链路追踪,仍然无法完整排查微服务调用问题。

小结

Spring Cloud 负载均衡的核心是:调用方根据服务名拿到实例列表,再在本地选择一个实例发起请求。老项目常见 Ribbon,新项目推荐 Spring Cloud LoadBalancer。负载均衡解决的是“选哪个实例”,但不能替代超时、重试、熔断、限流、降级和幂等设计。