Skip to content

服务网关设计:Gateway vs Zuul vs Kong,路由/限流/熔断/鉴权一体化

为什么需要网关层?

微服务架构中,服务数量从几个增长到几十个,每个服务都有自己的 IP 和端口。如果客户端直接调用各个服务,会面临几个现实问题:

  • 每个服务都需要独立处理认证鉴权,重复代码多,安全策略难统一
  • 客户端需要知道所有服务的地址,服务地址变更时客户端也得改
  • 缺乏统一的流量控制,某个服务被刷流量时没有防护
  • 协议转换、日志记录、跨域处理等横切关注点散落在各服务中

网关层作为统一入口层,把路由转发、限流熔断、鉴权、日志、协议转换集中到一层,让后端服务只需要关注业务逻辑。

三种主流网关方案对比

Zuul 1.x:Netflix 的先行者

Zuul 1.x 是早期的微服务网关实现,基于 Servlet 同步阻塞 I/O。每个请求占用一个 Tomcat 线程,线程在等待后端响应期间一直阻塞。在高并发场景下,线程池耗尽导致新请求被拒绝——这是典型的 C10K 问题

核心问题剖析:Zuul 1.x 的线程模型决定了它的并发天花板。假设 Tomcat 线程池 200 个线程,每个请求平均响应时间 100ms,理论最大吞吐量是 2000 QPS。如果后端某个服务变慢到 2 秒,线程池瞬间被占满,吞吐量降到 100 QPS,活生生把并发干成了串行。

微信读书的 2018 年事故:某团队使用 Zuul 1.x 作为网关,节假日大促时后端支付服务 RT 从 50ms 飙到 3s,网关线程池 400 线程被全部占满,Tomcat 拒绝新连接,前端大量 502。事后复盘发现:Zuul 的线程池和后端服务直连,没有独立的线程池隔离,一个后端拖慢拖死整个网关。

Zuul 2.x 改用了 Netty 异步非阻塞架构,但社区活跃度低,Spring 官方也没有推出对应的集成,实际生产中使用 Zuul 2.x 的团队很少。Netflix 自己后来也转向了 Spring Cloud Gateway + Hystrix 的组合。

Spring Cloud Gateway:Spring 生态的官方选择

Spring Cloud Gateway 基于 WebFlux(Netty)异步非阻塞模型,采用 Reactor 响应式编程范式。它的线程模型是事件驱动的,少量线程处理大量并发连接,性能是 Zuul 1.x 的 3-5 倍。

线程模型对比图:

Zuul 1.x 线程模型(同步阻塞):
请求 → Tomcat 线程池 → 阻塞等待后端响应 → 线程释放
        线程1: ████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████ (等后端)
        线程2: ████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████
        线程3: ████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████
        请求堆积: ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄

Gateway 线程模型(事件驱动,Netty):
EventLoop 线程1: ██ request ██ ██ response ██ ██ request ██ ██ response ██
EventLoop 线程2: ██ request ██ ██ response ██ ██ request ██ ██ response ██
EventLoop 线程3: ██ request ██ ██ response ██ ██ request ██ ██ response ██
(线程不等待,IO 事件回调通知)
java
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        // 路由:按路径匹配
        .route("order-service", r -> r
            .path("/api/order/**")
            .filters(f -> f
                // 限流:令牌桶算法,每秒 10 个请求,突发 20 个
                .requestRateLimiter(config -> config
                    .setRateLimiter(redisRateLimiter())
                    .setKeyResolver(hostAddrKeyResolver()))
                // 熔断:慢调用比例超过 50% 触发
                .circuitBreaker(config -> config
                    .setName("orderServiceCB")
                    .setFallbackUri("forward:/fallback/order"))
                // 添加请求头传递用户信息
                .addRequestHeader("X-Auth-User-Id", "userId"))
            .uri("lb://order-service"))
        // 路由:按 Host 匹配
        .route("product-service", r -> r
            .host("product.example.com")
            .uri("lb://product-service"))
        .build();
}

