简单聊聊 SpringCloud 微服务调用链路源码

这篇基于 Spring Cloud 2021.0.x(对应 Spring Boot 2.7)+ OpenFeign 3.1.x + Spring Cloud LoadBalancer 3.1.x + Gateway 3.1.x + Sentinel 1.8.x 分析。
Spring Cloud 是一套基于 Spring Boot 的微服务工具集——它自己很少"造轮子",更多是定义接口与抽象(服务发现、负载均衡、熔断),再由各组件(Consul/OpenFeign/Gateway/Sentinel)实现,最后用 Spring Boot 的自动装配 + Import 机制织进应用。
所以聊 Spring Cloud 源码,主线只有一条:抽象怎么定义,组件怎么接进来,一次请求从网关入口到下游调用、再到流量防护,整条链路怎么串起来。这篇就从这条主线切入。


一、一句话总览

Spring Cloud 干的事概括成一句话:在 Spring Boot 之上定义微服务的标准抽象(DiscoveryClient、LoadBalancerClient、Feign),用 ImportSelector + 自动装配把各组件实现织入容器——入口由 Gateway 承接,调用由 Feign 发起,选实例交给 LoadBalancer,防护交给 Sentinel。

全貌先看一张图:

请求入口:       Spring Cloud Gateway(路由匹配 + 过滤器链)
                  │
应用层:        @FeignClient 接口        @LoadBalanced RestTemplate
                  │                        │
Spring Cloud:  OpenFeign(动态代理)     LoadBalancerInterceptor(拦截器)
                  │                        │
               FeignBlockingLoadBalancerClient ← 服务名 → 实际地址替换
                  │                        │
                LoadBalancerClient(抽象)   │
                  │                        │
实现层:       Spring Cloud LoadBalancer / Ribbon(已退役)
                  │
               DiscoveryClient(抽象) ← Consul / Eureka / Nacos 实现
                  │
注册中心:       Consul Server(Agent + Server 架构,服务注册表)

防护层:       Sentinel(限流 / 熔断 / 降级,横切所有调用)

面试点:Spring Cloud 的所有组件都是这套模式——上层定义接口 + SPI 收集实现 + 自动装配按条件启用。理解了 Feign 一个组件的接入方式,其他组件(Gateway、Sentinel、Config)是同一套模子。

二、底座:Spring Cloud Context 与 Bootstrap 上下文

为什么需要 Bootstrap 上下文

Spring Boot 只有一个容器,但 Spring Cloud 传统上有两个:bootstrap(父容器)和 application(主容器)。为什么?因为配置中心的数据必须在主容器启动之前拿到——主容器里 @Value("${db.url}")、数据源配置都依赖配置中心的参数,而加载配置中心的连接信息(配置中心地址、用户名)本身也写在配置文件里,形成一个"鸡生蛋"问题。

解法就是两层容器:bootstrap 容器先用 bootstrap.yml 里的极少量参数连上配置中心、拉下远程配置、合并进 Environment,然后主容器再基于合并后的 Environment 启动。

BootstrapApplicationListener 是怎么起作用的

public class BootstrapApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment environment = event.getEnvironment();
        // 开关:spring.cloud.bootstrap.enabled
        if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, true)) {
            return;
        }
        ...
        // 创建 bootstrap 上下文,并把它设为主容器的 parent
        ConfigurableApplicationContext context = bootstrapServiceContext(environment, event.getSpringApplication());
        event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
        apply(context, event.getSpringApplication(), environment);
    }
}

关键点:

  1. 它监听的是 ApplicationEnvironmentPreparedEvent——还记得上一篇 SpringBoot 执行流程吗?这个事件发生在 prepareEnvironment 阶段,早于主容器创建,所以 bootstrap 容器能"抢先"把远程配置拉下来塞进 Environment;
  2. bootstrapServiceContext 内部用 SpringApplicationBuilder 建了一个独立的上下文,加载 bootstrap.yml + BootstrapImportSelectorConfiguration,最后 addParentContext 把 bootstrap 容器设为主容器的 parent——主容器通过 parent 链能"看到"bootstrap 容器里的 bean(比如配置中心的 PropertySourceLocator)。

