限流(Rate Limiting)

限流是一种基础的流量控制技术,它限制客户端在特定时间窗口内 可以向 API 或 Web 服务发起的请求数量, 保护基础设施免受过载、拒绝服务攻击(DoS/DDoS)、自动化 滥用、恶意爬取以及对有限计算资源的不当使用。在 现代 API 每秒处理来自移动应用、合作伙伴集成、合法机器人 以及潜在恶意攻击者的数百万次请求的场景下,缺乏 适当的限流可能导致运营成本激增、合法用户的性能 严重下降、易受撞库(credential stuffing)和暴力 破解攻击,甚至导致服务完全不可用。有效实现限流 需要深入理解各种算法(token bucket、leaky bucket、fixed window、 sliding window)、关于分布式架构的考量(多个服务器需要 共享速率计数器)、客户端识别策略(IP、API key、JWT、 session)、按用户层级(free、premium、enterprise)的差异化策略,以及 通过标准化 HTTP headers 进行清晰沟通的机制,以告知客户端限制、 当前消耗量和重置时间。本文探讨用于构建健壮限流系统的算法、实现模式、 工具和最佳实践(best practices)。

限流算法

1. Token Bucket(最常用)

      # 概念:具有最大 token 容量的 bucket
      # Token 以恒定速率添加
      # 每个 request 消耗 1 个 token
      # 如果 bucket 为空,request 被拒绝
      容量:100 tokens
      补充速率:10 tokens/秒
      优点:
      - 允许受控的 bursts(bucket 已满)
      - 实现简单
      - 对不同流量模式具有灵活性
      缺点:
      - 可能允许使系统过载的 bursts
      - 需要跟踪上次补充
      实现:
      class TokenBucket {
      constructor(capacity, refillRate) {
      this.capacity = capacity;
      this.tokens = capacity;
      this.refillRate = refillRate;
      this.lastRefill = Date.now();
      }
      consume(count = 1) {
      this.refill();
      if (this.tokens >= count) {
      this.tokens -= count;
      return true;
      }
      return false;
      }
      refill() {
      const now = Date.now();
      const elapsed = (now - this.lastRefill) / 1000;
      const tokensToAdd = elapsed * this.refillRate;
      this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
      this.lastRefill = now;
      }
      }
      

2. Leaky Bucket

      # 概念:以恒定速率漏出的 queue
      # Requests 进入 bucket(queue)
      # 以恒定速率处理(leak)
      # 如果 queue 已满,requests 被拒绝
      优点:
      - 平滑 bursts(traffic shaping)
      - Output rate 恒定且可预测
      - 保护 backend 免受 spikes 影响
      缺点:
      - 可能增加延迟(queueing)
      - 实现复杂
      用途:
      - Traffic shaping
      - Network gateways
      - 当恒定的 output rate 至关重要时
      

3. Fixed Window Counter

      # 概念:按固定时间窗口的计数器
      # 示例:每分钟 100 个 requests
      # 在每分钟开始时重置(XX:00, XX:01, XX:02...)
      优点:
      - 极其简单
      - Memory efficient
      - 易于理解
      缺点:
      - Edge case:1 秒内 200 个 requests
      (第 1 分钟末尾 100 个,第 2 分钟开头 100 个)
      - 在窗口边界允许 bursts
      Redis 实现:
      INCR user:123:2024-01-15:14:30
      EXPIRE user:123:2024-01-15:14:30 60
      GET user:123:2024-01-15:14:30  # 如果 > 100,reject
      

4. Sliding Window Log

      # 概念:requests 时间戳的 log
      # 移除窗口外的 requests
      # 统计滑动窗口内的 requests
      优点:
      - 完美的精确度
      - 没有 fixed window 的 edge cases
      - 均匀分布速率
      缺点:
      - Memory intensive(存储所有 timestamps)
      - 在 high traffic 下性能下降
      Redis 实现(Sorted Set):
      ZADD user:123 <timestamp> <request-id>
      ZREMRANGEBYSCORE user:123 0 <timestamp-60s>  # 移除旧的
      ZCARD user:123  # Count requests
      如果 > 100,reject
      

5. Sliding Window Counter(混合)

      # 概念:将 fixed window 与 sliding 结合
      # 使用前几个窗口的计数器并加权
      # 估算滑动窗口中的速率
      示例:限制 100/分钟
      当前窗口(14:30):70 个 requests
      前一窗口(14:29):90 个 requests
      当前窗口中已过去:40s(66.7%)
      估算:90 * (1 - 0.667) + 70 = 30 + 70 = 100
      优点:
      - 精确度接近 sliding log
      - Memory efficient(仅 2 个计数器)
      - 平滑 bursts
      缺点:
      - 估算(不精确)
      - 比 fixed window 更复杂
      

