ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP上传几MB图片为什么CPU飙到100%?从图片尺寸到异步处理的实战

PHP上传几MB图片为什么CPU飙到100%?从图片尺寸到异步处理的实战 最近维护一个 PHP 图片上传功能时遇到一个比较奇怪的问题用户上传一张普通图片文件大小只有几 MB但是服务器 CPU 很快就升到了 100%。与此同时PHP-FPM 出现大量慢请求PHP请求耗时 8 秒PHP请求耗时 15 秒PHP请求耗时 30 秒最终表现为图片上传越来越慢页面一直转圈PHP-FPM进程增加服务器CPU持续升高一开始很容易怀疑是不是图片文件太大但后来检查发现有些图片实际上只有2MB3MB5MB真正的问题却不是文件大小。而是图片的像素尺寸太大。一、真实开发场景网站有一个头像上传接口/api/upload_avatar.php用户上传avatar.jpg服务器收到图片以后需要检查文件类型创建图片资源缩放到 300 × 300保存缩略图返回图片地址最开始代码$image imagecreatefromjpeg($_FILES[‘file’][‘tmp_name’]);$thumb imagescale($image,300,300);imagejpeg($thumb,$savePath,85);普通图片测试1920 × 1080一切正常。但是某些用户上传12000 × 9000甚至20000 × 15000服务器马上开始吃 CPU 和内存。二、为什么文件只有几MB处理起来却这么重这是这次问题最容易误判的地方。JPEG 文件3MB并不代表 PHP 在内存中只需要3MB图片被 GD 处理时需要把压缩后的 JPEG 解码成像素数据。例如一张12000 × 9000的图片12000 × 9000 108,000,000 像素如果按照 RGB 三通道粗略计算108,000,000 × 3≈ 324MB实际 GD 运行过程中还会有额外的数据结构、临时缓冲区、目标图片等内存开销。所以磁盘文件大小和图片处理时的内存/CPU成本完全不是一回事。三、第一步不要先改代码先确认到底是不是图片处理导致的遇到 CPU 飙升时第一步应该定位。Linux服务器可以观察top或者htop如果看到php-fpmphp-fpmphp-fpmphp-fpm持续占用大量 CPU就继续检查 PHP 请求。可以在上传接口中记录处理时间$start microtime(true);// 图片处理代码$cost microtime(true) - $start;error_log(image process cost: ’ .round($cost, 3) .‘s’);例如日志image process cost: 0.12simage process cost: 0.15simage process cost: 11.82simage process cost: 18.42s如果只有某些图片处理时间特别长就基本可以确定问题出在图片处理阶段。四、第二步先限制图片文件大小第一道防线仍然是文件大小。例如限制$maxSize 5 * 1024 * 1024;检查if ($_FILES[‘file’][‘size’] $maxSize) {exit(‘图片不能超过5MB’);}完整一点if (!isset($_FILES[‘file’]) ||$_FILES[‘file’][‘error’] ! UPLOAD_ERR_OK) {exit(‘上传失败’);}$maxSize 5 * 1024 * 1024;if ($_FILES[‘file’][‘size’] $maxSize) {exit(‘图片不能超过5MB’);}但是这里有一个非常重要的问题只限制文件大小还不够。因为2MB的图片依然可能拥有非常大的像素尺寸。五、第三步检查图片真实尺寸PHP可以使用getimagesize()读取图片尺寸$info getimagesize($_FILES[‘file’][‘tmp_name’]);例如if ($info false) {exit(‘无效图片’);}$width $info[0];$height $info[1];然后限制$maxWidth 5000;$maxHeight 5000;if ($width $maxWidth ||$height $maxHeight) {exit(‘图片尺寸过大’);}这样5000 × 5000以内可以继续处理。而20000 × 15000直接拒绝。六、不要只限制宽高还要限制总像素还可以增加$maxPixels 25000000;然后$pixels $width * $height;if ($pixels $maxPixels) {exit(‘图片像素过大’);}例如4000 × 4000等于16,000,000 像素可以接受。而10000 × 8000等于80,000,000 像素直接拒绝。这种方式比单独限制文件大小更加可靠。七、第四步不要相信用户提交的MIME类型很多上传代码会写$type $_FILES[‘file’][‘type’];然后判断if ($type ‘image/jpeg’) {// 允许}这不是很可靠。因为这个值来自上传请求不能单独作为安全判断依据。至少应该配合finfo_file()例如$finfo new finfo(FILEINFO_MIME_TYPE);$mime $finfo-file($_FILES[‘file’][‘tmp_name’]);然后$allowed [‘image/jpeg’,‘image/png’,‘image/webp’];if (!in_array($mime, $allowed, true)) {exit(‘不支持的图片格式’);}八、第五步限制扩展名只是辅助可以检查$extension strtolower(pathinfo($_FILES[‘file’][‘name’],PATHINFO_EXTENSION));例如$allowedExt [‘jpg’,‘jpeg’,‘png’,‘webp’];if (!in_array($extension, $allowedExt, true)) {exit(‘文件格式错误’);}但不要把扩展名当成唯一的安全判断。比较合理的是扩展名MIME图片尺寸实际图片解析一起判断。九、一个比较完整的上传检查可以先统一封装function validateImageUpload(array $file): array {if ( !isset($file[error]) || $file[error] ! UPLOAD_ERR_OK ) { throw new RuntimeException( 文件上传失败 ); } $maxSize 5 * 1024 * 1024; if ($file[size] $maxSize) { throw new RuntimeException( 图片不能超过5MB ); } $tmp $file[tmp_name]; $finfo new finfo( FILEINFO_MIME_TYPE ); $mime $finfo-file($tmp); $allowed [ image/jpeg, image/png, image/webp ]; if (!in_array($mime, $allowed, true)) { throw new RuntimeException( 不支持的图片格式 ); } $info getimagesize($tmp); if ($info false) { throw new RuntimeException( 图片文件无效 ); } $width $info[0]; $height $info[1]; $maxWidth 5000; $maxHeight 5000; $maxPixels 25000000; if ( $width $maxWidth || $height $maxHeight || ($width * $height) $maxPixels ) { throw new RuntimeException( 图片尺寸过大 ); } return [ mime $mime, width $width, height $height ];}上传接口try {$info validateImageUpload( $_FILES[file] ); // 后续图片处理} catch (Throwable $e) {echo $e-getMessage();}这样图片还没有进入真正的处理阶段就已经把异常图片挡住了。十、不要在用户请求里做太重的图片处理假设网站需要原图↓缩略图↓头像↓列表图↓WebP如果全部在用户请求期间完成用户上传↓PHP接收↓读取原图↓生成缩略图↓生成头像↓转换WebP↓压缩↓返回用户就必须等待整个过程。如果图片比较大就会出现接口耗时PHP-FPM占用CPU升高因此更加合理的架构是用户上传↓快速保存原图↓立即返回↓后台任务处理↓缩略图↓WebP↓其他尺寸十一、简单项目可以先使用任务队列例如数据库增加image_tasks记录idfile_pathstatuscreated_at上传完成INSERT INTO image_tasks(file_path, status)VALUES(?, ‘pending’)然后后台定时任务pending↓取一条↓处理图片↓success这样用户请求就不需要一直等待图片处理完成。十二、图片处理还要注意内存例如imageimagecreatefromjpeg(image imagecreatefromjpeg(imageimagecreatefromjpeg(file);处理结束以后不要忘记imagedestroy($image);缩略图$thumb imagescale($image,300,300);imagejpeg($thumb,$savePath,85);imagedestroy(thumb);imagedestroy(thumb); imagedestroy(thumb);imagedestroy(image);虽然 PHP 请求结束后一般会自动回收资源但是在批量处理图片定时任务Worker长时间运行进程中主动释放资源尤其重要。十三、不要让用户控制最终保存路径上传文件时不要直接使用$path ‘/upload/’ .$_FILES[‘file’][‘name’];因为原始文件名不可直接信任。更合理$extension ‘jpg’;$fileName bin2hex(random_bytes(16)) .‘.’ .$extension;得到a82f91d7c4e83a….jpg然后$savePath $uploadDir . ‘/’ . $fileName;这样可以避免文件名冲突以及一些路径处理问题。十四、一个完整的头像上传示例?php header( Content-Type: application/json; charsetutf-8 ); try { if (!isset($_FILES[file])) { throw new RuntimeException( 请选择图片 ); } $file $_FILES[file]; if ($file[error] ! UPLOAD_ERR_OK) { throw new RuntimeException( 上传失败 ); } if ($file[size] 5 * 1024 * 1024) { throw new RuntimeException( 图片不能超过5MB ); } $tmp $file[tmp_name]; $finfo new finfo( FILEINFO_MIME_TYPE ); $mime $finfo-file($tmp); $allowed [ image/jpeg jpg, image/png png, image/webp webp ]; if (!isset($allowed[$mime])) { throw new RuntimeException( 图片格式不支持 ); } $info getimagesize($tmp); if ($info false) { throw new RuntimeException( 无效图片 ); } $width $info[0]; $height $info[1]; if ( $width 5000 || $height 5000 || $width * $height 25000000 ) { throw new RuntimeException( 图片尺寸过大 ); } $extension $allowed[$mime]; $fileName bin2hex(random_bytes(16)) . . . $extension; $uploadDir __DIR__ . /uploads; if (!is_dir($uploadDir)) { mkdir( $uploadDir, 0755, true ); } $savePath $uploadDir . / . $fileName; if (!move_uploaded_file( $tmp, $savePath )) { throw new RuntimeException( 保存图片失败 ); } echo json_encode([ code 0, msg 上传成功, data [ name $fileName, url /uploads/ . $fileName ] ], JSON_UNESCAPED_UNICODE); } catch (Throwable $e) { http_response_code(400); echo json_encode([ code 400, msg $e-getMessage() ], JSON_UNESCAPED_UNICODE); } 这个版本有一个重要特点 上传和图片重处理分开。 先验证 文件大小 MIME 图片格式 宽高 总像素 然后保存原文件。 如果后面还需要 缩略图 WebP 裁剪 压缩 多尺寸 再交给后台任务处理。 十五、最终优化后的架构 原来的方式 用户上传 ↓ PHP读取原图 ↓ GD处理 ↓ 生成多个尺寸 ↓ 转换格式 ↓ 返回结果 如果遇到大尺寸图片 CPU ↑ 内存 ↑ PHP-FPM ↑ 响应时间 ↑ 优化以后 用户上传 ↓ 检查文件大小 ↓ 检查MIME ↓ 检查图片尺寸 ↓ 保存原图 ↓ 快速返回 ↓ 后台任务 ↓ GD/ImageMagick ↓ 生成缩略图 ↓ 生成WebP 用户请求和图片重处理彻底解耦。 十六、这次问题真正的根因 最终排查下来问题并不是 “PHP上传图片太慢” 而是多个问题叠加 图片文件大小限制不足 没有限制图片像素尺寸 直接在PHP请求中处理原图 GD需要大量内存和CPU 多个用户同时上传 最终导致 CPU持续升高 ↓ PHP-FPM处理变慢 ↓ 请求排队 ↓ 接口超时 ↓ 用户认为网站卡死 十七、总结 PHP图片上传真正需要限制的不只是 文件大小 还包括 图片宽度 图片高度 总像素 图片格式 处理时间 并发处理数量 特别需要记住 一张只有2MB的JPEG也可能是一张非常“重”的图片。 因此实际项目中建议至少做 文件大小限制 MIME检测 getimagesize尺寸检测 总像素限制 随机文件名 上传目录隔离 如果还需要 缩略图 裁剪 WebP 压缩 多尺寸生成 不要全部塞进用户当前请求里。 对于图片处理量较大的项目可以进一步使用 任务队列 后台Worker 独立图片处理服务 这样即使突然出现大量图片上传也不会因为某一张超大图片直接把整个 PHP-FPM 服务拖慢。
返回列表