API网关:微服务的大门守护者

说起API网关,很多面试者第一反应就是“反向代理加负载均衡”。确实,这是个起点,但远远不够。我面试过不少人,能讲清楚网关作用的人不到一半。而能说出Zuul和Gateway核心区别的,更是凤毛麟角。

今天我们就把这个知识点彻底吃透。API网关到底是什么?说白了,它就是微服务架构的外部入口点。所有来自客户端的请求,先打到网关,再由网关转发到后面的具体服务。

一句话总结:API网关是微服务的统一入口,负责请求路由、协议转换、认证授权、限流熔断、日志监控等横切关注点。

API网关的核心作用

网关在微服务架构里扮演着“守门员”的角色。我把它总结为五个核心职责,面试时只要把这五点讲透,基本就稳了。

  • 统一入口:所有客户端请求先到网关,再由网关分发。这样外部系统只需要知道一个地址。
  • 协议转换:内部服务可能用http,也可能用gRPC或者Dubbo。网关可以完成协议适配。
  • 路由转发:根据请求路径、参数、Header等信息,把请求分发到对应的后端服务。
  • 横切关注点:认证、授权、限流、熔断、日志、监控,这些通用功能统一在网关层实现。
  • 灰度发布:通过路由规则,可以把部分流量引入新版本服务,实现平滑上线。

我在项目中遇到过最典型的场景:最开始服务少,每个服务都暴露自己的端口,运维噩梦。后来上了网关,只要把域名指向网关,路由规则维护好,后面的事情就简单多了。嗯,这里要注意,网关不是万能药,你不能把所有逻辑都塞进去,否则它会变成一个单点瓶颈。

主流实现方式:Zuul vs Gateway

Java生态里,最常用的两种网关方案就是Netflix Zuul和Spring Cloud Gateway。很多人搞不清楚它们的区别,我来给你梳理一下。

对比维度 Zuul 1.x Spring Cloud Gateway
底层模型 Servlet阻塞式I/O WebFlux非阻塞响应式
性能 长连接场景效率低 高并发场景优势明显
长连接支持 不支持,只能HTTP短连接 原生支持WebSocket
路由配置 基于路径匹配,相对简单 基于路由谓词和过滤器链
过滤机制 Filter(pre/route/post/error) GatewayFilter + GlobalFilter
官方维护状态 已进入维护模式(2020年末) Spring官方积极维护
社区生态 丰富但逐渐向Spring迁移 与Spring Cloud深度集成

我个人习惯在新项目中首选Spring Cloud Gateway。为什么?因为Zuul从1.x开始就存在阻塞问题。虽然Zuul 2.x也做了非阻塞改造,但那时候Gateway已经站稳了脚跟。Spring官方直接推荐Gateway,咱们就没必要逆流而上了。

避坑提醒:我曾经有个团队还在用Zuul 1.x跑线上业务。赶上一次双11大促,网关直接撑不住了。后来分析发现,Zuul 1.x每个请求都占用一个Servlet线程,连接数上去之后线程池直接炸了。换到Gateway之后,同样机器配置,性能提升了接近3倍。选择技术方案时,不只看功能,还得看并发场景。

路由配置:怎么玩转路径分发

路由是网关的基础功能。你配置的每一条路由,都在告诉网关:“这个请求,应该交给谁处理。”Gateway的配置有两种方式,YAML和Java DSL。我更喜欢YAML,因为配置清晰,好维护。

来看一段典型的路由配置:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Gateway-Request, user-gateway

        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

这段配置里,我解释几个关键点:

  • lb:// 表示负载均衡地址,Gateway会通过服务发现拿到实例列表
  • StripPrefix=1 去掉一级路径前缀,比如 /api/user/list 变成 /user/list
  • Path 是谓词,表示匹配哪些路径的请求才走这条路由
  • RequestRateLimiter 是一个内置的限流过滤器,基于令牌桶算法

为什么不把所有服务写在一个路由里?你想想看,如果所有请求都匹配同一个路由,那就没法做精细化控制。不同的服务有不同的限流配置、认证策略,分路由配置才是最合理的方式。

过滤机制:在请求的路上做手脚

过滤器是网关的灵魂。它让你在请求进来之后、响应出去之前,执行一些自定义逻辑。比如校验Token、改写请求参数、记录日志、加解密等。

Gateway的过滤器分两种:

  • 局部过滤器(GatewayFilter):只作用在特定路由上,配置在路由的filters中
  • 全局过滤器(GlobalFilter):作用于所有路由,需要写Java代码