面试点:Spring Cloud 2020.0(2020 年 12 月)之后,bootstrap 上下文默认关闭了(spring.cloud.bootstrap.enabled 默认 false)。要沿用老方式必须显式引入 spring-cloud-starter-bootstrap。原因:双容器模型太复杂、bean 查找容易混淆,新方案改用 spring.config.import=nacos:xxx 机制,在 Spring Boot 标准的配置加载流程里直接拉远程配置,不再需要独立容器。这是高频追问:“你用的是 Spring Cloud 哪个版本?为什么引了 nacos-config 还要再引 starter-bootstrap?”

三、抽象层:DiscoveryClient 与 LoadBalancerClient

Spring Cloud 的"框架价值"就在这两个接口上:

// 服务发现抽象:任何注册中心都要实现它
public interface DiscoveryClient extends Ordered {
    String description();
    List<ServiceInstance> getInstances(String serviceId);  // 按服务名拉实例列表
    List<String> getServices();
}

// 负载均衡抽象:选一个实例 + 把服务名 URL 重写成实例 URL
public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> ServiceInstance choose(String serviceId);                        // 选实例
    <T> ServiceInstance choose(String serviceId, Request<T> request);
    <T> T execute(String serviceId, LoadBalancerRequest<T> request);     // 选完执行
    URI reconstructURI(ServiceInstance instance, URI original);          // 重写 URL
}

上层组件(Feign、RestTemplate、Gateway)只依赖这两个接口,不关心底下是 Consul 还是 Eureka。这就是 Spring Cloud 的核心设计:面向接口编程,实现可插拔

@EnableDiscoveryClient 的原理也很简单:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)   // 又是 ImportSelector
public @interface EnableDiscoveryClient { }

EnableDiscoveryClientImportSelector 按条件注册 AutoServiceRegistrationConfiguration 等配置。不过 Spring Boot 2.x 之后,只要 classpath 上有 DiscoveryClient 实现(Consul/Eureka 的 starter),自动装配就会生效,@EnableDiscoveryClient 其实可写可不写——面试可以答:“这个注解在旧版本是开关,现在主要起语义标记作用,真正的装配靠自动配置类。”

四、核心一:OpenFeign 源码执行流程(面试重灾区)

1. 入口:@EnableFeignClients 扫描注册

@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients { }

FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar,在容器解析 @Import 时回调,干两件事:

  • registerDefaultConfiguration:注册每个 FeignClient 的独立配置(FeignClientSpecification,放进 FeignContext);
  • registerFeignClientsregisterFeignClient:扫描指定包下所有标注 @FeignClient 的接口,把每个接口注册成一个 FeignClientFactoryBean 的 BeanDefinition

注意这里的技巧:注册的 bean 类型是 FeignClientFactoryBean(一个 FactoryBean),但 BeanDefinition 的 beanClass 却指向接口本身。所以注入时:

@Autowired
private UserService userService;   // 类型是接口,容器交给你的却是 FeignClientFactoryBean.getObject() 的产物

这就是"接口没有实现类却能注入"的秘密:注入的是 FactoryBean 生产的代理对象,不是接口的实现类。

2. 构建:FeignClientFactoryBean.getObject()

懒加载时机(第一次注入/使用该 bean)触发 getObject()getTarget():

<T> T getTarget() {
    FeignContext context = beanFactory.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);   // 组装 Encoder/Decoder/Contract/Client 等组件
    ...
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context,
            new HardCodedTarget<>(type, name, url));
}

Feign.Builder 是 OpenFeign 的核心装配器,默认组件和可扩展点一目了然:

