一次请求背后,Nginx 悄悄做了多少次 stat
一个静态图片请求,Nginx 表面上只是把文件读出来发走。但在拿到文件之前,它其实做了一连串系统调用:打开目录、读取目录项、对目标文件做 stat 判断是否存在、是不是目录、大小多少、修改时间是什么。这些操作单个只要几微秒,但当你的站点跑在低配 VPS 上、每秒要处理几百个静态请求时,累积起来的磁盘元数据查询开销就非常可观了。
更糟的是,很多站长为了「检查文件是否被篡改」,还在配置里加了 if (-f $request_filename) 之类的判断,等于每个请求多做好几次 stat。open_file_cache 就是为解决这个问题而生的——它把文件元数据缓存在内存里,让后续请求不再访问磁盘。
open_file_cache 到底缓存了什么
需要先明确:open_file_cache 不缓存文件内容,文件内容依然由操作系统 page cache 负责(这就是为什么你手动改完文件,访问立刻生效)。它缓存的是三类东西:
- 文件描述符:已经 open 的 fd,避免重复 open/close 系统调用。
- 文件元数据:size、mtime、inode 等,也就是 stat 的结果。
- 文件查找失败的记录:即「这个路径不存在」这个结论,也缓存起来,避免目录扫描。
第三点非常关键。对于大量 404 请求(比如被扫描器狂扫 /wp-login.php 的站点),缓存「不存在」的结论可以极大减少磁盘目录遍历,这在实战中往往比缓存真实文件收益还大。
基础配置与参数含义
http {
# 开启缓存:最多 10000 个元素,20 秒内未访问则失效
open_file_cache max=10000 inactive=20s;
# 校验周期:60 秒检查一次缓存元素的有效性
open_file_cache_valid 60s;
# 一个文件在 30 秒内被访问 2 次以上,才进入缓存
open_file_cache_min_uses 2;
# 缓存文件查找失败(404)的结果
open_file_cache_errors on;
}
逐个解释参数的取舍:
max:缓存元素数量上限。Nginx 使用 LRU(最近最少使用)淘汰策略。10000 对于大多数个人站点足够;如果站点静态文件超过几万个,可以适当调大到 20000–50000,但要注意内存占用——每个缓存元素大约占几百字节到 1KB 左右的内存,10000 个大约消耗 5–10MB,非常划算。
inactive:元素多长时间没被访问就被移除。这个值要和 open_file_cache_valid 配合理解。设成 20s 意味着访问不活跃的文件会较快释放内存;如果你站点访问分布很散,可以放宽到 60s。
open_file_cache_valid:多久去磁盘重新验证一次缓存是否仍然有效。这是最重要的一个值——它决定了你更新文件后,多久能被 Nginx 感知。设成 60s 意味着你改了一个静态文件,最长 60 秒后才会生效。如果你需要「改完立即生效」,就得设小,比如 10s,但代价是更频繁的 stat。
open_file_cache_min_uses:冷启动门槛。只有被访问至少 N 次的文件才进入缓存。设成 2 可以避免一次性访问的临时文件污染缓存。如果你的站点文件数很少、访问很集中,可以设成 1,让首次访问就缓存。
open_file_cache_errors:是否缓存「查找失败」的结果。强烈建议打开,尤其是公网暴露的站点。
什么时候该用,什么时候不该用
不是所有站点都能从 open_file_cache 受益。判断标准是「文件打开操作在整个请求处理中占比是否明显」。
适合用的场景:
- 访问量较大的静态资源站点(图床、CDN 回源、下载站)。
- 反向代理场景:Nginx 只做转发,但依然要 stat 判断 root 下的临时文件、日志文件。
- 存在大量 404 扫描流量的站点(开了
open_file_cache_errors收益明显)。 - 文件数量多、目录层级深的站点,目录遍历成本高。
不太需要甚至有害的场景:
- 文件更新极其频繁(比如每分钟都在生成新文件),缓存反而导致读到旧元数据。
- 开发环境:你希望改完文件立刻生效,缓存的 60s 延迟会让你怀疑人生。
- 文件数极少(比如就一个 index.html),省下的 stat 微乎其微。
与 sendfile、aio 的配合关系
open_file_cache 常和另外两个指令一起讨论:sendfile 和 aio。它们负责的是不同环节,不冲突。
http {
sendfile on; # 数据从磁盘到 socket 走内核零拷贝
tcp_nopush on; # 配合 sendfile,攒够一个 MSS 再发
tcp_nodelay on; # keepalive 长连接下尽快发送
aio on; # 异步 IO,避免 worker 进程被磁盘 IO 阻塞
directio 8m; # 超过 8MB 的文件绕过 page cache 直接读
open_file_cache max=20000 inactive=30s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}
用一句话概括分工:sendfile 解决「数据怎么搬」,aio 解决「搬的时候别阻塞 worker」,open_file_cache 解决「别每次都去问磁盘这文件在不在、多大」。三者是叠加收益。
注意 aio 在 Linux 上依赖内核支持,Linux 4.x 以上一般直接可用(基于 io_uring 的版本更佳)。如果 nginx -t 报 aio 不支持的错,说明你的 Nginx 编译时未带 --with-file-aio,去掉即可。
如何验证真的生效了
配完重启,怎么知道它起作用了?最直接的方法是用 strace 观察一个 worker 进程的系统调用。
# 找到 worker 进程 PID
ps -eo pid,cmd | grep "nginx: worker"
# 跟踪它 10 秒,只统计 openat / newfstatat 调用次数
sudo strace -f -p <PID> -e trace=openat,newfstatat -c -q -w &
sleep 10
kill %1
更好的办法是对比实验:先在关闭 open_file_cache 的情况下压测,记录 newfstatat 调用次数;再加上配置重载,同样压测,对比次数下降幅度。典型结果是一次请求的 stat 从 5–8 次降到 0–1 次。
另一个更省事的观察点是 Nginx 的 stub_status,但它不直接暴露缓存命中率。想更精细地观测,建议配合错误日志的 debug 级别(仅在测试环境开启,生产环境 debug 日志量极大)。
几个必踩的坑
坑位一:改完文件不生效,以为缓存出 bug。 这是最常见的。「我明明更新了 CSS,用户还是看到旧样式」——先检查 open_file_cache_valid 和浏览器缓存,大多数情况不是 Nginx 缓存的锅,而是浏览器或 CDN 缓存。用 curl -I 看 Last-Modified 是否已更新来区分。
坑位二:把 open_file_cache 当成页面缓存。 它不缓存内容,PHP 页面、动态接口一概不受影响。想让动态页面变快,得用 fastcgi_cache 或 proxy_cache。
坑位三:max 设得过大,内存吃紧。 见过把 max=1000000 的配置,在 1GB 内存的小机器上直接触发 OOM。加法则是:元素数 × 约 0.5–1KB。按这个估算,同机还跑着 MySQL 的话,max 保守设在 10000 以内。
坑位四:inactive 设得比 valid 还小。 如果 inactive=5s 而 open_file_cache_valid=60s,元素会在验证之前就被移除了,缓存等于白开。经验值是 inactive 不小于 valid,两者保持同一量级。
常见问题答疑
问:开了 open_file_cache 之后,我更新了 CSS/JS 但浏览器还是旧样式,怎么办? 先分清是谁在缓存。用 curl -I https://你的域名/style.css 看响应头里的 Last-Modified:如果这个时间已经是你更新后的时间,说明 Nginx 元数据缓存正常,问题在浏览器或 CDN;如果还是旧时间,才需要调小 open_file_cache_valid。更根本的解法是给静态资源加版本指纹(如 style.css?v=20260921),让文件名变化绕过所有缓存层。
问:max 到底设多大合适?我的 VPS 只有 1GB 内存。 按每个缓存元素约 0.5–1KB 估算:max=10000 大致占 5–10MB,max=20000 约 10–20MB。1GB 内存的机器还要跑 MySQL 和 PHP-FPM,建议保守设在 10000 以内,优先保证数据库的 innodb_buffer_pool_size。缓存不是越大越好,超过实际工作集的部分只会白白占内存。
问:open_file_cache 和 fastcgi_cache 有什么区别,能一起用吗? 两者层次完全不同,可以也应该一起用。open_file_cache 缓存的是「文件在不在、多大、什么时候改的」这类元数据,针对的是磁盘 stat 开销;fastcgi_cache 缓存的是「PHP 渲染出来的完整页面内容」,针对的是 PHP 执行开销。前者对所有请求生效(静态、动态都受益),后者只对动态页面生效。二者叠加,静态请求省 stat,动态请求省 PHP,收益互不冲突。
问:为什么我配置了 open_file_cache_errors on,404 日志还是那么多? 缓存「查找失败」的结果不等于不记录日志。它省的是「每次 404 都要重新遍历磁盘目录去确认文件不存在」的开销,但访问日志和错误日志仍然会照常记录每一条请求——这是好事,否则你排查扫描攻击时就瞎了。如果嫌 404 日志刷屏,应该做的是在 server 块里对明显的扫描路径直接 return 444(Nginx 会直接断开连接且不记录访问日志),而不是关闭 _errors 缓存。
问:这是不是「银弹」,配完网站就一定会变快? 不是。它是一个「低成本、低风险、稳定小幅正收益」的优化项,收益大小取决于你站点的文件访问密度和磁盘性能。对固态硬盘的 VPS 收益偏小,对机械盘或磁盘 IO 紧张的机器收益更明显;对日均几百 IP 的小站,人眼基本感觉不到差别。真正的提速大头依然是:缓存策略、gzip/brotli、HTTP/2、CDN、以及数据库查询优化。open_file_cache 属于「顺手做了不亏」的那一类。
给低配 VPS 的推荐配置
最后给一份可以直接抄的配置,针对 1–2GB 内存、跑着 LNMP 的个人站点,平衡了收益和资源占用:
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 压缩也要开,和缓存是叠加收益
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml
image/svg+xml;
gzip_vary on;
}
改完后用 nginx -t 校验语法,systemctl reload nginx 平滑生效,再按前面说的方法用 strace 对比 stat 次数。对于日均几千 IP 的小站,这套配置不会带来「肉眼可见」的提速,但它是那种「成本几乎为零、收益稳定为正」的优化——在服务器性能调优的清单里,属于值得打勾的一项。