起因
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 业务规则。
踩过的坑
-
zone 写错放在 location 里:
limit_req_zone必须在http {}段
定义;limit_req才在server/location里用。 -
rate=10r/m 不是 10r/min:nginx 写法是
10r/m表示 10 requests
per minute;10r/s是每秒。别混。 -
测试时永远 429:自己开发用浏览器刷页面 + 1r/s 限流自然超。
测试环境配宽点(如 100r/s)或者 dev 走不同 server block。 -
X-Forwarded-For被伪造:如果你直接 trustX-Forwarded-For[0],
攻击者可以伪造一个 IP 在 header 里,绕过限流。
real_ip_header+set_real_ip_from限定可信代理范围才安全。 -
zone 太小:10MB zone 满了 → 老条目被新条目挤出 → 老 IP 一段时间
没访问 + 现在又来访问会被当作"新连接"立刻消耗 burst。生产至少 50MB
per zone。
登录后参与评论。