Gateway 高级主题
本文汇总 Gateway 的进阶能力与生产经验:限流原理、WebSocket/gRPC 代理、性能调优与一线踩坑。
令牌桶限流实现原理
算法模型
┌──────────────┐
│ 令牌桶容量 N │
│ (burstCapacity)│
└──────┬───────┘
│ 每秒补充 replenishRate 个令牌
▼
请求进入 → 获取 1 个令牌(requestedTokens)
├─ 有令牌 → 放行
└─ 无令牌 → 429 拒绝| 参数 | 含义 | 示例 |
|---|---|---|
| replenishRate | 每秒补充速率(稳态 QPS) | 10 |
| burstCapacity | 桶容量(突发上限) | 20 |
| requestedTokens | 每请求消耗令牌 | 1 |
Redis Lua 脚本实现(原子操作)
Gateway 的 RedisRateLimiter 用 Lua 脚本在 Redis 中原子执行令牌桶逻辑:
lua
-- 简化版令牌桶脚本
local tokens_key = KEYS[1] -- 当前令牌数
local timestamp_key = KEYS[2] -- 上次补充时间
local rate = tonumber(ARGV[1]) -- replenishRate
local capacity = tonumber(ARGV[2]) -- burstCapacity
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- 1. 读取当前令牌数与上次时间
local last_tokens = tonumber(redis.call("get", tokens_key) or capacity)
local last_refreshed = tonumber(redis.call("get", timestamp_key) or 0)
-- 2. 计算补充的令牌数
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
-- 3. 判断是否放行
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
-- 4. 写回
redis.call("setex", tokens_key, window, new_tokens)
redis.call("setex", timestamp_key, window, now)
-- 5. 返回 [是否放行, 剩余令牌]
return { allowed, new_tokens }原子性保证:并发请求下令牌扣除不超发。
限流键粒度
java
// 按 IP
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
// 按用户
exchange.getRequest().getHeaders().getFirst("X-User-Id")
// 按接口
exchange.getRequest().getURI().getPath()KeyResolver 返回值即 Redis key 后缀,粒度决定限流对象。
生产限流要点
- 网关限流 + 服务端限流双层(网关粗粒度防滥用,服务端细粒度保业务)
- Redis 高可用(哨兵/集群),Redis 故障时限流器行为需评估(默认放行或拒绝)
- 按业务区分:登录接口严格限流,读接口宽松
WebSocket 代理
配置
yaml
spring:
cloud:
gateway:
routes:
- id: websocket-route
uri: ws://chat-service:9000 # ws:// 或 wss://
predicates:
- Path=/ws/**代理机制
客户端 WebSocket 连接
│
▼
Gateway(WebsocketRoutingFilter,order MAX-1,先于 HTTP 转发)
│
├─ 1. 识别 Upgrade: websocket 请求
├─ 2. 与下游建立 WebSocket 连接
└─ 3. 双向转发帧(消息透传)HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.getUpgrade() != null && "websocket".equals(headers.getUpgrade().toLowerCase())) {
// 走 WebSocket 代理
return handleWebsocket(...);
}WebSocket 代理注意点
- 下游地址必须是
ws:///wss://协议 - 鉴权:鉴权 GlobalFilter 需在 WebsocketRoutingFilter 之前(order 小于 2147483646)
- 心跳:WebSocket 长连接需客户端/服务端保活机制
- 断线重连:客户端侧处理
gRPC 代理
Gateway 本身不直接代理 gRPC(HTTP/2 二进流),常用两种方案:
方案一:HTTP/2 + 转发
yaml
spring:
cloud:
gateway:
httpclient:
h2c: true # 下游 gRPC 服务开启 h2cgRPC 是基于 HTTP/2 的二进制协议,Gateway 配置 h2c 后作为透明转发层。
方案二:stubby + grpc-gateway(推荐)
gRPC 生态提供 HTTP/1.1 JSON 桥接:
客户端 HTTP/JSON → Gateway → grpc-gateway(转换)→ gRPC 服务yaml
- id: grpc-route
uri: http://grpc-bridge:8080 # grpc-gateway 桥接服务
predicates:
- Path=/v1/**选型建议
| 场景 | 方案 |
|---|---|
| 内部服务 gRPC 直连 | 跳过网关,走服务发现 |
| 对外暴露 gRPC API | h2c 透明转发 |
| 对外 JSON API + 内部 gRPC | grpc-gateway 桥接 |
性能调优
线程与事件循环
yaml
spring:
cloud:
gateway:
httpclient:
threads: 32 # Netty 工作线程数(默认 CPU×2)
pool:
type: fixed
max-connections: 1000Netty 事件循环线程数 ≈ CPU 核数 × 2
业务阻塞操作必须异步化(不要在线程内 sleep/同步 IO)关键调优项
| 项 | 建议 |
|---|---|
| 事件循环线程 | 默认即可,勿过度调大(避免上下文切换) |
| 连接池 | max-connections 与下游吞吐匹配,acquire-timeout 适中 |
| 响应超时 | 避免全局超时过短导致误伤慢接口 |
| 日志 | 生产关闭 DEBUG 级访问日志 |
| 压缩 | 开启 response 压缩(大 JSON 传输收益明显) |
| 内存 | 关注 heap 与 direct memory(Netty 用堆外内存) |
性能基准参考
| 配置 | 吞吐量(简单透传) |
|---|---|
| 单实例默认 | 数万 TPS 级别(视机器) |
| 瓶颈点 | 连接池、事件循环、下游能力 |
Gateway 性能远高于阻塞式 Zuul,通常不会成为瓶颈;真正的瓶颈在下游服务。
生产踩坑集
坑 1:Spring MVC 与 Gateway 冲突
错误:Gateway 工程引入了 spring-boot-starter-web
表现:DispatcherServlet 冲突,路由不生效
解决:Gateway 工程只用 WebFlux,不引入 MVC starter坑 2:RewritePath 的 $ 转义
yaml
# 错误($ 被 YAML 吞掉)
- RewritePath=/api/order/(?<seg>.*), /$seg
# 正确(转义)
- RewritePath=/api/order/(?<seg>.*), /$\{seg}坑 3:lb:// 路由 503
表现:lb:// 路由一直 503
原因:未引入注册中心客户端,或服务不在注册中心
排查:LoadBalancerClientFilter 是否执行、实例列表是否为空坑 4:大 body 限流/日志丢失
表现:请求体大的接口日志空、限流不生效
原因:Body 被过滤器消费一次后为空(Flux 不可重复读)
解决:CacheRequestBodyGlobalFilter 缓存 body,或过滤器在转发前读取坑 5:超时配置不生效
yaml
# 注意单位差异
response-timeout: 30s # Duration 类型
connect-timeout: 10000 # 毫秒(int)坑 6:WebSocket 鉴权失效
表现:WebSocket 连接绕过鉴权
原因:WebsocketRoutingFilter order 极大,鉴权 GlobalFilter 若 order 更大则后执行
解决:鉴权过滤器 order 设为负数(早执行)监控与告警
yaml
management:
endpoints:
web:
exposure:
include: gateway, health, metrics, prometheus常用指标:
| 指标 | 意义 |
|---|---|
| gateway.requests | 路由请求计数(按 route/status 分桶) |
| httpclient 连接池指标 | 连接数、等待时长 |
| 全局 JVM/Netty 指标 | 线程、内存 |
Spring Cloud Gateway Actuator 指标:
路由级:每条路由的请求量/耗时/错误
HTTP: 按状态码/URI 的请求统计常见问题
- 令牌桶为什么允许突发? 桶容量(burstCapacity)缓存了空闲期积累的令牌,突发流量先消费令牌池。
- WebSocket 与 HTTP 路由如何共存? 不同 predicate(Path 前缀)区分,ws:// 与 http:// 各自路由。
- Gateway 吞吐不够怎么扩展? 水平扩展(多实例)+ 前置 LB;单体调优先查连接池与事件循环。
- Redis 挂了限流怎么处理? 默认 Lua 调用失败会放行(fail-open),如需 fail-closed 需自定义异常处理。