网站上传大文件失败怎么办?Nginx、PHP与应用层限制排查

"上传 5MB 的图片没问题,一传 50MB 的视频就失败"——这是企业站、CMS、在线教育、OA 系统里非常典型的问题。它麻烦的地方在于:一次上传要穿过浏览器、Nginx、PHP(或后端应用)、框架、甚至对象存储等多道关卡,每一道都有自己独立的体积和超时限制,任何一层设小了,都会卡住或直接报错。而且不同层报出来的现象还不一样,有的直接 413,有的转到一半 502,有的页面空转没反应。

这篇文章把大文件上传的整条链路拆开:先讲清请求要经过哪些层、各层有哪些限制参数,再用一张对照表把参数和常见默认值列全,然后分层给出 Nginx、PHP-FPM、后端应用、前端表单的配置改法与真实报错定位方法,最后讲分片上传这类更适合大文件的方案。读完你能准确定位"到底是哪一层拦住了",并按量级把参数调对,而不是把每个参数都调到很大了事。

先记住一条排查铁律:上传失败要看"在哪一步失败"。如果请求刚发出就被拒(还没到 100%),多半是前置的体积限制(Nginx / PHP 的 post_max_size);如果进度走到 100% 才失败,多半是应用处理、超时或落盘环节的问题。

一、大文件上传要穿过几道关卡

一次普通的表单上传,请求的旅程大致是这样:

浏览器表单 → [Nginx: client_max_body_size] → [PHP-FPM / 后端应用: post_max_size / maxRequestSize]
          → [应用框架: 各部分 multipart 限制] → [临时目录/磁盘] → [业务逻辑/对象存储]

每一层的职责不同:

  • 前端/浏览器:表单的 enctype 必须是 multipart/form-data,否则文件根本传不上去;部分场景还有 fetch/XHR 的超时与进度控制。
  • Nginx:作为反向代理或 web server,用 client_max_body_size 限制单个请求体大小,还会读超时。
  • PHP/后端运行时:PHP 有 upload_max_filesize、post_max_size、memory_limit、max_execution_time、max_input_time 一串限制;Java 的 Spring 有 max-file-size、max-request-size;.NET 有 MaxRequestBodySize。
  • 框架/业务层:很多框架再叠一层文件大小校验。
  • 存储层:临时目录空间、磁盘配额、对象存储的单片大小限制。

排查的核心思路就是:顺着链路逐层确认限制值,找到第一个"比你的文件小"的参数。

二、各层限制参数对照表

先把各层的关键参数和常见默认值列出来,对照自己的环境逐个核对。下表默认值是常见经验值,不同版本和发行版可能不同,以实际配置为准。

层级 参数 常见默认 作用
Nginx client_max_body_size 1m 请求体(含文件)最大字节数
Nginx client_body_timeout 60s 读请求体的超时
Nginx client_body_buffer_size 8k/16k 请求体缓冲,超出写入临时文件
PHP upload_max_filesize 2M 单个上传文件上限
PHP post_max_size 8M 整个 POST 请求体上限(须 ≥ 上者)
PHP memory_limit 128M 脚本可用内存
PHP max_execution_time 30s 脚本执行超时
PHP max_input_time -1/60s 解析输入(含上传)超时
PHP-FPM request_terminate_timeout 0 单请求最长执行时间
Spring Boot spring.servlet.multipart.max-file-size 1MB 单文件上限
Spring Boot spring.servlet.multipart.max-request-size 10MB 整个请求上限
ASP.NET Core MaxRequestBodySize 30M 请求体上限

关键关系:PHP 里 post_max_size 必须大于等于 upload_max_filesize,否则单文件限制再大也没用,因为整个 POST 被 post_max_size 先卡住了。这是最常见的配置错误。

三、Nginx 层配置

Nginx 侧最核心的是 client_max_body_size。它可以在 http、server、location 三个层级设置,范围小的覆盖范围大的。

