不少团队在业务上量之后遇到过一个相同的问题:后端应用明明做了接口优化、数据库加了索引,CPU和内存占用依然居高不下。排查日志后发现,大量请求其实是在拉取图片、样式表、脚本这类静态文件,它们每次都穿透Nginx打到后端Tomcat或者Node服务上,后端不得不腾出资源去处理这些本可以直接返回的请求。Nginx自带的proxy_cache模块正是为这个场景设计的,把静态文件缓存在代理层,后续请求直接由Nginx响应,后端压力立刻就能降下来。下面从配置核心指令、完整示例、缓存清理三个方面展开讲。

一、proxy_cache核心配置指令详解
要启用Nginx缓存,首先要理解几个关键指令的作用。proxy_cache_path用来定义缓存文件的存储位置和行为,它必须写在http块中。其中levels参数决定缓存目录的层级结构,比如levels=1:2表示用两级目录存放缓存文件,这样做可以避免单目录下文件过多导致文件系统性能下降。keys_zone定义共享内存区的名称和大小,这块内存用来存放缓存键的索引,1MB大约可以存放8000个键,需要根据缓存对象数量估算。
inactive和proxy_cache_valid是两个容易混淆的参数。inactive指定的是文件未被访问的过期时间,如果一个缓存在指定时间内没有被请求过,就会被清理;而proxy_cache_valid是根据响应状态码设置缓存有效期,比如对200和302响应缓存12小时。max_size则限制缓存总大小,超过后Nginx会自动删除最少使用的缓存数据。
缓存键的设计也很关键,proxy_cache_key默认是$scheme$proxy_host$request_uri,大多数场景够用了。但如果你的站点对同一个URL会根据请求头返回不同内容,比如根据Accept-Encoding返回压缩或未压缩版本,就必须把相应变量加入缓存键,否则会出现gzip内容被返回给不支持的客户端这类问题。
二、完整配置示例:让静态文件请求不再穿透到后端
下面给出一个可以直接落地的配置。假设后端服务运行在本机8080端口,我们希望对图片、样式、脚本等静态资源缓存12小时,动态接口不缓存。
http {
# 定义缓存路径,共享内存区名为static_cache,占用100M内存
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=static_cache:100m
max_size=2g inactive=24h use_temp_path=off;
upstream backend {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name example.ipipp.com;
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?|svg)$ {
proxy_pass http://backend;
proxy_cache static_cache;
proxy_cache_key $uri$is_args$args;
# 对200和301响应缓存12小时
proxy_cache_valid 200 301 12h;
# 后端异常时使用旧缓存兜底
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
# 缓存未命中时允许回源请求,加锁防止缓存击穿
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
# 响应头中标识缓存命中状态,方便调试
add_header X-Cache-Status $upstream_cache_status;
}
location / {
proxy_pass http://backend;
# 后端返回的Cache-Control头优先级高于proxy_cache_valid
proxy_ignore_headers Cache-Control Expires;
}
}
}
配置里有几处值得展开说明。proxy_cache_use_stale是一个非常实用的兜底机制,当后端出现错误、超时或者正在更新缓存时,Nginx可以继续返回旧的缓存内容,用户完全感知不到后端的抖动,这对可用性要求高的系统很有价值。
proxy_cache_lock解决的是缓存击穿问题。某个热门缓存过期的一瞬间,可能有成百上千个请求同时打到后端去重建缓存,这就是击穿。开启锁之后,同一时刻只允许一个请求回源,其余请求等待它写完缓存后直接读取,后端瞬时压力大幅下降。$upstream_cache_status变量会在响应头中标记MISS、HIT、EXPIRED等状态,是排查缓存是否生效的第一手段,建议上线初期保留。
另外要注意proxy_ignore_headers的取舍。如果后端对静态资源返回了Cache-Control: no-cache之类的头,默认情况下Nginx会遵守它而不缓存。使用这个指令可以让Nginx忽略后端的缓存控制头,完全按照proxy_cache_valid的规则来。是否启用取决于你对后端行为的掌控程度,建议和后端团队约定好缓存策略,而不是单方面强行覆盖。
三、缓存清理与命中率优化实战
缓存配置好之后,运维层面还有两个绕不开的问题:怎么清理指定缓存,以及怎么提高命中率。先说清理。Nginx社区版没有提供删除指定缓存的原生指令,常用的方案有三种。第一种是删除整个缓存目录后reload,粗暴但有效;第二种是使用第三方模块nginx_cache_purge,支持按URL精确清除,适合有后台管理需求的场景;第三种是文件更新时通过改变URL版本号让旧缓存自然失效,也就是常见的静态资源文件名加hash的方式,这是前端工程化中最推荐的做法,等于从源头规避了清理问题。
提升命中率方面,首先要审视缓存键的粒度。把Cookie之类的变量拼进静态资源的缓存键会导致命中率暴跌,因为每个用户的Cookie都不同。静态文件应尽可能使用干净的键,比如只包含URI和查询参数。其次,合理设置inactive时间,热门资源的访问间隔一般远小于过期时间,让它们长期驻留内存索引。最后,可以定期通过日志统计X-Cache-Status的分布,如果HIT比例长期低于70%,就要回头检查缓存键设计和有效期策略了。
还有一个容易被忽视的点是缓存盘的选择。缓存目录所在的磁盘IO能力直接影响响应速度,如果条件允许,把缓存目录放在SSD上,甚至用tmpfs挂载到内存,效果会非常明显。不过tmpfs方案在重启后缓存会丢失,需要权衡冷启动时的回源压力。整体来说,一套调优过的Nginx静态缓存方案,通常能把后端60%以上的请求挡在代理层,配合浏览器本地缓存和CDN,可以让后端专注于真正的业务计算,系统的整体稳定性会有质的提升。