如果你调用 file_exists($path)$path 指向一个 Amazon S3 存储桶,你并不是在访问本地磁盘——而是在发起网络请求。这一行简单的检查会变成一次对 S3 的往返请求(round-trip),为每个请求增加数十毫秒的延迟。

PHP 通过流包装器(stream wrappers)将远程存储隐藏起来。诸如 fopen()file_exists()is_dir()unlink()file_put_contents() 等函数都会通过这些包装器进行路由,将调用转换为 S3 API 操作。其映射关系非常直接:

  • file_exists()HeadObject
  • is_dir()ListObjects
  • unlink()DeleteObject
  • file_put_contents()PutObject

每次调用都会产生与相应 S3 API 请求相同的延迟。当脚本检查数十个文件时,会形成一种 N+1 模式:每一次检查都会产生一次网络跳转(network hop)。

为什么现在这很重要

在典型的开发环境中,文件系统位于同一台机器上,因此检查几乎是瞬间完成的。但在生产环境中,资源存储在 S3 上,同样的代码会导致性能骤降(performance cliff)。AWS SDK 会在内存中缓存部分结果,使得在单个 PHP 进程中的重复检查看起来很快。然而,大多数 PHP 部署会为每个 Web 请求启动一个全新的进程,从而每次都清空缓存。结果是:每个请求中的每个不同路径都会进行一次完整的网络往返。

一个 WordPress 网站曾需要 10 秒钟才能渲染其首页。数据库查询很快,但页面仅为了组装内容就触发了 73 次独立的 S3 调用。每次调用都是一次冷请求(cold request),将看似无害的 file_exists() 变成了明显的延迟。

缓解策略

  • 跨请求持久化缓存 – 将进程内缓存转移到 Redis 等共享存储中。当一个请求发现某个键存在于 S3 上时,后续请求将读取缓存的结果,而不是再次联系 S3。
  • 采用“本地优先”工作流 – 将文件写入本地磁盘,然后异步同步到 S3。主请求路径仅操作本地文件系统;网络开销则转移到了后台任务中。
  • 审计代码库 – 系统地搜索可能解析到远程包装器的文件系统函数。标记出任何出现在密集循环(tight loops)或请求关键路径中的函数。
  • 检查 APM 数据 – 如果响应时间激增而数据库指标保持平稳,请深入研究存储 SDK 的耗时情况。SDK 延迟的突然上升通常指向隐藏的 S3 调用。

核心结论:一个看起来像是微秒级本地检查的函数,可能会隐藏数十毫秒的网络延迟。请将 S3 上的 file_exists() 视为外部调用,缓存其结果,并将繁重的工作从请求路径中移走。否则,隐藏的延迟将通过一次又一次不可见的网络请求,持续拖慢你的网站。