public static class Builder {
    private final List<RequestInterceptor> requestInterceptors = new ArrayList<>(); // 请求拦截器:加 header 等
    private Logger.Level logLevel = Logger.Level.NONE;      // 日志
    private Contract contract = new Contract.Default();     // 契约:解析接口注解
    private Client client = new Client.Default(null, null); // HTTP 客户端
    private Retryer retryer = new Retryer.Default();        // 重试器
    private Encoder encoder = new Encoder.Default();        // 请求编码
    private Decoder decoder = new Decoder.Default();        // 响应解码
    ...
}

Spring Cloud OpenFeign 替换了几个关键组件:

  • ContractSpringMvcContract:让 Feign 认识 @RequestMapping/@GetMapping/@PathVariable 这些 Spring MVC 注解(原生 Feign 只认自己的 @RequestLine);
  • Encoder/Decoder → Spring 的 HttpMessageConverter 体系(默认 Jackson);
  • ClientFeignBlockingLoadBalancerClient(下一节讲,这是和负载均衡结合的点)。

3. 生成代理:ReflectiveFeign.newInstance

targeter.target 最终走到 OpenFeign 核心 ReflectiveFeign:

public <T> T newInstance(Target<T> target) {
    // ① 解析接口:把每个方法的注解转成 RequestTemplate 模板(路径、方法、参数位置)
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();
    ...
    // ② 创建 InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    // ③ JDK 动态代理
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
            new Class<?>[] { target.type() }, handler);
    return proxy;
}

三步里值得展开的是 ①:SpringMvcContract.parseAndValidateMetadata 在启动时把每个方法解析成 MethodMetadata,把注解信息"编译"成元数据缓存起来——/user/{id} 路径、HTTP 方法、参数名/参数位置、header 全部固化。之后每次调用不再解析注解,直接拿模板填参,这就是 Feign 性能好的原因之一(注解解析只做一次)。

面试点:为什么 Feign 用 JDK 动态代理?因为被代理的是接口,JDK 代理天然只支持接口、无需第三方库、性能开销小。反向对比 MyBatis Mapper 也是同一招(JDK 代理 + FactoryBean),可以一起说。

4. 调用链路:一次调用怎么走完

userService.getById(1L)
  → FeignInvocationHandler.invoke()          // JDK 代理拦截
    → methodToHandler.get(method)            // 拿到该方法对应的 Handler
      → SynchronousMethodHandler.invoke(argv)
        → buildTemplateFromArgs.create(argv) // 填参:把实参填进缓存的 RequestTemplate
        → executeAndDecode(template, options)
          → targetRequest(template)          // 生成 Request(URL/header/body 就绪)
          → client.execute(request, options) // ★ Client = FeignBlockingLoadBalancerClient
              → 负载均衡:把 http://user-service/user/1 的 user-service 替换成 192.168.1.10:8080
              → Client.Default.execute       // 真正的 HTTP 调用(java.net.HttpURLConnection / OkHttp)
          → decode(response)                 // 反序列化返回值

executeAndDecode 的核心片段:

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);
    Response response;
    try {
        response = client.execute(request, options);   // ★ 发起 HTTP
    } ...
    if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
            return null;
        } else {
            return decode(response);                   // 状态码 2xx 才解码返回
        }
    } else {
        throw errorDecoder.decode(metadata.configKey(), response);  // 否则按 ErrorDecoder 抛异常
    }
}

还有两点面试常考:

  • 重试:SynchronousMethodHandler.invoke 里包了一层 Retryer,默认 Retryer.NEVER_RETRY(Feign 默认不重试!很多人记成默认重试,那是 Ribbon 时代的习惯);
  • 超时:Feign 的 Options(connectTimeout/readTimeout,默认 10s/60s)由 feign.client.config.<服务名>.connectTimeout 配置,Ribbon 时代的 ribbon.ReadTimeout 在 LoadBalancer 时代已不生效——版本混搭是线上踩坑高发区。

五、核心二:Spring Cloud LoadBalancer —— 服务名怎么变成 IP:端口

