限流、防刷与连接数控制

站点被打的第一次通常不是 DDoS,而是有人写了个脚本循环请求登录接口,或者一个爬虫把全站翻了一遍。这类流量不占多少带宽,却能把数据库拖慢、把 CPU 打满。Nginx 的 limit_reqlimit_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

burstnodelay 的关系值得单独说清: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 给一个宽松速率 + 登录类接口给严格速率,突发用 burstnodelay,状态码统一 429」,并且一定要排除健康检查与静态资源;$binary_remote_addr 在 CDN 后面必须先用 real_ip 模块还原,否则限的是 CDN 节点;最重要的是想清楚边界——Nginx 挡的是粗暴流量,真正的防刷要靠应用层按账号与设备做风控,两者缺一不可。

笔记加载中…