Skip to content

Nginx代理缓存、过期与一致性全过程

Nginx代理缓存不是“打开proxy_cache就能加速”。缓存Key遗漏租户或权限会造成数据串读,TTL过长会返回旧数据,缓存锁配置错误会在过期瞬间把后端打垮,多个Nginx实例又各自拥有本地缓存。必须从写入、命中、过期、更新、淘汰和失效全过程理解。

学习目标

  1. 区分浏览器/CDN缓存、Nginx代理缓存、应用缓存和Redis。
  2. 解释keys_zone元数据、磁盘文件、Loader与Manager。
  3. 推导缓存Key并识别权限与租户串读风险。
  4. 区分MISS、HIT、EXPIRED、STALE、UPDATING、BYPASS等状态。
  5. 解释Cache-Control、Set-Cookie、Vary和Authorization边界。
  6. 使用cache lock、stale和background update治理击穿。
  7. 区分bypass与no_cache。
  8. 设计版本化URL、短TTL、事件失效和多实例一致性。

一、缓存位于哪一层

mermaid
flowchart TD
    A["浏览器缓存"] --> B["CDN缓存"]
    B --> C["Nginx代理缓存"]
    C --> D["应用本地缓存"]
    D --> E["Redis"]
    E --> F["数据库或对象存储"]

越靠近用户,命中时越快、回源越少,但失效和个性化越复杂。Nginx适合缓存公开、可复用、读取多的HTTP响应,不适合无脑缓存用户订单、权限、余额和医疗隐私数据。

二、proxy_cache_path定义什么

nginx
proxy_cache_path /data/nginx/cache
    levels=1:2
    keys_zone=api_cache:100m
    inactive=60m
    max_size=20g
    use_temp_path=off;
参数含义
路径缓存响应文件存放目录
levels按Key摘要切分子目录,避免单目录文件过多
keys_zone共享内存中的Key和元数据索引
inactive多久未被访问可被清理,不等于响应TTL
max_size磁盘缓存目标上限,清理不是瞬时硬边界
use_temp_path临时文件是否使用单独路径

keys_zone主要保存元数据,不保存所有响应Body。Body通常在磁盘缓存文件中。

三、Loader与Manager

mermaid
flowchart TD
    A["Nginx启动"] --> B["Cache Loader分批扫描磁盘文件"]
    B --> C["把Key和元数据加载到keys_zone"]
    C --> D["请求可通过共享索引查找缓存"]
    E["Cache Manager周期运行"] --> F["按inactive、max_size等清理"]

重启后磁盘文件还在,但共享内存索引需要逐步重建,因此冷启动早期命中和磁盘行为可能变化。Loader分批工作是为了避免一次扫描巨大目录阻塞过久。

四、一次缓存请求全过程

mermaid
flowchart TD
    A["请求到达"] --> B["计算cache key"]
    B --> C{"是否允许读取缓存"}
    C -- "bypass" --> D["直接请求upstream"]
    C -- "允许" --> E{"Key是否存在且新鲜"}
    E -- "HIT" --> F["读取缓存响应"]
    E -- "MISS/EXPIRED" --> G["请求upstream"]
    G --> H{"响应是否允许存储"}
    H -- "否" --> I["只返回客户端"]
    H -- "是" --> J["写临时文件并原子进入缓存"]
    J --> K["更新共享元数据"]
    F --> L["返回客户端"]
    I --> L
    K --> L

五、启用缓存的最小配置

nginx
proxy_cache_path /data/nginx/cache keys_zone=api_cache:100m max_size=20g inactive=60m;

