☰
Spring Cloud Gateway面试与生产排障:路由限流过滤器
2026/10/27 6:31:11 网站建设 项目流程

我面过不少后端岗位的候选人,简历上写着“熟悉微服务”,一问到网关这块,能顺畅讲下来的不到三成。Spring Cloud Gateway 面试题几乎成了微服务方向的分水岭:会背概念的能说出路由、断言、过滤器三个词,真正在生产环境里调过的人,才答得出线程模型为什么不能阻塞、限流参数怎么算、路由明明配了为什么还是 404。这篇就把这道题从面试官的视角和候选人的视角同时拆开,把常被追问的知识点、能直接复现的配置、以及踩过的坑一次性铺开。不管你是准备面试,还是刚接手一个网关模块,照着捋一遍都能用得上。

1. 面试官到底想考什么:Gateway 的定位与选型逻辑

1.1 为什么网关成了微服务面试的必考题

网关这个位置太特殊了,它是所有外部流量进入系统的第一道关口。面试官问 Gateway,表面在问一个框架,实际在验证你有没有站在系统边界上看问题。一个合格的答案应该能说清楚:网关承担了路由转发、统一鉴权、限流熔断、协议转换、日志埋点、跨域处理这些横切关注点,把这些从各个业务服务里抽出来,业务团队才能专注自己的领域逻辑。

我通常会在面试里先抛一个开放问题:“你们线上网关是干什么活的?”答“就是转发请求”的,基本聊不下去;能说出“入口统一做 JWT 校验,把用户 ID 塞进请求头往下传,同时对第三方接口做限流”的,说明真上手过。这个问题的价值在于,网关的职责边界决定了后面所有的技术选型,职责想不清楚,配置写得再花哨也是白搭。

还有一层考察意图,是看你对“边界”的理解。网关不是万能的,它不该承担业务逻辑,不该做重计算,更不该在里面查数据库拼数据。你答得出哪些事该放网关、哪些事必须下沉到服务里,面试官就知道你分得清架构层次。这一点比背出十几个断言工厂名字重要得多。

1.2 Gateway、Zuul 与 Nginx 的取舍对比

这道对比题出现的频率极高,答得好能直接加分。核心差异在阻塞模型上:Zuul 1.x 基于 Servlet 阻塞式 IO,一个请求占一个线程,高并发下线程池很快被打满;Spring Cloud Gateway 基于 WebFlux 和 Reactor Netty,用少量事件循环线程撑起大量连接,非阻塞地处理请求。这也是 Spring Cloud 官方在第二代组件里用 Gateway 替代 Zuul 的根本原因。

至于 Nginx,它和 Gateway 不在一个层次上。Nginx 更适合做四层和七层的流量入口、静态资源、TLS 卸载,性能极强但业务扩展性弱,你很难在 Nginx 里写一段逻辑去解析 JWT 再把用户信息透传下去。Gateway 是业务网关,跑在应用层,能直接复用 Java 生态里的安全框架、配置中心、服务发现。生产环境里常见的是 Nginx 在最前面做负载和证书,后面接一组 Gateway 实例做业务网关,两层配合而不是二选一。

面试答题时,建议用一张表把三者摆开,比口述清楚得多:

维度Zuul 1.xSpring Cloud GatewayNginx
IO 模型Servlet 阻塞WebFlux 非阻塞事件驱动非阻塞
扩展方式Java 过滤器GlobalFilter / GatewayFilterLua 脚本、C 模块
与服务发现集成较好原生集成,支持 lb://需额外方案同步
业务逻辑能力强强弱
典型定位已被替代业务网关流量入口

这张表里最容易被追问的是 IO 模型那一行。面试官很可能会接着问:“非阻塞具体省在哪?”你要能答出:阻塞模型下每个连接绑定一个线程,线程内存开销大、上下文切换成本高,连接数一多就顶不住;非阻塞模型下少量线程轮询处理就绪事件,连接本身几乎不占线程资源,所以在长连接多、响应时间不稳定的场景里优势明显。

