← 返回资讯
赵一鸣
产品评测编辑
已审核

API 限流实战:从被刷爆到稳如磐石

上个月,我们的 API 被恶意刷了 200 万次请求,服务器直接宕机。

API 限流实战:从被刷爆到稳如磐石

API 限流实战:从被刷爆到稳如磐石

上个月,我们的 API 被恶意刷了 200 万次请求,服务器直接宕机。

经过一周的优化,现在即使面对 10 倍流量也能稳定运行。

问题背景

被攻击的那天

CODE
2026-06-15 03:22:15 [ERROR] Connection pool exhausted
2026-06-15 03:22:16 [ERROR] Database timeout
2026-06-15 03:22:17 [ERROR] 503 Service Unavailable

监控显示:

根本原因

1. 没有限流:任何人都可以无限请求

2. 没有缓存:每次请求都查数据库

3. 没有降级:压力大了直接崩溃

限流策略

策略 1:固定窗口限流

最简单的限流方式。

PYTHON
from datetime import datetime
import redis

class FixedWindowRateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
    
    def is_allowed(self, key: str, limit: int, window: int) -> bool:
        """
        key: 限流键(如 user_id 或 ip)
        limit: 窗口内最大请求数
        window: 窗口大小(秒)
        """
        current_minute = datetime.now().strftime("%Y%m%d%H%M")
        redis_key = f"rate:{key}:{current_minute}"
        
        count = self.redis.incr(redis_key)
        if count == 1:
            self.redis.expire(redis_key, window)
        
        return count <= limit

# 使用
limiter = FixedWindowRateLimiter(redis_client)

@app.route("/api/data")
def get_data():
    user_id = get_current_user_id()
    if not limiter.is_allowed(user_id, limit=100, window=60):
        return jsonify({"error": "Too many requests"}), 429
    
    return jsonify({"data": "..."})

问题:窗口边界可能突发 2 倍流量。

策略 2:滑动窗口限流

解决固定窗口的边界问题。

PYTHON
class SlidingWindowRateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
    
    def is_allowed(self, key: str, limit: int, window: int) -> bool:
        now = time.time()
        window_start = now - window
        
        redis_key = f"rate:{key}"
        
        # 使用 sorted set 记录请求时间
        pipe = self.redis.pipeline()
        pipe.zremrangebyscore(redis_key, 0, window_start)
        pipe.zadd(redis_key, {str(now): now})
        pipe.zcard(redis_key)
        pipe.expire(redis_key, window)
        
        results = pipe.execute()
        count = results[2]
        
        return count <= limit

# 使用
limiter = SlidingWindowRateLimiter(redis_client)

@app.route("/api/data")
def get_data():
    user_id = get_current_user_id()
    if not limiter.is_allowed(user_id, limit=100, window=60):
        return jsonify({"error": "Too many requests"}), 429
    
    return jsonify({"data": "..."})

策略 3:令牌桶算法

允许一定程度的突发流量。

PYTHON
import time

class TokenBucketRateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
    
    def is_allowed(self, key: str, capacity: int, refill_rate: float) -> bool:
        """
        capacity: 桶容量
        refill_rate: 每秒补充的令牌数
        """
        redis_key = f"token:{key}"
        
        # 获取当前状态
        data = self.redis.hgetall(redis_key)
        
        now = time.time()
        
        if not data:
            # 首次请求,满桶
            tokens = capacity - 1
            last_refill = now
        else:
            tokens = float(data[b'tokens'])
            last_refill = float(data[b'last_refill'])
            
            # 计算补充的令牌
            elapsed = now - last_refill
            tokens = min(capacity, tokens + elapsed * refill_rate)
            
            if tokens >= 1:
                tokens -= 1
            else:
                return False
        
        # 保存状态
        self.redis.hset(redis_key, mapping={
            'tokens': tokens,
            'last_refill': now
        })
        self.redis.expire(redis_key, 60)
        
        return True

# 使用
limiter = TokenBucketRateLimiter(redis_client)

@app.route("/api/data")
def get_data():
    user_id = get_current_user_id()
    # 允许 100 个突发,每秒补充 10 个
    if not limiter.is_allowed(user_id, capacity=100, refill_rate=10):
        return jsonify({"error": "Too many requests"}), 429
    
    return jsonify({"data": "..."})

多层限流架构

层级设计

CODE
┌─────────────────────────────────────┐
│  Layer 1: Nginx 限流                │
│  - IP 级别                          │
│  - 1000 req/min                     │
├─────────────────────────────────────┤
│  Layer 2: 应用层限流                │
│  - 用户级别                         │
│  - 100 req/min                      │
├─────────────────────────────────────┤
│  Layer 3: 接口级别限流              │
│  - 特定接口                         │
│  - 10 req/min                       │
└─────────────────────────────────────┘

Nginx 配置

NGINX
# /etc/nginx/nginx.conf

# 定义限流区域
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=100r/s;
limit_req_zone $http_x_user_id zone=user_limit:10m rate=50r/s;

server {
    listen 80;
    
    # IP 级别限流
    location /api/ {
        limit_req zone=ip_limit burst=50 nodelay;
        limit_req_status 429;
        
        proxy_pass http://backend;
    }
    
    # 用户级别限流
    location /api/user/ {
        limit_req zone=user_limit burst=20 nodelay;
        limit_req_status 429;
        
        proxy_pass http://backend;
    }
    
    # 特定接口限流
    location /api/expensive/ {
        limit_req zone=ip_limit rate=5r/s burst=10 nodelay;
        limit_req_status 429;
        
        proxy_pass http://backend;
    }
}

应用层限流

