大部分运维人员配置Nginx限流时,最先想到的就是limit_req_zone和limit_conn_zone这两个模块。在面对CC攻击时,很多人直接把速率调低,比如限制每秒1个请求,然后发现正常用户开始频繁看到503错误,攻击却依然能穿透。这个问题的根源在于对Nginx限流模块工作机理的理解停留在表面,没有针对CC攻击的特征做精细化调优。
CC攻击本质上不是洪水级的带宽耗尽,而是大量看似合法的请求占满应用层资源。攻击者往往利用代理IP池,每个IP的请求频率并不高,但并发总量巨大。这时候基于单一IP速率的限制会陷入两难:阈值设高了拦不住,设低了误杀正常用户。所以配置限流的第一件事不是调参数,而是先搞清楚你的业务模型——哪些URL是重资源的,哪些是轻量的,用户正常的访问路径是什么。
limit_req_zone的burst参数被严重误解绝大多数配置文档会告诉你burst是“突发容量”,这个说法没错但容易误导。实际上burst的工作方式依赖于nodelay选项是否开启。不开启nodelay时,burst内的请求会被排队延迟处理,这等于在帮攻击者消耗你的连接资源。看一个典型的有问题配置:
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
server {
location / {
limit_req zone=perip burst=5;
proxy_pass http://backend;
}
}
这个配置下,如果攻击者以每秒10个请求的速率打过来,前1个立即处理,后面5个进入burst排队,剩下4个被拒绝。表面看拦截了40%的请求,但实际上那5个排队的请求会持续占用worker连接,后端应用服务器依然要处理这些请求,只是时间上被拉长了。对于数据库连接池这类稀缺资源,延迟处理并不能减轻压力。
正确的做法是绝大多数场景下应该加上nodelay:
limit_req zone=perip burst=5 nodelay;
这样burst内的请求会被立即处理,但超过速率加burst总量的请求直接拒绝。nodelay让限流从“延迟”变成了真正的“丢弃”,保护后端的效果立竿见影。不过这里又引出一个新问题:burst设多大?很多人拍脑袋设个5或者10,这完全脱离业务实际。如果你的页面平均有20个静态资源,用户在加载页面时会在短时间内发起大量请求,burst太小会导致页面加载残缺。正确的做法是分析正常用户在一个页面访问周期内的请求数量,burst至少覆盖这个数量的1.2倍。
基于IP的限流在代理环境下形同虚设现在大量用户通过移动运营商NAT网关或者企业代理上网,一个IP后面可能有成百上千的真实用户。反过来,攻击者用代理IP池发起攻击时,每个IP的请求量可能只有个位数。这时候基于$binary_remote_addr的限流完全失效。很多人知道要用$http_x_forwarded_for来获取真实IP,但直接替换变量是危险的:
# 危险配置,X-Forwarded-For可以被伪造 limit_req_zone $http_x_forwarded_for zone=perip:10m rate=1r/s;
攻击者可以每个请求换一个伪造的X-Forwarded-For头,限流模块就完全成了摆设。必须配合real_ip模块,只信任你确认来源的代理层IP:
set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; real_ip_header X-Forwarded-For; real_ip_recursive on;
这样$binary_remote_addr才会被替换为经过验证的真实客户端IP,后续的limit_req_zone才能正常工作。但即便如此,面对大规模代理IP池攻击,单IP限流依然不够用。
多维度的组合限流策略单靠IP维度限流对抗CC攻击是远远不够的。需要引入请求特征维度,最有效的是结合URL和请求方法。攻击通常集中在登录接口、搜索接口、支付回调等特定URL上,这些URL往往消耗资源较大。针对这些高危接口单独配置更严格的限流:
limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m;
location /api/login {
limit_req zone=login burst=2 nodelay;
proxy_pass http://backend;
}
登录接口每分钟3次足够正常用户使用,但对撞库攻击能起到明显的遏制作用。更进一步,可以结合请求方法做区分。同一个URL的GET和POST请求对后端资源的消耗完全不同,可以创建复合key:
limit_req_zone $binary_remote_addr$request_method$uri zone=dynamic:20m rate=10r/s;
这个配置把IP、请求方法和URI三者组合作为限流键,粒度更细。但要注意zone的大小,10m大约能存储16万个key,如果业务复杂、URI变体多,zone很容易耗尽。监控zone的使用情况是必须的,否则限流会静默失效。
limit_conn的误用与连接数陷阱limit_conn用来限制并发连接数,很多人认为这是对抗CC的利器,实际上这个模块对慢速攻击和连接耗尽型攻击更有效,对常规CC攻击效果有限。CC攻击的请求通常是快速的、短连接的,并发连接数并不高。如果错误地把limit_conn设得很低,比如限制每个IP只能有5个并发连接,正常浏览器通常会开启6到8个并发连接来加载页面资源,结果就是正常用户被误伤。
limit_conn真正发挥作用的地方是限制那些容易产生长连接的接口,比如WebSocket、文件下载、流媒体等。对于普通的HTTP API,limit_req配合合理的burst和nodelay才是主角。另外limit_conn可以和$server_name变量结合,在虚拟主机层面做总量控制,防止单个站点耗尽所有worker连接。
日志与监控是调优的基础没有日志分析的限流调优就是盲调。Nginx默认的访问日志不会记录请求是否被限流拒绝,需要自定义日志格式:
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'limit_req_status=$limit_req_status';
access_log /var/log/nginx/ratelimit.log ratelimit;
$limit_req_status变量会记录请求被限流时的状态,值为PASSED、DELAYED、REJECTED、DELAYED_DRY_RUN、REJECTED_DRY_RUN。通过分析这个字段,可以精确知道有多少请求被延迟处理、多少被直接拒绝。如果DELAYED的比例很高,说明burst在大量被使用,需要检查是否应该开启nodelay或者调高速率。如果REJECTED比例高但业务正常,说明限流阈值可能太严格。
dry_run模式是上线前的安全网直接在生产环境修改限流参数是运维的噩梦,一旦配置过严就会造成大面积用户投诉。Nginx从1.17.0版本开始支持dry_run模式,这是个被严重低估的功能:
limit_req zone=perip burst=5 nodelay dry_run;
开启dry_run后,限流逻辑照常运行,但不会真正拒绝请求,只是在日志中记录哪些请求会被拒绝。通过分析dry_run日志,可以安全地评估新策略的影响范围,调整参数直到误杀率在可接受范围内,再去掉dry_run正式启用。这个功能应该成为任何限流策略变更的标准流程。
CC攻击下的动态限流思路静态的限流阈值无法同时兼顾日常流量和攻击时的流量特征。攻击发生时,手动调整配置再reload的响应时间太长。可以利用Nginx的共享内存和Lua模块实现动态限流,或者更简单地,使用Nginx Plus提供的API动态修改限流参数。对于开源Nginx,可以通过编写简单的Lua脚本,根据特定条件动态调整限流开关:
location /api/ {
access_by_lua_block {
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("perip", 10, 5)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate limit.req: ", err)
return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
}
proxy_pass http://backend;
}
这种方式可以在Lua脚本中读取外部数据源,比如Redis中的黑名单或实时速率配置,实现更灵活的限流策略。不过引入Lua会增加性能开销,需要权衡。
多层限流体系的架构设计单靠Nginx一层限流无法应对复杂的CC攻击场景。合理的架构应该是在Nginx层面做第一层粗粒度限流,拦截明显的异常流量;在应用层面做第二层业务限流,比如基于用户ID、会话token的限流,这能解决IP池攻击的问题;在更前端还可以接入专业的DDoS清洗服务做第三层防护。Nginx限流模块的定位是低成本、高效率的第一道防线,不要期望它解决所有问题。
最后需要强调的是,任何限流策略上线后都要持续观察业务指标。限流不是目的,保障服务可用性才是目的。如果限流策略导致正常用户转化率下降,那比CC攻击造成的损失可能更大。调优的过程就是在安全性和可用性之间找到平衡点,这个平衡点每个业务都不同,没有通用模板。