1.3 面试题背后的三层考察意图

我把 Gateway 相关的问题归成三层。第一层是概念层:三大组件是什么、断言有哪些、过滤器怎么分类。这一层答不出来直接淘汰。第二层是原理层:过滤器链怎么排序、请求进来经过哪些 Handler、为什么不能阻塞。这一层能筛掉只会抄配置的人。第三层是实战层:线上出过什么问题、限流参数怎么定、路由不生效怎么排查。这一层答得好,基本就是团队里能独立扛网关模块的人。

很多候选人卡在第二层和第三层之间,概念背得滚瓜烂熟,一问“你在 Gateway 里做过什么自定义逻辑”就开始含糊。其实面试官要的不是多高深的方案,而是你有没有真正把请求链路走通过一遍。哪怕你只是写过一个记录耗时的全局过滤器,只要能把 order 为什么设成那个值讲明白,也足够证明你理解执行顺序。

2. 核心原理拆解:响应式模型与三大组件

2.1 基于 WebFlux 的线程模型为什么是高频追问点

“Gateway 里能不能用阻塞调用?”这个问题我几乎每场面试都问。标准答案是尽量不能,原因要讲透:Gateway 跑在 Reactor Netty 上,默认的工作线程数等于 CPU 核数(可以通过参数调),所有请求共享这少量事件循环线程。一旦你在过滤器里写了同步的数据库查询、同步的 HTTP 调用,或者干脆 Thread.sleep 一下,被阻塞的线程就没法处理其他连接的读写事件,整个网关的吞吐会断崖式下跌。

这个现象在生产上表现为:平时 QPS 几千都很稳,某个下游接口变慢,网关跟着整体卡死,连不相关的路由也超时。我第一次遇到这个问题时排查了很久,最后定位到一个自定义过滤器里调用了同步的远程校验接口,那个接口高峰期响应从 20ms 涨到 2s,直接把事件循环线程全占住了。后来改成异步调用并用 Mono 包装,问题立刻消失。

正确的写法是把阻塞操作丢到弹性线程池里,或者换成响应式客户端。用 Reactor 的写法大致是这样,关键在于整条链路都保持 Mono 的惰性执行:

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return Mono.fromCallable(() -> remoteAuthService.checkBlocking(exchange.getRequest())) .subscribeOn(Schedulers.boundedElastic()) .flatMap(result -> { if (!result.isPass()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }); }

面试时如果能把 boundedElastic 这个调度器说出来,并解释它是为阻塞任务准备的弹性线程池,面试官对你的印象会立刻不一样。还要补一句:能不阻塞就别阻塞,弹性线程池只是兜底,不是鼓励你在网关里做重活。

2.2 Route、Predicate、Filter 三位一体的设计

三大组件的关系用一句话说清楚:Route 是一条路由规则,由唯一 id、目标 uri、一组断言和一组过滤器组成;Predicate 决定这个请求是否匹配这条路由,多个断言之间是与的关系;Filter 在匹配成功后对请求和响应做加工,分为转发前执行的 pre 逻辑和收到响应后的 post 逻辑。

目标 uri 有两种常见形态,一种是写死的 http 地址,另一种是 lb://服务名,前者用于代理外部系统,后者走服务发现和负载均衡。面试里问“lb 是怎么生效的”,答案是 ReactiveLoadBalancerClientFilter 这个全局过滤器,它会在过滤器链里把 lb://服务名 解析成具体实例地址再继续往下走。这个细节答出来,说明你看过请求链路而不是只会写 yaml。

路由匹配的入口是 RoutePredicateHandlerMapping,它遍历所有路由,用每个路由的断言去测试当前请求,匹配上就交给 FilteringWebHandler 执行过滤器链。注意这里是按路由定义顺序匹配的,先匹配上的生效,所以路由顺序在某些重叠路径场景下会影响结果,这个点很少人主动提,提出来是加分项。

2.3 内置断言工厂逐条拆解与面试答法

