Nginx 限流与防 CC 攻击实战:limit_req、limit_conn 配置与封禁策略全解析


很多站长把网站部署到 VPS 之后,最怕的不是没人访问,而是突然涌进来的"僵尸流量"。CC 攻击(Challenge Collapsar)就是这类攻击的典型代表:攻击者控制大量代理 IP,模拟真实用户反复请求页面、登录接口、下载接口,瞬间把 Nginx 的 worker 进程和 PHP-FPM 打满,最终让 CPU 100%、内存耗尽、数据库连接数爆掉,正常用户只能看到 502 或卡死的页面。

防 CC 的手段很多,从硬件防火墙到 WAF 都有,但对一台自建 VPS 来说,成本最低、见效最快的方案就是直接用 Nginx 自带的限流模块做"流量闸门"。本文从零开始,讲清楚 Nginx 限流的原理、配置、封禁策略和常见排错,让你在不换服务器的情况下扛住大部分 CC 攻击。

一、认识 Nginx 两大限流模块

Nginx 内置了两个与限流强相关的模块:

ngx_http_limit_req_module  # 请求频率限流,基于"漏桶算法"
ngx_http_limit_conn_module # 并发连接数限流

两者职责不同:limit_req 限制的是"单位时间内能处理多少个请求",适合防刷接口、防 CC;limit_conn 限制的是"同一时刻最多允许多少个并发连接",适合防止某个 IP 占用大量连接。实际防 CC 场景中两者通常配合使用。

先确认 Nginx 是否已经编译了这两个模块(默认都会编译进去):

nginx -V 2>&1 | tr ' ' '\n' | grep -E 'limit_(req|conn)'
# 输出类似:
# --with-http_limit_req_module
# --with-http_limit_conn_module

如果没有输出,说明你的 Nginx 是精简编译版本,需要重新编译或换用官方源安装。

二、limit_req 请求频率限流:核心配置

限流的第一步是定义"限流区域"(zone),即把哪个维度的 key(通常是 IP)放进一个共享内存区域里计数。区域定义放在 http 块内,配置示例如下:

http {
    # 定义限流区域:key 取客户端 IP,内存 10m,平均速率 10r/s
    limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;

    # 记录超限请求日志(可选,建议开启方便排错)
    limit_req_log_level notice;
}

参数说明:

zone=req_limit:10m  # 区域名称 req_limit,共享内存 10MB(约能存 16 万个 IP)
rate=10r/s          # 平均速率,支持 r/s(每秒)和 r/m(每分钟),如 600r/m

然后在需要限流的 server 或 location 中引用这个区域:

server {
    listen 80;
    server_name www.example.com;

    location / {
        limit_req zone=req_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:8080;
    }
}

这里 burst=20 表示允许一次性"借" 20 个请求的缓冲额度,超过额度的请求直接拒绝;nodelay 表示缓冲的请求不排队延时,立刻处理(否则超出的请求会被延迟处理,相当于排队)。

如果希望限流只针对 API 接口而不是全站,可以把限流放在 location /api/ 里:

location ~ ^/api/ {
    limit_req zone=req_limit burst=5 nodelay;
    proxy_pass http://127.0.0.1:8080;
}

三、limit_conn 并发连接数限制

请求频率限流挡不住"慢速连接"型攻击(每个连接挂着不放),所以还需要限制并发连接数:

