流量突然暴涨,日志里全是同一个 IP
个人站长最怕的不是没人来,而是来的人太多——尤其是当这些"人"其实是一台脚本机器的时候。某天早上打开监控,发现服务器 CPU 100%、带宽跑满、Nginx 的 access.log 一秒钟刷出几百行,全是同一个 IP 或者同一段 C 段在请求 /?s= 搜索页、/wp-login.php 登录页或者某个动态接口。数据库连接数瞬间打满,正常用户打开网站转圈圈。
这就是典型的 CC 攻击(Challenge Collapsar),本质是用大量看起来合法的 HTTP 请求把你的应用层资源耗尽。它和 DDoS 的区别在于:DDoS 打的是带宽和网络层,CC 打的是 CPU、数据库连接和 PHP-FPM 进程。对个人站长来说,花钱买高防的成本太高,而 Nginx 自带的限流模块在七层就能挡掉绝大部分低级 CC,配置得当的话,几分钟就能让攻击流量对后端"隐身"。
本文讲清楚 Nginx 两个限流模块 ngx_http_limit_req_module(请求速率限制)和 ngx_http_limit_conn_module(并发连接数限制)的原理、配置方法、常见翻车点,以及如何配合日志快速定位攻击特征。
先搞清楚:限流到底限制的是什么
很多人第一次配限流,直接把网上的参数抄上去,结果把自己也挡在了门外。要配好,先得理解 Nginx 限流用的是漏桶算法(leaky bucket)。
想象一个底部有个小孔的桶:请求像水一样倒进去,水从底部小孔匀速流出。如果倒水速度超过漏水速度,桶里的水就会慢慢积起来,桶满了之后再倒进来的水就直接被拒绝。这里有两个关键参数:
- rate(速率):漏水的速度,也就是
limit_req_zone里写的rate=10r/s,表示平均每秒放行 10 个请求。 - burst(突发容量):桶的大小,允许短时间内攒下多少个超额请求。这些请求会被延迟处理,而不是立刻拒绝。
- nodelay:加了这个选项后,burst 里的请求不会被延迟排队,而是只要桶没满就立刻放行。
理解这三个参数是配好的前提。很多教程只给配置不讲原理,导致站长们不知道该把自己的站设成多大,最后要么太松挡不住,要么太紧误伤正常用户。
第一步:定义限流区域 limit_req_zone
限流区域必须写在 http 块里,通常在 /etc/nginx/nginx.conf 的 http 段,或者单独放一个 conf.d/limit.conf 再 include 进去。它的含义是:划定一块共享内存,用某个"键"来统计请求频率。
# 定义在 http {} 块内
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
# 针对特定接口更严格的桶
limit_req_zone $binary_remote_addr zone=req_login:10m rate=2r/s;
# 用请求 URI 做键,防止单一热门页面被刷
limit_req_zone $binary_remote_addr$uri zone=req_per_url:10m rate=5r/s;
# 并发连接数限制区域
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
几个要点:
$binary_remote_addr比$remote_addr省内存,因为它是二进制格式(4 字节 IPv4),10m 共享内存大约能存 16 万个 IP 的状态。rate的单位可以是r/s(每秒)或r/m(每分钟)。注意rate=2r/s这种值 Nginx 内部会按毫秒换算,实际效果是每 500ms 放行一个,允许的突发粒度比较粗。- 键可以拼接多个变量。比如
$binary_remote_addr$uri表示"同一个 IP 对同一个 URI 的请求频率",这样即使攻击者把你的首页刷爆,也不会影响他请求其它页面——但对正常的爬虫更友好。 - 共享内存区域一旦被占满(IP 数量超过容量),Nginx 会对新 IP 使用最近最少使用算法淘汰旧记录,此时统计会不准。所以高流量站点要适当加大,比如 20m。
第二步:在 location 里应用限流规则
定义了区域,还要在具体的 location 里"启用"它,否则不生效。这是新手最容易忘的一步。
server {
listen 443 ssl http2;
server_name www.example.com;
# 全局限流:所有请求都受这个规则约束
location / {
limit_req zone=req_per_ip burst=20 nodelay;
limit_conn conn_per_ip 20;
limit_req_status 429;
limit_conn_status 429;
try_files $uri $uri/ /index.php?$args;
}
# 登录接口单独加严
location = /wp-login.php {
limit_req zone=req_login burst=3 nodelay;
limit_req_status 429;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# 搜索页也是重灾区
location = /index.php {
if ($args ~* "s=") {
limit_req zone=req_login burst=5 nodelay;
}
# ... 正常 fastcgi 配置
}
}
关于 burst 和 nodelay 的取值,给个人站的建议是:静态首页 burst=20 nodelay,登录、搜索、评论提交这类重接口 burst=3~5 nodelay。不加 nodelay 的话,超额请求会被延迟排队,可能造成"网站变慢但没报错"的诡异现象——用户以为卡了,其实是 Nginx 在故意压着请求。
第三步:理解 limit_req_status 与日志
默认情况下,被 Nginx 限流拒绝的请求返回 503 Service Temporarily Unavailable。建议改成 429 Too Many Requests,语义更准确,也方便你在日志里单独统计。
限流是否生效,看日志最直观。Nginx 的 limit_req 模块会往错误日志(error.log)里写这样一行:
2026/10/02 09:14:22 [error] 21415#0: *9921 limiting requests, excess: 8.995 by zone "req_per_ip", client: 43.xx.xx.xx, server: www.example.com, request: "GET /?s=test HTTP/1.1", host: "www.example.com"关键字段是 excess(超出速率多少)和 client(客户端 IP)。你可以用一条命令快速找出限流最严重的 IP:
grep "limiting requests" /var/log/nginx/error.log \
| awk '{for(i=1;i<=NF;i++) if($i=="client:") print $(i+1)}' \
| sed 's/,//' | sort | uniq -c | sort -rn | head -20
如果某个 IP 被限流的次数远高于其它,基本可以断定是攻击源或者失控的爬虫。这时候可以在防火墙层面直接封掉,比如用 nftables:
nft add rule inet filter input ip saddr 43.xx.xx.xx drop
第四步:limit_conn 和 limit_req 的分工
这两个模块经常一起配,但作用不同:
| 模块 | 限制对象 | 典型用途 |
|---|---|---|
| limit_req | 单位时间内的请求数(速率) | 防刷接口、防暴力破解、防爬虫高频抓取 |
| limit_conn | 同时存在的连接数 | 防慢速攻击、防下载器多线程、防压测 |
慢速攻击(Slowloris)就是典型需要 limit_conn 的场景:攻击者建立大量连接但只发一点点数据,让你的连接数被占满。光靠限速挡不住,因为它的请求频率很低。这时候 limit_conn conn_per_ip 20 就非常重要。配合 client_body_timeout、client_header_timeout 效果更好:
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 15s;
keepalive_timeout 30s;
第五步:白名单与 CDN 场景的正确姿势
两个大坑必须提醒。
坑一:开了 CDN 之后,真实 IP 变成了 CDN 节点的 IP。 此时 $binary_remote_addr 拿到的是 CDN 回源节点的 IP,所有用户的请求在 Nginx 看来都来自同几个 IP,限流会误伤全世界。解决方案是用 ngx_http_realip_module 还原真实 IP:
# 只在信任 CDN 的前提下使用
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; # Cloudflare 用这个头
# 或者 real_ip_header X-Forwarded-For;
real_ip_recursive on;
配好之后 $binary_remote_addr 才是访客真实 IP。一定要确认 set_real_ip_from 只写了 CDN 的 IP 段,否则攻击者可以直接伪造 X-Forwarded-For 头绕过限流。
坑二:自己的监控、搜索引擎爬虫被误伤。 Googlebot、必应蜘蛛、你自己的拨测脚本可能触发限流,影响 SEO 收录。解决办法是用 geo 或 map 给可信来源建白名单:
# 在 http 块定义
map $http_user_agent $limit_key {
default $binary_remote_addr;
"~*Googlebot" "";
"~*bingbot" "";
"~*Baiduspider" "";
"" $binary_remote_addr;
}
limit_req_zone $limit_key zone=req_smart:10m rate=10r/s;当键为空字符串时,Nginx 不做限流统计。这样搜索引擎和信任来源就自动豁免了。
第六步:验证与调优
配完记得先 nginx -t 检查语法,再 nginx -s reload 平滑重载。然后用 ab 或 wrk 自测一下限流是否生效:
# 用 30 个并发发 500 个请求,观察有多少 429
ab -n 500 -c 30 https://www.example.com/
# 关注返回码分布
# Complete requests: 500
# Non-2xx responses: 412 <-- 这些就是被限流的
如果 429 太多,说明 rate 或 burst 设小了,正常用户会受影响;如果一个都没被限,说明设得太松。理想的配置是:正常流量完全无感,异常流量被平滑地削掉。
最后的经验总结
限流不是一次配完就万事大吉的东西。攻击者的手法会变,你的流量结构也会变。建议把下面几句话记在心里:
- 先上 limit_req 再考虑 Cloudflare 的高防。 免费方案能解决 90% 的低级 CC,不要一上来就想着花钱。
- 重接口(登录、搜索、评论、注册)一定单独设桶,这是攻击者最爱打的地方。
- 开了 CDN 必须先配 real_ip,否则限流等于自残。
- 记得给爬虫白名单,否则收录掉了都不知道为什么。
- 把 error.log 的 limiting requests 日志纳入监控,攻击发生时你能第一时间看见。
对个人站长来说,Nginx 限流是性价比最高的防护手段之一——零成本、见效快、不影响正常访问。花半小时把上面的配置吃透,比事后手忙脚乱地去买高防要划算得多。