断言工厂是面试里最容易被连续追问的部分,因为名字多、参数细。我整理了一份常用的清单,配上典型用法,答题时挑几个说清楚比全背一遍更有效:

断言工厂作用典型场景
Path按路径匹配,支持通配最常用,按接口前缀分流
Method按 HTTP 方法匹配区分读写接口
Header按请求头匹配按版本号或租户分流
Query按查询参数匹配灰度标记走不同后端
Cookie按 Cookie 匹配特定客户端走特殊路由
Host按 Host 头匹配多域名共用一套网关
After / Before / Between按时间窗口匹配定时上线的活动入口
Weight按权重分流灰度发布、AB 测试
RemoteAddr按来源 IP 匹配内网接口限制
XForwardedRemoteAddr按转发链中的来源 IP 匹配经过前置代理后的真实来源判断

最容易被深挖的是 Path。面试官会问路径匹配之后前缀怎么处理,答案是 StripPrefix 过滤器,参数是去掉的层数。比如外部路径是 /api/order/create,后端服务实际接口是 /order/create,就配 StripPrefix=1。这个参数写错是新手最常见的 404 原因,我在排查章节里还会专门讲。

Weight 断言也值得多说两句,它接收分组名和权重值,同一分组内按权重比例分流。灰度发布时用它把 10% 的流量导到新版本,配合 Header 断言做定向验证,是很实用的组合。不过要注意权重是相对值,同组内所有权重加起来是分母,不是百分比,很多人第一次配会理解错。

2.4 过滤器链的执行顺序与自定义排序

过滤器分两类:GatewayFilter 是绑定在具体路由上的,GlobalFilter 是全局生效的,两者最终会被合并成一条链按 order 排序执行。order 值越小越先执行,pre 阶段从小到大,post 阶段则反过来从大到小,这一点和 Servlet 过滤器链的思维模型一致,理解起来不难,难的是记住内置过滤器的量级。

几个关键的 order 值需要心里有数:RouteToRequestUrlFilter 在 10000,负责把路由的 uri 转成实际请求地址;ReactiveLoadBalancerClientFilter 在 10150,负责 lb 的解析;NettyRoutingFilter 的 order 是 Integer.MAX_VALUE,也就是真正发出下游请求的地方。你的自定义过滤器如果要在转发前生效,order 设成负数或者小于 10000 的值就行;如果要拿到响应做后处理,靠 post 逻辑即可,不需要刻意把 order 设得很大。

这里有个坑我踩过:同一个 GlobalFilter 如果被 @Component 注解注册了一次,又在配置里作为 bean 显式声明了一次,会执行两遍。表现是日志打印两次、响应头被加两次,排查时盯着 order 看半天看不出来。解决办法很简单,全局过滤器只保留一种注册方式,别混着来。

3. 高频实操题:从零搭一个能跑通的路由配置

3.1 依赖版本对齐与配置文件写法

面试里常问“搭 Gateway 要注意什么”,第一个要答的就是不能同时引入 spring-boot-starter-web。Gateway 基于 WebFlux,Servlet 容器和响应式容器会冲突,应用启动直接报错。如果你的项目里公共依赖带进来了 web starter,得用 exclude 排掉。

第二个是版本对齐。Spring Cloud 的版本必须和 Spring Boot 严格对应,错配会导致各种莫名其妙的 NoSuchMethodError。常见对应关系是:Spring Boot 2.7.x 配 Spring Cloud 2021.0.x,Spring Boot 3.0.x 配 Spring Cloud 2022.0.x,Spring Boot 3.2.x 配 Spring Cloud 2023.0.x。面试时能说出“版本用 BOM 统一管理,别单独指定子模块版本”这句话,说明你被依赖冲突毒打过。

一段能跑的最小配置长这样,注意我把服务发现和路由都写全了:

server: port: 8080 spring: application: name: edge-gateway cloud: gateway: discovery: locator: enabled: false routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** - Method=GET,POST filters: - StripPrefix=1 - AddRequestHeader=X-Gateway-Source, edge

