云计算百科
云计算领域专业知识百科平台

【Spring Cloud】Gateway面试到底考什么

「说说 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,一个事件循环线程能挂几千个连接,等待期间线程被释放去处理别的请求,所以固定几个线程就能顶住很高的并发。

面试官:那响应式没有代价?

候选人:有,而且不小。三点:

  • 心智负担——不能写阻塞调用,一次 Thread.sleep 或一次 JDBC 查询就能把事件循环线程卡死,这条线程上所有请求一起卡。
  • 调试困难——堆栈里全是 lambda,异常抛出位置和业务代码对不上。
  • 生态——Servlet 时代的大量库(阻塞式 ORM、同步 HTTP 客户端)不能直接用。
  • 这一问很多人只答「响应式性能好」而不提代价,反而暴露没上过生产——任何技术选型都该能说出它放弃了什么。

    第二轮:往深了问

    追问一:过滤器链的顺序是怎么定的

    面试官:你说全局过滤器也进链,那 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 做业务相关的鉴权与灰度——各干各擅长的。

    上表的连接量级是量级参考,取决于机器规格、请求特征与内核参数,不能当基准测试结论用。

    对比速查表

    维度Spring Cloud GatewayZuul 1.x
    编程模型 响应式(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。

    三条结论:

  • 网关的价值不在「转发」,在「把横切逻辑从 N 个服务收敛到 1 个地方」。
  • 响应式换来的是并发密度,付出的是编程约束——block() 是网关的头号杀手。
  • 过滤器链是洋葱不是队列,order 决定的是嵌套层级,而不是简单的执行先后。
  • 延伸学习顺序:Reactor 的 Mono / Flux 与背压 → WebFlux 的 HandlerMapping 与 WebFilter → Gateway 的 FilteringWebHandler 源码 → ServiceInstanceListSupplier 与自定义负载均衡 → Resilience4j 的熔断与舱壁隔离。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Spring Cloud】Gateway面试到底考什么
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!