server {
    location /public-api/ {
        proxy_cache api_cache;
        proxy_cache_key "$scheme|$request_method|$host|$request_uri";
        proxy_cache_valid 200 1m;
        proxy_pass http://public_backend;
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

先只缓存明确公开接口,使用响应Header观察状态。不要把proxy_cache放在覆盖所有 /api/ 的父location中。

六、缓存Key决定隔离边界

如果Key只有URI:

text
/api/profile

用户A响应可能被用户B命中,造成数据泄露。Key设计要考虑:

  • scheme、host和URI。
  • 查询参数是否顺序或无关参数导致碎片。
  • HTTP方法。
  • 租户。
  • 内容协商Header。
  • 语言、地区和设备版本。
  • 权限与用户身份。

但把用户ID放Key会让缓存退化成每用户一份,还要解决退出、权限变化和隐私删除。敏感个性化接口通常更适合不在Nginx缓存。

七、缓存Key投毒与Host

若未限制Host,攻击者可能用异常Host制造缓存Key或污染绝对链接。应使用明确server_name/default_server,并让cache key中的host来自已匹配可信虚拟主机口径。Header参与Key时必须限制长度和值域,防止高基数填满缓存。

八、哪些请求默认适合缓存

通常优先考虑GET/HEAD公开响应。POST、PUT、DELETE具有写语义,不应为了提高命中率随意加入缓存方法。即使GET,如果接口错误地执行写操作,也不能安全缓存。

缓存状态码和时长可配置:

nginx
proxy_cache_valid 200 302 1m;
proxy_cache_valid 404 10s;
proxy_cache_valid any 5s;

404短缓存可防止不存在资源被反复回源,但数据刚创建后可能继续返回旧404,需要评估负缓存一致性。any还可能缓存500等错误响应,生产通常应明确列出允许状态,不要为了“兜底”把所有错误缓存。

九、响应Header控制缓存

upstream常通过以下Header表达策略:

  • Cache-Control
  • Expires
  • Set-Cookie
  • Vary
  • X-Accel-Expires等Nginx相关Header。

privateno-store、Set-Cookie、Authorization等通常意味着个性化或敏感响应,应谨慎缓存。不要使用忽略Header指令强行缓存,除非已经证明隔离与一致性安全。

十、Vary为什么会放大缓存

响应:

text
Vary: Accept-Encoding

表示不同请求Header值可能对应不同响应版本。若Vary包含高基数或客户端任意值,会造成大量变体和低命中率。Vary: *通常表示响应不适合共享缓存。

十一、缓存状态怎么读

nginx
add_header X-Cache-Status $upstream_cache_status always;

常见状态概念:

状态含义
MISS没有可用缓存,回源
HIT命中新鲜缓存
EXPIRED缓存过期,需要更新
STALE使用过期缓存兜底
UPDATING其他请求正在更新,当前使用旧内容等
BYPASS按规则跳过读取缓存
REVALIDATED通过条件请求确认缓存仍有效

具体状态和值随版本和配置变化,以真实日志为准。

十二、TTL与inactive不是一回事

  • TTL/valid:内容多久算新鲜。
  • inactive:缓存文件多久没有被访问可被清理。

一个TTL已过期的文件仍可能留在磁盘等待更新或清理;一个TTL很长但长期无人访问的对象可能因inactive被删除。

十三、缓存击穿如何形成

mermaid
flowchart TD
    A["热点Key到期"] --> B["同时到达1000个请求"]
    B --> C["每个请求都发现EXPIRED"]
    C --> D["1000个请求同时回源"]
    D --> E["后端线程池、DB被打满"]

缓存只在命中时保护后端,到期瞬间如果没有并发更新控制,可能放大峰值。

十四、proxy_cache_lock

nginx
proxy_cache_lock on;
proxy_cache_lock_timeout 3s;
proxy_cache_lock_age 5s;

同一Key MISS/过期时,典型目标是只让一个请求回源填充,其他请求等待、使用旧值或按超时策略处理。

  • lock_timeout过长会让大量客户端等待。
  • 过短会有更多请求绕过锁回源。
  • lock_age用于处理填充请求长期不完成等情况,具体语义按版本验证。
  • 即使有锁,不同Nginx实例仍各自有锁和缓存。

十五、stale兜底

nginx
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

当upstream错误、超时或缓存正在更新时,可以返回过期缓存,降低故障影响。

适合:公开商品详情、字典、非强一致展示。

不适合:余额、库存承诺、权限、支付状态、医疗最新状态等不能接受旧数据的场景。

stale提升可用性,但明确牺牲新鲜度,必须在产品和业务SLA中定义。

十六、background update

可结合配置让一个请求后台更新,其他请求继续使用旧缓存,减少用户等待。具体指令支持和stale条件需要按版本验证。

概念流程:

text
缓存过期
→ 第一个请求触发后台更新
→ 当前/其他请求使用旧值
→ 更新成功后切换新缓存
→ 更新失败继续按stale策略

十七、bypass与no_cache区别

nginx
proxy_cache_bypass $skip_cache;
proxy_no_cache $skip_cache;
  • bypass:当前请求不从缓存读取,直接回源。
  • no_cache:当前响应不写入缓存。

很多场景两者一起使用,例如带调试Header或登录态请求;只配置一个可能出现“不读旧缓存但把个性化响应写进去”或“读到旧缓存却不写新值”等意外。

$skip_cache必须来自可信规则,不能让公网用户任意带一个Header就持续回源形成DoS。

十八、缓存临时文件与原子替换

回源响应可能先写临时文件,完成后再重命名进入缓存目录。临时目录和缓存目录位于不同文件系统时,rename可能变成复制,增加IO。use_temp_path=off可让临时文件靠近缓存路径,但仍需验证磁盘容量、权限和性能。

十九、磁盘容量不是硬瞬时上限

Cache Manager周期性清理,max_size通常是管理目标,不保证任何瞬间绝不超过。并发写入、临时文件、日志和清理速度都可能造成短暂超额。

监控:

bash
df -h
df -i
du -sh /data/nginx/cache
iostat -x

同时看容量、inode、IO延迟和删除速度。

二十、多实例缓存不共享

三台Nginx通常各有自己的本地磁盘和keys_zone:

text
Nginx A MISS → 回源并写A缓存
Nginx B MISS → 再次回源并写B缓存
Nginx C MISS → 再次回源并写C缓存

扩容会引起冷缓存和回源增加。可使用CDN、共享上层缓存、预热、分批扩容或一致性路由降低影响;直接共享网络文件系统还需评估锁、原子操作、IO和官方支持边界。

二十一、缓存一致性策略

策略优点风险
短TTL简单、自动收敛窗口内旧数据、回源较多
版本化URL静态资源可永久缓存需要构建版本管理
事件驱动失效变化后快速清理事件丢失、重试、权限
主动Purge精确开源版能力、第三方模块和多实例广播
不缓存敏感数据一致性简单后端压力更高

开源Nginx的Purge能力并非所有发行版标准内置,可能需要商业版或第三方模块。不能假定 proxy_cache_purge在当前二进制可用。

二十二、静态资源最佳实践

构建产物使用内容Hash文件名:

text
app.8f31c2.js

可以返回长缓存:

nginx
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

HTML入口使用较短缓存或协商更新,因为它引用新Hash资源。发布回滚应保留旧版本资源,避免旧HTML引用已经删除的文件。

二十三、API微缓存

公开高频接口可以只缓存1到5秒:

nginx
proxy_cache_valid 200 2s;

即使TTL很短,在高QPS下也能显著减少回源。仍要确保:

  • 接口公开且无用户数据。
  • Key包含必要参数。
  • 写后短暂旧数据可接受。
  • 错误和Set-Cookie不被缓存。
  • 多实例回源峰值可控。

二十四、缓存投毒与数据串读

高风险原因:

  • Key遗漏Host或租户。
  • 响应受Authorization/Cookie影响但Key未包含身份。
  • 不可信Header参与后端响应却未进入Key。
  • 攻击者制造异常查询参数填满磁盘。
  • 忽略Set-Cookie/Cache-Control强行缓存。

发生用户数据串读时应立即停用相关缓存、隔离日志和缓存文件、评估泄露范围、清理所有实例并修复Key/缓存边界。

二十五、缓存命中率低排查

  1. 统计 $upstream_cache_status
  2. 规范化分析cache key,不记录敏感原值。
  3. 检查查询参数顺序和追踪参数。
  4. 检查Cookie、Authorization、Set-Cookie和Vary。
  5. 检查TTL和inactive。
  6. 检查多实例流量分散。
  7. 检查磁盘清理和zone容量。
  8. 检查配置是否命中正确location。

二十六、缓存目录磁盘满排查

查看缓存、临时文件、日志和deleted-open文件。磁盘满可能让缓存写失败,也可能影响Nginx其他临时文件和日志。不要直接删除随机缓存子目录;先确认进程、挂载、权限、manager状态和回源容量,避免清空后形成缓存雪崩。

二十七、商业场景:商品缓存过期打挂数据库

热点商品2秒TTL到期,1000个请求同时MISS,应用查询同一商品和库存,数据库热点行与连接池被打满。

治理:cache lock、stale while updating、应用缓存、随机TTL、热点预热、数据库保护和降级。库存承诺不能直接使用陈旧代理缓存,需要分离展示信息与强一致库存校验。

二十八、商业场景:租户数据串读

请求URI都为 /api/dashboard,后端根据受信租户Header返回不同数据,但cache key没有租户。第一个租户响应被后续租户HIT。

治理:敏感租户接口默认不做共享代理缓存;确需缓存时使用认证后的可信租户ID隔离Key,并设计权限变化、退出和删除流程,完成安全测试。

二十九、完整安全Demo

nginx
proxy_cache_path /data/nginx/cache
    levels=1:2
    keys_zone=public_cache:100m
    inactive=30m
    max_size=10g
    use_temp_path=off;

map $http_authorization $skip_private {
    default 1;
    "" 0;
}

server {
    listen 8080;

    location /public/catalog/ {
        proxy_cache public_cache;
        proxy_cache_key "$scheme|$host|$request_uri";
        proxy_cache_valid 200 10s;
        proxy_cache_valid 404 2s;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 2s;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_bypass $skip_private;
        proxy_no_cache $skip_private;
        add_header X-Cache-Status $upstream_cache_status always;
        proxy_pass http://catalog_backend;
    }
}

这是公开目录示例,不应用到订单、余额和权限接口。生产还需处理Cookie、Vary、查询参数和多实例。

三十、面试标准回答

keys_zone里存的是响应Body吗

不是。keys_zone主要保存缓存Key和元数据索引,响应Header/Body通常保存在磁盘缓存文件中。Nginx重启后Cache Loader会分批扫描磁盘并重建共享索引,Cache Manager按inactive、max_size等清理。

proxy_cache_lock解决什么

它用于同一Key MISS或过期时减少并发回源,通常只让一个请求填充缓存,其他请求等待或按超时/stale策略处理。它只在当前缓存实例范围生效,多台Nginx仍可能各自回源,锁超时过长还会增加客户端等待。

stale有什么风险

stale在upstream错误、超时或更新时返回过期内容,提高可用性但牺牲新鲜度。适合公开展示和字典,不适合余额、权限、库存承诺、支付状态等强一致数据,必须由业务SLA定义可接受陈旧窗口。

bypass和no_cache区别

bypass控制当前请求是否跳过读取缓存,no_cache控制当前上游响应是否写入缓存。个性化请求通常两者都需要,避免既回源又把私人响应写进共享缓存;条件必须来自可信信息,不能让攻击者任意强制回源。

多台Nginx缓存是否共享

通常不共享,每台有自己的keys_zone和磁盘文件。扩容会产生冷缓存和多次回源,cache lock也只保护单实例。可通过CDN、上层统一缓存、预热和渐进扩容治理,不能默认共享网络盘安全可用。

三十一、学习验收

  1. 画出cache key、共享索引、磁盘文件、Loader和Manager。
  2. 为公开API设计Key并证明不会跨Host/租户串读。
  3. 区分MISS、HIT、EXPIRED、STALE、UPDATING和BYPASS。
  4. 解释TTL与inactive不同。
  5. 制造热点到期并比较有无cache lock的回源量。
  6. 设计stale策略并列出禁止使用的强一致数据。
  7. 证明bypass与no_cache只配一个的风险。
  8. 扩容三台Nginx,观察冷缓存和重复回源。
  9. 定位一次命中率低、磁盘满和用户数据串读。
  10. 为静态资源、公开API和敏感API分别设计缓存策略。

关联知识点