这里面 discovery.locator.enabled 我特意设成 false。默认的自动路由会用服务名拼路径,规则不可控,生产环境基本都关掉,改成手写显式路由。这个细节面试时提一句,能体现你对默认行为有警惕心。

3.2 代码方式定义路由与断言组合

配置方式适合固定路由,需要动态拼装或者做条件判断时,用代码方式更灵活。下面这段定义了两条路由,一条走服务发现并加了超时控制,一条代理到外部地址:

@Configuration public class RouteConfig { @Bean public RouteLocator customRoutes(RouteLocatorBuilder builder) { return builder.routes() .route("user-service", r -> r .path("/api/user/**") .and().method(HttpMethod.GET, HttpMethod.POST) .filters(f -> f .stripPrefix(1) .addRequestHeader("X-Trace-Id", "gw") .retry(config -> config.setRetries(2) .setSeries(HttpStatus.Series.SERVER_ERROR))) .uri("lb://user-service")) .route("partner-api", r -> r .path("/partner/**") .filters(f -> f.setPath("/open/api")) .uri("https://partner.example.com")) .build(); } }

代码里有两个值得讲的点。retry 过滤器设置重试次数时,一定要限定只对 5xx 重试,因为对 4xx 重试毫无意义,对非幂等的 POST 重试还可能造成重复下单。setPath 则是把外部路径整体改写,适合合作方接口的路径映射。面试官问“重试怎么配才安全”,你就从幂等性和状态码范围两个角度答,基本满分。

3.3 自定义全局过滤器实现统一鉴权

这是实操题里出场率最高的一个。要求是:所有请求进来先校验令牌,校验通过把用户信息塞进请求头传给下游,失败直接返回 401 并给出统一格式的错误体。下面这版是我实际项目里简化过的写法:

@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private final TokenVerifier tokenVerifier; public AuthGlobalFilter(TokenVerifier tokenVerifier) { this.tokenVerifier = tokenVerifier; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); if (path.startsWith("/open/") || path.equals("/health")) { return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (token == null || token.isBlank()) { return reject(exchange, "缺少凭证"); } return tokenVerifier.verify(token) .flatMap(uid -> { ServerHttpRequest mutated = exchange.getRequest().mutate() .header("X-User-Id", uid) .build(); return chain.filter(exchange.mutate().request(mutated).build()); }) .switchIfEmpty(reject(exchange, "凭证无效")); } private Mono<Void> reject(ServerWebExchange exchange, String msg) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] body = ("{\"code\":401,\"msg\":\"" + msg + "\"}").getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(body); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return -100; } }

order 设成 -100,是为了让鉴权在所有路由级过滤器之前完成,避免无凭证请求先被转发到下游。这里有个容易忽略的细节:把用户 ID 写进请求头时,一定要用 mutate 生成新的 request 对象,直接改原对象在某些场景下不生效。还有,白名单判断建议放在最前面,用路径前缀而不是正则,性能更好也更不容易写错。

3.4 跨域配置的正确姿势

跨域问题面试里问得不算多,但一旦问到,答得含糊就很掉分。Gateway 里配跨域有两种方式,推荐 yaml 全局配置,简单直接:

spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true cors-configurations: '[/**]': allowedOrigins: "https://front.example.com" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true maxAge: 3600

有两个坑必须提前说。第一,allowCredentials 为 true 时,allowedOrigins 不允许写成星号,浏览器会直接拒绝,必须写明确的域名列表。第二,如果你在网关配了跨域,下游服务又配了一次,响应头里会出现两个 Access-Control-Allow-Origin,浏览器同样会报错。解决办法是跨域统一在网关收口,下游全部关掉。add-to-simple-url-handler-mapping 这个参数的作用是让预检请求也能被 CORS 处理器接管,不设成 true 的话 OPTIONS 请求可能直接 404,这个参数我见过太多人漏配。

4. 限流与熔断:面试最爱追问的稳定性设计