为什么 Ribbon 退役了

  • Netflix 2018 年停止维护 Ribbon(它属于 Netflix OSS 套件);
  • Ribbon 是阻塞式 API,和响应式(WebFlux/Gateway)不搭;
  • Spring Cloud 2020.0 起移除 Ribbon,官方替代品是 Spring Cloud LoadBalancer(基于 Reactor,响应式阻塞通吃)。

替换服务名的关键代码

以 Feign 集成为例,自动配置 FeignLoadBalancerAutoConfiguration 把默认 Client 换掉:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignLoadBalancerAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(LoadBalancerClient loadBalancerClient, ...) {
        return new FeignBlockingLoadBalancerClient(new Client.Default(null, null),
                loadBalancerClient, loadBalancerClientFactory);
    }
}

FeignBlockingLoadBalancerClient.execute 干的事非常直白:

public Response execute(Request request, Request.Options options) throws IOException {
    URI originalUri = URI.create(request.url());          // http://user-service/user/1
    String serviceId = originalUri.getHost();             // serviceId = user-service
    ...
    // ① 选一个实例
    ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
    ...
    // ② 把 URL 里的服务名替换成实例的 host:port
    String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
    Request newRequest = buildRequest(request, reconstructedUrl);   // http://192.168.1.10:8080/user/1
    return delegate.execute(newRequest, options);         // ③ 再交给真正的 HTTP 客户端
}

三步一句话:服务名 → choose 选实例 → 重建 URL → 发真实请求。RestTemplate 的 @LoadBalanced 走的是同一个 LoadBalancerClient(通过 LoadBalancerInterceptor 拦截),殊途同归。

负载均衡算法在哪

BlockingLoadBalancerClient.chooseRoundRobinLoadBalancer.choose:

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
    if (instances.isEmpty()) {
        return new EmptyResponse();                       // 没有可用实例
    }
    int pos = Math.abs(this.position.incrementAndGet());  // 原子计数器 +1
    ServiceInstance instance = instances.get(pos % instances.size());  // 取模 → 轮询
    return new DefaultResponse(instance);
}

实例列表从哪来?DiscoveryClientServiceInstanceListSupplier → 调 DiscoveryClient.getInstances(serviceId) → 从注册中心(Consul/Eureka)拉实例列表并本地缓存。所以整条数据链是:

Consul Server(注册表)
  → DiscoveryClient.getInstances()      拉取 + 缓存(只取健康实例)
    → ServiceInstanceListSupplier       提供列表(可加过滤/加权扩展)
      → RoundRobinLoadBalancer.choose   轮询/随机选一个
        → Feign 拦截处重建 URL

背后的通用设计:NamedContextFactory

FeignContextLoadBalancerClientFactory 都继承同一个抽象类 NamedContextFactory:

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> {
    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

    public AnnotationConfigApplicationContext getContext(String name) {
        return this.contexts.computeIfAbsent(name, this::createContext);
    }
}

每个服务名(name)对应一个独立的子 ApplicationContext,这就是"给 user-service 配 3s 超时、给 order-service 配 10s 超时、给不同服务配不同负载均衡策略"的底层机制:配置按服务名隔离,互不干扰。这是 Spring Cloud 里被低估的一个设计,面试说出来很加分。

六、注册发现全链路(以 Consul 为例)

Consul 和 Nacos 最大的架构差异要先记住:Consul 是 Agent + Server 两层结构——每台应用机器上跑一个本地 Consul Agent,应用只和本机 Agent 通信(HTTP),由 Agent 转发到 Server 集群(多副本、Raft 共识);Nacos 是客户端直连 Server。所以 Consul 的"注册"分两跳:

服务提供者(注册 + 健康检查):

