目录

Nginx-08 缓存机制

前置阅读:Nginx-07 动静分离与静态资源优化网络-13 HTTP 缓存

上一篇讲的是浏览器端的缓存(让客户端别来请求)。这一篇讲Nginx 自己的缓存(请求来了不回源)。两者是不同层次,配合使用效果最好。

1. proxy_cache 基础

1.1 最小可用配置

http {
    # 定义缓存区(必须在 http 层,不能在 server/location 里)
    proxy_cache_path /var/cache/nginx/proxy
                     levels=1:2
                     keys_zone=my_cache:100m
                     max_size=10g
                     inactive=60m
                     use_temp_path=off;

    server {
        location /api/ {
            proxy_pass http://backend;

            proxy_cache my_cache;                    # 启用哪个缓存区
            proxy_cache_valid 200 302 10m;           # 2xx/302 缓存 10 分钟
            proxy_cache_valid 404 1m;                # 404 缓存 1 分钟
            proxy_cache_valid any 1m;                # 其他状态码

            add_header X-Cache-Status $upstream_cache_status;   # 便于调试
        }
    }
}

1.2 proxy_cache_path 参数详解

proxy_cache_path /var/cache/nginx/proxy
                 levels=1:2
                 keys_zone=my_cache:100m
                 max_size=10g
                 inactive=60m
                 min_free=1g
                 manager_files=100
                 manager_sleep=200ms
                 manager_threshold=300ms
                 use_temp_path=off;
