限流保护的是什么
限流不是为了防住所有攻击,而是保证系统在超过处理能力时,仍然能把一部分请求服务好。没有限流,突发流量会把线程池、连接池、数据库连接全部占满,最后所有请求一起超时;有了限流,超出部分快速失败,至少让正常用户还能用。
Nginx 提供两种维度的限制:limit_req 按速率限制请求数,基于漏桶算法;limit_conn 限制并发连接数。前者防“请求太多”,后者防“连接占着不放”。两者可以叠加,但阈值要结合后端真实容量来定,拍脑袋写一个 100r/s 没有意义。
限流配置
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_req_status 429;
limit_conn_status 503;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
proxy_pass http://order_backend;
}
}
}
rate=10r/s 是平均速率,burst=20 允许突发 20 个请求排队,nodelay 表示突发请求立即处理而不是排队延迟。不加 nodelay 时,超过速率的请求会被延迟到下一个时间窗口,适合对平滑度要求高的场景;加了 nodelay,突发流量会被快速放行,但更容易压到后端。
limit_req_zone 的 10m 共享内存大约能存 16 万个 IP 状态,超出后会淘汰旧记录,限流精度下降。按用户 ID 限流时,把 key 换成 $http_x_user_id 之类的变量,但要确保这个头不能被客户端伪造。上线前可以用 limit_req_dry_run on 只记录不拦截,观察真实触发量。
缓存配置
静态资源、商品详情这类读多写少的数据,可以在 Nginx 层做缓存,挡住回源的流量:
proxy_cache_path /data/nginx/cache
levels=1:2
keys_zone=api_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;
server {
location /api/products/ {
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_lock on;
proxy_cache_background_update on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://product_backend;
}
}
proxy_cache_key 决定了缓存粒度,默认只按 URI,带查询参数的接口要显式把参数拼进去。proxy_cache_lock 让同一个 key 的并发回源只有一个请求,其余等待结果,能防止缓存击穿。proxy_cache_use_stale 在后端故障时返回旧缓存,用“可能过期”换“可用”。
X-Cache-Status 是排查缓存是否命中的关键,常见值有 MISS、HIT、EXPIRED、UPDATING、BYPASS。响应头里看不到这个字段,先检查 add_header 是不是被后面的 location 覆盖了。
常见坑
1. 把用户私有数据缓存了
缓存 key 只用了 URL,没带用户 ID 或 Authorization,A 用户看到了 B 用户的订单。带登录态的接口要么加 proxy_cache_bypass,要么在 proxy_cache_key 里加入用户标识,并且确保响应头有 Cache-Control: private。
2. 缓存了 Set-Cookie
后端响应带 Set-Cookie,Nginx 默认不会缓存,但如果配置了 proxy_ignore_headers Set-Cookie,登录态就会被广播给所有人。除非明确知道自己在做什么,不要忽略 Set-Cookie 和 Vary。
3. 按 IP 限流在 NAT 后面误伤
公司出口、学校、CDN 后面大量用户共享一个 IP,按 IP 限流会把正常用户一起限掉。可以按用户 ID、API Key 或设备号限流,IP 只作为兜底;同时要防止客户端伪造 X-Forwarded-For,只信任自己代理链路上的头。
4. burst 设得太大
burst=1000 看起来能抗突发,实际上是让一千个请求同时打到后端,限流形同虚设。burst 应该根据后端能承受的瞬时并发来定,通常是个位到几十的量级。
5. 429 没有告诉客户端何时重试
直接返回 429,客户端可能立即重试,形成重试风暴。响应里加 Retry-After,客户端配合指数退避,才能真正把压力降下来。
6. 缓存和限流互相干扰
限流触发后返回 429,如果没有排除错误响应,可能被缓存下来,后续正常请求也拿到 429。用 proxy_cache_valid 明确只缓存 200、404 这类状态,不要把 5xx 和 429 缓存进去。
排查步骤
先验证限流是否按预期触发:
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/api/ping
done | sort | uniq -c
正常应该看到一部分 200、一部分 429;如果全是 200,检查 limit_req 是否写在正确的 location,或者 key 变量取到了空值。如果全是 429,检查 rate 和 burst 是不是太小。
缓存问题先看响应头:
curl -sI http://127.0.0.1/api/products/1001 | grep -i x-cache-status
curl -sI -H 'Cache-Control: no-cache' http://127.0.0.1/api/products/1001
MISS 后再次请求应该是 HIT;一直 MISS,检查 proxy_cache_key 是否包含了每次都变化的参数,比如时间戳。BYPASS 说明请求带了跳过缓存的头或 cookie。最后看 error.log 和缓存目录权限,Nginx worker 用户没有写权限时,缓存会静默失败。
小结
限流和缓存的参数都不难写,难的是确定边界:后端到底能扛多少、哪些响应可以缓存、按什么维度限流。先压测拿到容量,再用 dry_run 观察真实流量,最后才把阈值收紧。没有容量数据的限流配置,只是给自己一个心理安慰。
评论