我来写一个全局过滤器的示例,用于记录请求耗时:

@Component
public class TimeFilter implements GlobalFilter, Ordered {

    private static final Logger log = LoggerFactory.getLogger(TimeFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long startTime = System.currentTimeMillis();
        String path = exchange.getRequest().getURI().getPath();

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            long cost = System.currentTimeMillis() - startTime;
            log.info("请求路径: {}, 耗时: {}ms", path, cost);
        }));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

这段代码其实很简单。关键点在于 getOrder() 方法,返回值越小,优先级越高,执行顺序越靠前。我曾经踩过一个坑:过滤器顺序没控制好,导致认证过滤器比限流过滤器后执行。结果大量非法请求先触发了限流,把正常用户的额度都占用了。自那以后,我一直把认证和限流过滤器的order值严格定义好。

小技巧:在Gateway里实现一个“动态路由”也很简单。你可以写一个GlobalFilter,从请求Header中读取版本号,然后动态修改路由目标。这样就能实现灰度发布,不用每次改配置重启网关。

限流策略:保护后端不被打垮

限流是网关的必修课。为什么?因为你永远不知道客户端会发起什么样的流量。一次促销活动,甚至一次恶意攻击,都可能让你的后端服务瞬间崩溃。

常见的限流算法有四种:

算法 原理 优点 缺点 适用场景
计数器 单位时间内累计请求数,超过阈值就拒绝 实现最简单 临界点突刺,无法平滑处理 不推荐线上使用
滑动窗口 把时间窗口切分成小格子,滑动统计 比计数器更平滑 粒度越细,内存消耗越大 精度不高的限流场景
令牌桶 一定速率往桶里放令牌,请求消耗令牌 允许一定的突发流量 需要合理配置参数 大多数业务场景
漏桶 请求先进入桶内,以固定速率流出 流出速率恒定,保护下游 不能应对突发 保护数据库、消息队列

Gateway默认集成了基于令牌桶的限流器 RequestRateLimiter,配合Redis使用非常方便。配置刚才在路由部分已经展示过了,我再补充一个自定义KeyResolver:

@Bean
public KeyResolver userKeyResolver() {
    return exchange -> {
        // 从Header中获取用户ID,如果没有则取客户端IP
        String userId = exchange.getRequest().getHeaders()
            .getFirst("X-User-Id");
        if (StringUtils.isBlank(userId)) {
            userId = exchange.getRequest().getRemoteAddress()
                .getAddress().getHostAddress();
        }
        return Mono.just(userId);
    };
}

为什么用Redis配合令牌桶?因为限流状态需要分布式共享。如果网关多实例部署,每个实例各自限流,那总流量就等于实例数乘以单机限流阈值,完全失控了。

注意:限流参数的设置不要拍脑袋。我建议先压测你的后端服务,算出单实例能承受的最大QPS,再乘以实例数,打个七八折作为网关的限流值。留点余量,给系统一点喘息空间。

再来一张图,把网关定位看清楚

光说文字可能有点干。我画一张图,把API网关在微服务架构中的位置和作用说清楚。

API网关在微服务架构中的位置 客户端请求 API 网关 路由·认证·限流·日志·协议转换 后端微服务集群 用户服务 /api/user/** 订单服务 /api/order/** 支付服务 /api/pay/** 说明:客户端只与网关通信,不直接访问后端服务 网关统一处理横切关注点后再转发到具体服务

这张图展示了典型的网关部署模型。客户端只认网关这一个地址,后面有多少服务、服务内部怎么调用,客户端完全不需要知道。网关把横切关注点都处理完之后,再精准地把请求路由到对应服务。这就是解耦的魅力。

小结一下核心要点

API网关这块,面试官最常问的就是这几个问题:

  1. 为什么需要网关? 统一入口、协议转换、集中管控、灰度发布。一句话:不要在架构中裸奔。
  2. Zuul和Gateway怎么选? 新项目无脑Gateway,老项目如果还在用Zuul 1.x,尽快迁移。
  3. 路由和过滤怎么配置? 路由用YAML或DSL配置,过滤分局部和全局,全局需实现GlobalFilter接口。
  4. 限流怎么做? 推荐令牌桶+Redis分布式限流,参数设置要基于压测数据,别拍脑袋。

我一直在强调一个观点:网关不是一个BFF(后端给前端用)那么简单。它是整个系统的第一道防线,也是架构中的核心枢纽。把网关设计好,后面服务的容错压力会小很多。面试的时候,只要你把这些点讲得透彻,再加一两个亲身经历的坑,考官一定会对你印象深刻。