4.1 RequestRateLimiter 的令牌桶实现

限流是网关面试的压轴题,因为涉及算法、参数和落地细节,一问就能分出深浅。Gateway 内置的 RequestRateLimiter 基于 Redis 加 Lua 脚本实现令牌桶,支持分布式限流,因为是原子操作,多实例部署下不会出现计数漂移。

基本配置如下:

spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@ipKeyResolver}"

三个参数的含义要讲清楚:replenishRate 是每秒补充的令牌数,也就是稳态放行的 QPS;burstCapacity 是桶容量,决定允许多大的瞬时突发;requestedTokens 是每次请求消耗的令牌数,默认 1,如果某个接口单次请求成本高,可以调成 2 或更大。KeyResolver 决定限流维度,按 IP、按用户、按接口路径都行,实际项目里我一般按用户维度限流,匿名请求再退回按 IP。

4.2 参数计算与压测校核

面试官很爱问“这两个数你怎么定的”,光说默认值会显得没做过容量规划。我的算法是:先看下游服务压测出的稳定承载能力,比如订单服务单实例压测 300 QPS 稳定,部署 4 个实例,理论上限 1200,再看历史峰值流量大概多少,假设峰值 800,那 replenishRate 定在 800 左右就够,留 20% 余量可以定 1000。burstCapacity 一般设成 replenishRate 的 1.5 到 2 倍,用来吸收秒级抖动。

这里有一个反直觉的点:burstCapacity 设得太大会让限流失效。因为桶容量大,瞬时涌入的请求全被放行,下游可能直接被压垮,令牌桶更像是个缓冲垫不是挡箭牌。我见过有团队把 burst 配成 10000,结果限流形同虚设,压测时下游直接雪崩。

调完参数必须压测验证,不能拍脑袋上线。验证方法是拿压测工具打目标路由,观察三件事:网关的限流拒绝率是否符合预期、下游服务的 RT 和错误率是否平稳、Redis 的 QPS 是否成为新瓶颈。第三点经常被忽略,限流是每个请求都要访问一次 Redis 的,QPS 高的时候 Redis 压力不小,建议单独部署或者用集群。

再补一个按用户限流的 KeyResolver 写法:

@Bean public KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); if (userId != null && !userId.isBlank()) { return Mono.just(userId); } String ip = exchange.getRequest().getRemoteAddress() != null ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() : "unknown"; return Mono.just(ip); }; }

这个写法有个前提:鉴权过滤器必须排在限流之前,否则拿不到用户 ID。order 的设置在这里体现出了实际意义,面试时能把这种过滤器之间的依赖关系讲出来,比背概念有说服力得多。

4.3 熔断降级的接入方式

熔断这块,Gateway 提供了 CircuitBreaker 过滤器,底层可对接 Resilience4j。配置上看是这样:

filters: - name: CircuitBreaker args: name: orderCircuit fallbackUri: forward:/fallback/order

fallbackUri 指向网关内部的一个接口,熔断打开时请求会被转发过去返回兜底内容。要注意 fallback 接口本身不能再去调下游,否则熔断就失去意义了。另外熔断的阈值配置在 Resilience4j 那边,比如滑动窗口大小、失败率阈值、半开状态探测次数,这些参数面试里能说出“失败率 50% 以上、窗口内请求数超过最小阈值才触发”就足够专业。

实际项目里我更倾向用 Sentinel 做网关限流和熔断,因为规则可以动态推送,控制台可视化配置,比改配置文件重启灵活。但面试时两套都要能讲,内置的 RequestRateLimiter 是必答项,Sentinel 是加分项。

5. 常见问题与排查技巧实录

5.1 路由不生效的排查清单