Route Predicate 体系详解:Gateway 的 Predicate 能组合多种条件,面试常问的匹配优先级:

java
// 精确路径优先于通配路径
// 匹配顺序:/api/order/detail > /api/order/** > /api/**
// 多个 Predicate 同时满足(AND 逻辑)
.route("specific-route", r -> r
    .path("/api/order/**")
    .and().header("X-Region", "cn-shanghai")
    .and().method(HttpMethod.GET)
    .uri("lb://order-service"))

Filter 执行顺序的坑:Pre 过滤器按 Ordered 正序执行,Post 过滤器按逆序执行。这意味着如果两个 Filter 都设置了 Ordered.LOWEST_PRECEDENCE,Pre 阶段先注册的先执行,Post 阶段后注册的先执行。这个细节在面试中经常被问,也是排查问题的关键——如果日志顺序不对,先检查 Filter 的 getOrder() 返回值。

Filter 链时序图:

    请求到达


  ┌──────────┐
  │ Pre Filter 1 │  ← 鉴权 (order=-100)
  └─────┬────┘

  ┌──────────┐
  │ Pre Filter 2 │  ← 限流 (order=0)
  └─────┬────┘

  ┌──────────┐
  │ Pre Filter 3 │  ← 请求改写 (order=10)
  └─────┬────┘

   ┌────┴────┐
   │ 路由转发  │
   └────┬────┘

  ┌───────────┐
  │ Post Filter 3│  ← 响应改写 (order=10, 逆序最先)
  └──────┬───┘

  ┌───────────┐
  │ Post Filter 2│  ← 日志 (order=0, 逆序)
  └──────┬───┘

  ┌───────────┐
  │ Post Filter 1│  ← 响应头处理 (order=-100, 逆序最后)
  └──────┬───┘


     返回客户端

Kong:基于 OpenResty 的高性能网关

Kong 基于 OpenResty(Nginx + Lua),底层是 C 语言,性能最极致。它采用插件化架构,官方插件市场有 200+ 插件覆盖认证、限流、日志、监控等功能。

Kong 的部署模式比较重:需要独立部署,依赖 PostgreSQL 或 Cassandra 存储配置,运维成本高于 Spring Cloud Gateway。但它的性能优势明显,适合流量极大(万级 QPS 以上)的场景。

Kong 的限流插件实现原理:Kong 的 rate-limiting 插件支持本地计数器(内存)和集群计数器(Redis),默认使用滑动窗口算法。Redis 模式的实现细节:

lua
-- 简化版 Kong 滑动窗口限流(Redis 版)
-- 窗口大小 60 秒,允许 100 次请求
local function sliding_window(red, key, limit, window_sec)
    local now = ngx.time()
    local window_start = now - window_sec
    -- 清理过期数据
    red:zremrangebyscore(key, 0, window_start)
    -- 当前窗口请求数
    local count = red:zcard(key)
    if count >= limit then
        return false, "rate limit exceeded"
    end
    -- 记录当前请求
    red:zadd(key, now, now .. "_" .. math.random())
    red:expire(key, window_sec * 2)  -- 2 倍过期时间防毛刺
    return true
end

踩坑:Kong 的 PostgreSQL 模式有个经典问题——配置变更需要 reload 而非 hot reload,高峰期 reload 会丢几十毫秒的连接。某大厂踩过这个坑,解决方案是改用声明式配置文件(kong.yml)配合 kong reload 脚本,把变更窗口控制在 50ms 以内。

一体化网关设计:四层过滤链

生产级的网关不是简单的"请求转发",而是完整的过滤链,通常分四层:

客户端请求


┌──────────────────────────────┐
│  第一层:路由匹配              │
│  Path / Host / Header 匹配    │
│  匹配失败 → 404               │
└──────────┬───────────────────┘

