个人站长最头疼的攻击方式之一就是 CC 攻击。攻击者不需要打满你的带宽,只需要模拟大量正常请求,把 Nginx 的并发连接和 PHP 进程池占满,你的网站就会陷入假死状态:数据库连接数爆满、CPU 持续 100%、用户访问一片空白。对于没有硬件防火墙的小服务器来说,Nginx 自带的 limit_req 和 limit_conn 模块就是第一道也是最有效的防线。本文从原理到实战,完整讲解如何用这两个模块给网站加上可靠的限流保护。
一、为什么要用 Nginx 限流
CC 攻击的特点是请求量巨大但每个请求本身看起来很正常,传统的 IP 封禁手段很难奏效,因为攻击者可以不断更换代理 IP。而限流的思想完全不同:不管请求来自哪里,只要单位时间内的请求频率或者并发连接数超过阈值,就直接拒绝或者排队处理。这样即使攻击者换了成千上万个 IP,每个 IP 能打进来的请求数量也是有限的,服务器的压力始终可控。
Nginx 的限流模块是官方自带的,不需要额外安装第三方组件,配置简单、性能极高。其中 limit_req 模块负责限制请求速率,limit_conn 模块负责限制并发连接数,两者配合使用可以覆盖绝大多数场景。
二、limit_req:请求频率限制
2.1 基本语法与配置位置
limit_req 的使用分两步:第一步在 http 块中定义限流区域(zone),第二步在 server 或者 location 块中调用。先看一个最简单的例子:
http {
# 定义限流区域:以客户端 IP 为 key,共享内存 10m,速率 5r/s
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=5r/s;
server {
listen 80;
server_name www.example.com;
location / {
# 应用限流,允许 10 个突发请求
limit_req zone=req_zone burst=10;
}
}
}limit_req_zone 指令有三个关键参数:第一个是 key,一般用 $binary_remote_addr 表示客户端 IP,相比 $remote_addr,二进制格式只占 4 字节(IPv4),更节省共享内存;第二个参数 zone=名字:大小,指定限流区域名称和共享内存大小,10m 大约能存储 16 万个 IP 的状态;第三个参数 rate 是速率,支持 r/s(每秒请求数)和 r/m(每分钟请求数),例如 30r/m 表示每分钟 30 个请求。
2.2 burst 与 nodelay 参数详解
直接应用 limit_req 后,超过速率的请求会立即被拒绝,但这样容易误伤正常用户。比如 rate=5r/s 表示每秒只能处理 5 个请求,用户快速刷新页面时连续发出 10 个请求,后面 5 个会被直接返回 503。burst 参数就是为了解决这个问题:它允许一定数量的突发请求进入排队队列,超出速率的部分先排队,再按速率慢慢处理。
默认情况下排队请求会引入延迟,也就是令牌桶算法的实现:每个请求需要等待前面的请求处理完。如果不想让用户等待,可以加上 nodelay 参数,表示突发队列中的请求立即处理,不排队。注意 nodelay 只对 burst 范围内的请求生效,超过 burst 的请求依然会被拒绝。实际配置中建议 burst 设置成 rate 的 2 到 5 倍,既能抵御突发的正常流量,又能拦截真正的攻击。
三、limit_conn:并发连接数限制
频率限制防的是短时间内的大量请求,但有些攻击是慢速连接型:攻击者建立大量连接后不发送完整请求,或者慢慢发送数据,把服务器的连接数占满。这时候需要 limit_conn 模块来限制单个 IP 的并发连接数:
http {
limit_conn_zone $binary_remote_addr zone=conn_zone:10m;
server {
location / {
# 每个 IP 最多同时建立 20 个连接
limit_conn conn_zone 20;
# 超过限制时返回 503
limit_conn_status 503;
}
}
}limit_conn_zone 的语法和 limit_req_zone 类似,同样是定义 key 和共享内存。limit_conn 的值需要根据网站类型来定:普通内容站每个用户同时打开的连接数一般不超过 10 个(浏览器对同一域名的并发连接数有限制),图片较多的网站可以适当放宽到 30 到 50。如果设置得太小,用户打开页面时图片加载会被截断,影响体验。
四、按 URI 精细化限流
CC 攻击最喜欢打的是消耗资源的动态接口,比如登录接口、搜索接口、评论提交接口。这些接口每执行一次都要查询数据库,压力远大于静态页面。因此合理的做法是:全站做宽松限流,关键接口做严格限流。
http {
# 全站限流:每 IP 每秒 20 个请求
limit_req_zone $binary_remote_addr zone=global:10m rate=20r/s;
# 登录接口限流:每 IP 每分钟 5 个请求
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location / {
limit_req zone=global burst=40 nodelay;
}
location /wp-login.php {
limit_req zone=login burst=3 nodelay;
}
location /api/search {
limit_req zone=login burst=5 nodelay;
}
}
}这里有两个细节值得注意。第一,zone 的名字可以自定义,但同一个 zone 不能在不同位置重复定义;第二,如果需要按照 URI 来区分限流,可以使用 $request_uri 变量作为 key,例如 limit_req_zone $request_uri zone=uri_zone:10m rate=10r/s,这样每个 URI 单独计数,可以防止攻击者用同一个 IP 轮换不同路径绕过限制。
五、白名单与放行策略
限流是一把双刃剑,搜索引擎爬虫、CDN 回源 IP 都可能被误伤。如果百度蜘蛛被限流,收录就会受影响。解决方法是给可信 IP 放行,常见做法是配合 geo 模块:
http {
# 定义可信 IP 分组
geo $limited {
default 1;
127.0.0.1 0;
192.168.1.0/24 0;
220.181.108.0/24 0; # 百度蜘蛛网段示例
}
# 用 map 把标记转换成限流开关
map $limited $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
server {
location / {
# $limit_key 为空时不会匹配任何 zone,相当于不限流
limit_req zone=req_zone burst=10 nodelay;
}
}
}这个写法的巧妙之处在于:当 $limited 为 0 时,$limit_key 被 map 成空字符串,limit_req 不会对空 key 计数,从而实现了白名单放行。如果不想维护 IP 段,也可以把限流 key 换成 $http_user_agent 结合 map 判断,把含有 Baiduspider、Googlebot 的请求单独分组。需要注意的是,伪造 User-Agent 的成本很低,所以基于 UA 的白名单只能作为辅助手段,不能完全依赖。
六、错误处理与自定义提示
默认情况下,被限流的请求会返回 503 Service Temporarily Unavailable。对于用户体验来说,503 太笼统了,更好的做法是返回 429 Too Many Requests 并给出明确的提示。Nginx 1.18 及以上版本支持 limit_req_status 和 limit_conn_status 指令:
server {
location / {
limit_req zone=req_zone burst=10 nodelay;
limit_req_status 429;
limit_conn conn_zone 20;
limit_conn_status 429;
error_page 429 /429.html;
}
}
location = /429.html {
root /var/www/html;
internal;
}error_page 429 /429.html 会把被限流的请求重定向到自定义页面,注意 429.html 要放在 root 指定的目录下,并且 location 要加上 internal 关键字防止被直接访问。同时建议开启限流日志:limit_req_log_level notice,这样在错误日志中可以看到类似 limit_req zone "req_zone" burst 之类的记录,方便分析攻击来源。
七、与 Fail2ban 联动封禁
限流只能控制请求速率,不能彻底阻止攻击者。对于持续攻击的 IP,可以结合 Fail2ban 实现自动封禁:Fail2ban 监控 Nginx 的错误日志,当某个 IP 在短时间内出现大量 429 或者 503 记录时,自动调用 iptables 或者 firewalld 封禁该 IP 一段时间。配置思路如下:
# /etc/fail2ban/filter.d/nginx-limitreq.conf
[Definition]
failregex = ^.*\[error\].*limit_req.*client: <HOST>.*
# /etc/fail2ban/jail.local
[nginx-limitreq]
enabled = true
filter = nginx-limitreq
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 3600
action = iptables-multiport[name=nginx, port="http,https"]注意 failregex 中的 <HOST> 是 Fail2ban 的占位符,需要写成小于号和大于号包围的形式,这里为了在 HTML 中显示做了转义。这个组合拳的效果是:限流负责兜底,保证服务器不死;Fail2ban 负责反击,把高频攻击 IP 直接拉黑。
八、完整实战配置示例
最后给出一份可以直接套用的完整配置,覆盖了请求频率限制、并发连接限制、白名单放行和自定义错误页:
http {
# 可信 IP 放行
geo $limited {
default 1;
127.0.0.1 0;
your_server_ip 0;
}
map $limited $limit_key {
0 "";
1 $binary_remote_addr;
}
# 全局限流
limit_req_zone $limit_key zone=global:20m rate=20r/s;
# 登录接口严格限流
limit_req_zone $limit_key zone=strict:10m rate=5r/m;
# 并发连接限制
limit_conn_zone $limit_key zone=conn:20m;
server {
listen 80;
listen 443 ssl;
server_name www.example.com;
limit_req_status 429;
limit_conn_status 429;
error_page 429 /429.html;
location / {
limit_req zone=global burst=40 nodelay;
limit_conn conn 30;
# 正常站点处理逻辑
try_files $uri $uri/ /index.php?$query_string;
}
location ~ ^/(wp-login\.php|xmlrpc\.php|admin) {
limit_req zone=strict burst=3 nodelay;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
}
location = /429.html {
root /var/www/html;
internal;
}
}
}配置完成后执行 nginx -t 检查语法,然后 nginx -s reload 平滑生效,不需要重启服务,正在处理的连接不会中断。
九、常见问题与踩坑记录
1. 限流误伤正常用户。最常见的表现是用户正常浏览时频繁出现 429。解决办法是把 rate 适当放宽,同时加大 burst 值。另外注意限流是按 key 计数的,如果网站部署在 CDN 后面,所有用户的 IP 都是 CDN 节点 IP,会被当成同一个客户端,此时应该改用 $http_x_forwarded_for 或者 CDN 提供的真实 IP 变量作为 key,否则会大面积误伤。
2. 多个 worker 进程下限流是否准确。limit_req 的计数保存在共享内存中,所有 worker 进程共享同一份状态,所以不用担心计数不准的问题。但要注意 zone 内存大小,如果内存耗尽,Nginx 会报出 zone exhausted 错误,此时需要调大 zone 大小。
3. 静态资源也被限流。图片、CSS、JS 等静态文件通常应该直接交给 Nginx 处理而不走限流,可以把静态资源的 location 放在限流 location 之前,或者给静态资源单独设置宽松的 zone,避免页面加载时并发请求被误杀。
4. 和缓存插件配合。如果网站开启了 Nginx 缓存或者 CDN 缓存,大部分请求在到达后端之前就被缓存命中,限流压力会小很多。建议限流配置放在缓存层之后,只对回源请求限流,这样既能保护后端又能减少误伤。
十、总结
limit_req 和 limit_conn 是 Nginx 内置的限流利器,配置得当可以挡住绝大多数 CC 攻击,让个人小站在一台 1 核 1G 的廉价 VPS 上也能稳定运行。核心要点可以概括为:频率限制用 limit_req_zone 定义区域再在 location 中引用,突发流量用 burst 加 nodelay 吸收,并发控制用 limit_conn,白名单用 geo 加 map 实现,攻击反击交给 Fail2ban。限流的本质是取舍:牺牲极小部分极端流量,换取服务器整体稳定。建议在正式上线前用 ab 或者 wrk 压测工具模拟高并发,验证限流参数是否合理,再逐步调整到最优值。