nginx limit_req + limit_conn:按 IP / API key 限速

起因

API 上线后两天,账单显示某个客户调用量是其它人的 50 倍。
查 log:他用 curl 写了个无 sleep 的循环,1000 QPS 持续 12 小时。
正常使用 < 1 QPS。
应用层加限流来不及,先在 nginx 层挡住才是第一道闸。

解决方案

1. 按 IP 限速:limit_req

# /etc/nginx/conf.d/limits.conf
# 共享内存区 zone:用 10MB 存 IP -> 计数器
limit_req_zone $binary_remote_addr zone=ip_lim:10m rate=10r/s;

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location /v1/ {
        # burst=20: 允许短期突发 20 个请求超出 10r/s
        # nodelay: 突发请求立刻处理(不排队延迟)
        limit_req zone=ip_lim burst=20 nodelay;
        # 超出限制返回 429(默认 503,改 429 更标准)
        limit_req_status 429;

        proxy_pass http://app;
    }
}

$binary_remote_addr 是把 IP 压缩成 4 字节(IPv4)/ 16 字节(IPv6),
$remote_addr 字符串省内存。10MB 能存约 16 万个 IP。

rate=10r/s 等价 1r per 100ms。"突发 20 + 持续 10r/s"是常见配置。

2. 按 API key 限速:用 map + limit_req_zone

# 把 Authorization header / query param 抽出 key
map $http_authorization $api_key {
    ~^Bearer\s+(?<token>.+) $token;
    default                  '';
}

# 优先用 API key 限流,没 key 才用 IP
map $api_key $rate_key {
    ''      $binary_remote_addr;
    default $api_key;
}

limit_req_zone $rate_key zone=api_lim:20m rate=20r/s;

不同 key 独立计数,恶意 key 不影响别人。

3. 按用户等级限流(基于 key 的差异化)

# /etc/nginx/conf.d/api_tiers.conf
# 假设你有个文件 /etc/nginx/api_keys.conf 维护 key → tier 映射
# 用 njs 或外部 lookup(auth_request)拿 tier 信息

# 用 split_clients 或 geo / map 决定走哪个 zone
map $api_key $rate_zone {
    "free-key-abc"    "free";
    "free-key-def"    "free";
    "pro-key-xyz"     "pro";
    "ent-key-mno"     "ent";
    default           "free";
}

limit_req_zone $api_key zone=free:10m rate=1r/s;
limit_req_zone $api_key zone=pro:10m  rate=10r/s;
limit_req_zone $api_key zone=ent:10m  rate=100r/s;

server {
    location /v1/ {
        # 按 tier 选 zone(nginx 不能动态选 zone,用 if + return)
        # 实际生产用 auth_request + 后端决定
        ...
    }
}

复杂 tier 路由通常上 nginx + Lua(OpenResty)或 Kong / Tyk API gateway。

4. 按连接数限:limit_conn

防"单 IP 大量并发长连接占资源"(如 download 慢慢传):

limit_conn_zone $binary_remote_addr zone=conn_lim:10m;

server {
    location /downloads/ {
        limit_conn conn_lim 5;     # 同 IP 最多 5 个并发连接
        limit_rate 1m;              # 每连接限速 1 MB/s
    }
}

limit_rate 控制响应字节速率;适合大文件下载 / 视频流。

5. 看效果 / 监控

# 看被 429 拒绝的请求
sudo tail -f /var/log/nginx/access.log | awk '$9 == 429'

# 统计
sudo awk '$9 == 429 {print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head

把 nginx access log status 暴露给 Prometheus:

# 用 nginx-prometheus-exporter 抓 stub_status
location /metrics {
    allow 127.0.0.1;
    deny all;
    stub_status;
}

PromQL:

rate(nginx_429_total[5m])

6. 给客户友好的 429 响应

limit_req_status 429;

# 自定义 429 页面
error_page 429 /errors/429.json;
location = /errors/429.json {
    internal;
    add_header Content-Type 'application/json' always;
    return 429 '{"error": "rate_limit_exceeded", "retry_after": 60}';
}

# 加 Retry-After header
location /v1/ {
    limit_req zone=ip_lim burst=20 nodelay;
    add_header Retry-After 60 always;
    add_header X-RateLimit-Limit 10 always;
    add_header X-RateLimit-Remaining 0 always;
    proxy_pass http://app;
}

效果

  • 滥用 client 在 nginx 层立刻被 429 挡住,application 不再被打扰
  • 账单回归正常水平
  • 正常用户感知 0(10 r/s 远超 UI 触发的频率)
  • 监控 dashboard 上有 429 趋势曲线,能看到攻击 / 滥用模式

limit_req 几个 trap

burst vs nodelay

  • burst=20(无 nodelay):允许 20 个排队,按 10r/s 速度处理 → 第 20 个
    请求要等 2 秒才被响应(用户感受很差)
  • burst=20 nodelay:20 个突发请求立刻处理,之后超限的直接 429
    → 用户感受好,但服务端瞬时压力大
  • burst=20 delay=5:前 5 个突发请求 nodelay,第 6-20 个排队,超过 20 拒绝

API 服务建议 burst+nodelay;下载服务建议 burst 排队(保护后端)。

$binary_remote_addr 在 CDN 后:所有请求源 IP 都是 CDN,限流没意义。
$http_x_forwarded_for$http_cf_connecting_ip

limit_req_zone $http_cf_connecting_ip zone=ip_lim:10m rate=10r/s;

与 application 层限流配合

nginx 层适合"按 IP / key 限速防滥用"。
应用层适合"按业务逻辑限流"(如"每用户每天 100 个 AI 生成请求")。
两者互补:nginx 挡明显异常,应用 enforce 业务规则。

踩过的坑

  1. zone 写错放在 location 里limit_req_zone 必须在 http {}
    定义;limit_req 才在 server / location 里用。

  2. rate=10r/m 不是 10r/min:nginx 写法是 10r/m 表示 10 requests
    per minute;10r/s 是每秒。别混。

  3. 测试时永远 429:自己开发用浏览器刷页面 + 1r/s 限流自然超。
    测试环境配宽点(如 100r/s)或者 dev 走不同 server block。

  4. X-Forwarded-For 被伪造:如果你直接 trust X-Forwarded-For[0]
    攻击者可以伪造一个 IP 在 header 里,绕过限流。
    real_ip_header + set_real_ip_from 限定可信代理范围才安全。

  5. zone 太小:10MB zone 满了 → 老条目被新条目挤出 → 老 IP 一段时间
    没访问 + 现在又来访问会被当作"新连接"立刻消耗 burst。生产至少 50MB
    per zone。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。