┌──────────────────────────────┐
│  第二层:鉴权认证              │
│  JWT 签名校验 / Token 解析    │
│  鉴权失败 → 401               │
└──────────┬───────────────────┘

┌──────────────────────────────┐
│  第三层:限流                  │
│  令牌桶 / 滑动窗口 / 漏桶      │
│  超限 → 429                   │
└──────────┬───────────────────┘

┌──────────────────────────────┐
│  第四层:熔断降级              │
│  慢调用 / 异常比例触发         │
│  熔断 → 503 + fallback        │
│  状态机:CLOSED → OPEN → HALF_OPEN → CLOSED
└──────────┬───────────────────┘

┌──────────────────────────────┐
│  转发层                       │
│  负载均衡 + 重试 + 超时        │
│  → 后端服务                   │
└──────────────────────────────┘

熔断器状态机细节(面试高频考点):

        ┌───────────────────────────────┐
        │                               │
        ▼                               │
   ┌─────────┐    失败阈值(50%)  ┌──────────┐
   │ CLOSED  │ ────────────────→ │   OPEN   │
   │ (正常)  │                   │ (熔断中)  │
   └────┬────┘                   └─────┬────┘
        │                              │
        │ 成功计数重置                  │ 超时(30s)
        │                              │
        │                    ┌──────────▼────────┐
        │                    │    HALF_OPEN       │
        │                    │ (试探性放行1个请求) │
        │                    └──────────┬─────────┘
        │                               │
        │                    ┌──────────┴──────────┐
        │                    │ 成功 → CLOSED       │
        │                    │ 失败 → OPEN (重新计时) │
        └────────────────────┴─────────────────────┘

CLOSED 状态:正常转发请求,维护一个滑动窗口(比如最近 10 秒的请求失败率),当失败率超过阈值(如 50%)且调用量达到最小计数(如 20 次),切换到 OPEN。

OPEN 状态:直接返回 fallback,不转发请求。持续一段时间(默认 30 秒),之后切换到 HALF_OPEN。

HALF_OPEN 状态:放行一个试探性请求。如果成功,切换到 CLOSED;如果失败,重新回到 OPEN 并重启计时器。

面试追问:HALF_OPEN 状态只放行一个请求够吗?假如这个请求刚好是慢请求(不走业务逻辑),会误判。Resilience4j 的解决方案是 permittedNumberOfCallsInHalfOpenState 配置,默认 10 次,取多数成功/失败决策。

网关鉴权的性能陷阱

网关层最常见的性能瓶颈不是代理转发,而是鉴权逻辑的复杂度

错误做法:网关层每次请求都查数据库做用户权限校验。 正确做法:网关层只做 Token 签名校验和过期验证,权限校验下沉到业务服务。

JWT 鉴权全流程时序图:

Client                    Gateway                    Auth Service              Business Service
  │                         │                           │                         │
  │  POST /login            │                           │                         │
  │ ──────────────────────→ │  POST /api/auth/login      │                         │
  │                         │ ─────────────────────────→ │                         │
  │                         │                           │ 验证用户名密码           │
  │                         │                           │ 生成 JWT(含角色)         │
  │                         │  JWT {sub, roles, exp}     │                         │
  │                         │ ←───────────────────────── │                         │
  │  Set-Cookie: Bearer JWT │                           │                         │
  │ ←────────────────────── │                           │                         │
  │                         │                           │                         │
  │  GET /api/order/123     │                           │                         │
  │  Authorization: Bearer  │                           │                         │
  │ ──────────────────────→ │                           │                         │
  │                         │ JWT 签名校验(本地)       │                         │
  │                         │ 检查 exp 过期时间          │                         │
  │                         │ 提取 sub + roles           │                         │
  │                         │ X-Auth-User-Id: uid       │                         │
  │                         │ X-Auth-Roles: role        │                         │
  │                         │ ───────────────────────────────────────────────────→ │
  │                         │                           │  校验权限(如 order:read) │
  │                         │                           │  (不查数据库, 只比较角色) │
  │                         │ 200 OK + order data       │                         │
  │  ← 200 OK              │ ←─────────────────────────────────────────────────── │
  │                         │                           │                         │