http {
    # 定义连接数限制区域:key 为客户端 IP
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        listen 80;
        server_name www.example.com;

        location / {
            limit_conn conn_limit 10;   # 每个 IP 最多 10 个并发连接
            limit_req zone=req_limit burst=20 nodelay;
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

注意:limit_conn 限制的是并发连接(含 keepalive 长连接),对 HTTP/1.1 下浏览器同时建立的多个连接数要留足余量,一般按"页面资源数量 + 冗余"设置,比如单 IP 20~50 比较稳妥,太小会误伤正常用户。

四、超限后的封禁策略:503 还是 444

默认情况下,超限请求会返回 503 Service Temporarily Unavailable。但攻击者往往不在乎 503,反而会继续猛刷。更狠的做法是直接返回 444(Nginx 特有的"不发送任何响应并断开连接"状态码),让攻击者每个请求都干等超时,成本更高:

server {
    location / {
        limit_req zone=req_limit burst=20 nodelay;
        # 超限时返回 444,直接断开连接
        limit_req_status 444;
    }
}

如果是 API 场景,希望客户端能感知到限流并做重试,则保留 503 并附上 Retry-After 响应头会更友好。Nginx 本身不支持直接配置 Retry-After,可以配合 lua(OpenResty)实现,这里不做展开。

还有一种思路:超限次数达到阈值后,把该 IP 加入 deny 黑名单。Nginx 原生做不到"动态封禁",但可以配合 fail2ban 读取 Nginx 错误日志实现(见下文排错章节)。

五、结合 geo 做白名单与精细化限流

搜索引擎爬虫(Googlebot、Bingbot)和健康检查请求不应该被限流,否则会影响收录。使用 geo 模块区分"可信 IP"和"普通 IP",对可信 IP 不限流或放宽限流:

http {
    geo $limit_key {
        default  $binary_remote_addr;   # 默认按 IP 限流
        127.0.0.1 "";
        192.168.0.0/16 "";             # 内网不限流
        66.249.64.0/19 "";             # Googlebot 网段不限流
    }

    limit_req_zone $limit_key zone=req_limit:10m rate=10r/s;

    server {
        location / {
            limit_req zone=req_limit burst=20 nodelay;
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

注意 geo 块中 key 为空的请求不会计入限流,但 $limit_key 为空时 limit_req_zone 会报错吗?不会,空字符串作为 key 时所有命中空 key 的请求共享一个桶,实际效果等于"不限流"。这是比较经典的写法。

六、限流配置的验证与压测

配置改完先测试语法并重载:

nginx -t && nginx -s reload

然后用 ab(ApacheBench)压测验证限流是否生效。以 30 个并发、共 300 个请求打一个速率为 10r/s 的接口为例:

ab -n 300 -c 30 http://127.0.0.1/

# 结果里关注:
# Failed requests 数量 > 0,说明有请求被限流拒绝
# Non-2xx responses 数量 ≈ 被 burst 消化后剩余的超限请求数

更直观的方式是看 Nginx 错误日志,被限流的请求会记录 limiting requests, excess: ...:

tail -f /var/log/nginx/error.log | grep limiting
# 输出示例:
# 2026/10/24 10:00:01 [notice] 12345#0: *678 limiting requests, excess: 3.994 by zone "req_limit", client: 1.2.3.4

看到这条日志,说明限流已经生效,1.2.3.4 这个 IP 正在被限流。

七、常见排错与避坑

坑 1:Nginx 拿不到真实客户端 IP
网站前面挂了 CDN 或负载均衡时,$binary_remote_addr 取到的是 CDN 节点 IP,导致所有用户共享同一个限流桶,正常用户也会被误杀。解决办法是用 realip 模块还原真实 IP:

http {
    set_real_ip_from 103.21.244.0/22;  # CDN 的回源网段
    real_ip_header CF-Connecting-IP;   # 根据实际情况选择 X-Forwarded-For / CF-Connecting-IP
    real_ip_recursive on;

    limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
}

坑 2:burst 设得太小,正常用户被限
页面包含大量静态资源时,浏览器会并发建立多个连接、瞬间发出很多请求。如果 rate 和 burst 过小,首屏加载就会触发 503。建议先用 ab 或浏览器开发者工具统计正常访问的请求峰值,再按 2~3 倍余量设置。

坑 3:静态资源也被限流,拖慢访问
静态资源(图片、CSS、JS)通常没必要限流,可以把限流只放在动态 location 上,静态文件单独走一个不限流的 location:

location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg)$ {
    expires 7d;                # 静态资源长缓存,减少请求量
    access_log off;
    # 不写 limit_req 指令,即不限流
}

坑 4:动态封禁怎么做
Nginx 原生限流只能"限",不能"封"。如果需要"连续超限 X 次就把 IP 封 1 小时",可以结合 fail2ban 的 nginx-limit-req filter:

# /etc/fail2ban/filter.d/nginx-limit-req.conf
[Definition]
failregex = limiting requests, excess:.*client: <HOST>
ignoreregex =
# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 60
bantime = 3600

重载 fail2ban 后,同一个 IP 在 60 秒内被限流 5 次就会被 iptables 封禁 1 小时,前端直接丢包,连 Nginx 都到不了。

坑 5:限流对 PHP-FPM 的保护不够
limit_req 只能控制"请求到达 Nginx 的速率",但一个请求可能触发 PHP-FPM 执行 1 秒。此时还需要在 PHP-FPM 侧限制 pm.max_children 和 request_terminate_timeout,否则 Nginx 放行后后端照样被打爆。合理组合是:Nginx 限流挡 90% 的垃圾流量,PHP-FPM 参数兜底剩余部分。

八、总结

给 VPS 上的 Nginx 加限流,本质上是在"正常用户"和"恶意流量"之间划一条线:用 limit_req 卡请求速率,用 limit_conn 卡并发连接,用 limit_req_status 444 提高攻击成本,再配合 geo 白名单和 fail2ban 动态封禁形成完整防线。部署时务必先用 ab 压测验证,观察 error.log 中 limiting requests 日志确认生效,并留意 CDN 回源带来的真实 IP 问题。掌握这套组合拳,绝大多数 CC 攻击都能在 Nginx 这一层被拦下来。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

Nginx 访问日志分析实战:GoAccess 实时统计、GeoIP 归属地解析与自动化报表

Nginx location 匹配与 rewrite 重写规则实战:优先级排序、配置示例与排错指南

评 论
更换验证码