# 放在 server 或 location 里(推荐按需设置,不要全局放太大)
server {
    listen 80;
    server_name upload.example.com;

    # 允许最大 500MB 请求体
    client_max_body_size 500m;

    # 大文件上传耗时长,读请求体超时也要放宽
    client_body_timeout 300s;

    location /upload {
        proxy_pass http://127.0.0.1:8080;
        client_max_body_size 500m;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        proxy_request_buffering off;   # 大文件直传后端,减少磁盘中转
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

几个要点:

  • 值支持单位后缀:k/K、m/M、g/G,例如 500m、2g;0 表示不限制(不推荐)。
  • Nginx 超限直接返回 413,通常连后端都不会打到,表现为请求"秒失败"。
  • proxy_request_buffering off 让 Nginx 边收边转发,避免把整个大文件先缓存到本地磁盘再转发,能省临时空间、降低首字节延迟。

改完校验并重载:

nginx -t && nginx -s reload

风险提示:client_body_buffer_size 相关的临时目录由 client_body_temp_path 指定,大文件会落在那里(默认 /var/lib/nginx/body 或 /var/cache/nginx/client_temp)。若关掉缓冲直传后端,也要保证后端能承受流式接收。改端口/路径前先备份,误删临时目录可能导致正在进行的上传中断。

四、PHP 层配置

PHP 站的限制最多,php.ini 和 PHP-FPM 池配置都要改。先找到生效的 php.ini:

php --ini                 # 看 Loaded Configuration File
php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit'
# 或用 phpinfo 页面查看

修改 php.ini(路径示例):

  • RHEL/CentOS/Rocky:/etc/php.ini
  • Debian/Ubuntu:/etc/php/8.2/fpm/php.ini(版本号按实际)
; 单文件上限 500M
upload_max_filesize = 500M
; 整个 POST 上限,必须 >= upload_max_filesize
post_max_size = 512M
; 单请求内存,处理大文件常需要调大
memory_limit = 512M
; 脚本执行与输入解析超时
max_execution_time = 600
max_input_time = 600

; 上传临时目录,确保所在分区有足够空间
upload_tmp_dir = /var/lib/php/upload_tmp

PHP-FPM 池配置(www.conf)里还要放宽请求终止时间:

; RHEL: /etc/php-fpm.d/www.conf
; Debian/Ubuntu: /etc/php/8.2/fpm/pool.d/www.conf
request_terminate_timeout = 600

改完重启对应服务(注意 PHP-FPM 要重启而不是 reload):

# RHEL/CentOS/Rocky
sudo dnf install -y php-fpm 2>/dev/null   # 若未安装
sudo systemctl restart php-fpm
# Debian/Ubuntu
sudo systemctl restart php8.2-fpm

提示:max_execution_time 对 CLI 默认 0(不限制),但 Web 请求受它约束;上传大文件时如果解析阶段就超时,要看 max_input_time。

五、应用层与框架限制

后端框架往往还有一层限制,常见于 Java、.NET、Python 等。

Spring Boot(application.yml):

spring:
  servlet:
    multipart:
      max-file-size: 500MB
      max-request-size: 512MB

ASP.NET Core:

// Program.cs / Startup.cs
builder.Services.Configure(o => {
    o.MultipartBodyLengthLimit = 512L * 1024 * 1024; // 512MB
});
// Kestrel 层
builder.WebHost.ConfigureKestrel(o => {
    o.Limits.MaxRequestBodySize = 512L * 1024 * 1024;
});

Django(settings.py):

DATA_UPLOAD_MAX_MEMORY_SIZE = 512 * 1024 * 1024   # POST 数据上限
FILE_UPLOAD_MAX_MEMORY_SIZE = 20 * 1024 * 1024    # 超过则写临时文件

改完按各框架的方式重启应用。这一层若设小了,通常会抛异常(如 MaxUploadSizeExceededException),后端日志里能看到明确提示。

六、前端与浏览器侧

前端看似简单,但有两个高频坑:


  • enctype 缺失:默认是 application/x-www-form-urlencoded,文件只会传个文件名,内容丢失。这是"传上去是空的"最常见原因。
  • XHR/fetch 超时:前端设的 timeout 太短,大文件还没传完就本地超时中断,表现为进度条走到一半突然失败。
// fetch 本身无内置超时,用 AbortController 控制
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 600000);   // 10 分钟
const fd = new FormData();
fd.append('file', fileInput.files[0]);
await fetch('/upload', { method: 'POST', body: fd, signal: ctrl.signal });
clearTimeout(t);

七、按报错定位问题

不同现象对应不同层,对照下面这张表能快速收窄范围。

现象 最可能的原因 排查动作
立即 413 Request Entity Too Large Nginx client_max_body_size 太小 看 Nginx error.log,调大该值
页面直接空白/瞬间失败,无进度 PHP post_max_size 太小,POST 被丢弃 php -i 核对 post_max_size
进度到 100% 才失败 应用处理/落盘/超时问题 看后端日志、磁盘空间、超时
上传到一半 502 后端处理大文件时崩了或超时 查后端日志、proxy_read_timeout
504 Gateway Timeout 后端处理太慢,Nginx 等超时 调大 proxy_read_timeout,优化后端
上传成功但文件为空/0 字节 表单 enctype 错、临时目录不可写 检查表单、upload_tmp_dir 权限
部分用户失败、部分正常 网络、代理层、文件大小差异 按大小和网络分别复现

定位大文件问题的固定顺序:看现象 → 看 Nginx 日志(有没有 413)→ 看 PHP/后端日志(有没有超限异常)→ 核对各层参数值 → 核对磁盘与临时目录。

# 一键查看各层关键值(经验命令组合)
nginx -T | grep -i client_max_body_size
php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit|max_execution_time'
df -h /var/lib/php /tmp /var/cache/nginx
tail -n 50 /var/log/nginx/error.log | grep -i '413\|too large'

八、超大文件:分片上传更靠谱

当文件达到几百 MB 乃至 GB 级时,把 client_max_body_size 调到很大并不是好方案——单请求传输时间长、中间任何抖动都会前功尽弃、内存和临时空间压力也大。分片上传是更工程化的做法:

  • 前端把文件切成固定大小(如 5MB)的多个分片,逐个或并发上传。
  • 服务端接收分片后用唯一 ID 暂存,全部到齐再合并。
  • 支持断点续传(记录已上传的分片)、秒传(文件哈希命中已存在文件)。
# 示例:用 curl 模拟分片(实际由前端 JS 完成)
split -b 5m bigfile.zip part_
for p in part_*; do
  curl -X POST -F "chunk=@$p" -F "uploadId=abc123" https://example.com/upload/chunk
done
curl -X POST -d 'uploadId=abc123&total=20' https://example.com/upload/merge

分片方案下,每一层的大小限制只需容纳单个分片(如 5MB),client_max_body_size 不必调得很大,整体的健壮性和失败重试能力都更好。很多成熟的前端上传库都内置了这套机制。

常见问题

Q:改了 upload_max_filesize 还是不行,为什么?
八成是 post_max_size 没同步调大,或者改的 php.ini 不是实际加载的那份(用 php --ini 确认路径),亦或改完没重启 PHP-FPM。PHP 的 post_max_size 是整个 POST 的上限,必须 ≥ upload_max_filesize。

Q:Nginx 报 413,但后端日志里什么都没有,正常吗?
正常。client_max_body_size 超限时 Nginx 直接拒绝,请求不会转到后端,所以后端无感知。改大 Nginx 的该参数即可。注意它可能配在 http/server/location 任一层,以实际生效的为准(用 nginx -T 看最终值)。

Q:进度到 100% 了还失败,是哪一层的问题?
说明请求体已经完整送达,问题在处理阶段:后端把文件写入临时目录、做业务校验、或落盘时超时/失败。重点看后端日志、max_execution_time、request_terminate_timeout、磁盘空间和临时目录权限。

Q:为什么同一份文件,内网传很快、外网传就失败?
外网上传慢,容易触碰超时类限制(Nginx client_body_timeout、PHP max_input_time、前端超时)。大文件在外网建议用分片,把单次传输量降下来,并对超时留足余量。

Q:把 client_max_body_size 设成 0 是不是就万事大吉?
0 表示不限制,但这是懒办法:不限制意味着任何人都能发超大请求,容易被拖垮带宽、占满磁盘、引发 DoS。正确做法是按业务真实需要设一个合理上限(例如 500m),超大文件走分片,而不是一刀切不限制。

总结

大文件上传失败的本质是"多层限制中最小的那个生效了",排查要顺着浏览器 → Nginx → PHP/后端 → 框架 → 存储逐层比对参数,找到第一个比文件小的限制。

最常被忽略的两点:PHP 的 post_max_size 必须 ≥ upload_max_filesize;表单 enctype 必须是 multipart/form-data。

定位看现象:立即 413 是 Nginx 体积限制,瞬间失败多为 PHP post_max_size,到 100% 才失败是应用处理/超时/落盘问题,504 是后端处理超时。

参数调整要按量级和业务设置合理上限,不要一律设 0;同时记得配套调大超时(client_body_timeout、max_execution_time、proxy_read_timeout)。

几百 MB 以上的大文件优先上分片上传 + 断点续传,让每层只需容纳单个分片,健壮性和体验都更好。