PHP-FPM 上传临时文件越积越多:upload_tmp_dir、清理时机与失败回收
凌晨收到磁盘告警,通常不会第一时间想到根因出在上传临时文件上。如果你的PHP-FPM服务器上的 /tmp 或者自定义 upload_tmp_dir 占用空间持续上涨,同时最近上传接口刚出现过超时、进程重启或是业务报错,临时文件没落到最终存储路径也没被及时回收,基本就是头号排查目标。处理这类问题的核心不是登服务器删掉存量文件就完事,而是把临时文件从创建、校验、移动到失败清理的整条链路逐段核对清楚。
upload_tmp_dir只决定PHP接收上传数据的临时存放位置,和业务最终的文件目录完全是两回事。- 只有通过
is_uploaded_file()校验并成功move_uploaded_file()后,临时文件才算完成归属交接。 - 异常分支要在同一次请求流程里清理临时文件,定时任务只做兜底,不能替代代码层面的失败回收逻辑。
- 清理之前先按文件生成时间、PHP-FPM活跃请求列表和目录白名单做校验,避免误删还在传输中的合法文件。
先确认增长来源:是临时目录满了,还是最终业务目录在变大
我更推荐先摸清楚文件增长的位置和时间分布,再动手输清理命令。下面给出的操作只会读取目录信息,不会改动或删除任何文件:
php --ini | grep -E 'Loaded Configuration|Scan for additional'
php -r 'echo ini_get("upload_tmp_dir"), PHP_EOL;'
find /tmp -maxdepth 1 -type f -mmin +30 -printf '%TY-%Tm-%Td %TH:%TM %s %p\n' | sort -k3 -nr | head -20
df -h /tmp /var/www/app/storage/uploads
如果上传临时目录里的文件是批量生成、生命周期短、大小和请求体特征匹配的状态,而业务最终存储目录的体积没有同步上涨,问题大概率出在数据接收完成后没走完交接流程。要是新生成的文件都集中在最近几分钟,先别按统一时间阈值批量清理,很可能还有耗时较长的慢上传请求正在运行。
PHP上传文件实际经过哪些状态
PHP收到上传请求后,会先把请求体里的文件数据写入临时位置,再把对应的元数据放到 $_FILES 里。业务代码得把这个临时文件当成还没完成交接的中转资源,而不是已经属于业务最终目录的正式文件。下面这张图对应的就是从接收、校验到移动或者失败回收的完整最小链路。