这类问题我处理过太多次,基本能按清单往下捋。第一步看请求路径和 Path 断言是否匹配,注意通配符的语义:/api/** 能匹配 /api/a/b,但 /api/* 只匹配一层。第二步看 StripPrefix 层数对不对,去多了或去少了都是 404,判断方法是打开 gateway 的 debug 日志,看最终转发出去的 URL 是什么。

第三步看 lb:// 后面的服务名是否和服务注册中心里的一致,大小写、连字符都算。第四步看路由顺序,多条路由路径重叠时先匹配的生效,比如 /api/** 写在 /api/order/** 前面,订单请求就被前一条吃掉了。第五步看服务发现客户端是否正常,注册中心的连接状态、本地缓存是否过期,这些在日志里都有痕迹。

还有一个隐蔽的原因:路由配置在配置中心里改过但没刷新。Gateway 的路由支持动态刷新,但需要配合事件监听或者 refresh 端点触发,光改配置中心的文件不会自动生效。我建议排查时先用 actuator 的 gateway/routes 端点把当前生效的路由全打出来,一眼就能看出配置有没有真正加载。

5.2 请求头丢失、响应异常与大文件上传

请求头丢失多见于跨服务调用链路:网关把用户信息塞进请求头,下游用 Feign 再调下一个服务时,头默认不会传递,得配 RequestInterceptor。这个不算网关的锅,但排查时经常被误认为是网关吃掉了头,面试里能主动提到这一点,说明你对整条链路有认识。

响应异常里最常见的是中文乱码,原因是响应头里的 Content-Type 没带 charset。解决办法是在网关统一加一个过滤器,给响应补上 UTF-8,或者让下游返回时就带全。另一种异常是响应被压缩后前端解析失败,检查一下网关有没有重复添加 Content-Encoding。

大文件上传是必踩的坑。Gateway 默认的请求体内存缓冲有上限,超过就报 DataBufferLimitException。解决办法是调大 spring.codec.max-in-memory-size,同时下游服务的上传大小限制也要一起调。要注意这个参数调太大会吃内存,合理做法是对上传路由单独配置,而不是全局调到几个 G。我一般会建议大文件走对象存储的直传方案,网关只负责签名和回调,从架构上绕开这个问题。

5.3 高频问题速查表

把上面这些整理成表,面试前扫一眼,排查时也能当清单用:

现象可能原因处理方式
路由返回 404路径断言不匹配、StripPrefix 层数错开 debug 日志看最终转发 URL
服务找不到lb 服务名不一致、未注册成功核对注册中心实例列表
修改配置不生效未触发热刷新调用 refresh 端点或重启
全局过滤器执行两次注解与 bean 重复注册只保留一种注册方式
高并发下整体超时过滤器内有阻塞调用改异步或下沉到弹性线程池
跨域报错重复配置跨域头网关统一收口,下游关闭
上传大文件失败请求体缓冲上限调整 max-in-memory-size
限流不生效burstCapacity 过大按压测结果重算参数
响应乱码Content-Type 缺 charset统一补充响应头
504 网关超时下游响应慢、连接池不足调超时与连接池参数

连接池这块补一句,Gateway 的 httpclient 参数可以配最大连接数、获取连接超时、空闲回收时间,高并发场景下默认值往往不够用,表现是偶发的连接等待超时。压测时把 pool 相关参数和下游 RT 放在一起看,很容易定位。

6. 我对这道面试题的一点个人看法

面了这么多场,我发现 Gateway 这道题真正拉开差距的地方不在知识量,而在“有没有在生产环境里被它坑过”。背得出断言工厂清单的人很多,能说清楚事件循环线程被阻塞后整个网关雪崩是什么现象的人很少;知道 StripPrefix 的人很多,能讲出路由顺序导致路径被吃掉这种问题的很少。

如果让我给准备面试的人一个建议,就是把网关当成一个真实的系统去走一遍:自己搭一个,接两个服务,配好鉴权、限流、跨域,然后用压测工具打一打,故意把下游弄慢、故意把参数配错,看报错长什么样。走完这一圈,面试官问什么你都有话说,因为这些坑你都亲眼见过。参数怎么定、order 怎么排、过滤器之间谁先谁后,这些东西文档里写不全,只有真跑过一遍才会变成你自己的判断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询