启动 → ConsulServiceRegistry.register(registration)
  → ConsulClient.agentServiceRegister(...)       // ① HTTP 调本机 Agent:PUT /v1/agent/service/register
    → Agent 转发写入 Server 集群(Raft 复制)      // ② 注册表落库
  → TtlScheduler 定时"报平安"                     // ③ 续命
    → ConsulClient.agentCheckPass(serviceId)     // PUT /v1/agent/check/pass/service:{id}

注意 Consul 不是 Nacos 那种"客户端 5s 发一次心跳"的模型,而是健康检查模型:

  • 默认注册 TTL 检查:spring.cloud.consul.discovery.heartbeat.ttl-value(默认 30s)声明一个有效期,Spring Cloud 内部的 TtlScheduler 按 TTL 的 2/3(约 20s)调一次 agentCheckPass 续命;
  • 引入了 actuator 时可改用 HTTP 检查:spring.cloud.consul.discovery.health-check-path=/actuator/health,由 Agent 主动 GET 探测;
  • 检查连续失败超过阈值 → 实例被标记 critical从健康列表消失,消费者不再拿它。

服务消费者(发现 + 负载均衡):

Feign 调用 → LoadBalancer.choose(serviceId)
  → ConsulDiscoveryClient.getInstances(serviceId)
    → ConsulClient.getHealthServices(serviceId, passing=true)
      // GET /v1/health/service/{name}?passing=true,只返回通过检查的实例
  → 轮询/随机选中实例 → 调用

关键差异:getHealthServices 查询时带 ?passing=true,注册中心层面直接过滤掉不健康实例——这就是"服务挂了,调用方不再调到它"的机制来源:不是调用方感知失败,而是查询时就只拿健康列表

七、配置中心与 @RefreshScope

动态刷新配置的原理链路:

POST /actuator/refresh
  → RefreshEndpoint.refresh()
    → ContextRefresher.refresh()
      → refreshEnvironment()   // ① 重新拉取远程配置,更新 Environment
      → scope.refreshAll()     // ② 清空 RefreshScope 里的缓存
        → 下次 getBean 时重新创建 bean → 新配置注入新实例

@RefreshScope 本质:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")              // 就是给 bean 挂了一个自定义 scope
public @interface RefreshScope { }

RefreshScope extends GenericScope,标注的 bean 实例化后被缓存在 scope 里;refreshAll() 只是清缓存(销毁旧实例),并不立刻重建——下次真正使用时才重新 createBean,走一遍依赖注入,新值生效。所以"动态刷新"的本质是:懒重建 + 重新注入,不是修改现有对象。

面试点:@RefreshScope 能刷新基本类型和普通 bean,但改不了连接池/线程池等已初始化的底层资源(DataSource 重建非常危险),这些场景要上配置中心推 + 自定义事件监听重建。

八、核心三:Gateway —— 一次请求怎么穿过网关

Gateway 是 WebFlux 栈的 API 网关,本质就是一句话:根据 Predicate 匹配路由,把请求交给一串按 Order 排序的 Filter 依次处理,最后由 Netty 转发到下游服务并把响应写回。

三个核心概念

// 路由 = 断言 + 过滤器 + 目标地址
public class Route {
    private final String id;
    private final URI uri;                                     // 目标:lb://user-service 或 http://...
    private final AsyncPredicate<ServerWebExchange> predicate; // 断言:Path=/user/** 等
    private final List<GatewayFilter> gatewayFilters;          // 该路由专属过滤器
}
  • Route:路由,由 yaml 里的 spring.cloud.gateway.routes 定义(RouteDefinition);
  • Predicate:匹配条件(PathMethodHostHeader 等),决定请求走哪条路由;
  • Filter:处理逻辑,分两种——GatewayFilter(单路由专属)和 GlobalFilter(所有路由生效,核心转发逻辑都是它)。

执行流程