PYTHON
from functools import wraps
from flask import request, jsonify

def rate_limit(limit: int, window: int, key_func=None):
    """限流装饰器"""
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            # 获取限流键
            if key_func:
                key = key_func()
            else:
                key = request.headers.get('X-User-ID') or request.remote_addr
            
            # 检查限流
            if not limiter.is_allowed(key, limit, window):
                return jsonify({
                    "error": "Too many requests",
                    "retry_after": window
                }), 429
            
            return f(*args, **kwargs)
        return wrapped
    return decorator

# 使用
@app.route("/api/data")
@rate_limit(limit=100, window=60)
def get_data():
    return jsonify({"data": "..."})

@app.route("/api/expensive")
@rate_limit(limit=10, window=60, key_func=lambda: f"expensive:{get_current_user_id()}")
def expensive_operation():
    return jsonify({"result": "..."})

降级策略

策略 1:缓存降级

PYTHON
from functools import lru_cache
import time

class CacheWithFallback:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.local_cache = {}
    
    def get(self, key, fetch_func, ttl=300):
        # 1. 尝试 Redis 缓存
        try:
            cached = self.redis.get(key)
            if cached:
                return json.loads(cached)
        except redis.RedisError:
            pass  # Redis 不可用,继续
        
        # 2. 尝试本地缓存
        if key in self.local_cache:
            data, timestamp = self.local_cache[key]
            if time.time() - timestamp < ttl:
                return data
        
        # 3. 获取新数据
        try:
            data = fetch_func()
            # 写入缓存
            try:
                self.redis.setex(key, ttl, json.dumps(data))
            except redis.RedisError:
                pass
            self.local_cache[key] = (data, time.time())
            return data
        except Exception:
            # 4. 返回过期缓存
            if key in self.local_cache:
                return self.local_cache[key][0]
            raise

策略 2:功能降级

PYTHON
class FeatureFlags:
    def __init__(self):
        self.degraded_features = set()
    
    def degrade(self, feature: str):
        self.degraded_features.add(feature)
    
    def restore(self, feature: str):
        self.degraded_features.discard(feature)
    
    def is_degraded(self, feature: str) -> bool:
        return feature in self.degraded_features

flags = FeatureFlags()

@app.route("/api/recommendations")
def get_recommendations():
    if flags.is_degraded("recommendations"):
        # 返回简单推荐
        return jsonify({"items": get_popular_items()})
    
    # 正常推荐逻辑
    return jsonify({"items": get_personalized_recommendations()})

# 监控系统自动降级
def check_system_health():
    cpu = get_cpu_usage()
    if cpu > 90:
        flags.degrade("recommendations")
        flags.degrade("analytics")
    elif cpu < 70:
        flags.restore("recommendations")
        flags.restore("analytics")

监控与告警

关键指标

PYTHON
from prometheus_client import Counter, Histogram

# 限流触发次数
rate_limit_hits = Counter(
    'rate_limit_hits_total',
    'Number of rate limit hits',
    ['endpoint', 'limit_type']
)

# 请求延迟
request_latency = Histogram(
    'request_latency_seconds',
    'Request latency',
    ['endpoint']
)

# 在限流装饰器中记录
def rate_limit(limit: int, window: int):
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            key = get_rate_limit_key()
            if not limiter.is_allowed(key, limit, window):
                rate_limit_hits.labels(
                    endpoint=request.path,
                    limit_type='user'
                ).inc()
                return jsonify({"error": "Too many requests"}), 429
            
            return f(*args, **kwargs)
        return wrapped
    return decorator

告警规则

YAML
# Prometheus alerting rules
groups:
  - name: rate_limit_alerts
    rules:
      - alert: HighRateLimitHits
        expr: rate(rate_limit_hits_total[5m]) > 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High rate limit hit rate"
          description: "Rate limit hits > 100/s for 5 minutes"
      
      - alert: PossibleDDoS
        expr: rate(requests_total[1m]) > 10000
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Possible DDoS attack"
          description: "Request rate > 10000/s"

效果对比

| 指标 | 优化前 | 优化后 |

|------|--------|--------|

| 最大 QPS | 5000(崩溃) | 50000(稳定) |

| 限流精度 | 无 | 99.9% |

| 降级响应 | 无 | < 100ms |

| 恢复时间 | 30 分钟 | < 1 分钟 |

总结

限流的核心原则:

1. 多层防护:Nginx + 应用层 + 接口层

2. 灵活策略:固定窗口、滑动窗口、令牌桶按需选择

3. 优雅降级:压力大了自动降级非核心功能

4. 实时监控:发现问题立即告警

做好这些,API 就能稳如磐石。


优化时间:2026年6月

攻击规模:200 万请求/分钟

优化效果:10 倍流量稳定运行

#API #限流 #RateLimit #高可用

515
10319 阅读
5 评论
分享
链接已复制
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年07月10日 17:58

赵一鸣

产品评测编辑

前产品经理,现专注 AI 工具评测。实测过 30+ 款 AI 产品,擅长横向对比和用户体验分析。

读者评论 5

运营小陈 1周前
转发到团队群了,大家都觉得有参考价值。
回复 点赞 (4)
数据分析师 1周前
数据引用很扎实,建议补充一下近三个月的最新数据。
回复 点赞 (9)
产品经理阿杰 2天前
从产品角度看,这个方向确实有机会,但商业化路径还需要验证。
回复 点赞 (15)
张工 5天前
写得很实在,特别是实测对比那部分,跟我自己的使用感受一致。
回复 点赞 (12)
前端工程师 1周前
代码示例很清晰,直接用到项目里了。
回复 点赞 (6)