JWT 的 token 吊销问题:JWT 签发后无法主动吊销,除非引入黑名单机制。某团队在网关层维护了一个 Redis 黑名单,每次鉴权时先查黑名单再验签名。问题在于黑名单的过期时间:如果 JWT 过期时间 2 小时,黑名单里的 token 也得存 2 小时,高峰期 Redis 内存暴涨 30GB。解决方案:缩短 JWT 过期时间(15 分钟),配合 refresh token 轮换机制。

java
// 网关层:JWT 解析 + 缓存校验,不查数据库
@Component
public class JwtAuthFilter implements GlobalFilter, Ordered {

    private final ReactiveRedisTemplate<String, String> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders()
            .getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "Missing token");
        }

        try {
            // 只做 JWT 签名校验和过期验证,不解密数据库
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(publicKey)
                .build()
                .parseClaimsJws(token.replace("Bearer ", ""))
                .getBody();

            // 将用户信息注入请求头,透传给下游
            ServerWebExchange mutated = exchange.mutate()
                .request(r -> r.header("X-Auth-User-Id", claims.getSubject())
                               .header("X-Auth-Roles", claims.get("roles", String.class)))
                .build();
            return chain.filter(mutated);
        } catch (JwtException e) {
            return unauthorized(exchange, "Invalid token");
        }
    }

    @Override
    public int getOrder() {
        return -100; // 最先执行
    }
}

事故案例:某团队将所有鉴权放在网关层,每次请求都查询用户权限表,网关 Redis 缓存命中率只有 70%,高峰期网关线程池膨胀到 1000+ 线程,最终 OOM。解决方案:JWT 中嵌入角色信息,网关只做签名校验,权限校验下沉到业务服务,网关线程数降到 50 以内。

另一个真实踩坑:某团队在网关层使用 JWT 后,发现 Bearer 头的 token 长度超过 2KB(因为嵌入了全部用户信息),导致 HTTP 请求头超限(Nginx 默认 large_client_header_buffers 4 个 8KB,实际 Header 值超过 8KB 导致 400 错误)。解决方案:JWT 只放最小必要信息(sub/roles/exp),用户详情由业务服务通过 RPC 获取。

网关限流算法选择

网关限流常见的三种实现及其适用场景:

算法实现方式精度流量整形突刺处理适用场景
令牌桶定时生成令牌,消费令牌允许一定突发大多数场景,突发可接受
漏桶恒定速率流出,超出丢弃完全平滑保护下游,要求严格平稳
滑动窗口时间窗口内计数不允许突发精准限流,控制总调用量

令牌桶漏桶代码对比(Redis Lua 实现):

lua
-- 令牌桶 (Redis Lua)
-- 每 100ms 生成 1 个令牌,桶容量 10
local key = KEYS[1]
local rate = tonumber(ARGV[1])    -- 每秒生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])     -- 当前时间戳(ms)

local tokens = redis.call("hget", key, "tokens")
local last_refill = redis.call("hget", key, "last_refill")

if not tokens then
    tokens = capacity
    last_refill = now
end

-- 计算需要补充的令牌
local elapsed = (now - last_refill) / 1000
local new_tokens = math.min(capacity, tokens + elapsed * rate)
last_refill = now

if new_tokens >= 1 then
    redis.call("hset", key, "tokens", new_tokens - 1, "last_refill", last_refill)
    redis.call("expire", key, 2)  -- 防内存泄漏
    return 1  -- 放行
else
    redis.call("hset", key, "tokens", new_tokens, "last_refill", last_refill)
    return 0  -- 限流
end
lua
-- 漏桶 (Redis Lua)
-- 恒定流出速率 5 req/s,桶容量 10
local key = KEYS[1]
local rate = tonumber(ARGV[1])     -- 每秒处理速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])

