Nginx 限流防刷实战:用 limit_req 与 limit_conn 挡住 CC 攻击和爬虫,别等被打挂了才想起来
我的一个博客站曾经被一个小爬虫温柔地拖死过。它不打 POST,不发洪水,就是老老实实 GET 列表页,但一秒并发几十个请求,把 PHP-FPM 的进程池全占住,正常访客打开就是 504。查访问日志发现是一个 IP 段在猛刷。这类「慢速高压」的攻击和恶意采集,用防火墙封 IP 治标不治本,真正的第一道防线应该在 Nginx 层做限流。这篇文章讲清楚 Nginx 的 limit_req(请求频率限制)和 limit_conn(并发连接限制)怎么配、怎么调,以及真实 IP 被 CDN 吃掉后怎么处理。
为什么限流要放在 Nginx 而不是应用层?
很多人第一反应是在 PHP 里写个计数器限流。这样做有两个致命缺陷:一是请求已经进了 PHP-FPM,进程已经被占用了,限流只是让它早点返回,资源照样消耗;二是 PHP 层的计数需要存储(文件或 Redis),高并发下锁竞争反而成新瓶颈。Nginx 处理一个请求的资源开销极小,在它这里直接 return 503,后面的 PHP、数据库、Redis 都不会被碰到,这才是性价比最高的拦截点。Nginx 内置的 ngx_http_limit_req_module 和 ngx_http_limit_conn_module 是编译时默认开启的,无需额外安装。
认识两个核心模块与漏桶算法
limit_req 用的是漏桶算法。想象一个桶,请求像水一样滴进去,桶底有个洞按固定速率(rate)漏水,桶本身能存一定量(burst)的水。如果水进来的速度超过漏水速度,桶里的水越积越多;桶满了,后来的请求就被丢弃返回 503。这个模型的好处是允许一定程度的突发:平时请求稀稀拉拉,偶尔来一波小峰值(在 burst 范围内)也能平滑放行,而不是僵硬地「每秒只能过 N 个」。
limit_conn 限制的是同时处于活动状态的连接数。它防的是「一个客户端同时开几百个连接慢慢传」这类攻击,比如 Slowloris(慢速 HTTP 攻击)或者大文件下载盗链。频率限制管不了它,因为它总请求数可能不多,但连接一直占着。
配置一:基于客户端 IP 的请求频率限制
限流要先定义一个「限流维度」的共享内存区,通常按 IP。写在 http 块里:
http {
# 定义一个 10MB 的共享内存,键是客户端 IP,1MB 约能存 1.6 万个 IP 状态
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
# 记录被限流拒绝的日志级别,默认 error,调成 warn 便于观察
limit_req_log_level warn;
# 被拒绝时返回的状态码,默认 503,可改成 429(语义更准确)
limit_req_status 429;
server {
location / {
# burst=10 允许突发 10 个请求排队;nodelay 表示突发请求立即处理不延迟
limit_req zone=perip burst=10 nodelay;
proxy_pass http://127.0.0.1:9000;
}
}
}逐项解释。 $binary_remote_addr 是二进制的客户端 IP,比 $remote_addr 省内存(IPv4 只占 4 字节)。rate=5r/s 表示每个 IP 平均每秒放行 5 个请求,注意这是按「毫秒精度平滑」的,不是每秒重置——即每 200 毫秒放一个。
burst 和 nodelay 是最容易配错的地方,必须说清楚:
- 不加 burst:严格按 rate,只要某一瞬间超过速率就 503。一个页面同时加载 20 个静态资源,浏览器并发请求,立刻就被误杀,体验极差。
- 加 burst=10 不加 nodelay:允许排队 10 个,但它们是「延迟放行」的——第 11 个之后才丢弃。排队会让响应变慢,用户感觉页面卡。
- 加 burst=10 nodelay:突发的前 10 个请求立即放行(不延迟),超出才丢弃。这是绝大多数站点的最佳选择,既容忍正常突发又不拖慢响应。
rate 设多少合适?正常用户浏览一个页面可能触发 10 到 30 个资源请求,几乎集中在头一两秒。如果你希望单个 IP 在两秒内加载完一个页面,rate 可以设 10r/s、burst 设 30。不要设得太紧,否则正常用户被误伤;也不要设太松,否则起不到防护作用。建议先设宽松值观察日志,再逐步收紧。
配置二:并发连接数限制
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
server {
location /download/ {
# 每个 IP 最多同时 4 个连接
limit_conn addr 4;
}
}
}这里 limit_conn addr 4 表示同一个 IP 最多同时保持 4 个活动连接,第 5 个请求直接 429。适合放在下载、导出、图片处理这类耗时接口上。限流的两个模块可以叠加使用,形成「频率 + 并发」双重防线。
关键问题:套了 CDN 之后 $remote_addr 全是 CDN 的 IP
这是最常翻车的地方。如果你的站前面挂了 Cloudflare 或阿里云 CDN,所有请求的 $remote_addr 都是 CDN 回源节点的 IP,你按 IP 限流等于把整个 CDN 当成一个 IP 来限,结果要么误杀所有人,要么形同虚设。解决办法是从 CDN 插入的请求头里取真实 IP。以 Cloudflare 为例,真实 IP 在 CF-Connecting-IP 头里。
更通用的做法是用 ngx_http_realip_module(编译时需带 --with-http_realip_module,官方包默认带),配置:
http {
# 告诉 Nginx:当请求来自这些可信网段时,信任它给出的真实 IP
set_real_ip_from 173.245.48.0/20; # Cloudflare 网段之一
set_real_ip_from 103.21.244.0/22;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
# 注意:limit_req_zone 要放在 set_real_ip_from 之后声明也不影响,
# 但 $binary_remote_addr 会被 realip 模块改写
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
}安全警告:set_real_ip_from 只能填你真正信任的 CDN 回源 IP 段。如果图省事写 set_real_ip_from 0.0.0.0/0,那么任何人都能伪造 CF-Connecting-IP 头来绕过限流——等于没防。必须去 CDN 官方文档抄最新的 IP 段列表,并定期更新。
进阶:白名单与更精细的限流
搜索引擎的爬虫、你自己的监控探针、内网服务不该被限。可以在限流 key 上做文章:
http {
# 对白名单 IP,key 为空字符串则不限流
map $remote_addr $limit_key {
default $binary_remote_addr;
"1.2.3.4" "";
127.0.0.1 "";
}
limit_req_zone $limit_key zone=perip:10m rate=5r/s;
}当 key 为空字符串时,limit_req 不生效——这就是白名单技巧。更进一步,可以按 URI 区别限流:登录接口最容易被暴力破解,可以用单独的、更严格的 zone:
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
location = /admin/login.php {
limit_req zone=login burst=3 nodelay;
...
}登录接口 rate=1r/s、burst=3,正常人几秒内最多点几次登录,暴力破解脚本会被立刻拦下。
观察与调参:别配完就不管了
被限流的请求会记进 error.log(limiting requests, excess: ... by zone "perip")。上线后先看几天日志,如果发现大量正常用户被限,说明 rate 或 burst 太小;如果发现攻击流量依然穿过去,说明限制太松,或者真实 IP 没取到。可以写个简单的统计脚本:
grep 'limiting requests' /var/log/nginx/error.log | \
grep -oP 'client: \K[0-9.]+' | sort | uniq -c | sort -rn | head -20这条命令列出被限流最多的 IP,一目了然。如果某个 IP 反复出现,配合 fail2ban 直接封禁整个 IP 是最省心的组合拳。
总结一下:Nginx 限流是个人站长对抗 CC 攻击和恶意采集性价比最高的手段。limit_req 管频率、limit_conn 管并发,两者配合 burst + nodelay 平衡安全与体验;套 CDN 的站务必用 realip 模块取真实 IP 并严格限定可信网段;上线后靠 error.log 持续调参。把这些配好,你的站才不会被一个不起眼的小爬虫轻易打趴。