Nginx open_file_cache 文件缓存实战:减少 stat 系统调用、加速静态资源与 404 扫描防护

一次请求背后,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 常和另外两个指令一起讨论:sendfileaio。它们负责的是不同环节,不冲突。

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 -taio 不支持的错,说明你的 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 -ILast-Modified 是否已更新来区分。

坑位二:把 open_file_cache 当成页面缓存。 它不缓存内容,PHP 页面、动态接口一概不受影响。想让动态页面变快,得用 fastcgi_cacheproxy_cache

坑位三:max 设得过大,内存吃紧。 见过把 max=1000000 的配置,在 1GB 内存的小机器上直接触发 OOM。加法则是:元素数 × 约 0.5–1KB。按这个估算,同机还跑着 MySQL 的话,max 保守设在 10000 以内。

坑位四:inactive 设得比 valid 还小。 如果 inactive=5sopen_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 的小站,这套配置不会带来「肉眼可见」的提速,但它是那种「成本几乎为零、收益稳定为正」的优化——在服务器性能调优的清单里,属于值得打勾的一项。

Last modification:September 21st, 2026 at 12:25 pm

Leave a Comment