简单聊聊 SpringCloud 微服务调用链路源码
简单聊聊 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);
}
}
关键点:
- 它监听的是
ApplicationEnvironmentPreparedEvent——还记得上一篇 SpringBoot 执行流程吗?这个事件发生在prepareEnvironment阶段,早于主容器创建,所以 bootstrap 容器能"抢先"把远程配置拉下来塞进 Environment; 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);registerFeignClients→registerFeignClient:扫描指定包下所有标注@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 替换了几个关键组件:
Contract→SpringMvcContract:让 Feign 认识@RequestMapping/@GetMapping/@PathVariable这些 Spring MVC 注解(原生 Feign 只认自己的@RequestLine);Encoder/Decoder→ Spring 的HttpMessageConverter体系(默认 Jackson);Client→FeignBlockingLoadBalancerClient(下一节讲,这是和负载均衡结合的点)。
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.choose → RoundRobinLoadBalancer.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
FeignContext 和 LoadBalancerClientFactory 都继承同一个抽象类 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:匹配条件(
Path、Method、Host、Header等),决定请求走哪条路由; - 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 转成真正的 AsyncPredicate 和 GatewayFilter(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(第九节)。”
更多推荐
所有评论(0)