问题背景
在一次企业项目中,客户要求能够通过系统上传 100MB 以上的工程图纸和视频文件。当我把文件上传功能开发完成后,测试时却发现:上传超过 2MB 的文件就直接报错,没有任何提示信息。
排查后发现,PHP 默认的上传限制只有 2MB。这个限制由 php.ini 中的多个参数共同控制,而且不仅要改 PHP 的配置,还要检查 Nginx/Apache 等 Web 服务器的配置。任何一个环节的限制都会导致上传失败。

本文将完整讲解 PHP 文件上传大小限制的所有相关配置参数,并提供 Nginx 和 Apache 的配套配置方案、大文件分片上传代码,以及生产环境部署检查清单。
php.ini 的六大关键参数
要支持上传大文件(以 500MB 为例),需要修改 php.ini 中的以下六个参数。它们必须同时修改,缺一不可。
参数详解
1. file_uploads
作用:开关参数,控制是否允许 HTTP 文件上传。如果设置为 Off,所有上传请求都会被 PHP 直接拒绝。
默认值:On
注意事项:即使其他参数都配置正确,只要这一项是 Off,上传就无法工作。
2. upload_max_filesize
1
| upload_max_filesize = 500M
|
作用:单个上传文件的最大大小。这是最直接的限制参数。
默认值:2M
说明:这里的 M 表示兆字节(Megabytes)。也可以使用 G 表示吉字节,如 upload_max_filesize = 1G。
3. post_max_size
作用:POST 请求体的最大大小。这个值必须大于或等于 upload_max_filesize。
默认值:8M
原理解释:文件上传是通过 HTTP POST 请求实现的,文件数据包含在 POST Body 中。如果 POST Body 的总大小超过了这个限制,即使单个文件没超过 upload_max_filesize,也会被拒绝。
重要规则:post_max_size >= upload_max_filesize。如果你的表单中还有其他字段(文本框、多选文件等),post_max_size 需要设置得更大。
4. max_execution_time
1
| max_execution_time = 1800
|
作用:PHP 脚本的最大执行时间(单位:秒)。上传大文件需要更长的时间,如果脚本在上传完成前就超时了,上传会失败。
默认值:30(30秒)
计算参考:
| 文件大小 |
网络带宽 |
上传耗时 |
建议 max_execution_time |
| 10MB |
10Mbps |
~8秒 |
60秒 |
| 100MB |
10Mbps |
~80秒 |
180秒 |
| 500MB |
10Mbps |
~400秒 |
600秒 |
| 500MB |
100Mbps |
~40秒 |
180秒 |
作用:PHP 接收输入数据(POST、GET、PUT 等)的最大时间限制。与 max_execution_time 不同,这个参数专门限制接收请求数据的阶段。
默认值:60(60秒)
说明:对于大文件上传,接收数据的时间可能很长,这个参数必须设置得足够大。通常建议和 max_execution_time 保持一致。
6. memory_limit
作用:单个 PHP 进程可使用的最大内存。在处理上传文件时,PHP 可能需要将文件暂存到内存中。
默认值:128M
注意事项:memory_limit 应该大于 post_max_size,否则当 PHP 尝试处理大 POST 数据时可能触发内存溢出。
推荐比例:
1
| memory_limit > post_max_size >= upload_max_filesize
|
完整配置示例
将以下内容添加到 php.ini 中(如果已有对应参数,直接修改其值):
1 2 3 4 5 6 7 8 9 10
| file_uploads = On upload_max_filesize = 500M post_max_size = 500M max_execution_time = 1800 max_input_time = 1800 memory_limit = 256M
upload_tmp_dir = /tmp/php_uploads
|
修改完成后,必须重启 Web 服务器才能生效。
Nginx 配置
如果你使用 Nginx 作为反向代理或 Web 服务器,Nginx 自身也有上传大小限制,需要单独配置。
核心参数:client_max_body_size
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| server { listen 80; server_name example.com;
client_max_body_size 500m;
client_body_timeout 120s;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params;
fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; } }
|
配置生效
1 2 3 4 5
| nginx -t
nginx -s reload
|
Apache 配置
如果使用 Apache,配置方式有所不同。
修改 httpd.conf 或 .htaccess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <Directory "/var/www/html"> LimitRequestBody 524288000
php_value upload_max_filesize 500M php_value post_max_size 500M php_value max_execution_time 1800 php_value max_input_time 1800 php_value memory_limit 256M </Directory>
php_value upload_max_filesize 500M php_value post_max_size 500M php_value max_execution_time 1800 php_value max_input_time 1800 php_value memory_limit 256M
|
Apache + PHP-FPM 模式
如果使用 Apache 通过 ProxyPass 连接 PHP-FPM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <VirtualHost *:80> ServerName example.com
ProxyTimeout 1800
<Proxy "fcgi://127.0.0.1:9000"> ProxySet timeout=1800 </Proxy>
<FilesMatch "\.php$"> SetHandler "proxy:fcgi://127.0.0.1:9000" </FilesMatch> </VirtualHost>
|
PHP 端处理上传文件
基本上传代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) { $file = $_FILES['upload_file'];
if ($file['error'] !== UPLOAD_ERR_OK) { die('上传失败,错误码: ' . $file['error']); }
if ($file['size'] > 500 * 1024 * 1024) { die('文件大小不能超过 500MB'); }
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) { die('不支持的文件类型: ' . $mimeType); }
$uploadDir = '/var/www/uploads/'; $filename = uniqid() . '_' . basename($file['name']); $destination = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) { echo '上传成功: ' . $filename; } else { echo '文件移动失败'; } } ?>
|
上传错误码对照表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
$uploadErrors = [ UPLOAD_ERR_OK => '上传成功', UPLOAD_ERR_INI_SIZE => '文件大小超过 php.ini 中 upload_max_filesize 的限制', UPLOAD_ERR_FORM_SIZE => '文件大小超过 HTML 表单中 MAX_FILE_SIZE 的限制', UPLOAD_ERR_PARTIAL => '文件只有部分被上传', UPLOAD_ERR_NO_FILE => '没有文件被上传', UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹', UPLOAD_ERR_CANT_WRITE => '文件写入失败', UPLOAD_ERR_EXTENSION => 'PHP 扩展阻止了文件上传', ];
$errorCode = $_FILES['upload_file']['error']; echo $uploadErrors[$errorCode] ?? '未知错误'; ?>
|

大文件分片上传方案
对于 GB 级别的文件,直接上传很容易失败(网络中断、超时等)。分片上传是工业级解决方案。
前端 JavaScript 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>大文件分片上传</title> </head> <body> <input type="file" id="fileInput"> <button onclick="uploadFile()">开始上传</button> <div id="progress"></div>
<script> const CHUNK_SIZE = 5 * 1024 * 1024; const MAX_CONCURRENT = 3;
async function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('请选择文件'); return; }
const fileId = generateFileId(); const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
console.log(`文件名: ${file.name}, 大小: ${formatSize(file.size)}, 分片数: ${totalChunks}`);
const uploadedChunks = await getUploadedChunks(fileId);
let uploaded = uploadedChunks.length; const promises = [];
for (let i = 0; i < totalChunks; i++) { if (uploadedChunks.includes(i)) continue;
const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end);
promises.push(uploadChunk(fileId, file.name, i, totalChunks, chunk) .then(() => { uploaded++; updateProgress(uploaded, totalChunks); }) );
if (promises.length >= MAX_CONCURRENT) { await Promise.all(promises.splice(0, MAX_CONCURRENT)); } }
await Promise.all(promises);
await mergeChunks(fileId, file.name, totalChunks); document.getElementById('progress').textContent = '上传完成!'; }
async function uploadChunk(fileId, fileName, index, total, chunk) { const formData = new FormData(); formData.append('file_id', fileId); formData.append('file_name', fileName); formData.append('chunk_index', index); formData.append('total_chunks', total); formData.append('chunk', chunk);
const response = await fetch('/upload_chunk.php', { method: 'POST', body: formData });
if (!response.ok) { throw new Error(`分片 ${index} 上传失败`); } }
async function mergeChunks(fileId, fileName, totalChunks) { const response = await fetch('/merge_chunks.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_id: fileId, file_name: fileName, total_chunks: totalChunks }) }); return response.json(); }
function updateProgress(uploaded, total) { const percent = Math.round((uploaded / total) * 100); document.getElementById('progress').textContent = `上传进度: ${percent}% (${uploaded}/${total})`; }
function generateFileId() { return 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); }
function formatSize(bytes) { if (bytes < 1024) return bytes + 'B'; if (bytes < 1048576) return (bytes / 1024).toFixed(2) + 'KB'; if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + 'MB'; return (bytes / 1073741824).toFixed(2) + 'GB'; } </script> </body> </html>
|
后端 PHP 处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <?php
$uploadDir = '/tmp/chunks/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); }
$fileId = $_POST['file_id']; $chunkIndex = intval($_POST['chunk_index']); $chunkFile = $_FILES['chunk']['tmp_name'];
$chunkPath = $uploadDir . $fileId . '_' . str_pad($chunkIndex, 6, '0', STR_PAD_LEFT);
if (move_uploaded_file($chunkFile, $chunkPath)) { echo json_encode(['status' => 'ok', 'chunk' => $chunkIndex]); } else { http_response_code(500); echo json_encode(['status' => 'error', 'message' => '分片保存失败']); } ?>
<?php
$data = json_decode(file_get_contents('php://input'), true); $fileId = $data['file_id']; $fileName = $data['file_name']; $totalChunks = intval($data['total_chunks']);
$uploadDir = '/tmp/chunks/'; $finalDir = '/var/www/uploads/'; $finalPath = $finalDir . $fileName;
$finalFile = fopen($finalPath, 'wb'); if (!$finalFile) { die('无法创建目标文件'); }
for ($i = 0; $i < $totalChunks; $i++) { $chunkPath = $uploadDir . $fileId . '_' . str_pad($i, 6, '0', STR_PAD_LEFT); if (!file_exists($chunkPath)) { fclose($finalFile); die("分片 {$i} 不存在"); }
$chunkData = file_get_contents($chunkPath); fwrite($finalFile, $chunkData); unlink($chunkPath); }
fclose($finalFile); echo json_encode([ 'status' => 'ok', 'file' => $fileName, 'size' => filesize($finalPath) ]); ?>
|
踩坑经验
踩坑一:改了 php.ini 但不生效
这是最常见的问题。可能的原因有:
- 没有重启服务:修改 php.ini 后必须重启 Apache/Nginx + PHP-FPM
- 修改了错误的 php.ini:通过
phpinfo() 查看实际加载的配置文件路径
- .htaccess 或虚拟主机配置覆盖了 php.ini 的设置
踩坑二:Nginx 报 413 Request Entity Too Large
当 Nginx 的 client_max_body_size 小于 PHP 的 post_max_size 时,请求在到达 PHP 之前就被 Nginx 拦截了。
排查顺序:
- 检查 Nginx 错误日志:
tail -f /var/log/nginx/error.log
- 确认
client_max_body_size 值是否足够大
nginx -s reload 重新加载配置
踩坑三:上传成功但文件为空
可能是 upload_tmp_dir 配置的目录没有写权限:
1 2 3 4 5 6
| mkdir -p /tmp/php_uploads chmod 777 /tmp/php_uploads
upload_tmp_dir = /tmp/php_uploads
|
踩坑四:memory_limit 不足导致上传中断
即使 post_max_size 设置正确,如果 memory_limit 较小,PHP 在处理大文件时也可能触发内存溢出。确保 memory_limit > post_max_size。
生产环境部署检查清单
上线前,请逐项检查以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| [ ] 1. php.ini 配置检查 [ ] file_uploads = On [ ] upload_max_filesize = 500M [ ] post_max_size = 500M [ ] max_execution_time = 1800 [ ] max_input_time = 1800 [ ] memory_limit = 256M
[ ] 2. Web 服务器配置检查 [ ] Nginx: client_max_body_size = 500m [ ] Apache: LimitRequestBody = 524288000 [ ] FastCGI 超时设置 >= 300s
[ ] 3. 目录权限检查 [ ] upload_tmp_dir 可写 [ ] 最终上传目录可写 [ ] 目录权限 755 或 775
[ ] 4. 安全加固 [ ] 验证文件 MIME 类型(不只看扩展名) [ ] 限制可上传的文件类型白名单 [ ] 对上传文件做病毒扫描(ClamAV) [ ] 上传目录禁止执行 PHP 脚本
[ ] 5. 功能测试 [ ] 测试上传 1MB 文件 ✓ [ ] 测试上传 100MB 文件 ✓ [ ] 测试上传 500MB 文件 ✓ [ ] 测试超出限制的文件是否正常拒绝 ✓ [ ] 测试并发上传 ✓ [ ] 测试断点续传(如有) ✓
|
总结
PHP 文件上传大小限制不是由单一参数控制的,而是涉及 PHP 配置(6 个参数) + Web 服务器配置 + 目录权限 的完整链路。任何一个环节的限制都会成为瓶颈。

对于 GB 级别的大文件,推荐使用分片上传方案,它不仅能突破配置限制,还能实现断点续传和进度展示,是工业级文件上传的标准解决方案。