网关层优化: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);  // 触发熔断的最小请求数

降级策略我一般分三级:

  1. 一级降级:返回缓存数据或默认值(比如商品库存显示「紧张」而非精确数字)
  2. 二级降级:直接返回错误提示(比如「服务繁忙,请稍后重试」)
  3. 三级降级:静默丢弃请求(只记录日志,不返回任何响应)

注意:熔断恢复后,一定要做「预热」。我见过一个案例:熔断刚恢复,所有积压的请求瞬间涌入,直接把后端又打挂了。正确的做法是:半开状态下只放行少量请求,确认服务正常后再逐步放开。

请求路由与负载均衡

路由规则我一般分三层:

  • 第一层:路径匹配 —— /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的缓存。

网关层知识体系总览

下面这张图把网关层的核心模块和它们之间的关系梳理清楚了:

API网关层知识体系 API网关层知识体系 API 网关 限流策略 熔断与降级 路由与负载均衡 网关缓存 令牌桶 漏桶 滑动窗口 熔断器 降级策略 半开恢复 路径匹配 一致性哈希 灰度路由 L1本地缓存 L2分布式缓存 TTL与刷新 四大模块协同工作,保障后端服务稳定高效

网关层的优化,说白了就是「把流量管好,把风险挡住」。限流防止突发流量冲垮后端,熔断防止故障蔓延,路由保证请求正确分发,缓存减少后端压力。这四个模块配合好了,你的系统就能扛住绝大多数流量冲击。

我最后想强调一点:网关层的配置一定要可动态调整。你永远不知道线上会发生什么。限流阈值、熔断时间窗口、路由权重,这些都应该支持运行时修改,而不是改代码重启。我习惯把这些配置放在配置中心(如Nacos、Apollo),配合监控大盘实时调整。