限流、防刷与连接数控制
站点被打的第一次通常不是 DDoS,而是有人写了个脚本循环请求登录接口,或者一个爬虫把全站翻了一遍。这类流量不占多少带宽,却能把数据库拖慢、把 CPU 打满。Nginx 的 limit_req 与 limit_conn 是最便宜的第一道闸门:几行配置就能挡掉绝大多数粗暴刷量。但要知道它的能力边界——限流必须与应用层配合,只靠 Nginx 一定会误伤或者被绕过。
要解决的问题
- 单个 IP 每秒请求几十次,怎么限制又不影响正常用户?
- 登录、短信验证码接口被暴力尝试,怎么做更严的限流?
- 一个 IP 开 200 个并发连接拖着不释放,怎么防?
- 限流把公司出口 IP、CDN 回源、健康检查一起封了怎么办?
- 被限流后返回 503,怎么变成语义正确的 429?
- 限流到底该配在 Nginx 还是应用里?
限流配置
# 放在 http 块:先定义共享内存区
limit_req_zone $binary_remote_addr zone=req_all:10m rate=20r/s; # 通用接口
limit_req_zone $binary_remote_addr zone=req_login:10m rate=1r/s; # 登录/验证码,严格
limit_conn_zone $binary_remote_addr zone=conn_perip:10m; # 单 IP 并发连接
limit_req_status 429; # 默认 503,改成 429 语义更准确
limit_conn_status 429;
limit_req_log_level warn; # 被限流的请求记到 error.log 的 warn 级
server {
# 登录接口:1 次/秒,允许 5 个突发,突发立即处理
location = /api/login {
limit_req zone=req_login burst=5 nodelay;
limit_conn conn_perip 5;
proxy_pass http://app_backend;
}
# 通用接口:20 次/秒,允许 40 个突发
location /api/ {
limit_req zone=req_all burst=40 nodelay;
limit_conn conn_perip 20;
proxy_pass http://app_backend;
}
# 健康检查与静态资源不限流,否则会把监控和正常访问一起误伤
location = /healthz { return 200 "ok\n"; access_log off; }
location ~* \.(?:js|css|png|jpe?g|svg|woff2?)$ {
root /srv/www/site;
expires 1y;
}
}
关键参数逐条解释
| 参数 | 含义 | 取值建议 |
|---|---|---|
$binary_remote_addr | 二进制形式的客户端 IP,比 $remote_addr 省内存 | 固定用它 |
zone=name:10m | 共享内存区名称与大小,10m 约存 16 万个 IP 状态 | 按预估 IP 数给,太小会淘汰计数 |
rate=20r/s | 平均速率,也可写 rate=30r/m | 先按真实峰值的 2~3 倍设,再收紧 |
burst=40 | 超出速率后允许排队的请求数 | 一般设成 rate 的 1~2 倍 |
nodelay | 突发请求立即处理(仍受 burst 上限),不加则按 rate 匀速放行、用户会感到变慢 | 面向用户接口建议加 |
limit_conn | 单 key 的并发连接数上限 | 普通站点 20~50,登录类 5 |
limit_req_status / limit_conn_status | 被限流时返回的状态码 | 统一设 429 |
burst 与 nodelay 的关系值得单独说清:burst 是桶的容量,nodelay 决定桶里的请求是立刻处理还是排队慢慢放。rate=1r/s burst=5 nodelay 意味着「平均每秒 1 个,但同一秒内最多能过 5 个,第 6 个直接 429」。所以 burst 不能设得太大,否则限流形同虚设;nodelay 不加时,一个页面同时发 6 个请求就会被拖成 6 秒才加载完,体验比报错还差。
与应用层限流的分工
| 层次 | 能防住什么 | 防不住什么 | 建议定位 |
|---|---|---|---|
| Nginx 限流 | 单 IP 速率/并发、粗暴 CC、无脑爬虫 | 分布式多 IP 刷、按用户维度的刷 | 第一道闸门,兜底成本最低 |
| CDN / WAF | 已知恶意 IP 库、常见攻击特征、大流量清洗 | 业务逻辑刷单 | 有预算就放在最前面 |
| 应用层限流 | 按用户/设备/手机号/验证码计数、风控评分 | 连接层洪水 | 业务规则的唯一正确位置 |
| 应用层业务限制 | 验证码、加锁、幂等、日限额 | 协议层攻击 | 与限流并列,不能互相替代 |
「限流要与应用层配合,别只靠 Nginx」:Nginx 只能看见 IP,攻击者用 1000 个代理 IP 每个只发 5 次请求,Nginx 限流一点感觉都没有;反过来,同一个公司出口 IP 后面几百个正常用户,Nginx 一限就是整栋楼被挡。真正的防护要靠应用层按账号、手机号、设备指纹做风控,Nginx 只负责把最粗暴的那部分挡在门外。
真实 IP:CDN 之后的必修课
# 接 CDN 或反向代理之后,$remote_addr 会是 CDN 节点 IP,限流会按节点限而不是按用户限
set_real_ip_from 100.64.0.0/10; # CDN 回源网段,以厂商文档为准
real_ip_header X-Forwarded-For;
real_ip_recursive on;
这段需要 ngx_http_realip_module(官方包默认编译进去)。配好之后 $binary_remote_addr 才是真实用户 IP,访问日志里的 $remote_addr 也才正确。不要偷懒用 $http_x_forwarded_for 做限流 key——这个头客户端可以随便伪造,等于没限。
验证方法
# 1) 语法与生效确认
sudo nginx -t && sudo systemctl reload nginx
nginx -T | grep -n 'limit_req\|limit_conn'
# 2) 压测登录接口,看 429 数量(ab 属于 apache2-utils)
sudo apt -y install apache2-utils
ab -n 200 -c 20 https://api.example.com/api/login
# 关注输出里的 Non-2xx responses 与 429/503 计数
# 3) 用 wrk 打通用接口
wrk -t4 -c50 -d20s --latency https://api.example.com/api/list
# 4) 从日志统计限流命中
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
sudo grep 'limiting requests' /var/log/nginx/error.log | tail -20
ab 压 HTTPS 时本机 CPU 可能先成为瓶颈,压测机最好与目标机分开;-c 20 并发配合 -n 200 足够验证「超过 burst 后是否返回 429」。看到 limiting requests, excess: ... 说明规则真的在生效。
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
| 忘了排除健康检查 | 监控拨测被限流,一直告警 | /healthz、静态资源目录不套限流 |
burst 设成 1000 | 限流看似配了,实际毫无效果 | burst 取 rate 的 1~2 倍 |
不给 nodelay | 页面并发请求被匀速排队,加载变慢 | 面向用户接口加 nodelay |
| 仍返回 503 | 客户端误判为服务故障,触发重试放大流量 | limit_req_status 429; |
| CDN 后限流失效 | 所有用户共享 CDN 节点 IP,互相误伤 | 配 real_ip 还原真实 IP |
限流 key 用 $http_x_forwarded_for | 客户端伪造头即可绕过 | 用 $binary_remote_addr |
zone 给 1m 却有几万 IP | 计数被频繁淘汰,限流不准 | 按 10m ≈ 16 万 IP 估算 |
| 只靠 Nginx 挡刷 | 多 IP 分布式刷量完全无感 | 应用层按账号/设备风控,Nginx 只是第一道闸门 |
小结:限流的最小可用方案是「通用接口按 IP 给一个宽松速率 + 登录类接口给严格速率,突发用 burst 加 nodelay,状态码统一 429」,并且一定要排除健康检查与静态资源;$binary_remote_addr 在 CDN 后面必须先用 real_ip 模块还原,否则限的是 CDN 节点;最重要的是想清楚边界——Nginx 挡的是粗暴流量,真正的防刷要靠应用层按账号与设备做风控,两者缺一不可。