「说说 Spring Cloud Gateway」——这题到底在考什么
开场:这题为什么高频
微服务拆到一定阶段,网关必然出现。面试官问它,通常不是想听你背「路由、断言、过滤器」三个词,而是想确认三件事:你理解不理解响应式编程的代价、知不知道网关在链路里的真实位置、踩没踩过那些只有上过生产才会遇到的坑。
先给一句话定义:Spring Cloud Gateway 是 Spring 官方为微服务提供的第二代 API 网关,用「路由 + 断言 + 过滤器」这套模型,把鉴权、限流、灰度、日志这些横切逻辑从各个业务服务里抽出来,统一放在流量入口处理。
打个比方,网关像一栋大楼的一楼前台。所有访客(请求)先到前台,前台核对证件(鉴权过滤器),看你要找哪个部门(路由断言),然后指路或带你过去(转发)。访客太多时,前台还能顺手限流——先排队。
这个类比不准的地方是:前台是个人,忙不过来就排队;而 Gateway 底下是事件循环,几个线程就能挂着上万个并发连接,「忙」的表现不是排队,而是响应变慢、背压堆积。这个差别正是它替代 Zuul 1.x 的根本原因,第二轮会展开。
下面是完整的一轮对话。
第一轮:基础题
面试官:先说说 Spring Cloud Gateway 里最核心的几个概念。
候选人:三个——Route、Predicate、Filter。
- Route 是一条转发规则,由 id、目标 uri、断言集合、过滤器集合组成;
- Predicate 是匹配条件,决定「这个请求归不归我这条路由管」,内置了 Path、Method、Header、Query、Cookie、时间窗口等十几种;
- Filter 是处理逻辑,分 GatewayFilter(绑在单条路由上)和 GlobalFilter(全局生效)。
面试官:三者怎么协作?
候选人:请求进来后按路由逐条匹配断言,第一条匹配上的路由接管它,然后执行「这条路由自己的 GatewayFilter + 所有 GlobalFilter」合并排序后的链条。
面试官:配置一条给我看看。
候选人:代码式比 YAML 直观:
// JDK 17 / Spring Boot 3.2 / Spring Cloud 2023.0.x
// 额外依赖:spring-cloud-starter-circuitbreaker-reactor-resilience4j(用到 circuitBreaker 时)
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
// 断言:/api/order/** 开头的请求走这条路由
.route("order-route", r -> r.path("/api/order/**")
.filters(f -> f
.stripPrefix(1) // 去掉第一段 /api 再转发给下游
.addRequestHeader("X-From", "gateway")
.circuitBreaker(c -> c.setName("orderCb") // 熔断器名,需在配置里定义规则
.setFallbackUri("forward:/fallback")))
.uri("lb://order-service")) // lb:// 表示走服务发现 + 负载均衡
.route("user-route", r -> r.path("/api/user/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://user-service"))
.build();
}
}
这段要证明的是三段式怎么落到代码上:path(…) 是断言,filters(…) 是过滤器,uri(…) 是目标。注意 lb:// 这个 scheme,它不是真实协议前缀,而是交给 ReactiveLoadBalancerClientFilter 解析成具体实例地址的标记。
面试官:和 Zuul 比,凭什么用它?
候选人:一句话——线程模型从「一请求一线程」换成了「事件循环」。
Zuul 1.x 基于 Servlet,一个请求占一个 Tomcat 线程,等下游响应时线程是阻塞的,默认 200 个线程就是它的并发天花板,想提升只能加线程,而线程栈与上下文切换的开销线性增长。Gateway 基于 WebFlux + Reactor Netty,一个事件循环线程能挂几千个连接,等待期间线程被释放去处理别的请求,所以固定几个线程就能顶住很高的并发。
面试官:那响应式没有代价?
候选人:有,而且不小。三点:
这一问很多人只答「响应式性能好」而不提代价,反而暴露没上过生产——任何技术选型都该能说出它放弃了什么。
第二轮:往深了问
追问一:过滤器链的顺序是怎么定的
面试官:你说全局过滤器也进链,那 order 小的先执行还是后执行?
候选人:pre 阶段小的先执行;post 阶段反过来,大的先执行。
面试官:为什么?
候选人:因为过滤器链是「洋葱」而不是队列。每个过滤器的后置逻辑写在 chain.filter(exchange).then(…) 里,意思是「等下游全部处理完我再干活」。链是嵌套调用的,最外层的先进、最后出。
陷阱在哪:最常见的错误答案是「order 小的先执行、后结束」,漏掉 post 阶段逆序这个关键点。这个逆序有真实影响——如果你在 order = -100 的鉴权过滤器里直接往响应头写用户信息,那次写入发生在响应已提交之后,会静默失效,日志里连报错都没有。要看响应结果,逻辑必须挂在 order 更大的过滤器里,或者用 chain.filter(exchange).then(Mono.fromRunnable(…)) 补后置钩子。
内建顺序参考(Spring Cloud Gateway 4.x):RemoveCachedBodyFilter 是最高优先级,NettyWriteResponseFilter 是 -1,NettyRoutingFilter 是最低优先级。也就是转发永远在最后,写响应紧挨着转发之后。
把下面这段扔进任一 Gateway 工程,启动后访问任意路由,控制台会依次打印 A pre → B pre → B post → A post,逆序当场可见:
// JDK 17 / Spring Cloud Gateway 4.x / 依赖 Lombok:验证 pre 正序、post 逆序
@Slf4j
public class OrderProbeFilter implements GlobalFilter, Ordered {
private final String name;
private final int order;
public OrderProbeFilter(String name, int order) { this.name = name; this.order = order; }
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[{}] pre", name);
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> log.info("[{}] post", name))); // 后置逻辑逆序
}
@Override
public int getOrder() { return order; }
}
@Configuration
class ProbeConfig {
@Bean OrderProbeFilter filterA() { return new OrderProbeFilter("A", 1); }
@Bean OrderProbeFilter filterB() { return new OrderProbeFilter("B", 2); }
}
追问二:请求体为什么只能读一次
面试官:我需要在网关做接口签名校验,得读请求体,怎么做?
候选人:不能直接读。ServerHttpRequest#getBody() 返回的是 Flux<DataBuffer>,一条只能消费一次的数据流,读完就空了,下游收到的是空 body。
面试官:那怎么解决?
候选人:读完先缓存成字节数组,用 ServerHttpRequestDecorator 把 getBody() 重写成从缓存里反复供给的 Flux,再用 exchange.mutate().request(…) 塞回链里。
陷阱在哪:两个,都是线上级问题。
第一,DataBufferUtils.join 之后必须显式 release 原始 buffer。Netty 的 buffer 是池化的,不释放就不归还,跑上一两周会看到 direct memory 缓慢上涨,最后 OutOfDirectMemoryError —— 这是网关最常见的内存故障,而且堆 dump 里看不出来(占的是堆外)。
第二,大文件上传不能这么干。把整个 body 读进内存,一个 500MB 的上传就能把网关打挂。正确做法是按 Content-Type 和 Content-Length 判断,非 JSON 或超阈值直接放行。
// JDK 17 / Spring Cloud Gateway 4.x:读一次 body,缓存后重新供给下游
// 这段略长(约 25 行),因为装饰器必须写完整才能跑
@Component
public class BodyCacheFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MediaType type = exchange.getRequest().getHeaders().getContentType();
if (type == null || !MediaType.APPLICATION_JSON.isCompatibleWith(type)) {
return chain.filter(exchange); // 表单、文件流直接放行,避免打爆内存
}
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer); // 关键:归还池化的堆外内存
exchange.getAttributes().put("cachedBody", new String(bytes, StandardCharsets.UTF_8));
ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() { // 下游读的是重新包装的 Flux
return Flux.defer(() -> Mono.just(
exchange.getResponse().bufferFactory().wrap(bytes)));
}
};
return chain.filter(exchange.mutate().request(decorated).build());
})
.switchIfEmpty(chain.filter(exchange)); // 空 body 直接放行
}
@Override
public int getOrder() { return –100; } // 必须早于转发过滤器
}
追问三:为什么网关工程里不能有 spring-boot-starter-web
面试官:有些项目网关里同时引了 web 和 gateway,照样能启动。
候选人:能启动,但路由不生效。spring-boot-starter-web 会把 web 应用类型推断成 SERVLET,Boot 起的是 Tomcat;而 Gateway 的 RoutePredicateHandlerMapping 是 WebFlux 体系的 HandlerMapping,在 Servlet 容器里根本不会被调度到。表现就是启动日志一切正常,所有请求 404。
陷阱在哪:很多人以为 Gateway 检测到冲突会直接报错退出。实际是——自动配置会发现 classpath 上的 DispatcherServlet 并打印一条 incompatible 的告警,然后继续启动。问题要到运行期才暴露,而且报的是 404,极容易被误判成「路由配错了」,白查两小时。
最快的判别方法:看启动日志里是 Netty started on port 8080 还是 Tomcat started on port 8080。日志说 Tomcat,就已经错了。
排查手法:mvn dependency:tree -Dincludes=org.springframework:spring-webmvc,把内部 SDK 传递进来的 web 依赖一个个排除。
面试官:还有别的常见坑吗?
候选人:跨域。网关和下游都配了 CORS,浏览器会收到两个 Access-Control-Allow-Origin,直接报「header contains multiple values」。规范做法是只在网关配,下游全部关掉。
还有一个连带问题:网关的鉴权过滤器若对所有请求生效,浏览器发的 OPTIONS 预检请求不带 token,会被直接 401,跨域就彻底不通了。要在鉴权过滤器里放行 OPTIONS,或者让 CorsWebFilter 排在鉴权之前。
第三轮:场景题
面试官:给个场景。订单服务要做灰度发布,新版本只对 10% 的内部员工开放;同时网关要做统一鉴权和每秒 500 次的限流。怎么设计?
先自己想一想,再往下看。
候选人:三块,都挂在过滤器链上,靠 order 串起来。
第一块,鉴权。一个 order = -200 的 GlobalFilter:从 Header 取 token,校验通过后把 userId、角色、是否灰度用户写进 exchange.getAttributes(),供后续过滤器读。失败就直接 response.setStatusCode(401) 加 setComplete() 返回,不再调 chain.filter()。
第二块,灰度。两条路,差别很大:
- 简单做法:Weight 断言,按权重把流量分别打到两个 lb:// 目标。这是纯随机分流,做不到「只给内部员工」。
- 准确做法:自定义 ServiceInstanceListSupplier,在负载均衡阶段按 exchange 里的灰度标记过滤实例列表。实例标签来自注册中心(Nacos 的 metadata、K8s 的 label)。
第三块,限流。RequestRateLimiter 过滤器 + RedisRateLimiter(Redis + Lua 实现的令牌桶)+ 自定义 KeyResolver:
// JDK 17 / Spring Cloud Gateway 4.x / 依赖 spring-boot-starter-data-redis-reactive
// 限流维度:已登录按 userId,未登录按 IP
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userOrIpKeyResolver() {
return exchange -> {
String userId = exchange.getAttribute("userId"); // 由鉴权过滤器写入
if (userId != null) {
return Mono.just("u:" + userId);
}
InetSocketAddress addr = exchange.getRequest().getRemoteAddress();
String ip = addr == null ? "unknown" : addr.getAddress().getHostAddress();
return Mono.just("ip:" + ip);
};
}
}
# Spring Cloud Gateway 4.x:令牌桶按用户维度限流
spring:
cloud:
gateway:
routes:
– id: order–route
uri: lb://order–service
predicates:
– Path=/api/order/**
filters:
– name: RequestRateLimiter
args:
key-resolver: "#{@userOrIpKeyResolver}"
redis-rate-limiter.replenishRate: 100 # 每秒补 100 个令牌 = 稳态 QPS
redis-rate-limiter.burstCapacity: 200 # 桶容量 = 允许的瞬时突发
redis-rate-limiter.requestedTokens: 1 # 每个请求扣 1 个
这段要证明的是限流的三个旋钮各管什么:KeyResolver 决定「按谁算」,replenishRate 决定稳态速率,burstCapacity 决定突发上限。第四个参数 requestedTokens 最容易被忽略——把它调大就能做「重接口多扣令牌」的加权限流,比给每个接口单独配路由省事。
面试官:限流为什么用 Redis,不用本地令牌桶?
候选人:因为网关是多实例。本地桶各算各的,3 个实例配 500 QPS,实际会放过 1500。Redis + Lua 保证多实例共享同一个桶,且「读 – 判断 – 扣减」三步原子。代价是每次判定多一次网络往返(同机房通常亚毫秒),Redis 挂了限流直接失效——所以要么做本地桶降级兜底,要么接受短时间放行。
它和网关的关系:限流是「因为网关是唯一入口」才成立的能力。在服务内部做限流,永远只能看到自己那一份流量,看不见全局面。
面试官视角:这题到底在考什么
一路追问下来,其实在测四个层次:
| 概念层 | Route / Predicate / Filter、内置断言与过滤器 | 只会背三个词,说不清 lb:// 是什么 |
| 原理层 | 事件循环、过滤器洋葱模型、DataBuffer 生命周期 | 认为 order 只是「谁先执行」 |
| 工程层 | 依赖冲突、CORS、请求体缓存、超时配置 | 没在网关里读过 body |
| 架构层 | 灰度、限流、熔断、鉴权的落点选择 | 把网关当成「就是个转发器」 |
它和 Reactor 的关系:Reactor 是理解 Gateway 的地基——不把 Mono / Flux 理解成「数据流 + 订阅回调」而当成「异步的 List」,你就写不出正确的过滤器,只会往里塞 block() 把事件循环卡死。
它和 Spring WebFlux 的关系:WebFlux 提供编程模型和不阻塞的运行时,Gateway 是建立在这套模型之上的一个应用,不是并列关系。
它和服务发现 + LoadBalancer 的关系:这是「按服务名转发」的前提。lb://order-service 里的 order-service 不是域名,而是一个要去注册中心查实例列表的标记,真正的地址由 ReactiveLoadBalancerClientFilter 在运行时填进去。
它和 Redis 限流的关系:这是网关限流能跨实例生效的基础,本地令牌桶在集群下必然失真。
它和 Resilience4j 的关系:这是「下游挂了别把网关一起拖死」的手段,和限流是一对——限流保护自己不被压垮,熔断保护自己不被拖死。
它和 Zuul 1.x 的关系:同一道题的两种解法,差别在线程模型;选型看存量系统是 Servlet 还是响应式。
知识地图按关系类型摊开:
#mermaid-svg-aLkf4PXgH98d34Sg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aLkf4PXgH98d34Sg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aLkf4PXgH98d34Sg .error-icon{fill:#552222;}#mermaid-svg-aLkf4PXgH98d34Sg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aLkf4PXgH98d34Sg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aLkf4PXgH98d34Sg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aLkf4PXgH98d34Sg .marker.cross{stroke:#333333;}#mermaid-svg-aLkf4PXgH98d34Sg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aLkf4PXgH98d34Sg p{margin:0;}#mermaid-svg-aLkf4PXgH98d34Sg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-aLkf4PXgH98d34Sg .cluster-label text{fill:#333;}#mermaid-svg-aLkf4PXgH98d34Sg .cluster-label span{color:#333;}#mermaid-svg-aLkf4PXgH98d34Sg .cluster-label span p{background-color:transparent;}#mermaid-svg-aLkf4PXgH98d34Sg .label text,#mermaid-svg-aLkf4PXgH98d34Sg span{fill:#333;color:#333;}#mermaid-svg-aLkf4PXgH98d34Sg .node rect,#mermaid-svg-aLkf4PXgH98d34Sg .node circle,#mermaid-svg-aLkf4PXgH98d34Sg .node ellipse,#mermaid-svg-aLkf4PXgH98d34Sg .node polygon,#mermaid-svg-aLkf4PXgH98d34Sg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aLkf4PXgH98d34Sg .rough-node .label text,#mermaid-svg-aLkf4PXgH98d34Sg .node .label text,#mermaid-svg-aLkf4PXgH98d34Sg .image-shape .label,#mermaid-svg-aLkf4PXgH98d34Sg .icon-shape .label{text-anchor:middle;}#mermaid-svg-aLkf4PXgH98d34Sg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aLkf4PXgH98d34Sg .rough-node .label,#mermaid-svg-aLkf4PXgH98d34Sg .node .label,#mermaid-svg-aLkf4PXgH98d34Sg .image-shape .label,#mermaid-svg-aLkf4PXgH98d34Sg .icon-shape .label{text-align:center;}#mermaid-svg-aLkf4PXgH98d34Sg .node.clickable{cursor:pointer;}#mermaid-svg-aLkf4PXgH98d34Sg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aLkf4PXgH98d34Sg .arrowheadPath{fill:#333333;}#mermaid-svg-aLkf4PXgH98d34Sg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aLkf4PXgH98d34Sg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aLkf4PXgH98d34Sg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aLkf4PXgH98d34Sg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aLkf4PXgH98d34Sg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aLkf4PXgH98d34Sg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aLkf4PXgH98d34Sg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aLkf4PXgH98d34Sg .cluster text{fill:#333;}#mermaid-svg-aLkf4PXgH98d34Sg .cluster span{color:#333;}#mermaid-svg-aLkf4PXgH98d34Sg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aLkf4PXgH98d34Sg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aLkf4PXgH98d34Sg rect.text{fill:none;stroke-width:0;}#mermaid-svg-aLkf4PXgH98d34Sg .icon-shape,#mermaid-svg-aLkf4PXgH98d34Sg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aLkf4PXgH98d34Sg .icon-shape p,#mermaid-svg-aLkf4PXgH98d34Sg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aLkf4PXgH98d34Sg .icon-shape .label rect,#mermaid-svg-aLkf4PXgH98d34Sg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aLkf4PXgH98d34Sg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aLkf4PXgH98d34Sg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aLkf4PXgH98d34Sg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
前置
前置
运行时
核心组成
排序机制
组合
组合
因果
对比
深坑
Spring Cloud Gateway
Reactor: Mono / Flux
Spring WebFlux
Reactor Netty 事件循环
Route / Predicate / Filter
Ordered 与洋葱模型
服务发现 + Spring Cloud LoadBalancer
Redis 限流 / Resilience4j 熔断
横切逻辑下沉:鉴权、灰度、日志
Zuul 1.x / Nginx / APISIX / Higress
DataBuffer 只能消费一次
答不上来怎么办:前置知识补课
Mono / Flux:Mono 是 0 或 1 个元素的异步序列,Flux 是 0 到 N 个。最大的认知坎不是「异步」,是懒执行——不 subscribe() 什么都不发生。
- 类比:Flux 像水管,subscribe() 才是拧开水龙头,不拧就没有水流。
- 局限:水管解释不了背压——Flux 能感知下游消费速度并反压上游,水管不会。
事件循环(Event Loop):少量线程轮询 IO 事件,谁就绪就处理谁。
- 类比:一个服务员同时照看十桌,谁举手就去谁那儿,不守在某一桌旁边等出菜。
- 局限:现实里服务员能同时照看的桌数有上限;事件循环线程一旦被阻塞调用占住,整个循环停摆——这正是「网关里禁止 block()」的由来。
Ordered 接口:Spring 的通用排序契约,值越小优先级越高。它在 Gateway 里的特殊性是——只决定嵌套层级,不决定全部执行时间点。
ServerHttpRequestDecorator:装饰器模式,只覆盖 getBody() 一个方法就能替换请求体,不必实现整个接口。
加分项:换一门栈看同一个问题
网关这个位置,几乎每种技术栈都有自己的答案。横向摆开能说明白「选型从来不是性能单维度」:
| Spring Cloud Gateway 4.x | JVM + Reactor Netty | Java 写 GlobalFilter | 万级 | Java 栈、需与业务共享模型与 SDK |
| Zuul 1.x | JVM + Servlet | Java 写 ZuulFilter | 千级(受线程数限制) | 存量系统、迁移成本敏感 |
| Nginx / OpenResty | C + Lua | Lua 脚本 | 十万级 | 纯转发、静态资源、极高性能要求 |
| APISIX | OpenResty + etcd | Lua 插件 | 十万级 | 需要成熟插件生态与动态配置 |
| Higress / Envoy | C++ | WASM / 配置 | 十万级 | 云原生、Mesh 数据面统一 |
真正的差异不在数字,在扩展成本:Java 程序员写一个 GlobalFilter 是十分钟的事;写 Nginx 模块要重编译,写 Lua 插件要另学一套生态。反过来,C++ / Lua 方案的内存占用和连接密度,JVM 方案短期内追不上。
所以不少团队最终选两层网关:边缘层用 Nginx 或 Envoy 做 TLS 卸载和四层转发,应用网关层用 Spring Cloud Gateway 做业务相关的鉴权与灰度——各干各擅长的。
上表的连接量级是量级参考,取决于机器规格、请求特征与内核参数,不能当基准测试结论用。
对比速查表
| 编程模型 | 响应式(WebFlux) | 阻塞(Servlet) |
| 运行时 | Reactor Netty | Tomcat / Jetty |
| 过滤器抽象 | GlobalFilter + GatewayFilter | ZuulFilter(pre / route / post / error) |
| 动态路由 | RouteDefinitionRepository + RefreshRoutesEvent | 需自行扩展 |
| 限流 | 内置 RequestRateLimiter(Redis 令牌桶) | 需自行实现或依赖第三方 |
收尾:一句话答案模板
只有 30 秒时,这么答:
Spring Cloud Gateway 是 Spring 官方的第二代 API 网关,基于 WebFlux + Reactor Netty,用 Route、Predicate、Filter 三段式模型,把鉴权、限流、灰度这些横切逻辑从业务服务收敛到流量入口统一处理。相对 Zuul 1.x 的核心优势是线程模型从「一请求一线程」换成事件循环,少量线程就能维持上万连接;代价是不能写阻塞代码、调试更麻烦。落地要盯三件事:工程里不能有 spring-boot-starter-web、过滤器 order 在 post 阶段是逆序、读请求体必须缓存并显式释放 DataBuffer。
三条结论:
延伸学习顺序:Reactor 的 Mono / Flux 与背压 → WebFlux 的 HandlerMapping 与 WebFilter → Gateway 的 FilteringWebHandler 源码 → ServiceInstanceListSupplier 与自定义负载均衡 → Resilience4j 的熔断与舱壁隔离。
网硕互联帮助中心





评论前必须登录!
注册