
刚接触Nginx缓存的时候我一度以为缓存清理就是一行rm命令的事。直到后来负责的站点在更新活动页后用户连续几个小时刷到的都是旧页面领导还在群里问是不是服务器挂了我才明白Nginx缓存清理这活儿看着简单真要处理好还挺讲究。你不仅要知道缓存写在哪个目录还得搞清楚它是由哪个key生成的甚至连Nginx进程的工作用户是谁、目录权限对不对都会影响清理效果。这篇文章我会从底层原理到具体操作把Nginx缓存清理这件事拆开揉碎讲清楚你照着做基本就不会被缓存问题坑了。1. Nginx缓存为什么要清理先搞清缓存类型1.1 容易被人忽视的“两级缓存”模型在接触业务站点之前我以为缓存只有一套实际上常规架构里一般存在两层。一是用户浏览器里的缓存由服务器通过Cache-Control、Expires等响应头告诉浏览器能缓存多久二是Nginx自己维护的中间层缓存比如proxy_cache、fastcgi_cache、uwsgi_cache。用户发来的请求先到NginxNginx如果没命中缓存才回源到后端应用服务器如果命中了就直接把磁盘上或内存里的副本返回给用户。这两级缓存经常交错影响。比如你更新了商品详情页后端数据库和页面模板都已经变了浏览器里的老页面却还在这是第一层缓存的问题但如果用户清掉浏览器缓存后访问看到的还是旧页面那就得怀疑Nginx这一层了。也就是说所谓Nginx缓存清理绝大部分场景指的是清第二层也就是在Nginx进程内部生成的缓存副本。有一个判断手段非常实用在Nginx配置里加上add_header X-Cache-Status $upstream_cache_status然后刷新页面试试。第一次请求看到MISS第二次请求看到HIT基本就能确认请求命中了Nginx缓存。看到EXPIRED表示超过了配置的过期时间Nginx会回源重新获取看到BYPASS表示请求绕过了缓存。这一步定位能避免我们对着浏览器缓存瞎忙活。1.2 Nginx自身缓存与浏览器缓存的差别用生活化的类比来解释Nginx的缓存有点像小区快递柜包裹先存在柜子里你去取的时候随时都在浏览器缓存则像你家冰箱提前放进去的食物打开冰箱就能拿。前者由小区的管理员也就是Nginx管理后者由你自己也就是浏览器管理清理方式自然完全不同。从操作层面看Nginx自身的缓存清理是服务端行为。你要么删除缓存目录下的文件要么借助缓存清理模块精准删除某一个URL对应的缓存副本。浏览器缓存清理则是客户端行为普通用户只能通过浏览器设置清空或者开发人员通过修改响应头让浏览器明白“这个资源不缓存”“这几个接口不能缓存”再配合强制刷新常见快捷键。这也是很多团队容易产生误会的地方。后端同学说“我已经清了Nginx缓存”前端同学说“我也清了自己的浏览器缓存”但页面依然不对。出现这种情况时要去检查是否存在CDN的中间缓存或者看请求到底有没有走预期的那一台Nginx比如负载均衡后面有多台Nginx实例只清了其中某一台另外几台还存着旧缓存。这些细节我放到第五部分详细讲。1.3 什么情况下你会迫切需要一个清理动作缓存清理不是每天都要做的事但下面几个场景几乎是必踩的上线新版本。页面模板或静态资源被替换但缓存文件还是旧文件的内容用户看到旧页面甚至JS、CSS版本更新了缓存里对应的URL还是老版本。图片、附件等二进制文件被更新。常见于直接把原图覆盖到服务器目录但Nginx缓存副本仍然保存旧图。接口数据异常。后端返回了带错误状态或临时促销数据的响应被Nginx缓存起来接口恢复正常后缓存还会继续输出错误内容一段时间。磁盘空间告急。proxy_cache_path虽然有max_size控制但业务增长太快缓存占用可能把分区撑满业务表现为写入失败页面出现5xx错误。权限变更。比如之前用root启动过Nginx后来改成普通用户运行缓存目录里残留的文件属主不对重新生成缓存时报权限错误Nginx就频繁回源。测试阶段调优。你改了缓存有效期、缓存键策略需要观察旧缓存对结果的影响就得先清掉旧数据再看效果。这些场景里有的是主动清理有的是被动排查。无论哪种关键都在第一件事找到缓存到底放在哪以及每个缓存文件对应什么请求。2. 动手之前先定位Nginx把缓存写到了哪里2.1 通过配置找到缓存根目录定位缓存目录最直接的方法就是看Nginx配置文件里proxy_cache_path、fastcgi_cache_path或uwsgi_cache_path的路径。典型的代理缓存配置长这样proxy_cache_path /data/nginx_cache/static levels1:2 keys_zonestatic_cache:10m max_size10g inactive60m use_temp_pathoff;字段含义是这样的/data/nginx_cache/static是缓存文件存储目录levels1:2是目录层级规则Nginx会把缓存文件分散到两层目录里避免单个目录下文件过多I/O效率下降keys_zonestatic_cache:10m是命名缓存区域大小10MB这个区域只存放cache key的索引元数据不存页面内容max_size10g表示控制缓存总上限超过之后由Nginx的cache manager进程按LRU策略清理inactive60m表示60分钟内都没有命中的缓存会被视为不活跃由后台进程清理。很多刚入门的人觉得levels最难理解。简单说levels1:2表示最终文件的相对路径是“一层目录名取最后一个字符二层目录名取最后两个字符再加上完整的MD5文件名”。比如某文件名为5e2fb1a3c4d56789...它可能存放在一级目录“9”下的二级目录“89”里。这种设计不是Nginx故意绕人而是防止大量缓存文件堆在同一个目录里导致stat、open等系统调用变慢。如果配置文件层级比较多可以用grep快速搜索nginx -T 2/dev/null | grep -E proxy_cache_path|fastcgi_cache_path|uwsgi_cache_path这条命令会输出所有加载生效的缓存路径配置。建议顺带检查是否有局部server块覆盖了全局缓存配置有些站点会在server或location级别重新指定缓存区域路径排查的时候容易漏。2.2 看懂缓存文件名的MD5密钥规则缓存文件为什么是一串看不懂的字母答案是Nginx默认用proxy_cache_key生成缓存键。键的默认值是“$scheme://$host$request_uri”也就是完整的访问URL串。既然请求URL是可见的缓存文件名就是对这个URL串做MD5哈希后的值。举例如果缓存键是http://example.com/product/123那么先算出MD5值printf %s http://example.com/product/123 | md5sum得到一串32位的十六进制字符串比如1a2b3c4d5e6f7890abcdef1234567890。根据levels1:2的规则一级目录用最后1个字符二级目录用最后2个字符文件本身则放在二级目录里面。也就是说最后的实际路径类似/data/nginx_cache/static/0/90/1a2b3c4d5e6f7890abcdef1234567890。如果配置里自己定义了proxy_cache_key比如写成“$host$request_uri”那计算时就不能带scheme前缀。这一点非常坑很多人在手工计算缓存文件名时删不到文件就是因为没有按实际配置的键格式去算。手工定位单个缓存文件时我习惯用一段脚本urlhttp://example.com/product/123 hash$(printf %s $url | md5sum | awk {print $1}) dir1${hash:31:1} dir2${hash:30:2} file/data/nginx_cache/static/$dir2/$dir1/$hash ls -l $file注意这里的层级顺序要根据实际配置调整我这边只是演示级联规则别照抄就完事。实际项目里更稳妥的做法是先确认缓存键是什么再从nginx -T输出里把proxy_cache_key找出来。2.3 用curl验证缓存命中状态有了配置和缓存键下一步就是实际请求确认。我常用两个curl参数-I只拿响应头-o /dev/null不保存响应体。先访问一次让缓存生成再访问第二次观察状态。curl -I http://example.com/product/123常见响应头里会带x-cache-status如果没带说明配置里没有添加add_header X-Cache-Status $upstream_cache_status那就先给Nginx配置加上再reload。不过要注意在add_header所在的块里如果想同时输出多个自定义头不能一股脑写多个add_header部分版本当存在add_header时会覆盖掉代理源设置的头部这也算一个历史遗留坑。验证完命中状态后你再清理缓存就很有底气。你删掉的到底是哪个URL的副本删完之后重新请求应该能从MISS开始重新缓存。如果清理后还是看到HIT要么是删错了位置要么是多机环境中请求被负载均衡转发到了另一台Nginx实例。3. Nginx缓存清理的几种实操方案3.1 简单粗暴但有效的全量清理最直接的方式清空缓存根目录。rm -rf /data/nginx_cache/static/*注意目录后面有一个星号不是直接删根目录本身。直接rm -rf缓存根目录也可以但目录正被Nginx进程使用时后续Nginx会重新创建必要目录不过如果缓存路径是由进程启动时创建的直接删掉根目录后Nginx在运行期可能进不去写入阶段必须重新reload或重启才恢复。所以稳妥做法是删除目录“内部”的文件保留目录本身。缓存文件量小时这条命令无所谓量大了千万别直接传rm。几万个甚至几十万个碎片文件的递归删除会让磁盘I/O飙升业务高峰期会拖慢所有读写操作。更舒服的做法是先把旧目录改名再建一个同路径的新目录mv /data/nginx_cache/static /data/nginx_cache/static_old mkdir -p /data/nginx_cache/static chown nginx:nginx /data/nginx_cache/static这样Nginx继续往新目录写旧的目录慢慢在后台删。删除时可以用ionice降低优先级ionice -c3 rm -rf /data/nginx_cache/static_oldionice -c3表示使用idle调度只在磁盘空闲时清理比较适合不想影响线上业务的场景。如果磁盘性能很弱宁可分批删除也不要一次性梭哈。3.2 按页面、API或正则表达式精确清理全量清理适合活动上线、迁移等大动作日常更常见的是精确清理某一条URL。原理就是前面说的先把URL按缓存键规则算成MD5再删对应文件。一个我在实操中常用的简化脚本#!/bin/bash urlhttp://example.com/product/123 key$(printf %s $url | md5sum | awk {print $1}) cache_base/data/nginx_cache/static find $cache_base -name $key -type f -delete这个方案不用关心两级目录的结构遇到多台Nginx机器时把同一个脚本分发到每台机器执行就行。需要匹配一组URL时可以先把URL列表保存在文件里逐行处理或者结合正则匹配文件内容。但缓存文件是二进制的文件内容里不一定能找到关键正则靠文件名MD5去匹配更靠谱。另外一种更省事的思路是故意让Nginx回源覆盖缓存调用接口时带一个特殊参数比如直接在URL后面加“?nocache随机数”。如果配置的cache key默认包含完整request_uri那么这个带参数的请求就不会命中原来的缓存。这个技巧适合即时验证不适合长期使用因为会污染缓存多出垃圾副本。3.3 定时任务自动清理自动清理分两个层次。第一层是Nginx自身的cache manager进程它会在后台按inactive、max_size等参数维护缓存生命周期。每当缓存项超过inactive时间或者整个缓存总量快达到max_sizeNginx会自动删除旧文件。这一层不归我们手动控制但对磁盘容量维护至关重要。第二层才是我们手动加上的定时任务。通常用在业务方出现大量“僵尸缓存”比如带有随机参数但未正确使用缓存键的接口生成了上百万个不会再次命中的URL。这时可以写个脚本每天深夜删除N天前创建的文件find /data/nginx_cache/static -type f -mtime 7 -delete配合crontab大约长这样0 4 * * * /usr/local/bin/nginx_cache_clean.sh /var/log/nginx_cache_clean.log 21这个脚本在执行前最好检查一下nginx进程是否在运行否则容易出现奇怪的权限错误还要注意find -delete在某些老版本和大量文件的高并发删除场景下也可能打满磁盘建议加上nice和ionice。定时任务的好处是减少人工介入坏处是如果脚本写得粗糙可能出现删错范围的风险所以我建议先用find统计数量再执行删除。3.4 使用缓存清理模块如果不想计算MD5也不想删除整个目录还可以用ngx_cache_purge模块。这是Nginx社区里常用来做精准清理的方案。它提供一个purge指令匹配到特定请求时直接删除对应缓存文件。你可以通过编译Nginx时加入参数或者用动态模块方式加载。这里给出配置思路location ~ /purge(/.*) { allow 127.0.0.1; allow 10.0.0.0/8; deny all; proxy_cache_purge static_cache $scheme://$host$1; }这样内部管理机需要清理静态资源时只要访问https://域名/purge/product/123模块就会把对应缓存删掉。这个动作等于把“清理”开放成了HTTP接口权限控制非常关键。如果这层location不加allow/deny限制等同于任何人访问都会触发对Nginx的删除操作很容易变成“在线事故”。使用模块的优点是可以被监控系统调用比如哨兵系统发现某个页面缓存异常时直接发一条PURGE请求就能快速恢复。缺点是编译和升级Nginx时要额外维护模块部分系统仓库版本没有现成包。要不要引入取决于你是不是经常需要按URL精确清理。3.5 内容过期参数让缓存自动失效手动清理再方便也不如让缓存本身设计得“短命”。在Nginx配置里通过proxy_cache_valid控制缓存时间proxy_cache_valid 200 304 12h; proxy_cache_valid 404 1m;意思是200和304状态码的响应缓存12小时404缓存1分钟。也可以配合后端返回的Cache-Control头来覆盖如果后端动态判断某些页面不能缓存Nginx就不会生成缓存。实际项目中我更推荐把“清理压力”前移后端应用在更新数据时主动调用一个内部接口清除相关URL或者直接把页面响应头设为不缓存。Nginx缓存清理是兜底动作真正高效的做法是让需要频繁变化的接口干脆不缓存或者快速过期。否则你今天清一次明天缓存又生成永远都在追赶问题。4. 不同场景下的缓存清理配置参考4.1 反向代理场景最常见的Nginx反向代理缓存是对后端源站的静态资源和页面做加速。基本配置长这样proxy_cache_path /data/nginx_cache/static levels1:2 keys_zonestatic_cache:10m max_size10g inactive60m; server { listen 80; server_name example.com; location /static/ { proxy_pass http://backend_upstream; proxy_cache static_cache; proxy_cache_key $scheme$host$request_uri; proxy_cache_valid 200 304 1h; add_header X-Cache-Status $upstream_cache_status; } }这种方式的好处是减少回源请求保护后端。风险是后端资源更新了缓存却不及时。如果你用了ngx_cache_purge模块可以在固定location里动态清理。没有模块的话按前面的MD5规则算好位置找到文件删除就行。反向代理里还有一个细节如果开启了gzip压缩并且源站响应头里的Vary或Content-Encoding有差异Nginx可能缓存出多个版本的压缩和非压缩副本。碰到这类问题建议先统一后端响应的请求头策略否则清理起来很容易漏掉其他版本副本。4.2 FastCGI动态页面场景动态站点比如PHP、Java等用fastcgi_cache作用相近。配置文件示意fastcgi_cache_path /data/nginx_cache/fastcgi levels1:2 keys_zonefcgi_cache:10m inactive60m; location ~ \.php$ { fastcgi_pass unix:/tmp/php-cgi.sock; fastcgi_cache fcgi_cache; fastcgi_cache_key $request_method$host$request_uri; fastcgi_cache_valid 200 60m; add_header X-Cache-Status $upstream_cache_status; }动态页面缓存最怕的是带登录态或用户个性化内容。如果页面里有用户ID信息而缓存键没排除Cookie用户A登录后看到的页面可能被缓存并返回给用户B这就是很严重的隐私泄漏问题。我通常会在配置里加上fastcgi_ignore_headers Cache-Control Set-Cookie;这行配置告诉Nginx忽略源站的Set-Cookie头避免响应带有Set-Cookie时直接跳过缓存。但注意这只是让响应进入缓存不代表用户数据安全。还是需要在业务里区分哪些页面能公共缓存哪些页面必须个性化。清理此类缓存时原理和代理缓存完全一致。但动态页面常常包含HTTP头、HTTPS、带参数等多种变体如果你平时没有把cache key整理规范临时排查会非常痛苦。建议在配置里统一把$request_method也加进键避免POST请求误命中GET缓存。4.3 静态文件与浏览器缓存场景静态文件通常由Nginx直接提供不一定走proxy_cache可能只是用了alias或root。这类文件被浏览器缓存的时间由expires指令控制location /assets/ { alias /data/site/assets/; expires 30d; add_header Cache-Control public, immutable; }浏览器在30天内不会再向服务器发起资源请求除非用户强制刷新。这种长期缓存的策略对性能很好但更新文件名时必须同步改名比如main.css改成main.20240506.css否则用户拿到旧文件名对应的旧缓存。线上更新时很多人只改了文件内容没改文件名结果怎么清Nginx缓存都没用因为浏览器压根没把请求发到Nginx。如果确实需要更新同名静态文件又无法改名可以临时把expires调成0或者设置Cache-Control为no-cache让浏览器在下一轮请求时强制回源。这个过程不是一个简单的Nginx清理动作能解决的要配合前端打包工具一起设计。真正懂得“缓存清理”的人一定会知道这个道理同样一个更新在Nginx层需要清proxy缓存在浏览器层需要改文件名或调整Cache-Control。顺带提一下open_file_cache它缓存的是文件句柄、元数据不是页面内容。如果文件被替换但inode变化Nginx一般会自动感知但如果用相同文件名覆盖并且系统时间戳未刷新测试时可能看到陈旧内容。常规项目里刷新静态文件最快的方式就是改文件名或者加query参数。5. 缓存清理过程中的高频坑与排查实录5.1 403问题权限、属主、autoindexNginx缓存清理场景里403频繁出现。一种情况是Nginx的worker进程没有权限访问缓存目录导致回源后无法写入缓存缓存一直MISS且页面日志出现403。原因通常是缓存目录的属主不对。比如你用sudo mkdir创建了目录目录属主是root而Nginx是nginx用户运行写入时毫无悬念地被拒绝。解决办法是把目录给Nginx工作用户chown -R nginx:nginx /data/nginx_cache chmod -R 700 /data/nginx_cache如果不知道Nginx运行用户看nginx.conf第一行的user指令或者执行ps aux | grep nginx观察worker进程属于哪个用户。另一种403和autoindex相关。有人为了方便文件浏览开启了autoindex on把某个目录直接暴露成下载列表。恰巧这个目录刚好是缓存目录等于把缓存文件的MD5文件名全部公开展示。如果站点文件夹下有敏感文件又开了autoindex就直接全文暴露了。我的建议是autoindex只在内网管理用途打开并且用location严格限制IP访问公网站点一般不要开。5.2 缓存清完仍旧内容浏览器与CDN残留这是排查率最高的“假缓存问题”。Nginx缓存确实已经清掉了但页面显示的还是旧内容。此时先不要怀疑清理动作应该观察响应头。用curl -I连续请求两次对比关键字段。如果看到Cache-Control: max-age3600且页面没有变化说明浏览器在本地缓存生效服务器根本没有收到请求。这时候你清Nginx缓存当然没用。解决方法是让用户强制刷新或者在发布流程里使用Cache-Control和ETag配合协商缓存确保文件更新后浏览器能够拿到200响应而不是直接读取本地缓存。如果页面源已经更新但用户访问区域还绕了一层CDN那你们还需要清CDN缓存。CDN和浏览器缓存、Nginx缓存三者经常一起出现Nginx缓存清理只是其中一环。排查顺序是先看浏览器是否有缓存再看请求有没有真正到达自己的Nginx最后看Nginx是否返回HIT。有些人一上来就清Nginx清了半天问题没解决那是因为前面的链路根本没查。5.3 Windows下Nginx配置cache的几个坑很多开发机用Windows上的Nginx调试报错里会出现类似[emerg] createfile() d:/phpstudy_pro/www/admin2.com/nginx.htaccess这类信息。这个错误看着吓人其实常见原因有三个。第一磁盘路径里存在中文、空格或者特殊字符Nginx在Windows下的win32 API对路径支持不够稳健。第二配置文件保存时使用了带BOM的UTF-8编码Nginx在读取文件时把头部BOM当成异常字节直接报错。第三nginx.htaccess这类文件名如果被当成了Nginx配置文件但它在Windows下指向的路径与实际磁盘位置不符或者文件不存在。遇到这类报错先用文本编辑器把配置文件另存为“无BOM的UTF-8”再检查路径是否和真实磁盘路径一致。Windows下尽量统一用正斜杠表示路径避免反斜杠转义混乱。测试配置时先用nginx -t确认没问题再启动。这里还要补充一点Windows下的Nginx一般只用于本地开发真正生产环境千万避免用Windows跑Nginx。一来文件句柄、缓存管理性能差很多二来很多开源性能优化在Windows上效果一般既然是要深入研究缓存清理还是把主力环境放到Linux更省心。5.4 清理缓存时磁盘IO飙升怎么办大目录清理最怕的是磁盘IO占用瞬间打满业务响应变慢。我踩过一次线上某台Nginx的缓存目录里有几十万个文件直接rm -rf后系统负载狂飙页面全部卡顿。从那以后我清理大目录都遵循一个原则先隔离再异步删除。隔离的意思是先把旧目录改名为old然后在原路径新建一个空目录。Nginx后续写入集中在新目录不会和删除操作抢同一批inode。删除旧目录时用ionice降低优先级cd /data/nginx_cache mv static static_old_$(date %Y%m%d) mkdir static ionice -c 3 rm -rf static_old_$(date %Y%m%d)另外Nginx自己的cache manager进程在max_size达到上限后就会开始淘汰缓存这个过程本身是平滑的。如果你设置了max_size但inactive时间又特别长缓存文件可能既不超期也不触发容量上限那它们会一直躺在磁盘上。这种时候再考虑手动清理也不迟。6. 几个实战排查案例帮你把前面知识串起来6.1 案例一磁盘满导致缓存目录无法写入有一次线上各接口突然大面积5xx一查磁盘使用率已经100%。定位后发现某个日志目录占了一大半空间Nginx缓存目录体积也不小。业务入口是Nginx反向代理Nginx尝试写入新缓存时磁盘没有空间只能回源同时因为临时文件无法落盘回源也受到影响。处理时我先用du -sh /data/nginx_cache/static找出缓存目录的体量再把大日志文件归档清空。接着把缓存目录整体mv到旁边新生成一个空目录这样不需要等待删除完成就能恢复服务。最后重新配置proxy_cache_path里的max_size把上限调到一个稳妥的范围。这个教训让我养成了一个习惯所有涉及Nginx的机器都会在监控里加上缓存目录使用率。6.2 案例二业务更新后留下大量僵尸缓存某次运营活动结束后产品方把活动页面里的图片批量替换为新版但活动页URL没有变。发布脚本只替换了源站图片没有清Nginx缓存导致活动页图一直显示旧版。排查时先用curl -I看x-cache-status确认是HIT然后按URL算出MD5找到缓存文件删除。清理完再请求一次状态变成MISS页面立即恢复。后面我在发布脚本里加了一步先调用清缓存接口再检查文件。不是所有发布都要求清缓存但至少给出一个可选参数让需要清理的时候不用临时登录服务器找缓存位置。这个过程让我意识到缓存清理不该是末日时刻的应急动作而应该是发布流程里的标准动作之一。6.3 案例三API接口数据被缓存污染某个接口返回了当前登录用户的信息刚开始没配置缓存后来为了优化性能在Nginx加了一层fastcgi_cache但cache key没有把Cookie或用户标识放进去。结果用户A登录时页面请求被缓存用户B访问时Nginx直接返回了A的响应。把整个页面的缓存清掉后临时正常下一轮又出现类似问题。最终解决是把这类含用户维度的API单独取出关掉Nginx缓存只允许公共接口走缓存。排查结论让我意识到缓存清理只能解决“缓存错了”的结果真正要解决的是“不该缓存的内容不能进缓存”。如果你发现自己整天在清理同一个接口更要想办法从缓存键和缓存策略上根除。这段时间摸下来我对Nginx缓存清理最大的感受是清理本身不复杂复杂的是搞懂什么能缓存、什么必须清理以及缓存生命周期到底在哪里管理。建议每一位刚接手的基础运维都先做一件事拿nginx -T把自己服务里所有cache相关配置和路径列出来再对照着每个页面验证一遍命中状态。这套功夫做在前头后面遇到“缓存清不掉”“页面不变”的情况你至少知道问题出在哪一层而不是盲目去删文件。缓存清理说到底只是一枚扳手真正值钱的是你知道该拧哪个螺丝以及为什么拧它。