请求进入 Netty
  → DispatcherHandler                            // WebFlux 中央调度器(对位 Spring MVC 的 DispatcherServlet)
    → 遍历 HandlerMapping,询问谁能处理
      → RoutePredicateHandlerMapping             // ★ Gateway 的路由定位器
        → lookupRoute(exchange)                  // 遍历所有 Route,逐个用 Predicate 匹配
          → 命中:返回 FilteringWebHandler(持有 Route + 全部 GlobalFilter)
    → FilteringWebHandler.handle()
      → GlobalFilter + 路由专属 GatewayFilter 合并,按 @Order 排序
        → DefaultGatewayFilterChain 依次执行:
          → RouteToRequestUrlFilter        // order=10000:把路由 URI 拼成真实请求 URL
          → LoadBalancerClientFilter       // order=10150:lb://user-service → 选实例换成 IP:端口
          → NettyRoutingFilter             // order=Integer.MAX_VALUE:Netty 客户端发起下游请求
          → NettyWriteResponseFilter       // order=-1:把下游响应写回客户端

两个关键源码:

// RoutePredicateHandlerMapping:路由怎么被选中
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
    return this.routeLocator.getRoutes()
            .filter(route -> route.getPredicate().test(exchange))   // 断言匹配,逐个过滤
            .next()
            .map(route -> { ... return route; });
}

// FilteringWebHandler:过滤器链怎么组装
public Mono<Void> handle(ServerWebExchange exchange) {
    Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
    List<GatewayFilter> gatewayFilters = route.getGatewayFilters();
    List<GatewayFilter> combined = new ArrayList<>(this.globalFilters); // GlobalFilter 适配后加入
    combined.addAll(gatewayFilters);
    AnnotationAwareOrderComparator.sort(combined);   // ★ 按 Order 排序,数字越小越先执行
    return new DefaultGatewayFilterChain(combined).filter(exchange);
}

路由定义从哪来:RouteDefinitionRouteLocator 读 yaml 里的 RouteDefinition(id/uri/predicates/filters),经 PredicateDefinition/FilterDefinition 转成真正的 AsyncPredicateGatewayFilter(XxxRoutePredicateFactory / XxxGatewayFilterFactory 命名约定的工厂),再由 CachingRouteLocator 缓存起来。

面试点:为什么 Gateway 用 WebFlux(NIO) 而不用 Spring MVC?网关是典型的 IO 密集转发场景:大量下游慢请求同时挂着,如果沿用 Servlet 同步模型"一个连接占一个线程",线程池很快耗尽;Netty 事件循环用少量线程撑住海量连接,吞吐是网关的第一诉求。所以 Gateway 只支持 WebFlux 栈,也顺带解释了为什么它不能和 spring-boot-starter-web 混用(引入 spring-mvc 会直接冲突)。

扩展方式

  • 自定义 GlobalFilter:实现接口 + 指定 getOrder()(注意插在 RouteToRequestUrlFilter 前后语义不同——之前看到的是服务名,之后看到的是真实地址);
  • 自定义 GatewayFilterFactory:类名 XxxGatewayFilterFactory,yaml 里直接 filters: - Xxx=参数;
  • 网关级限流:内置 RequestRateLimiterGatewayFilterFactory,基于 Redis + Lua 脚本实现令牌桶。

九、核心四:Sentinel —— 流量怎么被防护

先把三个词分清

机制 解决的问题 触发条件 效果
限流 流量不超过系统容量(防压垮) QPS 超过阈值 多余请求被拒绝/排队
熔断 下游故障不扩散(防雪崩) 失败率/慢调用超过阈值 一段时间内快速失败,不调下游
降级 给失败一个兜底 限流/熔断/异常时 返回默认值/缓存/友好提示

Spring Cloud 对熔断做了统一抽象:spring-cloud-circuitbreaker 定义 CircuitBreaker/CircuitBreakerFactory 接口,实现可插拔:Resilience4j、Sentinel(Hystrix 已退役)。

Sentinel 的核心机制

1. 埋点:SphU.entry

try (Entry entry = SphU.entry("getUserById")) {   // 给资源埋点
    // 业务逻辑
} catch (BlockException e) {
    // 被限流/熔断命中:走降级逻辑
}

