服务网关设计: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 事件回调通知)@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 能组合多种条件,面试常问的匹配优先级:
// 精确路径优先于通配路径
// 匹配顺序:/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 模式的实现细节:
-- 简化版 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 轮换机制。
// 网关层: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 实现):
-- 令牌桶 (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-- 漏桶 (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.x | Spring Cloud Gateway | Kong |
|---|---|---|---|
| 线程模型 | Servlet 同步阻塞 | WebFlux 异步非阻塞 | Nginx 事件驱动 |
| 性能 | 一般 | 优秀(Zuul 的 3-5 倍) | 极致(C 底层) |
| 集成度 | 低(2.x 已停更) | 与 Spring 生态无缝 | 独立部署,需运维 |
| 插件生态 | 有限 | Route + Filter 灵活 | 200+ 插件 |
| 适用场景 | 不推荐使用 | 中小型 Spring 团队 | 大型流量入口 |
| 限流实现 | 需自行开发 | 内置 RedisRateLimiter | 内置 rate-limiting 插件 |
| 熔断机制 | Hystrix 集成 | Resilience4j 集成 | 需插件或外部组件 |
| 动态配置 | 不支持 | 支持(结合 Nacos/Consul) | 支持(Admin API) |
面试回答路线图:
- 选型对比:先说线程模型差异(同步 vs 异步 vs 事件驱动),再说性能数据,最后说集成成本
- 四层过滤链:路由→鉴权→限流→熔断,每一层为什么放在这个位置,顺序能不能换
- 限流算法:令牌桶 vs 漏桶,给出 Redis Lua 实现,说明为什么选令牌桶
- 熔断状态机:CLOSED→OPEN→HALF_OPEN 的状态转换,触发条件,HALF_OPEN 的试探策略
- 鉴权陷阱:JWT 只做签名校验不下沉权限到网关,Token 吊销方案,请求头大小限制
网关选型没有银弹。Spring Cloud Gateway 适合大多数 Java 技术栈团队,Kong 适合流量大(万级 QPS 以上)或需要独立网关团队的场景。比选型更重要的是网关的职责边界——只做认证不做细粒度鉴权,只做路由不做业务逻辑,用四层过滤链把路由、鉴权、限流、熔断拆清楚,才是生产级网关的正确打开方式。
— 📚 小杰