问题背景

在一次企业项目中,客户要求能够通过系统上传 100MB 以上的工程图纸和视频文件。当我把文件上传功能开发完成后,测试时却发现:上传超过 2MB 的文件就直接报错,没有任何提示信息。

排查后发现,PHP 默认的上传限制只有 2MB。这个限制由 php.ini 中的多个参数共同控制,而且不仅要改 PHP 的配置,还要检查 Nginx/Apache 等 Web 服务器的配置。任何一个环节的限制都会导致上传失败。

文件上传限制

本文将完整讲解 PHP 文件上传大小限制的所有相关配置参数,并提供 Nginx 和 Apache 的配套配置方案、大文件分片上传代码,以及生产环境部署检查清单。


php.ini 的六大关键参数

要支持上传大文件(以 500MB 为例),需要修改 php.ini 中的以下六个参数。它们必须同时修改,缺一不可。

参数详解

1. file_uploads

1
file_uploads = On

作用:开关参数,控制是否允许 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

1
post_max_size = 500M

作用: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秒

5. max_input_time

1
max_input_time = 1800

作用:PHP 接收输入数据(POST、GET、PUT 等)的最大时间限制。与 max_execution_time 不同,这个参数专门限制接收请求数据的阶段。

默认值60(60秒)

说明:对于大文件上传,接收数据的时间可能很长,这个参数必须设置得足够大。通常建议和 max_execution_time 保持一致。

6. memory_limit

1
memory_limit = 256M

作用:单个 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
; === 大文件上传配置(支持最大 500MB) ===
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
# nginx.conf 或站点配置文件中
server {
listen 80;
server_name example.com;

# 允许最大请求体大小,必须 >= php.ini 中的 post_max_size
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 也需要设置超时
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
# 方法一:在 httpd.conf 中设置
<Directory "/var/www/html">
# 请求体大小限制(单位:字节,500MB = 524288000)
LimitRequestBody 524288000

# PHP 配置(如果 Apache 使用 mod_php)
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>

# 方法二:在 .htaccess 中设置(仅当 AllowOverride 开启时有效)
# .htaccess
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
/**
* 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; // 每片 5MB
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
// upload_chunk.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
// merge_chunks.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 但不生效

这是最常见的问题。可能的原因有:

  1. 没有重启服务:修改 php.ini 后必须重启 Apache/Nginx + PHP-FPM
  2. 修改了错误的 php.ini:通过 phpinfo() 查看实际加载的配置文件路径
1
2
3
4
<?php
phpinfo();
// 查找 "Loaded Configuration File" 一行,确认修改的是正确的文件
?>
  1. .htaccess 或虚拟主机配置覆盖了 php.ini 的设置

踩坑二:Nginx 报 413 Request Entity Too Large

当 Nginx 的 client_max_body_size 小于 PHP 的 post_max_size 时,请求在到达 PHP 之前就被 Nginx 拦截了。

排查顺序

  1. 检查 Nginx 错误日志:tail -f /var/log/nginx/error.log
  2. 确认 client_max_body_size 值是否足够大
  3. nginx -s reload 重新加载配置

踩坑三:上传成功但文件为空

可能是 upload_tmp_dir 配置的目录没有写权限

1
2
3
4
5
6
# 创建临时目录并设置权限
mkdir -p /tmp/php_uploads
chmod 777 /tmp/php_uploads

# 在 php.ini 中指定
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 可写
[ ] 最终上传目录可写
[ ] 目录权限 755775

[ ] 4. 安全加固
[ ] 验证文件 MIME 类型(不只看扩展名)
[ ] 限制可上传的文件类型白名单
[ ] 对上传文件做病毒扫描(ClamAV)
[ ] 上传目录禁止执行 PHP 脚本

[ ] 5. 功能测试
[ ] 测试上传 1MB 文件 ✓
[ ] 测试上传 100MB 文件 ✓
[ ] 测试上传 500MB 文件 ✓
[ ] 测试超出限制的文件是否正常拒绝 ✓
[ ] 测试并发上传 ✓
[ ] 测试断点续传(如有) ✓

总结

PHP 文件上传大小限制不是由单一参数控制的,而是涉及 PHP 配置(6 个参数) + Web 服务器配置 + 目录权限 的完整链路。任何一个环节的限制都会成为瓶颈。

配置链路关系图

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