Spring 集成后不用手写:Web 请求由 CommonFilter(SentinelWebAutoConfiguration)统一埋点,方法级用 @SentinelResource 注解,由 SentinelResourceAspect(切面)包住方法调用——本质还是 SphU.entry

2. 规则检查:slot 责任链

SphU.entry 内部为每个资源维护一条 ProcessorSlotChain(责任链模式),8 个 slot 按固定顺序执行,任何一个不通过就抛 BlockException:

NodeSelectorSlot → ClusterBuilderSlot → LogSlot → StatisticSlot(统计)
  → AuthoritySlot(黑白名单) → SystemSlot(系统保护)
    → FlowSlot(限流规则) → DegradeSlot(熔断规则)

3. 统计:滑动时间窗口

StatisticSlot 基于 LeapArray 滑动窗口(秒级窗口 + 分钟级窗口)统计每个资源的 QPS、RT、异常数——限流和熔断的规则判断都以它为准。对比固定窗口(整点清零)的"临界突变"问题,滑动窗口的流量统计更平滑、更准确。

4. 限流算法(FlowRule 三种控制行为)

  • 快速失败(默认):滑动窗口计数,超阈值直接拒绝;
  • Warm Up:令牌桶 + 预热(类似 Guava RateLimiter),冷启动时缓慢放量;
  • 匀速排队:漏桶模型,超出的请求排队等待(maxQueueingTimeMs 内匀速通过)。

5. 熔断(DegradeRule 三种策略)

慢调用比例 / 异常比例 / 异常数,任一超过阈值 → 熔断器打开 → 在 timeWindow 时间内所有请求直接快速失败(不调下游)→ 时间窗结束进入探测期(半开):放一个请求试探,成功则关闭熔断恢复调用,失败则继续熔断。

6. 规则怎么生效

默认模式下规则存在客户端内存(InMemRuleManager):控制台推送规则 → 客户端内存立即生效;客户端心跳上报统计数据 → 控制台展示。这就是"规则下发不重启应用"的原理。

对比 Resilience4j(面试常问)

  • CircuitBreaker:标准三态状态机 CLOSED → OPEN → HALF_OPEN,基于滑动窗口(计数型/时间型)统计,失败率默认阈值 50%,打开后经过 waitDurationInOpenState 进入半开放探测;
  • RateLimiter:令牌桶;
  • Bulkhead:舱壁隔离(信号量/线程池),把资源隔离成独立的"舱",一个舱超载不影响其他舱;
  • 相比 Sentinel:Resilience4j 是纯轻量库(无控制台,规则写在代码/配置里),Sentinel 带控制台、规则动态下发、更符合国内运维习惯。

面试点:① 限流与熔断的本质区别——限流防"外部流量"压垮自己,熔断防"下游故障"拖垮自己;② Sentinel 默认是信号量隔离(无线程切换开销),而 Hystrix 是线程池隔离(强隔离但线程开销大);③ slot 链支持 SPI 扩展,是"责任链模式"在流量防护里的典型落地。

十、高频追问速答

Q1:@FeignClient 的接口没有实现类,为什么能注入、能调用?
@EnableFeignClients 把接口注册成 FeignClientFactoryBean(FactoryBean)的 BeanDefinition,容器注入时交给你的实际是 getObject() 生成的 JDK 动态代理;代理的 InvocationHandler 把方法调用转成 HTTP 请求。

Q2:Feign 调用时注解解析是每次都做吗?
不是。启动时 SpringMvcContract 把每个方法的注解解析成 MethodMetadata 并缓存,运行时只做"模板填参 + 发请求",注解解析只有一次。

Q3:Feign 和负载均衡是怎么结合的?
自动配置把 Feign 默认的 Client 换成 FeignBlockingLoadBalancerClient(一个装饰器):先按 URL 里的 host 拿服务名 → LoadBalancerClient.choose 选实例 → reconstructURI 把服务名换成实例地址 → 交给被装饰的 Client 发真实请求。

