DDoS防护中Nginx层限速模块(主要是ngx_http_limit_req_module和ngx_http_limit_conn_module)与上游超时之间的矛盾,是高并发防护场景下最容易踩的坑。简单说:你在Nginx层开了限速,比如每秒只允许100个请求,但上游应用(PHP-FPM、Tomcat、Node.js等)处理一个请求需要5秒,那Nginx就会在1秒内把100个连接全部派发出去,上游瞬间被打满,然后Nginx自己也因为等不到上游响应而触发proxy_read_timeout报504错误。这不是防护,这是自残。核心解法是:限速模块的速率必须与上游处理能力、超时配置做联动计算,而不是拍脑袋设一个数字。
这个问题在实际运维中非常普遍。很多安全团队在Nginx上配了limit_req_zone和limit_conn_zone,觉得自己做了DDoS防护,结果一波攻击过来,Nginx没崩,后端服务先挂了,或者Nginx自己报大量超时错误,日志里全是upstream timed out。根本原因就是限速策略和超时策略之间没有做数学上的匹配。下面我从原理、配置、计算方法、实战调优四个维度把这件事讲透。
一、Nginx限速模块的工作原理与DDoS防护逻辑
Nginx自带两个核心限速模块。第一个是ngx_http_limit_req_module,它基于漏桶算法(Leaky Bucket)对请求速率进行限制,比如你设置rate=100r/s,意思是每秒最多处理100个请求,超出的直接返回503。第二个是ngx_http_limit_conn_module,它限制的是并发连接数,比如limit_conn_zone $binary_remote_addr zone=addr:10m,然后limit_conn addr 50,意思是每个IP最多同时保持50个连接。
在DDoS防护场景下,这两个模块通常组合使用。limit_conn先把单个IP的并发压下来,limit_req再把整体QPS压下来。看起来逻辑很完美,但问题出在:限速只管"放多少请求进去",不管"上游能不能消化"。你把请求放进去了,上游处理不过来,请求就堆积在Nginx的upstream队列里,直到超时。
这里有一个关键认知:Nginx的限速模块工作在请求进入阶段,而proxy_pass的超时工作在请求转发阶段。这两个阶段是串联的,但配置是独立的。很多人只配了前者忘了后者,或者配了后者但数值不合理,导致防护策略形同虚设甚至反向伤害。
二、上游超时的核心参数及其影响
Nginx作为反向代理时,与上游通信涉及多个超时参数,每个都有不同含义:
proxy_connect_timeout:Nginx与上游建立TCP连接的超时时间,默认60秒。这个一般不是瓶颈,除非上游服务挂了或者网络不通。
proxy_send_timeout:Nginx向上游发送请求体的超时时间,默认60秒。如果客户端上传大文件,这个值要调大。
proxy_read_timeout:Nginx等待上游返回响应的超时时间,默认60秒。这是最关键的一个,也是DDoS场景下最容易触发的。因为攻击流量进来后,上游处理慢,Nginx等不到响应就报504。
还有一个容易被忽略的参数:proxy_next_upstream。它定义了什么情况下Nginx会尝试下一个upstream服务器。如果你配置了多台上游,合理设置这个参数可以提高可用性。
http {
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
location / {
proxy_pass http://backend;
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_503;
}
}
}在DDoS防护场景下,proxy_read_timeout不建议设太大。设太大意味着Nginx会长时间等待上游,占用worker连接和内存,反而降低了Nginx自身的抗攻击能力。一般建议设为10-30秒,具体取决于你的业务正常响应时间。
三、限速与超时的数学关系:怎么算才对
这是整篇文章最硬核的部分。你需要理解一个公式:
Nginx最大并发请求数 = 限速速率(r/s)× 上游平均响应时间(s)
举个例子:你设了limit_req rate=100r/s,上游平均响应时间是2秒,那Nginx同时派发出去的请求就是100×2=200个。如果你的upstream配置了max_conns=150,那多出来的50个请求就会排队或者被拒绝。如果你没配max_conns或者配得很大,那这200个请求全部打到上游,上游如果只能处理150个并发,就会有50个请求超时。
所以正确的做法是反过来算:先知道上游能处理多少并发,再算限速应该设多少。
假设你的PHP-FPM配置了pm=static,pm.max_children=200,每个请求平均处理3秒。那上游最大吞吐量大约是200/3≈66r/s。你的limit_req就不应该设超过66r/s,否则就是在制造堆积。
更精确的计算还要考虑安全余量。一般建议限速速率设为上游理论吞吐量的70%-80%。因为攻击流量来的时候,响应时间会变长,上游实际吞吐量会下降。留20%-30%的余量给突发情况。
# 假设上游最大并发200,平均响应3秒
# 理论吞吐量 = 200 / 3 ≈ 66 r/s
# 安全限速 = 66 * 0.75 ≈ 50 r/s
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=50r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
location / {
limit_req zone=one burst=10 nodelay;
limit_conn addr 20;
proxy_pass http://backend;
proxy_read_timeout 15s;
}
}
}这里还要解释burst和nodelay参数。burst=10表示允许短时间内突发10个请求,nodelay表示这些突发请求不做延迟处理直接放行。如果不加nodelay,突发请求会被排队慢慢放,可能导致前面的请求还没处理完后面又来了,加剧超时问题。在DDoS防护场景下,一般建议burst设小一点,比如5-15,nodelay必须加上。
四、实战中常见的错误配置与修正方案
错误一:限速只针对总QPS,不区分IP。很多人配了一个全局的limit_req,比如rate=1000r/s,觉得够用了。但DDoS攻击往往是分布式的,每个IP只发一点,总量不大但IP数很多。正确做法是基于$binary_remote_addr做限速,同时再加一个全局限速做兜底。
# 基于IP的限速(防分布式攻击)
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# 全局限速(防总量攻击)
limit_req_zone $server_name zone=perserver:10m rate=500r/s;
server {
location / {
limit_req zone=perip burst=5 nodelay;
limit_req zone=perserver burst=50 nodelay;
}
}错误二:proxy_read_timeout设成几百秒。有些人怕超时,直接设成300秒。这在正常业务下可能没问题,但在DDoS场景下是灾难。因为每个worker能处理的连接数是有限的(由worker_connections决定),如果每个连接都等300秒,很快worker就被占满,新的合法请求也进不来。建议DDoS防护场景下proxy_read_timeout控制在10-20秒,快速失败快速释放。
错误三:忽略了upstream的keepalive配置。如果你的Nginx和上游之间没有开启keepalive,每个请求都要重新建立TCP连接,这会增加延迟,也会让proxy_connect_timeout更容易触发。正确配置是:
upstream backend {
server 127.0.0.1:8080;
keepalive 64;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}错误四:没有做分级限速。所有请求一视同仁地限速,静态资源和动态接口用同一个速率。实际上静态资源(图片、CSS、JS)可以放得宽一些,动态接口(登录、下单、查询)要严格限制。可以用map指令或者多个location块来实现分级。
五、进阶策略:动态限速与监控联动
静态配置的限速在面对变化的攻击流量时是不够的。更高级的做法是结合实时监控做动态调整。比如通过Nginx的stub_status模块或者第三方监控(如Prometheus+Grafana)实时获取上游响应时间和QPS,然后通过脚本动态修改limit_req的rate值。
虽然Nginx本身不支持动态修改limit_req_zone的rate(需要reload配置),但你可以通过Lua模块(ngx_http_lua_module)或者OpenResty实现运行时动态限速。比如根据当前上游的平均响应时间,自动计算并调整限速阈值。
-- OpenResty动态限速示例思路
local limit = ngx.shared.limit_data
local key = ngx.var.binary_remote_addr
local current_rate = limit:get(key) or 10
-- 根据上游响应时间动态调整
if upstream_avg_response_time > 5 then
limit:set(key, current_rate * 0.8) -- 降低限速
elseif upstream_avg_response_time < 1 then
limit:set(key, current_rate * 1.2) -- 适当提高
end这种方案的好处是:攻击来了自动收紧,攻击走了自动放松,不会误杀正常流量。但实现复杂度较高,需要有完善的监控体系和自动化运维能力。
六、总结:防护不是一个参数的事
DDoS防护在Nginx层做限速,本质上是一个系统工程。limit_req和limit_conn只是第一道门,proxy_read_timeout是第二道门,upstream的并发能力是地基。三者必须匹配,否则要么防护失效,要么自己把自己搞崩。
核心原则就三条:第一,限速速率不超过上游处理能力的70%-80%;第二,超时时间要短,快速失败快速释放资源;第三,分级分类限速,不要一刀切。把这三条做到位,Nginx层的DDoS防护才算真正有效,而不是只是看起来配了东西。
最后提醒一点:Nginx层限速只能应对应用层(L7)的DDoS攻击,对于大流量的网络层(L3/L4)攻击,比如SYN Flood、UDP Flood,Nginx本身就扛不住,需要在更上游的防火墙、CDN或者运营商层面做清洗。不要把所有希望都压在Nginx配置上,分层防御才是正道。
