Nginx代理缓存、过期与一致性全过程
Nginx代理缓存不是“打开proxy_cache就能加速”。缓存Key遗漏租户或权限会造成数据串读,TTL过长会返回旧数据,缓存锁配置错误会在过期瞬间把后端打垮,多个Nginx实例又各自拥有本地缓存。必须从写入、命中、过期、更新、淘汰和失效全过程理解。
学习目标
- 区分浏览器/CDN缓存、Nginx代理缓存、应用缓存和Redis。
- 解释keys_zone元数据、磁盘文件、Loader与Manager。
- 推导缓存Key并识别权限与租户串读风险。
- 区分MISS、HIT、EXPIRED、STALE、UPDATING、BYPASS等状态。
- 解释Cache-Control、Set-Cookie、Vary和Authorization边界。
- 使用cache lock、stale和background update治理击穿。
- 区分bypass与no_cache。
- 设计版本化URL、短TTL、事件失效和多实例一致性。
一、缓存位于哪一层
flowchart TD
A["浏览器缓存"] --> B["CDN缓存"]
B --> C["Nginx代理缓存"]
C --> D["应用本地缓存"]
D --> E["Redis"]
E --> F["数据库或对象存储"]越靠近用户,命中时越快、回源越少,但失效和个性化越复杂。Nginx适合缓存公开、可复用、读取多的HTTP响应,不适合无脑缓存用户订单、权限、余额和医疗隐私数据。
二、proxy_cache_path定义什么
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
flowchart TD
A["Nginx启动"] --> B["Cache Loader分批扫描磁盘文件"]
B --> C["把Key和元数据加载到keys_zone"]
C --> D["请求可通过共享索引查找缓存"]
E["Cache Manager周期运行"] --> F["按inactive、max_size等清理"]重启后磁盘文件还在,但共享内存索引需要逐步重建,因此冷启动早期命中和磁盘行为可能变化。Loader分批工作是为了避免一次扫描巨大目录阻塞过久。
四、一次缓存请求全过程
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五、启用缓存的最小配置
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:
/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,如果接口错误地执行写操作,也不能安全缓存。
缓存状态码和时长可配置:
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。
private、no-store、Set-Cookie、Authorization等通常意味着个性化或敏感响应,应谨慎缓存。不要使用忽略Header指令强行缓存,除非已经证明隔离与一致性安全。
十、Vary为什么会放大缓存
响应:
Vary: Accept-Encoding表示不同请求Header值可能对应不同响应版本。若Vary包含高基数或客户端任意值,会造成大量变体和低命中率。Vary: *通常表示响应不适合共享缓存。
十一、缓存状态怎么读
add_header X-Cache-Status $upstream_cache_status always;常见状态概念:
| 状态 | 含义 |
|---|---|
| MISS | 没有可用缓存,回源 |
| HIT | 命中新鲜缓存 |
| EXPIRED | 缓存过期,需要更新 |
| STALE | 使用过期缓存兜底 |
| UPDATING | 其他请求正在更新,当前使用旧内容等 |
| BYPASS | 按规则跳过读取缓存 |
| REVALIDATED | 通过条件请求确认缓存仍有效 |
具体状态和值随版本和配置变化,以真实日志为准。
十二、TTL与inactive不是一回事
- TTL/valid:内容多久算新鲜。
- inactive:缓存文件多久没有被访问可被清理。
一个TTL已过期的文件仍可能留在磁盘等待更新或清理;一个TTL很长但长期无人访问的对象可能因inactive被删除。
十三、缓存击穿如何形成
flowchart TD
A["热点Key到期"] --> B["同时到达1000个请求"]
B --> C["每个请求都发现EXPIRED"]
C --> D["1000个请求同时回源"]
D --> E["后端线程池、DB被打满"]缓存只在命中时保护后端,到期瞬间如果没有并发更新控制,可能放大峰值。
十四、proxy_cache_lock
proxy_cache_lock on;
proxy_cache_lock_timeout 3s;
proxy_cache_lock_age 5s;同一Key MISS/过期时,典型目标是只让一个请求回源填充,其他请求等待、使用旧值或按超时策略处理。
- lock_timeout过长会让大量客户端等待。
- 过短会有更多请求绕过锁回源。
- lock_age用于处理填充请求长期不完成等情况,具体语义按版本验证。
- 即使有锁,不同Nginx实例仍各自有锁和缓存。
十五、stale兜底
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;当upstream错误、超时或缓存正在更新时,可以返回过期缓存,降低故障影响。
适合:公开商品详情、字典、非强一致展示。
不适合:余额、库存承诺、权限、支付状态、医疗最新状态等不能接受旧数据的场景。
stale提升可用性,但明确牺牲新鲜度,必须在产品和业务SLA中定义。
十六、background update
可结合配置让一个请求后台更新,其他请求继续使用旧缓存,减少用户等待。具体指令支持和stale条件需要按版本验证。
概念流程:
缓存过期
→ 第一个请求触发后台更新
→ 当前/其他请求使用旧值
→ 更新成功后切换新缓存
→ 更新失败继续按stale策略十七、bypass与no_cache区别
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通常是管理目标,不保证任何瞬间绝不超过。并发写入、临时文件、日志和清理速度都可能造成短暂超额。
监控:
df -h
df -i
du -sh /data/nginx/cache
iostat -x同时看容量、inode、IO延迟和删除速度。
二十、多实例缓存不共享
三台Nginx通常各有自己的本地磁盘和keys_zone:
Nginx A MISS → 回源并写A缓存
Nginx B MISS → 再次回源并写B缓存
Nginx C MISS → 再次回源并写C缓存扩容会引起冷缓存和回源增加。可使用CDN、共享上层缓存、预热、分批扩容或一致性路由降低影响;直接共享网络文件系统还需评估锁、原子操作、IO和官方支持边界。
二十一、缓存一致性策略
| 策略 | 优点 | 风险 |
|---|---|---|
| 短TTL | 简单、自动收敛 | 窗口内旧数据、回源较多 |
| 版本化URL | 静态资源可永久缓存 | 需要构建版本管理 |
| 事件驱动失效 | 变化后快速清理 | 事件丢失、重试、权限 |
| 主动Purge | 精确 | 开源版能力、第三方模块和多实例广播 |
| 不缓存敏感数据 | 一致性简单 | 后端压力更高 |
开源Nginx的Purge能力并非所有发行版标准内置,可能需要商业版或第三方模块。不能假定 proxy_cache_purge在当前二进制可用。
二十二、静态资源最佳实践
构建产物使用内容Hash文件名:
app.8f31c2.js可以返回长缓存:
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}HTML入口使用较短缓存或协商更新,因为它引用新Hash资源。发布回滚应保留旧版本资源,避免旧HTML引用已经删除的文件。
二十三、API微缓存
公开高频接口可以只缓存1到5秒:
proxy_cache_valid 200 2s;即使TTL很短,在高QPS下也能显著减少回源。仍要确保:
- 接口公开且无用户数据。
- Key包含必要参数。
- 写后短暂旧数据可接受。
- 错误和Set-Cookie不被缓存。
- 多实例回源峰值可控。
二十四、缓存投毒与数据串读
高风险原因:
- Key遗漏Host或租户。
- 响应受Authorization/Cookie影响但Key未包含身份。
- 不可信Header参与后端响应却未进入Key。
- 攻击者制造异常查询参数填满磁盘。
- 忽略Set-Cookie/Cache-Control强行缓存。
发生用户数据串读时应立即停用相关缓存、隔离日志和缓存文件、评估泄露范围、清理所有实例并修复Key/缓存边界。
二十五、缓存命中率低排查
- 统计
$upstream_cache_status。 - 规范化分析cache key,不记录敏感原值。
- 检查查询参数顺序和追踪参数。
- 检查Cookie、Authorization、Set-Cookie和Vary。
- 检查TTL和inactive。
- 检查多实例流量分散。
- 检查磁盘清理和zone容量。
- 检查配置是否命中正确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
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、上层统一缓存、预热和渐进扩容治理,不能默认共享网络盘安全可用。
三十一、学习验收
- 画出cache key、共享索引、磁盘文件、Loader和Manager。
- 为公开API设计Key并证明不会跨Host/租户串读。
- 区分MISS、HIT、EXPIRED、STALE、UPDATING和BYPASS。
- 解释TTL与inactive不同。
- 制造热点到期并比较有无cache lock的回源量。
- 设计stale策略并列出禁止使用的强一致数据。
- 证明bypass与no_cache只配一个的风险。
- 扩容三台Nginx,观察冷缓存和重复回源。
- 定位一次命中率低、磁盘满和用户数据串读。
- 为静态资源、公开API和敏感API分别设计缓存策略。
