ElIauk

Nginx 限流与缓存配置

限流保护的是什么 限流不是为了防住所有攻击,而是保证系统在超过处理能力时,仍然能把一部分请求服务好。没有限流,突发流量会把线程池、连接池、数据库连接全部占满,最后所有请求一起超时;有了限流,超出部分快速失败,至少让正常用户还能用。 Nginx 提供两种维度的限制:limit_req 按速率限制请求数

评论
Nginx 限流与缓存配置

限流保护的是什么

限流不是为了防住所有攻击,而是保证系统在超过处理能力时,仍然能把一部分请求服务好。没有限流,突发流量会把线程池、连接池、数据库连接全部占满,最后所有请求一起超时;有了限流,超出部分快速失败,至少让正常用户还能用。

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 观察真实流量,最后才把阈值收紧。没有容量数据的限流配置,只是给自己一个心理安慰。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者