
一个接口被脚本刷的时候,最先出现的不是 502,是响应时间从 80 毫秒变成 4 秒,然后所有人一起骂服务器慢。限流要解决的不是「挡住攻击」,而是「在被打的时候,正常用户还能拿到一句回话」。
nginx 自带两个模块能做完这件事的大部分:limit_req 管速率,limit_conn 管并发。它们不需要装第三方模块,也不需要 Lua。难点从来不在写配置,在于搞清「一个请求」在你这条链路上到底算谁的。
下面这套配置在 Ubuntu 24.04 + nginx 1.24.0(官方 apt 源)上跑通,反向代理一个 FastAPI 服务,实测能在 30 条/秒的脚本压力下,把正常访问的响应时间压在 200 毫秒以内。
1. 先分清三件事,别混着配
限速、限并发、限带宽是三个正交的东西,配错一个,另外两个就白配了。
| 指令 | 管什么 | 典型场景 | 超限表现 |
|---|---|---|---|
| limit_req | 单位时间的请求数 | 登录、搜索、下单接口 | 429 或排队等待 |
| limit_conn | 同时打开的连接数 | 大文件下载、SSE 长连接 | 503 |
| limit_rate | 单连接的字节速率 | 视频、附件下载 | 变慢,不报错 |
很多人只用 limit_req 去治下载站被拖死的问题,配完发现没用——因为慢速下载的请求数很低,一条连接能耗半小时。速率低不等于压力小,这是限流配置最常见的误判。
2. 第一段:定义共享内存区
限流状态存在共享内存里,所以第一件事是给这块内存起名、定大小、定速率。limit_req_zone 只能写在 http 块里,写在 server 里会直接报错。
http {
# 每个 IP 每秒 5 个请求;10MB 共享内存约可容纳 8 万个 IPv4 状态
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 80;
server_name example.com;
...
}
}
两个细节决定这块内存够不够用。第一,官方文档给的估算是 1MB 约 16000 个状态,64 位系统上实际按一半算,也就是 8000 左右;10MB 对应 8 万个 IP。按「日活 IP 数 × 2」估容量,比按请求数估靠谱得多。
第二,用 $binary_remote_addr 而不是 $remote_addr。前者是 4 字节的二进制,后者是字符串,同样内存能多存四倍以上的键。这两个变量功能一样,成本差三倍,多数示例抄的是贵的那一个。
3. 第二段:在 location 里落地
速率定好之后,具体在哪个路径上执行、允许多大的突发,是在 location 里决定的。这是整份配置里唯一需要动脑子的地方。
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
limit_req_status 429;
limit_conn conn_limit 20;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location = /api/login {
limit_req zone=login_limit burst=3;
limit_req_status 429;
proxy_pass http://127.0.0.1:8000;
}
burst=10 是排队容量:超出速率的请求先压进队列,能等就等。nodelay 的意思是队列里的请求立刻放行,不按速率一个个吐。加了 nodelay,突发流量会被原样打给上游;不加,用户就要等,等久了浏览器自己先超时。
登录接口故意不加 nodelay,队列只留 3 个。这种接口的正确行为是「慢一点也可以,但不能被撞库」,让人等 200 毫秒,比让他试第 4 次密码更划算。同一个站点的不同接口,限流策略本来就该不一样。
4. 第三段:白名单、状态码与日志
限流最怕误伤。监控探针、合作方的回调、办公室出口 IP,都需要单独放过去。做法是把限流的键变成空值——nginx 的规则是「键为空时不计数」。
geo $limit_exempt {
default 0;
10.0.0.0/8 1;
192.168.0.0/16 1;
203.0.113.7 1; # 合作方回调出口
}
map $limit_exempt $limit_key {
0 $binary_remote_addr;
1 '';
}
limit_req_zone $limit_key zone=api_limit:10m rate=5r/s;
log_format limited '$remote_addr $request $status req=$limit_req_status';
access_log /var/log/nginx/access.log limited;
$limit_req_status 这个变量很关键,它记录了这次请求是被放行(PASSED)、排队(DELAYED)还是拒绝(REJECTED)。没有这个字段,你只能看到 429 的数量,看不出限流到底在替谁挡枪。
白名单用 IP 段而不是 UA 或 Referer。UA 可以伪造,IP 段不能,代价是运维要维护一份清单。凡是能被客户端随手改掉的特征,都不适合做访问控制的依据。
5. 完整目录与验收
把上面三段拼起来,文件结构是这样:
/etc/nginx/
├── nginx.conf # http 块:zone 定义、log_format、include
├── conf.d/
│ └── api.conf # server 块:location、burst、白名单
└── snippets/
└── proxy_params # 可复用的 proxy_set_header
改完先做语法检查,再热加载。这两步顺序不能反,reload 遇到语法错误会保留旧进程,日志里只有一行提示,很容易被当成「生效了」。
sudo nginx -t
sudo systemctl reload nginx
nginx -v
验收用 curl 打 30 次,看 429 出现的节奏。{1..30} 是 bash 的花括号展开,不涉及命令替换,在任何 shell 里都安全。
for i in {1..30}; do
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/api/health
done | sort | uniq -c
预期结果是二十来个 200 加几个 429,而不是三十个 200,也不是三十个 429。前一种说明限流没生效,后一种说明 burst 太小。限流是否有效,看的是「有没有恰好挡住超出的那部分」,不是「挡得多不多」。
6. 必然踩到的坑
坑一:套了 CDN 之后,所有请求都是同一个 IP。此时 $binary_remote_addr 拿到的是 CDN 节点的地址,全站共用一个桶,第一个用户就能把所有人限死。要先用 set_real_ip_from 声明可信来源,再用 real_ip_header X-Forwarded-For 还原真实 IP,而且 set_real_ip_from 只能写 CDN 的回源段,写 0.0.0.0/0 等于让任何人自带一个假 IP。
坑二:以为 burst 是「允许的并发数」。它是队列长度,配 nodelay 时这十个请求会立刻全部转发给上游。上游只有 2 个 worker,这十个请求照样把它压死。nginx 的限流保护的是 nginx 自己,不保护你的应用。
坑三:改了 rate 却感觉没生效。共享内存里的状态不会因为 reload 清零,旧的计数还在。rate 是编译进 zone 的属性,改完必须重启进程才会按新速率重建。稳妥的做法是换个 zone 名字,旧的自然废弃。
坑四:429 被缓存,或被上游重试放大。如果 429 响应带了较长的 Cache-Control,浏览器会把「你被限流了」缓存下来,用户刷新十次还是 429。另外,配了 proxy_next_upstream 的话,一次超限会变成多次回源。限流响应必须显式声明不缓存,并且从重试名单里排除。
还有一个不算坑但很费时间的点:limit_conn 计的是连接,不是请求。HTTP/1.1 开 keepalive 之后,一条连接上可以跑几十个请求,并发数看起来始终是 1;换成 HTTP/2 之后又是另一套算法。凡是和「连接」有关的指标,都要先确认协议版本再看数。
7. 一份保守的起步参数
不知道自己的站点该配多少,可以从这组数开始,观察一周再调。
- 普通页面:
rate=20r/s、burst=40、nodelay。正常浏览一个页面会带十几个静态请求,速率给低了会误伤。 - API 接口:
rate=5r/s、burst=10、nodelay,前端做好 429 的重试与提示。 - 登录与验证码:
rate=1r/s、burst=3,不加nodelay,让人等。 - 下载路径:
limit_conn 3、limit_rate 2m,速率和并发一起限。
参数没有通用值。同样 5r/s,对一个内部管理后台是宽松,对一个公开搜索接口是苛刻。能抄的只有结构,速率必须按自己的流量分布算。
写在最后
限流配置写完之后会有一种错觉:好像安全了。实际上它只做了一件事——把「一起死」改成了「按顺序活」。真正打穿带宽的攻击,nginx 拦不住,得靠上游清洗。
但这件事仍然值得做,因为绝大多数事故不是攻击,是一个写错的循环、一个忘了加 sleep 的脚本、一个被分享到群里的链接。运维里最有效的措施,往往不是防住坏人,是拦住自己人。
所以那行 rate=5r/s 的真实含义,不是「你只能来五次」,而是「我们这里一次只能接待这么多,你排一下队」。
你给哪个接口配过限流?当时的数字是拍出来的,还是算出来的?

评论 (0)