参数 说明
第一个参数 缓存文件的存放目录
levels=1:2 目录层级:一级目录名 1 个字符,二级 2 个字符
keys_zone=名字:大小 共享内存区,存缓存 key 的元数据。1MB 约能存 8000 个 key
max_size 缓存文件占用的磁盘上限,超了由 cache manager 按 LRU 淘汰
inactive 多久没被访问就删除(不管有没有过期
min_free 磁盘剩余空间低于此值就开始淘汰(1.19.1+)
use_temp_path=off 必须设 off,让临时文件直接写在缓存目录里
manager_* 控制 cache manager 的清理节奏,避免清理时 I/O 抖动

levels 为什么需要? 缓存文件名是 key 的 MD5 值。如果全放一个目录,几百万个文件会让文件系统的目录查找变得极慢(ext4 虽有 htree 索引但仍不理想)。分层后:

key = "httpGETbackend/api/user/1"
md5 = c0a8f5e2b1d4...3f7a
levels=1:2 → /var/cache/nginx/proxy/a/f7/c0a8f5e2b1d4...3f7a
                                    ↑  ↑
                              倒数第1位 倒数第2-3位

注意是从 MD5 的末尾取字符levels=1:2 产生 16 × 256 = 4096 个目录,每个目录放几千个文件比较合适;文件更多可以用 levels=2:2(65536 个目录)。

use_temp_path=off 为什么必须设? 默认情况下 Nginx 先把上游响应写到 proxy_temp_path,完成后再 rename 到缓存目录。如果这两个路径不在同一个文件系统上,rename 会退化成跨文件系统的复制,造成额外的磁盘 I/O。设 off 后临时文件直接写在缓存目录下,rename 是同文件系统内的原子操作,几乎零成本。

keys_zone 大小怎么算? 每个 key 的元数据占约 128 字节。100m ≈ 80 万个 key。共享内存满了的后果是开始按 LRU 强制淘汰,即使磁盘还有空间。所以 keys_zonemax_size 要匹配:

预估 key 数量 = max_size / 平均响应体大小
keys_zone 大小 = 预估 key 数量 × 128 字节 × 1.5(留余量)

例:max_size=10g,平均响应 20KB → 约 50 万个 key → keys_zone 需要 ≈ 96MB

1.3 缓存状态 $upstream_cache_status

含义
MISS 没命中缓存,回源了,响应已被缓存
HIT 命中缓存,没回源
EXPIRED 缓存存在但已过期,回源取了新的
STALE 用了过期的缓存(proxy_cache_use_stale 生效)
UPDATING 用了过期缓存,同时另一个请求正在更新(proxy_cache_use_stale updating
REVALIDATED 缓存过期,向上游验证后发现没变(proxy_cache_revalidate on
BYPASS proxy_cache_bypass 规则跳过,直接回源
- 这个 location 没启用缓存

调试时一定要把它加到响应头和日志里:

add_header X-Cache-Status $upstream_cache_status always;

log_format cache '$remote_addr "$request" $status '
                 'cache=$upstream_cache_status rt=$request_time '
                 'urt=$upstream_response_time';

统计命中率:

awk '{for(i=1;i<=NF;i++) if($i ~ /^cache=/) print $i}' access.log \
    | sort | uniq -c | sort -rn
#  85234 cache=HIT
#   8912 cache=MISS
#   1203 cache=EXPIRED
#    455 cache=BYPASS
# 命中率 = HIT / 总数 ≈ 89%

2. 缓存 key 设计

2.1 默认 key

proxy_cache_key $scheme$proxy_host$request_uri;
# 默认其实是:$scheme$proxy_host$request_uri

这个默认值有个问题:没有包含请求方法。GET 和 HEAD 会共用同一个缓存条目。实践中建议显式指定:

proxy_cache_key "$scheme$request_method$host$request_uri";

2.2 常见的 key 定制

# ① 区分登录用户(每个用户独立缓存)
# ⚠️ 慎用!会导致缓存条目数量爆炸
proxy_cache_key "$scheme$request_method$host$request_uri$cookie_uid";

# ② 区分设备类型(移动端和桌面端返回不同内容)
map $http_user_agent $device {
    default          "pc";
    ~*mobile|android "mobile";
}
proxy_cache_key "$scheme$request_method$host$request_uri$device";

# ③ 区分语言
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept_language";

# ④ 忽略无关的查询参数(提高命中率)
# 比如 ?utm_source=xxx 这类营销参数不影响响应内容
map $args $cache_args {
    default    $args;
    ~*^(?:.*&)?utm_[^&]*(?:&(?<rest>.*))?$   $rest;
}
proxy_cache_key "$scheme$request_method$host$uri?$cache_args";

# ⑤ 只按路径缓存,完全忽略查询参数
proxy_cache_key "$scheme$request_method$host$uri";

key 设计的核心权衡:key 越细,缓存越准确但命中率越低、条目越多;key 越粗,命中率越高但可能返回错误的内容给用户。

最危险的错误是「把用户相关的响应用粗 key 缓存」

# ⚠️ 严重安全事故
location /api/user/profile {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_valid 200 10m;
    # key 里没有用户标识!
    # 结果:用户 A 的个人信息被缓存,用户 B 请求时直接命中,看到 A 的数据
}

规则:带 Set-CookieAuthorization 或任何用户态信息的响应,默认不要缓存。

Nginx 有个保护机制:上游响应带 Set-Cookie 时默认不缓存。但如果你写了 proxy_ignore_headers Set-Cookie; 就把这层保护关掉了——这个指令要格外小心。

2.3 Vary 头的处理

上游返回 Vary: Accept-Encoding 时,Nginx 会自动把对应的请求头加入 key,为每个变体存独立的缓存条目。

Vary: User-AgentVary: * 会让缓存基本失效(UA 千变万化,每个都是独立条目)。如果上游返回了这种头:

# 让 Nginx 忽略 Vary(确认过内容不真的随 UA 变化再这么做)
proxy_ignore_headers Vary;

3. 什么该缓存、什么不该

3.1 缓存方法与条件

location /api/ {
    proxy_pass http://backend;
    proxy_cache my_cache;

    # 只缓存这些方法(默认只有 GET HEAD)
    proxy_cache_methods GET HEAD;

    # 至少被请求 N 次才缓存(过滤长尾冷数据,节省磁盘)
    proxy_cache_min_uses 2;

    # 什么情况跳过缓存直接回源(变量非空且不为 "0" 时跳过)
    proxy_cache_bypass $cookie_nocache $arg_nocache $http_x_bypass_cache;

    # 什么情况不存入缓存
    proxy_no_cache $cookie_nocache $arg_nocache;
}

proxy_cache_bypassproxy_no_cache 的区别

  • bypass的时候跳过缓存(直接回源),但响应仍会写入缓存
  • no_cache的时候不存入缓存

想「完全绕过缓存」要两个都配。

3.2 不缓存登录用户的完整方案

map $http_cookie $skip_cache {
    default   0;
    ~*session_id=  1;      # 有 session cookie 的一律不缓存
    ~*token=       1;
}

# 也把某些路径排除
map $request_uri $skip_cache_uri {
    default        0;
    ~^/api/user/   1;
    ~^/api/cart/   1;
    ~^/admin/      1;
}

map "$skip_cache$skip_cache_uri" $no_cache {
    default  1;
    "00"     0;      # 只有两个都是 0 才缓存
}

location /api/ {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_valid 200 5m;

    proxy_cache_bypass $no_cache;
    proxy_no_cache     $no_cache;

    add_header X-Cache-Status $upstream_cache_status always;
}

3.3 上游的缓存控制头

默认情况下,上游返回这些头会阻止 Nginx 缓存

  • Cache-Control: no-cache / no-store / private / max-age=0
  • Expires 是过去的时间
  • Set-Cookie(任何值)
  • Vary: *
  • X-Accel-Expires: 0

想强行缓存要用 proxy_ignore_headers

# ⚠️ 谨慎使用,你在覆盖上游明确表达的意图
proxy_ignore_headers Cache-Control Expires Set-Cookie X-Accel-Expires;
proxy_cache_valid 200 10m;

更好的做法是让上游正确设置头,而不是在 Nginx 层忽略。 上游可以用 X-Accel-Expires 精确控制 Nginx 的缓存时长,而不影响浏览器缓存:

// Go 后端:让 Nginx 缓存 5 分钟,但浏览器不缓存
c.Header("X-Accel-Expires", "300")
c.Header("Cache-Control", "no-store")

X-Accel-Expires 的优先级最高,且这个头不会被转发给客户端。这是控制 Nginx 缓存最干净的方式。

4. 缓存击穿与雪崩防护

这一节是缓存配置的精华,也是面试的深水区。

4.1 问题:缓存过期瞬间的惊群

一个热点 URL 的缓存过期了,此时有 1000 个并发请求打过来。默认行为是:1000 个请求全部回源,后端瞬间被打爆。这就是缓存击穿。

4.2 proxy_cache_lock —— 只让一个请求回源

location /api/ {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_valid 200 10m;

    # 同一个 key 只允许一个请求回源,其他请求等待
    proxy_cache_lock on;

    # 其他请求最多等多久,超时后自己也回源
    proxy_cache_lock_timeout 5s;

    # 持有锁的请求最多多久必须完成,否则释放锁让别人来
    proxy_cache_lock_age 5s;
}

效果:1000 个请求里只有 1 个回源,其余 999 个在 Nginx 里等着,第一个请求填充缓存后,它们直接读缓存返回。

注意 proxy_cache_lock 是每个 worker 独立还是全局的?基于共享内存的全局锁,所有 worker 共享。

代价:等待的请求会占用连接和内存。proxy_cache_lock_timeout 不要设太大。

4.3 proxy_cache_use_stale —— 用过期数据兜底

这是可用性的关键配置

location /api/ {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_valid 200 10m;

    # 后端出问题时,用过期的缓存顶上,而不是给用户 502
    proxy_cache_use_stale error timeout invalid_header
                          http_500 http_502 http_503 http_504
                          updating;

    # 后台异步更新:过期后先返回旧数据,同时后台去回源刷新
    # (1.11.10+,相当于 stale-while-revalidate)
    proxy_cache_background_update on;
}

updating 这个值特别有价值:缓存正在被某个请求更新时,其他请求直接用旧数据,不用等。配合 proxy_cache_background_update on 就实现了 stale-while-revalidate 语义:

请求到来 → 缓存已过期
立即返回过期的缓存内容(用户零等待,X-Cache-Status: STALE 或 UPDATING)
同时在后台发起子请求回源,更新缓存
下一个请求就拿到新数据了

这是缓存配置里性价比最高的两行。它同时解决了两个问题:

  1. 缓存过期时用户不用等回源(延迟毛刺消失)
  2. 后端挂了时用户还能看到(虽然是旧的)内容,而不是 502

4.4 proxy_cache_revalidate —— 省带宽

proxy_cache_revalidate on;

缓存过期后,Nginx 带着 If-Modified-Since / If-None-Match 去回源。上游如果返回 304,Nginx 就只更新缓存的过期时间,不重新下载响应体

对大响应体(大 JSON、图片)效果明显。状态显示为 REVALIDATED

4.5 缓存雪崩:过期时间打散

如果一批缓存在同一时刻集体过期(比如批量预热的),会造成瞬间的回源洪峰。

Nginx 没有内置的过期时间随机化,但可以用上游控制:

// Go 后端:给缓存时长加随机抖动
jitter := rand.Intn(120)   // 0-120 秒随机
c.Header("X-Accel-Expires", strconv.Itoa(600+jitter))

或者用 OpenResty:

set_by_lua_block $cache_ttl {
    return 600 + math.random(0, 120)
}
proxy_cache_valid 200 ${cache_ttl}s;   -- 注意:proxy_cache_valid 不支持变量

proxy_cache_valid 不支持变量,所以只能靠上游的 X-Accel-ExpiresCache-Control: max-age 来做动态 TTL。

4.6 完整的高可用缓存配置

把上面所有防护组合起来:

http {
    proxy_cache_path /var/cache/nginx/proxy
                     levels=1:2
                     keys_zone=api_cache:200m
                     max_size=20g
                     inactive=2h
                     use_temp_path=off;

    server {
        location /api/ {
            proxy_pass http://backend;
            include snippets/proxy-headers.conf;

            proxy_cache api_cache;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_valid 200 301 302 10m;
            proxy_cache_valid 404 1m;
            proxy_cache_min_uses 1;

            # 防击穿:只让一个请求回源
            proxy_cache_lock on;
            proxy_cache_lock_timeout 5s;
            proxy_cache_lock_age 5s;

            # 高可用:后端挂了用旧数据顶
            proxy_cache_use_stale error timeout invalid_header
                                  http_500 http_502 http_503 http_504
                                  updating;
            proxy_cache_background_update on;

            # 省带宽
            proxy_cache_revalidate on;

            # 登录用户不走缓存
            proxy_cache_bypass $no_cache;
            proxy_no_cache     $no_cache;

            add_header X-Cache-Status $upstream_cache_status always;
        }
    }
}

这套配置的效果:

  • 正常情况:高命中率,过期时只有一个请求回源
  • 缓存过期:用户立刻拿到旧数据,后台静默更新
  • 后端全挂:用户仍能看到过期内容(在 inactive 期限内),而不是 502

5. 主动清除缓存

5.1 商业版:proxy_cache_purge

# NGINX Plus
map $request_method $purge_method {
    PURGE 1;
    default 0;
}

location /api/ {
    proxy_cache my_cache;
    proxy_cache_purge $purge_method;
}
curl -X PURGE http://example.com/api/user/1

5.2 开源版方案一:ngx_cache_purge 模块

git clone https://github.com/FRiCKLE/ngx_cache_purge
./configure --add-module=../ngx_cache_purge ...
location ~ /purge(/.*) {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    proxy_cache_purge my_cache "$scheme$request_method$host$1";
}
curl http://127.0.0.1/purge/api/user/1

5.3 开源版方案二:直接删文件(最实用)

缓存文件名是 key 的 MD5,可以直接算出来删掉:

#!/bin/bash
# purge-cache.sh —— 删除指定 URL 的缓存
CACHE_DIR=/var/cache/nginx/proxy
LEVELS="1:2"
KEY="httpsGETexample.com$1"          # 必须和 proxy_cache_key 完全一致

MD5=$(echo -n "$KEY" | md5sum | awk '{print $1}')
# levels=1:2 → 取 MD5 末尾 1 位 / 末尾 2-3 位
L1=${MD5: -1}
L2=${MD5: -3:2}
FILE="$CACHE_DIR/$L1/$L2/$MD5"

if [ -f "$FILE" ]; then
    rm -f "$FILE" && echo "purged: $1"
else
    echo "not found: $1 (key=$KEY, file=$FILE)"
fi
./purge-cache.sh /api/user/1

批量清除(按内容匹配):

# 找出所有缓存了某个字符串的文件并删除
grep -rl "some-pattern" /var/cache/nginx/proxy/ | xargs rm -f

# 清空整个缓存(不用 reload,Nginx 会自动发现文件不存在)
find /var/cache/nginx/proxy -type f -delete

注意:直接删文件后共享内存里的 key 元数据还在,Nginx 下次访问时发现文件不存在会自动当成 MISS 处理并清理元数据。所以不需要 reload。

5.4 用 OpenResty 实现灵活的 purge

location ~ ^/purge(/.*)$ {
    allow 10.0.0.0/8;
    deny all;

    content_by_lua_block {
        local uri = ngx.var[1]
        local key = "https" .. "GET" .. ngx.var.host .. uri
        local md5 = ngx.md5(key)

        local l1 = md5:sub(-1)
        local l2 = md5:sub(-3, -2)
        local path = "/var/cache/nginx/proxy/" .. l1 .. "/" .. l2 .. "/" .. md5

        local ok, err = os.remove(path)
        if ok then
            ngx.say("purged: ", uri)
        else
            ngx.status = 404
            ngx.say("not found: ", uri, " (", tostring(err), ")")
        end
    }
}

6. 缓存预热

新上线的缓存节点是空的,第一批流量会全部回源。预热可以避免这个冲击:

#!/bin/bash
# warmup.sh —— 从访问日志里提取热门 URL 并预热

# 1. 从昨天的日志里找出访问量 top 1000 的 URL
zcat /var/log/nginx/access.log-$(date -d yesterday +%Y%m%d).gz \
  | awk '{print $7}' \
  | grep -E '^/api/' \
  | sort | uniq -c | sort -rn | head -1000 \
  | awk '{print $2}' > /tmp/hot_urls.txt

# 2. 并发预热(控制并发度,别把后端打爆)
cat /tmp/hot_urls.txt | xargs -P 10 -I{} \
    curl -s -o /dev/null -w "%{http_code} %{time_total}s {}\n" \
    "http://127.0.0.1{}"

预热时验证效果:

# 预热前后对比命中率
curl -s -o /dev/null -D - http://127.0.0.1/api/hot | grep -i x-cache

7. 多级缓存架构

一个完整的缓存体系是分层的,每一层拦掉一部分流量:

浏览器缓存(Cache-Control)
   ↓ miss
CDN 边缘节点
   ↓ miss
Nginx proxy_cache(本篇)
   ↓ miss
应用层缓存(本地内存 / Redis)
   ↓ miss
数据库

各层的定位:

命中率 适合缓存什么 TTL
浏览器 高(对单用户) 静态资源、用户专属数据 长(hash 资源)/ 短
CDN 很高 静态资源、公共 API 中长
Nginx 公共 API、页面片段 短(分钟级)
Redis 需要主动失效的业务数据 灵活
本地内存 低但极快 配置、字典表

Nginx 缓存的定位是「短 TTL、抗突发」。它的优势是零网络开销、零序列化开销;劣势是无法精确失效(不像 Redis 能 DEL key)、多台 Nginx 之间不共享。

所以:

  • 需要精确失效的业务数据 → Redis
  • 能容忍几分钟延迟的公共数据 → Nginx proxy_cache(TTL 设短一点,靠自然过期)
  • 静态资源 → CDN + 浏览器

7.1 用 Nginx 做小文件的本地缓存加速

一个实用场景:Nginx 缓存对象存储(S3/OSS)的内容,减少回源费用和延迟。

proxy_cache_path /var/cache/nginx/oss
                 levels=2:2
                 keys_zone=oss:500m
                 max_size=200g
                 inactive=30d
                 use_temp_path=off;

server {
    listen 80;
    server_name img.example.com;

    location / {
        proxy_pass https://my-bucket.oss-cn-hangzhou.aliyuncs.com;
        proxy_set_header Host my-bucket.oss-cn-hangzhou.aliyuncs.com;

        # 不传客户端的这些头给 OSS
        proxy_set_header Authorization "";
        proxy_set_header Cookie "";

        proxy_cache oss;
        proxy_cache_key "$uri";              # 图片按路径缓存就够了
        proxy_cache_valid 200 30d;           # 图片基本不变,缓存很久
        proxy_cache_valid 404 1m;
        proxy_cache_lock on;
        proxy_cache_use_stale error timeout updating http_5xx;
        proxy_cache_background_update on;

        # 忽略 OSS 返回的缓存控制头
        proxy_ignore_headers Cache-Control Expires Set-Cookie;

        # 给客户端的缓存头由我们自己控制
        expires 30d;
        add_header Cache-Control "public";
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

8. FastCGI / uwsgi / gRPC 缓存

同样的机制,换个前缀:

# PHP-FPM
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=php:100m
                   max_size=5g inactive=60m use_temp_path=off;

location ~ \.php$ {
    fastcgi_pass unix:/run/php-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    fastcgi_cache php;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    fastcgi_cache_valid 200 10m;
    fastcgi_cache_lock on;
    fastcgi_cache_use_stale error timeout updating http_500;
    fastcgi_cache_background_update on;
    fastcgi_no_cache $no_cache;
    fastcgi_cache_bypass $no_cache;

    add_header X-Cache-Status $upstream_cache_status always;
}

grpc_cache 不存在——gRPC 是流式的,Nginx 不支持缓存 gRPC 响应。

9. 缓存监控

9.1 关键指标

# 命中率
awk '/cache=HIT/{h++} /cache=/{t++} END{printf "hit rate: %.2f%%\n", h/t*100}' access.log

# 磁盘占用
du -sh /var/cache/nginx/proxy
# 应该接近但不超过 max_size

# 缓存文件数
find /var/cache/nginx/proxy -type f | wc -l

# 共享内存使用情况(需要 NGINX Plus 或第三方模块)
# 开源版只能间接判断:如果磁盘占用远小于 max_size 但一直在淘汰,
# 说明 keys_zone 满了

9.2 用 Prometheus 采集

配合 nginx-vts-modulenginx-module-vts

http {
    vhost_traffic_status_zone;

    server {
        location /status {
            vhost_traffic_status_display;
            vhost_traffic_status_display_format prometheus;
            allow 10.0.0.0/8;
            deny all;
        }
    }
}

输出里包含 nginx_vts_cache_* 系列指标(hit/miss/bypass/expired/stale 的字节数和次数),直接接 Prometheus。

9.3 命中率低的排查清单

可能原因 怎么确认
上游返回 Set-Cookie curl -I 看上游响应头
上游返回 Cache-Control: no-cache/private 同上
proxy_cache_key 里包含了变化的变量(如 $request_id、时间戳参数) 检查配置
请求 URL 带随机参数(?_=1234567890 看 access_log 里的 URI
keys_zone 太小,一直在 LRU 淘汰 磁盘占用远小于 max_size 但 MISS 很多
inactive 太短 冷门内容还没被再次访问就被删了
proxy_cache_min_uses 设得太高 检查配置
请求方法不是 GET/HEAD 看 access_log
Vary: User-Agent 导致每个 UA 独立缓存 curl -I 看上游响应头

10. 面试题

Q:Nginx 的 proxy_cache 是怎么工作的?

proxy_cache_key 计算一个 MD5 作为缓存标识,key 的元数据(过期时间、文件名、引用计数)存在 keys_zone 共享内存里,响应体存在磁盘文件里(路径按 levels 分层,从 MD5 末尾取字符)。请求到来时先查共享内存:命中且未过期直接读磁盘文件返回;未命中或过期则回源,同时把响应写入缓存。cache manager 进程按 max_size(LRU)和 inactive 淘汰文件,cache loader 在启动时把已有缓存的元信息载入共享内存。

Q:levels=1:2 是什么意思?为什么需要分层目录?

一级目录用 1 个字符,二级用 2 个字符,字符取自缓存 key MD5 值的末尾。分层是为了避免单个目录下文件过多——几百万个文件在同一目录会让文件系统的查找性能急剧下降。levels=1:2 产生 4096 个子目录。

Q:use_temp_path=off 为什么重要?

默认 Nginx 先把上游响应写到 proxy_temp_path,完成后 rename 到缓存目录。如果两个路径跨文件系统,rename 会退化成完整的文件复制,产生翻倍的磁盘 I/O。设 off 后临时文件直接写在缓存目录内,rename 是同文件系统的原子操作,几乎零开销。

Q:怎么防止缓存击穿(大量并发同时回源)?

proxy_cache_lock on; 让同一个 key 同时只有一个请求回源,其他请求等待(proxy_cache_lock_timeout 控制等待上限)。更好的组合是再加 proxy_cache_use_stale updating; + proxy_cache_background_update on;——过期时立即返回旧数据,后台异步更新,用户完全不用等。

Q:proxy_cache_use_stale 的作用是什么?

在指定条件下(后端 error/timeout/5xx、或缓存正在被更新)允许返回已过期的缓存内容。这是保证可用性的关键:后端全挂时用户仍能看到旧内容而不是 502。配合 proxy_cache_background_update on 就实现了 stale-while-revalidate 语义。

Q:proxy_cache_bypassproxy_no_cache 有什么区别?

proxy_cache_bypass 控制:条件成立时跳过缓存直接回源,但响应仍会被写入缓存。proxy_no_cache 控制:条件成立时响应不存入缓存。要完全绕过缓存需要两个都配。

Q:开源版怎么主动清除某个 URL 的缓存?

三种方式:(1) 编译 ngx_cache_purge 第三方模块,提供 PURGE 接口;(2) 按 proxy_cache_key 算出 MD5,直接删对应的缓存文件(不需要 reload,Nginx 下次访问发现文件不存在会自动按 MISS 处理);(3) 用 OpenResty 写一个 Lua 接口做同样的事。官方的 proxy_cache_purge 指令只有 NGINX Plus 有。

Q:keys_zone 设小了会有什么后果?

共享内存放不下更多 key 时会强制按 LRU 淘汰,即使磁盘还远没到 max_size。表现是磁盘占用远低于 max_size 但命中率上不去。估算方法:每个 key 约占 128 字节元数据,keys_zone 大小应该 ≥ 预期 key 数量 × 128 字节 × 1.5。

Q:为什么用户相关的接口不能随便缓存?

如果 proxy_cache_key 里不包含用户标识,用户 A 的响应会被缓存,用户 B 请求同一 URL 时直接命中,看到 A 的数据——这是严重的信息泄露。Nginx 有一层保护:上游返回 Set-Cookie 时默认不缓存,但 proxy_ignore_headers Set-Cookie 会把这层保护关掉,所以这个指令要格外小心。

Q:Nginx 缓存和 Redis 缓存怎么分工?

Nginx 缓存零网络开销、零序列化开销,但无法精确失效、多机不共享,适合短 TTL 的公共内容(靠自然过期)。Redis 支持精确 DEL、多机共享,适合需要主动失效的业务数据。典型分工:公共 API 响应用 Nginx 缓存几分钟抗突发,业务数据用 Redis 并在写操作时主动失效。


上一篇:Nginx-07 动静分离与静态资源优化 | 下一篇:Nginx-09 限流限连与安全防护