优先检查error属性和is_uploaded_file判断
不要只判断 $_FILES['attachment']['tmp_name'] 文件是否存在。上传大小超限、请求中途断开、临时目录不可写这类场景下,error 才是更早的分流判断信号:
$file = $_FILES['attachment'] ?? null;
if (!is_array($file) || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
http_response_code(400);
throw new RuntimeException('upload rejected');
}
$tmp = $file['tmp_name'];
if (!is_uploaded_file($tmp)) {
http_response_code(400);
throw new RuntimeException('temporary upload is invalid');
}
这时候先别急着改 upload_max_filesize 配置。如果错误码是 UPLOAD_ERR_PARTIAL,优先排查客户端断连、反向代理超时和PHP-FPM单请求最大时长限制;如果是 UPLOAD_ERR_CANT_WRITE,再去核对临时目录权限和磁盘inode占用情况。
落地处理:把成功交接和失败回收写在同一条逻辑路径里
最终存储的文件名不要直接沿用用户上传的原始名称。先生成业务侧唯一ID,再把文件移动到按日期分层的业务目录里;所有校验失败的分支,都尽量在当前请求结束前把临时文件清理掉。
$targetDir = __DIR__ . '/storage/uploads/' . date('Y/m/d');
if (!is_dir($targetDir) && !mkdir($targetDir, 0750, true)) {
throw new RuntimeException('target directory unavailable');
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'pdf'];
if (!in_array($extension, $allowed, true)) {
throw new RuntimeException('file type rejected');
}
$target = $targetDir . '/' . bin2hex(random_bytes(16)) . '.' . $extension;
if (!move_uploaded_file($tmp, $target)) {
throw new RuntimeException('temporary file handoff failed');
}
// 成功移动后,$tmp 不再是待回收对象
unset($tmp);
生产环境代码里,还要提前定好数据库记录和文件交接的先后顺序:如果先写数据库再移动文件,移动失败时必须给这条记录加“待补偿”状态;如果先移动文件再写数据库,数据库写入失败时就得把刚生成的目标文件丢进补偿队列。两种顺序都能正常跑通,只有逻辑含糊不清才会产生没人认领的孤儿文件。
给所有异常分支加窄范围的临时文件清理动作
$tmp = null;
try {
$file = $_FILES['attachment'] ?? null;
// 省略字段、大小和内容类型校验
$tmp = $file['tmp_name'];
if (!is_uploaded_file($tmp)) {
throw new RuntimeException('invalid upload');
}
// 生成目标路径并完成移动
$ok = move_uploaded_file($tmp, $target);
if (!$ok) {
throw new RuntimeException('handoff failed');
}
$tmp = null;
} finally {
if (is_string($tmp) && is_file($tmp)) {
unlink($tmp);
}
}
finally 不是绝对的保险机制。PHP-FPM被强制终止、宿主机意外断电这类场景下,当前请求根本没有机会运行后续的清理代码,所以还是需要一个只处理“远超正常生命周期的老文件”的兜底定时任务。
紧急回滚处理:先暂停入口,再清理确认过期的文件
收到磁盘告警时,先把上传入口切到维护状态或者临时限制大文件传输,不要一边高并发写入一边清理同一个目录。确认没有长时间运行的慢上传请求后,只处理超过预设保留窗口、且不在活跃进程占用范围内的普通文件:
# 先观察,不删除
find /srv/php-upload-tmp -maxdepth 1 -type f -mmin +60 -printf '%T@ %s %p\n' | sort -n | tail -20
# 维护窗口内按白名单清理,保留最近 60 分钟文件
find /srv/php-upload-tmp -maxdepth 1 -type f -mmin +60 -delete
如果执行清理后磁盘空间没有明显回升,去检查inode占用、容器挂载点映射关系和PHP-FPM实际加载的配置文件。php --ini 读取到的CLI模式配置不一定等于FPM池加载的生效配置,必要时可以在临时健康检查页面输出 ini_get('upload_tmp_dir') 做确认,排查完就立刻删掉这段临时代码。
告警验证:怎么确认不是删完临时文件就临时凑合用
空间恢复后至少完整观察一个上传业务高峰时段,重点关注三个指标:临时目录总文件数、临时目录总占用字节、上传请求失败率。单次 df -h 告警变绿只能说明磁盘空间暂时释放了,不能证明异常流程的失败回收逻辑已经闭环。
| 检查项 | 通过信号 | 异常时优先排查 |
|---|---|---|
| 临时文件年龄分布 | 业务高峰过后老文件数量持续下降 | 失败分支逻辑、FPM重启策略、目录权限配置 |
| 上传请求错误码 | UPLOAD_ERR_OK 占绝大多数 | 反向代理限制、链路超时、磁盘写入状态 |
| 最终文件与数据库记录 | 文件和数据库状态可以互相精准定位 | 文件移动与入库逻辑的补偿状态配置 |
后续优化:把临时文件状态变成可观测的业务信号
日志里记录上传请求ID、错误码、临时文件目录、文件字节数和交接耗时,但不要记录完整的客户端原始文件名或者文件内容。目录监控可以按文件数和总字节数分别设置阈值,比如文件数连续10分钟上涨但业务最终目录体积没有同步增加,就触发低级告警;磁盘使用率超过85%时直接触发限制大文件上传的预案。
还要把清理任务做成全配置可管控的模式:文件保留窗口、允许扫描的目标目录、单次最大删除数量都从配置中心读取,新配置首次上线时只输出待清理候选列表不实际执行删除。这样哪怕线上配置写错,也能先停下任务调整,不至于面对已经执行完的批量删除结果。