local water = redis.call("hget", key, "water")
local last_leak = redis.call("hget", key, "last_leak")

if not water then
    water = 0
    last_leak = now
end

-- 先漏水:根据时间差计算漏掉了多少
local elapsed = (now - last_leak) / 1000
local leaked = elapsed * rate
water = math.max(0, water - leaked)
last_leak = now

-- 判断能否加水
if water < capacity then
    water = water + 1
    redis.call("hset", key, "water", water, "last_leak", last_leak)
    redis.call("expire", key, 2)
    return 1  -- 放行
else
    redis.call("hset", key, "water", water, "last_leak", last_leak)
    return 0  -- 丢弃
end

关键区别:令牌桶允许突发(桶里有积攒的令牌),漏桶强制平滑(不管流量怎么来,出去的速率恒定)。面试中如果被问"你的网关怎么限流",回答"令牌桶,因为微服务场景允许短时突发"比说"漏桶"更合理——后端服务通常能承受瞬时压力,但受不了持续过载。

2025 年的趋势:网关 + 服务网格互补

2025 年,网关的职责变得清晰:

  • 网关(南北向流量):外部到内部的流量入口,负责外部认证、协议转换、限流
  • Service Mesh(东西向流量):服务间通信的流量治理,负责熔断、重试、超时、负载均衡

这种分工让网关职责减半,但也要求网关必须支持 gRPC-Web 转接HTTP/2 协议降级。如果团队使用了 Istio 做 Service Mesh,网关层只需要关注安全策略和外部流量管理,内部服务的治理逻辑全部交给 Sidecar。

真实场景案例:某电商团队在 2024 年将网关从 Spring Cloud Gateway 迁移到 Contour(基于 Envoy 的 Kubernetes Ingress Controller),配合 Istio 做服务间通信。原来网关层的熔断逻辑全部下沉到 Istio 的 DestinationRule,网关配置从 2000 行 YAML 减到 300 行。但代价是:团队需要学习 Envoy 的 xDS 协议和 Istio 的 VirtualService 配置,运维复杂度从"Spring 全家桶"变成了"K8s 全家桶"。

总结

维度Zuul 1.xSpring Cloud GatewayKong
线程模型Servlet 同步阻塞WebFlux 异步非阻塞Nginx 事件驱动
性能一般优秀(Zuul 的 3-5 倍)极致(C 底层)
集成度低(2.x 已停更)与 Spring 生态无缝独立部署,需运维
插件生态有限Route + Filter 灵活200+ 插件
适用场景不推荐使用中小型 Spring 团队大型流量入口
限流实现需自行开发内置 RedisRateLimiter内置 rate-limiting 插件
熔断机制Hystrix 集成Resilience4j 集成需插件或外部组件
动态配置不支持支持(结合 Nacos/Consul)支持(Admin API)

面试回答路线图

  1. 选型对比:先说线程模型差异(同步 vs 异步 vs 事件驱动),再说性能数据,最后说集成成本
  2. 四层过滤链:路由→鉴权→限流→熔断,每一层为什么放在这个位置,顺序能不能换
  3. 限流算法:令牌桶 vs 漏桶,给出 Redis Lua 实现,说明为什么选令牌桶
  4. 熔断状态机:CLOSED→OPEN→HALF_OPEN 的状态转换,触发条件,HALF_OPEN 的试探策略
  5. 鉴权陷阱:JWT 只做签名校验不下沉权限到网关,Token 吊销方案,请求头大小限制

网关选型没有银弹。Spring Cloud Gateway 适合大多数 Java 技术栈团队,Kong 适合流量大(万级 QPS 以上)或需要独立网关团队的场景。比选型更重要的是网关的职责边界——只做认证不做细粒度鉴权,只做路由不做业务逻辑,用四层过滤链把路由、鉴权、限流、熔断拆清楚,才是生产级网关的正确打开方式。

— 📚 小杰

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。