2. API网关与限流:网关架构模式、令牌桶与漏桶算法、分布式限流实现

2.1 API网关架构模式

API网关是微服务架构的流量入口,承担路由、认证、限流、监控等核心职责。当前主流网关包括 Kong(基于 OpenResty/Nginx)和 APISIX(基于 Lua + etcd),两者均采用插件化架构,但设计理念存在差异。

对比维度 Kong APISIX
配置存储 PostgreSQL / Cassandra etcd(分布式一致性)
热加载 DB 轮询 + 缓存刷新 etcd watch 实时推送
路由匹配 正则 + 优先级 前缀树 + 变量优先级
插件语言 Lua(自定义需写 C 扩展) Lua + Wasm + Java/Python 扩展
性能基准 ~20K QPS(单核) ~30K QPS(单核)
选型建议: 若团队已有 PostgreSQL 运维经验且对实时性要求不高,Kong 生态更成熟;若追求毫秒级配置生效、高并发场景,APISIX 的 etcd 架构优势明显。

2.2 网关请求处理流程

下图展示 API 网关在限流场景下的核心处理链路:

API网关限流处理流程 API网关限流处理流程 客户端 路由匹配 限流插件 后端服务 429 拒绝 说明:限流插件根据令牌桶/漏桶算法判断是否放行,超限请求直接返回 429 Too Many Requests

2.3 令牌桶算法与漏桶算法

限流算法的核心是控制请求速率,令牌桶和漏桶是两种最经典的实现方式,两者在突发处理能力上存在本质区别。

特性 令牌桶(Token Bucket) 漏桶(Leaky Bucket)
核心思想 以固定速率生成令牌,请求消耗令牌 以固定速率漏出请求,超量排队或丢弃
突发处理 允许短时突发(桶容量决定峰值) 强制平滑,不允许突发
适用场景 允许突发流量的业务(如秒杀) 要求严格平滑的场景(如数据库写入)
实现复杂度 中等(需维护定时器或时间戳) 较低(队列 + 固定速率消费)
典型实现 Guava RateLimiter、Redis-Cell Nginx limit_req、Kong rate-limiting
常见误区: 令牌桶的“突发”并非无限制——桶容量决定了最大突发量。若桶容量 = 100,令牌生成速率 = 10/s,则最大突发为 100 个请求,之后速率被限制为 10/s。

2.4 令牌桶算法工作原理

下图展示令牌桶算法的完整生命周期:

令牌桶算法工作原理 令牌桶算法工作原理 令牌生成器 固定速率 10/s 令牌桶 容量 = 100 当前令牌 = 50 请求进入 令牌 ≥ 1? 是 放行 + 扣减令牌 否 返回 429

2.5 分布式限流实现方案

单机限流无法应对多实例场景,分布式限流需要中心化存储协调状态。以下是三种主流实现:

  1. Redis + Lua 脚本:利用 Redis 单线程特性,通过 Lua 脚本原子执行令牌桶逻辑,保证并发安全。
  2. Redis-Cell 模块:Redis 4.0+ 的限流模块,内置 GCRA(通用细胞速率算法),性能优于 Lua 脚本。
  3. Sentinel 集群流控:阿里开源的限流组件,支持 Token Server 模式,适合 Java 生态。
方案 延迟 一致性 运维成本 推荐场景
Redis + Lua ~1ms 最终一致 低 中小规模,通用场景
Redis-Cell <0.5ms 强一致 中(需加载模块) 高并发,低延迟要求
Sentinel 集群 ~2ms 强一致 高(需部署 Token Server) Java 微服务,复杂规则

2.6 Redis + Lua 分布式限流实现

以下是一个基于令牌桶算法的 Redis Lua 脚本,支持动态调整速率和容量:

-- 令牌桶限流脚本
-- KEYS[1]: 限流 key
-- ARGV[1]: 令牌生成速率(个/秒)
-- ARGV[2]: 桶容量
-- ARGV[3]: 当前请求消耗令牌数(通常为1)
-- 返回值: 0=拒绝, 1=放行

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local requested = tonumber(ARGV[3]) or 1

-- 获取当前桶状态
local token_info = redis.call('HMGET', key, 'tokens', 'last_refresh')
local tokens = tonumber(token_info[1]) or capacity
local last_refresh = tonumber(token_info[2]) or 0

-- 计算时间差,补充令牌
local now = redis.call('TIME')[1]
local elapsed = math.max(0, now - last_refresh)
local new_tokens = math.min(capacity, tokens + elapsed * rate)

-- 判断是否放行
if new_tokens >= requested then
    new_tokens = new_tokens - requested
    redis.call('HMSET', key, 'tokens', new_tokens, 'last_refresh', now)
    redis.call('EXPIRE', key, 10)  -- 设置过期时间,避免内存泄漏
    return 1
else
    -- 令牌不足,更新刷新时间但不扣减
    redis.call('HMSET', key, 'tokens', new_tokens, 'last_refresh', now)
    return 0
end
性能要点: 上述脚本使用 TIME 命令获取 Redis 服务器时间,避免客户端时间不一致问题。每次调用仅执行 2-3 次 Redis 命令,单实例 QPS 可达 10 万+。建议将限流 key 按客户端 IP 或 API 路径做哈希分片,避免单 key 热点。

2.7 网关限流配置实战

以 APISIX 为例,通过 Admin API 配置基于令牌桶的限流插件:

# 创建路由并绑定限流插件
curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -X PUT -d '
{
  "uri": "/api/v1/orders",
  "plugins": {
    "limit-req": {
      "rate": 10,           -- 每秒请求速率
      "burst": 20,          -- 突发容量
      "rejected_code": 429, -- 拒绝状态码
      "key": "remote_addr"  -- 限流维度:客户端IP
    }
  },
  "upstream": {
    "type": "roundrobin",
    "nodes": {
      "192.168.1.100:8080": 1
    }
  }
}'
生产建议: 限流 key 应选择业务标识(如用户 ID、API Key),而非仅依赖 IP。NAT 环境下多个用户共享同一 IP,会导致误限。APISIX 支持 consumer_name、service_id 等更细粒度的 key。

2.8 限流治理最佳实践

  • 分层限流:网关层(全局 QPS)→ 服务层(业务限流)→ 资源层(DB 连接池),避免单点瓶颈。
  • 动态配置:限流参数应支持运行时调整,APISIX 的 etcd watch 机制可实现秒级生效。
  • 监控告警:记录限流触发次数、拒绝率、令牌桶水位,当拒绝率 > 5% 时触发扩容或降级。
  • 降级策略:限流后返回友好提示(如 429 + Retry-After 头),避免客户端无限制重试。
  • 压测验证:使用 wrk 或 Locust 模拟突发流量,验证限流曲线是否符合预期。
避坑指南: 分布式限流依赖 Redis 可用性,若 Redis 宕机,所有限流规则将失效。建议部署 Redis Sentinel 或 Cluster 集群,并在网关侧配置降级策略(如降级为本地限流,允许一定误差)。