Q4:为什么 Ribbon 被 Spring Cloud LoadBalancer 替代?
Netflix 停止维护;Ribbon 阻塞式 API 不适配响应式;Spring Cloud 2020.0 起移除。新组件基于 Reactor,响应式/阻塞通吃,通过 NamedContextFactory 做按服务名隔离配置。

Q5:bootstrap 上下文为什么默认关闭了?
双容器模型复杂、bean 查找容易混淆。Spring Cloud 2020.0 起默认不建 bootstrap 上下文,配置中心改用 spring.config.import 机制在标准配置加载流程里拉取;老项目升级要显式引 spring-cloud-starter-bootstrap

Q6:Feign 的重试和超时是谁控制的?
重试:Retryer(Feign 默认不重试);超时:Options 的 connectTimeout/readTimeout(默认 10s/60s),配在 feign.client.config.<服务名>.*。Ribbon 时代的 ribbon.ReadTimeout 已经失效。

Q7:一次完整的 Feign 调用,数据经过了哪些环节?
接口代理 → 模板填参 → Request 构建 → LoadBalancer 选实例 + 换 URL → HTTP 客户端执行 → 2xx 解码返回(否则 ErrorDecoder 抛异常);实例列表来自 DiscoveryClient 从注册中心拉取的数据。

Q8:Consul 和 Nacos 的健康机制有什么不同?
Consul 是"检查模型":注册时声明 TTL 检查(应用侧 TtlScheduler 定时上报 agentCheckPass 续命)或 HTTP 检查(Agent 主动探测 actuator 端点);检查失败标记 critical,消费者查询带 ?passing=true 直接过滤。Nacos 是"心跳模型":客户端 5s 一次发 beat,15s 没心跳标记不健康。架构上 Consul 是 Agent + Server 两层(Server 集群 Raft 强一致),Nacos 客户端直连 Server。

Q9:Gateway 为什么基于 WebFlux 而不是 Spring MVC?
网关是纯 IO 密集转发场景:大量下游慢请求同时挂着,Servlet 同步模型"一个连接占一个线程"会让线程池耗尽;Netty 事件循环 + 少量线程能撑住海量连接。因此 Gateway 只支持 WebFlux 栈,与 spring-boot-starter-web 互斥。

Q10:限流、熔断、降级分别解决什么问题?Sentinel 怎么做规则检查?
限流防外部流量压垮自己(QPS 超阈值拒绝),熔断防下游故障拖垮自己(失败率超阈值后快速失败),降级是前两者/异常时的兜底返回。Sentinel 通过 SphU.entry 埋点 → 8 个 slot 组成责任链逐个检查(StatisticSlot 滑动窗口统计 → FlowSlot 限流 → DegradeSlot 熔断),任一不通过抛 BlockException 走降级。

十一、总结:一套模式串起整个 Spring Cloud

  • 底座:Spring Cloud Context/Commons 定义抽象(bootstrap 上下文、DiscoveryClient、LoadBalancerClient、RefreshScope);
  • 接入:统一靠 Spring Boot 的 @Import + ImportBeanDefinitionRegistrar + 自动装配(每个组件都是 @EnableXxx → Registrar → FactoryBean/装饰器);
  • 调用:Feign 把接口变代理、LoadBalancer 把服务名变地址、DiscoveryClient 把注册表变实例列表——一条链路三层抽象,各组件只关心自己那一段;
  • 入口与防护:Gateway 是"路由定位 + 过滤器链",Sentinel 是"埋点 + slot 责任链 + 滑动窗口统计"——底层还是同一套"抽象 + 拦截 + 策略"三件套;
  • 面试时的表达模板:“Spring Cloud 用 Spring Boot 的装配机制把微服务抽象和实现解耦,以 Feign 为例……(然后走一遍第四、五节的链路),入口讲 Gateway(第八节),防护讲 Sentinel(第九节)。”

更多推荐