网关层优化:API网关的作用、限流策略、熔断与降级、请求路由与负载均衡、网关缓存
说到API网关,我脑子里第一个蹦出来的场景是——2018年双十一大促,我们有个核心服务直接被流量冲垮了。事后复盘才发现,如果当时在网关层做了限流和熔断,至少能保住80%的正常用户。嗯,从那以后,网关优化就成了我设计高并发系统的第一道防线。
说白了,API网关就是系统的「前厅接待」。所有外部请求都得先过它这一关。它不负责具体业务逻辑,但负责把流量管好、把风险挡住、把请求送到该去的地方。
API网关的核心职责
我习惯把网关的职责拆成四个维度:
- 安全防护:身份认证、IP黑白名单、防SQL注入
- 流量管控:限流、熔断、降级,防止后端被冲垮
- 路由转发:根据请求路径、Header、参数分发到不同后端服务
- 协议转换:比如把外部的HTTP/1.1转成内部的gRPC
你想想看,如果没有网关,每个微服务都得自己实现这些功能。那代码得多冗余?维护成本得多高?
我的经验:网关层不要做任何业务逻辑。一旦网关里塞了业务代码,它就变成了一个「胖网关」,升级、扩容、排障都会变得极其痛苦。我见过一个项目,网关里写了2000多行业务判断,最后重构时直接重写了整个网关层。
限流策略:令牌桶 vs 漏桶
限流是网关最核心的能力之一。我常用的两种算法:令牌桶和漏桶。很多人搞混它们,其实区别很直观。
| 特性 | 令牌桶 | 漏桶 |
|---|---|---|
| 核心思想 | 以固定速率生成令牌,请求拿到令牌才能通过 | 请求先进桶,桶以固定速率漏水(处理请求) |
| 应对突发流量 | 支持一定程度的突发(桶里可存令牌) | 不支持突发,桶满了直接丢弃 |
| 适用场景 | 允许短时间突发的业务(如秒杀、热点数据) | 要求平滑流量的场景(如数据库写入) |
| 实现复杂度 | 中等,需要定时生成令牌 | 较低,队列+定时器即可 |
我个人习惯用令牌桶。为什么?因为大部分业务场景都需要容忍突发流量。比如用户突然刷新页面,或者运营发了条推送,流量瞬间翻倍。令牌桶能扛住这种波动,而漏桶会直接拒绝。
来看一个简单的令牌桶实现(基于Redis):
// 伪代码:Redis令牌桶
public boolean tryAcquire(String key, int permits) {
String luaScript =
"local key = KEYS[1] " +
"local rate = tonumber(ARGV[1]) " + // 每秒生成令牌数
"local capacity = tonumber(ARGV[2]) " + // 桶容量
"local now = redis.call('TIME')[1] " +
"local lastTime = redis.call('GET', key .. ':time') " +
"if not lastTime then lastTime = now end " +
"local tokens = redis.call('GET', key .. ':tokens') " +
"if not tokens then tokens = capacity end " +
// 计算这段时间生成的令牌
"local delta = math.max(0, now - lastTime) " +
"tokens = math.min(capacity, tokens + delta * rate) " +
"redis.call('SET', key .. ':time', now) " +
"if tokens >= permits then " +
" redis.call('SET', key .. ':tokens', tokens - permits) " +
" return 1 " +
"else " +
" return 0 " +
"end";
// 执行Lua脚本
return redis.eval(luaScript, 1, key, rate, capacity);
}
避坑指南:我曾经在限流key的设计上踩过坑。如果按用户ID限流,大用户(比如爬虫)会占满配额。后来我改成「用户ID+接口路径」的组合key,既防止单个用户刷接口,又不会因为某个接口被刷而影响其他接口。
熔断与降级:别让故障蔓延
熔断和降级经常被混为一谈。我打个比方你就明白了:
- 熔断:电路过载时跳闸,保护整个电路。网关发现后端服务连续报错,直接切断对该服务的调用。
- 降级:主动关闭非核心功能,保证核心功能可用。比如大促时关闭「商品详情页的推荐模块」。
熔断有三个状态:关闭 → 打开 → 半开。我习惯用Hystrix或Sentinel来实现:
// Sentinel熔断规则配置示例
DegradeRule rule = new DegradeRule("userService")
.setGrade(RuleConstant.DEGRADE_GRADE_RT) // 按响应时间熔断
.setCount(500) // 响应时间超过500ms
.setTimeWindow(10) // 熔断后10秒进入半开状态
.setMinRequestAmount(5); // 触发熔断的最小请求数
降级策略我一般分三级:
- 一级降级:返回缓存数据或默认值(比如商品库存显示「紧张」而非精确数字)
- 二级降级:直接返回错误提示(比如「服务繁忙,请稍后重试」)
- 三级降级:静默丢弃请求(只记录日志,不返回任何响应)
注意:熔断恢复后,一定要做「预热」。我见过一个案例:熔断刚恢复,所有积压的请求瞬间涌入,直接把后端又打挂了。正确的做法是:半开状态下只放行少量请求,确认服务正常后再逐步放开。
请求路由与负载均衡
路由规则我一般分三层:
- 第一层:路径匹配 ——
/api/user/**路由到用户服务 - 第二层:Header匹配 —— 根据
X-Client-Version路由到不同版本的服务 - 第三层:权重路由 —— 灰度发布时,5%流量到新版本
负载均衡策略,我推荐用一致性哈希。为什么?因为网关层通常有缓存,如果请求每次都打到不同节点,缓存就废了。一致性哈希能保证同一个用户的请求始终路由到同一台后端机器。
// Spring Cloud Gateway + 一致性哈希配置
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb:http://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestHash
args:
hashHeader: X-User-Id # 按用户ID哈希
网关缓存:减少后端压力
网关缓存和业务缓存不一样。网关缓存只缓存那些变化不频繁、读多写少的数据。比如:
- 用户的验证结果(缓存5分钟)
- 商品的基础信息(缓存30秒)
- 配置中心的开关值(缓存1分钟)
我常用的缓存策略是「本地缓存+分布式缓存」两级:
- L1本地缓存:Caffeine,毫秒级响应,容量小
- L2分布式缓存:Redis,容量大,但多一次网络开销
查询时先查L1,没命中再查L2,L2还没命中才回源到后端服务。这样90%的请求在L1就能命中,延迟从10ms降到1ms以内。
我的习惯:网关缓存一定要设置合理的TTL,并且支持手动刷新。我曾经遇到过缓存了过期的配置,导致全站降级策略失效。后来我加了一个「缓存刷新接口」,运营或运维人员可以一键刷新指定key的缓存。
网关层知识体系总览
下面这张图把网关层的核心模块和它们之间的关系梳理清楚了:
网关层的优化,说白了就是「把流量管好,把风险挡住」。限流防止突发流量冲垮后端,熔断防止故障蔓延,路由保证请求正确分发,缓存减少后端压力。这四个模块配合好了,你的系统就能扛住绝大多数流量冲击。
我最后想强调一点:网关层的配置一定要可动态调整。你永远不知道线上会发生什么。限流阈值、熔断时间窗口、路由权重,这些都应该支持运行时修改,而不是改代码重启。我习惯把这些配置放在配置中心(如Nacos、Apollo),配合监控大盘实时调整。