文件上传临时存储清理

解读

在国内高并发业务场景(如电商大促、短视频上传、在线教育课件提交)中,PHP 默认把上传文件先放到系统临时目录(Linux 下通常是 /tmp,Windows 下是 C:\Windows\Temp),文件名形如 phpXXXXXX。
如果脚本因异常退出、磁盘打满、进程被 kill、用户重复刷新,临时文件极易堆积,导致 inode 耗尽、磁盘报警、新上传失败,甚至触发监控告警“磁盘只读”而让整个容器集群重启。
面试官问“怎么清理”并不是想听“cron+rm -rf”,而是考察你对 PHP 生命周期、SAPI 差异、php.ini 参数、安全陷阱、运维闭环的综合掌控力。回答必须体现“代码+配置+监控+演练”四位一体的工程化思维。

知识点

  1. PHP 生命周期:request_shutdown 阶段会自动调用 zend_file_handle_dtor,删除 upload_tmp_dir 下“本次请求”产生的临时文件;CLI 模式无此机制。
  2. 配置项:upload_tmp_dir、max_execution_time、max_input_time、upload_max_filesize、post_max_size、sys_temp_dir。
  3. 安全:open_basedir、sys_temp_dir 逃逸、条件竞争(race condition)、LFI 利用 /tmp 残留 webshell。
  4. 内核机制:php_request_shutdown → php_clean_buffer → php_destroy_uploaded_files → unlink。
  5. 容器场景:/tmp 可能是 tmpfs,重启即消失;但宿主机若挂载 emptyDir 或 hostPath,重启不删。
  6. 国内云厂商:阿里云 ACK、腾讯云 TKE、华为云 CCI 均提供“临时卷”回收策略,需显式声明 medium=Memory。
  7. 监控:node_exporter 的 node_filesystem_files_free、自研脚本统计 /tmp/phpxxx 数量、Prometheus + 钉钉/飞书告警。
  8. 法规:等保 2.0 要求“剩余信息清除”,临时文件含用户敏感数据(身份证、营业执照)时必须加密或即刻转存对象存储后删除。

答案

线上实战我们分四层治理:
一、代码层——确保业务脚本一定触发 unlink

  1. 接收完文件立即 move_uploaded_file 到持久目录,再显式 fclose 释放句柄;任何分支都用 try/finally 兜底 unlink($tmpName)。
  2. 若需异步处理(Kafka 队列),先把文件流读入内存或转存 OSS,然后 unlink,杜绝“先写消息再删文件”的时序倒置。
  3. 对超大分片上传,采用“流式直传 OSS”方案,PHP 端只接收 meta,不落盘,临时文件为零。

二、配置层——缩小临时区并缩短生命周期

  1. php.ini 里 upload_tmp_dir 指向独立 1G tmpfs 挂载 /var/tmp/phpupload,避免与系统 /tmp 混用;给予 1777 权限,防止 open_basedir 误杀。
  2. max_execution_time = 30s,max_input_time = 30s,保证最长 60s 内文件必被清理;容器场景再配 livenessProbe 30s 重启兜底。

三、运维层——兜底+观测

  1. Kubernetes 增加 sidecar 容器,每 5 分钟执行 find /var/tmp/phpupload -type f -mtime +3 -delete,配合 systemd timer 或 crontab 做双保险。
  2. Prometheus 采集目录文件数,>5k 即飞书告警;同时监控 inode 使用率 >80% 立即扩容或手动清理。
  3. 每月做一次“红蓝演练”:注入 10G 临时文件,验证 sidecar、告警、磁盘只读熔断链路的有效性,产出报告归档。

四、合规层——敏感数据不落地

  1. 含用户隐私的文件,先通过 KMS 加密再转存阿里云 OSS 私有桶,成功后立刻 unlink,并记录审计日志(who、when、filehash)。
  2. 等保测评时提供 /tmp 清理脚本、OSS 加密策略、审计日志截图,100% 通过。

一句话总结:让 PHP 自己的 request_shutdown 做第一责任人,运维兜底做第二责任人,监控做吹哨人,合规做守门员,四重保险保证临时文件“不过夜、不泄露、不爆炸”。

拓展思考

  1. 如果切换到 Swoole/FPM-常驻进程,生命周期变成长连接,request_shutdown 不再被调用,临时文件策略如何调整?
    答:在 onReceive 或 onRequest 回调里手动维护 SplObjectStorage,记录 $fd→tmpfile 映射,在 onClose 里统一清理;同时设置 max_request=3000,定期重启 Worker,兼顾内存泄漏与临时文件。

  2. 当集群规模达到 1k Pod,sidecar 定时 rm 会放大 inode 竞争,如何优化?
    答:改用 systemd-tmpfiles-clean.service 的 systemd 机制,或者把临时目录改成独立 EBS/云盘,打快照后整盘重建,秒级回收。

  3. 若业务允许用户“断点续传”,临时文件需要保留 24h,如何平衡“清理”与“续传”?
    答:把“未完成的切片”转移到带 TTL 的 Redis 流或 OSS 生命周期目录,/tmp 只保留当前正在写的分片,完成后即刻清理;同时给每个分片加 UUID+用户 token,防止横向越权下载残留文件。