分布式限流(Distributed Rate Limiting)

      # 问题:多个服务器需要共享 state
      # 解决方案:
      1. 集中式 Redis(最常用)
      const redis = require('redis');
      const client = redis.createClient();
      async function checkRateLimit(userId) {
      const key = \`rate:\$:\${getCurrentWindow()}\`;
      const count = await client.incr(key);
      if (count === 1) {
      await client.expire(key, 60); // 60 seconds
      }
      return count <= 100; // Limit: 100/min
      }
      2. Redis Lua Script(Atomic)
      const luaScript = \`
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local current = redis.call('incr', key)
      if current == 1 then
      redis.call('expire', key, ARGV[2])
      end
      if current > limit then
      return 0
      end
      return 1
      \`;
      3. Sticky Sessions + Local Counters
      - 始终将用户路由到同一 server
      - 服务器上的本地 counter
      - 问题:与 auto-scaling 配合不佳
      4. Gossip Protocol
      - 服务器通过 gossip 共享 state
      - Eventual consistency
      - 更复杂,用于 high scale
      

标准 HTTP Headers

      # Standards (RFCs)
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 45
      X-RateLimit-Reset: 1640000000  # Unix timestamp
      # 当超出限制时
      HTTP/1.1 429 Too Many Requests
      Retry-After: 60  # 距离 retry 的秒数
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 0
      X-RateLimit-Reset: 1640000060
      {
      "error": "Rate limit exceeded",
      "retryAfter": 60,
      "limit": 100
      }
      # GitHub 风格(更具信息量)
      X-RateLimit-Limit: 5000
      X-RateLimit-Remaining: 4999
      X-RateLimit-Reset: 1372700873
      X-RateLimit-Used: 1
      X-RateLimit-Resource: core
      

使用 Express.js 实现

      const rateLimit = require('express-rate-limit');
      const RedisStore = require('rate-limit-redis');
      const redis = require('redis');
      const client = redis.createClient();
      // Basic rate limiter
      const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in headers
      legacyHeaders: false,
      message: 'Too many requests, please try again later.'
      });
      app.use('/api/', limiter);
      // Redis-based distributed rate limiting
      const distributedLimiter = rateLimit({
      store: new RedisStore({
      client: client,
      prefix: 'rate-limit:',
      }),
      windowMs: 60 * 1000,
      max: 10,
      standardHeaders: true,
      });
      // Different limits per route
      const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5, // Stricter for auth endpoints
      skipSuccessfulRequests: true, // Don't count successful logins
      });
      app.post('/api/login', authLimiter, loginHandler);
      // Custom key function (rate limit by user ID instead of IP)
      const userLimiter = rateLimit({
      windowMs: 60 * 1000,
      max: 100,
      keyGenerator: (req) => req.user.id, // Requires auth middleware
      });
      

分层限流(Tiered Rate Limiting)

      // Different limits based on user tier
      function getRateLimit(user) {
      const tiers = {
      free: { windowMs: 3600000, max: 100 },      // 100/hour
      basic: { windowMs: 3600000, max: 1000 },    // 1000/hour
      premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
      enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
      };
      return tiers[user.tier] || tiers.free;
      }
      app.use(async (req, res, next) => {
      const user = await getUserFromToken(req);
      const limits = getRateLimit(user);
      const limiter = rateLimit({
      ...limits,
      keyGenerator: () => user.id,
      });
      limiter(req, res, next);
      });
      

工具与服务

  • Redis:分布式计数器,自动 expire
  • Kong:带有限流 plugin 的 API Gateway
  • Nginx rate limiting:limit_req_zone, limit_conn_zone
  • Cloudflare:CDN 级别的限流
  • AWS API Gateway:内置 throttling
  • express-rate-limit:Express 中间件
  • Tyk:开源 API gateway

最佳实践(Best Practices)

  • 选择合适的算法:通用 API 使用 token bucket,需要精确度时使用 sliding window
  • 差异化限制:Auth endpoints 更严格,read-only 更宽松
  • 清晰沟通:信息丰富的 headers,有用的错误消息
  • Whitelist:可信合作伙伴的 IP、health checks
  • Monitoring:当用户频繁达到限制时发出警报
  • Graceful degradation:尽可能返回 cached data
  • Distributed state:对多服务器部署使用 Redis
  • Cost-based limiting:昂贵的操作消耗更多 tokens

建议

对于现代 API,请实现 带 Redis 的 token bucket 以进行分布式限流。 当你需要精确度但不想要内存开销时,使用 sliding window counter。 配置差异化限制:login 为 5 req/min,读取为 100 req/min, 写操作为 10 req/min。始终返回信息丰富的 headers,并在客户端实现 带 exponential backoff 的 retry logic。