常见问题
upload_tmp_dir 和 sys_temp_dir 是同一个配置吗?
不是。upload_tmp_dir 专门控制文件上传产生的临时文件存放位置;sys_temp_dir 是PHP通用系统临时目录配置。两个配置都以FPM进程实际加载的生效值为准,不要只参考CLI模式下读取到的配置结果。
move_uploaded_file 执行成功后还需要手动调用 unlink 删除吗?
通常不需要。文件已经被移动到新路径,原来的临时路径下已经不存在对应文件,重复执行删除只会打满无意义的错误日志。只有异常分支场景下,才需要确认路径确实属于上传临时目录后再做清理。
可以每分钟跑个定时任务直接清空 upload_tmp_dir 目录吗?
不建议这么做。间隔过短的清理任务很容易删掉还在传输中的慢上传文件。要先设置明确的文件年龄阈值、先观察全量活跃请求状态、配置好目录白名单,预览完待清理列表再执行实际删除操作。
小结
PHP-FPM上传临时文件堆积,通常不是单独的“清理脚本出问题”,而是文件接收、校验、移动和异常回收整条链路里缺了一段可靠的交接逻辑。先定位进程实际在用的临时目录,再让成功路径和失败路径都能输出明确的状态,最后用超期文件兜底清理 + 文件数/字节数双维度告警做收口,线上处理这类问题才不会一直停留在出事就删文件的层面。
Redis 过期键为什么没有按时删除:惰性删除、定期扫描与通知验证
- 上一篇
- Redis 过期键为什么没有按时删除:惰性删除、定期扫描与通知验证
- 下一篇
- Go 1.25 容器里的 GOMAXPROCS 怎么迁移:cgroup CPU 限额、自动更新与旧环境兼容
-
- 文章 · php教程 | 1天前 | [] · []
- PHP 8.2 枚举实现后台角色权限:拒绝默认、策略映射与审计日志
- 495浏览 收藏
-
- 文章 · php教程 | 2天前 | [] · []
- PHP 8.5 的 #[NoDiscard] 如何落地:返回值误用、(void) 豁免与升级检查
- 390浏览 收藏
-
- 文章 · php教程 | 2天前 | 性能 · 面向对象 · PHP · PHP 8.4 · ReflectionClass PHP 8.4 Lazy Objects newLazyGhost newLazyProxy PHP懒加载
- PHP 8.4 Lazy Objects 怎么用:Ghost 与 Proxy 的初始化边界
- 310浏览 收藏
-
- 文章 · php教程 | 2星期前 |
- PHP 8.3 readonly class 做请求 DTO:反序列化校验与失败边界
- 105浏览 收藏
-
- 文章 · php教程 | 2星期前 | 依赖注入 · 架构设计 · PHP · PHP 8 · 属性 · ReflectionClass · 依赖注入 可测试代码 ReflectionClass 构造器注入 PHP 8 属性
- PHP 8 属性怎么做轻量依赖注入:反射注册与可测试边界
- 262浏览 收藏
-
- 文章 · php教程 | 2星期前 | 性能 · PHP · php-fpm · OPcache · 部署排查 · php-fpm 缓存排查 PHP OPcache 代码更新不生效 opcache.validate_timestamps
- PHP OPcache 更新代码后不生效怎么办:缓存验证、FPM 重载与回滚排查
- 372浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 4797次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4389次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4334次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4571次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4515次使用
-
- 国家医保服务平台亲情账户怎么绑定:给老人孩子用医保码要注意什么
- 2026-07-08 480浏览
-
- Go html/template 怎么安全把后端数据交给前端:别把 JSON 硬塞进 template.JS
- 2026-07-17 177浏览
-
- Go 项目 GitHub Actions 怎么设质量门禁:go vet、go test 与构建分阶段拦截
- 2026-07-17 485浏览
-
- Go API 错误响应怎么设计:统一错误码、字段语义与兼容迁移
- 2026-07-20 352浏览
-
- Go 重试循环为什么会越跑越慢:用 timer.Reset 控制退避与取消
- 2026-07-22 351浏览

