Nginx 限流实战:用 limit_req 与 limit_conn 挡掉 CC 攻击,漏桶算法、real_ip 还原与爬虫白名单

流量突然暴涨,日志里全是同一个 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 设小了,正常用户会受影响;如果一个都没被限,说明设得太松。理想的配置是:正常流量完全无感,异常流量被平滑地削掉。

最后的经验总结

限流不是一次配完就万事大吉的东西。攻击者的手法会变,你的流量结构也会变。建议把下面几句话记在心里:

  1. 先上 limit_req 再考虑 Cloudflare 的高防。 免费方案能解决 90% 的低级 CC,不要一上来就想着花钱。
  2. 重接口(登录、搜索、评论、注册)一定单独设桶,这是攻击者最爱打的地方。
  3. 开了 CDN 必须先配 real_ip,否则限流等于自残。
  4. 记得给爬虫白名单,否则收录掉了都不知道为什么。
  5. 把 error.log 的 limiting requests 日志纳入监控,攻击发生时你能第一时间看见。

对个人站长来说,Nginx 限流是性价比最高的防护手段之一——零成本、见效快、不影响正常访问。花半小时把上面的配置吃透,比事后手忙脚乱地去买高防要划算得多。

Last modification:October 2nd, 2026 at 09:24 pm

Leave a Comment