PHP 8中GridFS常见错误包括:混用openUploadStream与uploadFromStream导致上传失败;下载时未设HTTP头及流式输出致PDF/视频空白;chunkSize过小引发高CPU;删除后fs.chunks残留孤儿块;元数据与业务表不一致。

PHP 8中GridFS上传失败:openUploadStream和uploadFromFile别混用
常见错误是把流创建和文件上传逻辑搞反,比如调用openUploadStream后又传给uploadFromStream——前者返回的是一个UploadStream对象(用于写入),后者需要的是可读流(如fopen或stream_get_contents封装的源)。这会导致$bucket->uploadFromStream实际写入空内容或报错,后续下载时openDownloadStream查不到有效数据。
正确做法是:小文件(uploadFromFile;大文件或需断点续传时才用openUploadStream + write + close三步。示例:
if ($_FILES['file']['error'] === UPLOAD_ERR_OK) {
$fileId = $bucket->uploadFromFile(
$_FILES['file']['name'],
['source' => $_FILES['file']['tmp_name']]
);
}-
uploadFromFile自动处理临时文件读取、分块、元数据写入,出错会抛MongoDB\Driver\Exception\RuntimeException - 不要手动
fopen再传流,PHP 8.1+ 的uploadFromStream要求流必须支持seek和read,而$_FILES临时流不满足 - 上传后务必检查返回的
$fileId是否为ObjectId实例,避免存空值到业务文档
下载PDF/视频空白:HTTP头+流式输出缺一不可
PHP里用openDownloadStream拿到流后,直接echo stream_get_contents($stream)会OOM,且没设响应头浏览器就当文本渲染,显示为空白页。核心问题是没走流式管道,也没告诉客户端这是什么类型、多大。
必须显式设置Content-Type、Content-Length、Accept-Ranges,并用fpassthru或循环stream_copy_to_stream推送:
立即学习“PHP免费学习笔记(深入)”;
$fileId = new MongoDB\BSON\ObjectID($_GET['id']);
$fileMeta = $bucket->find(['_id' => $fileId])->toArray()[0] ?? null;
if (!$fileMeta) { http_response_code(404); exit; }
<p>header('Content-Type: ' . ($fileMeta['contentType'] ?? 'application/octet-stream'));
header('Content-Length: ' . $fileMeta['length']);
header('Accept-Ranges: bytes');
header('Content-Disposition: inline; filename="' . rawurlencode($fileMeta['filename']) . '"');</p><p>$stream = $bucket->openDownloadStream($fileId);
fpassthru($stream);-
fpassthru比stream_get_contents内存友好,它直接从资源句柄刷出,不缓存全文 - 如果要支持视频拖拽(Range请求),得自己解析
$_SERVER['HTTP_RANGE'],用openDownloadStream的start/end选项,GridFS本身不自动处理HTTP Range - 记得在
fpassthru前加ob_end_clean(),防止输出缓冲区干扰二进制流
chunkSize设256KB太小?PHP驱动下改大能明显降CPU
默认255KB分块在PHP里读100MB文件会触发近400次BSON解析和内存拷贝,top里看到php-fpm CPU飙升到90%+,但不是因为解压——decompressBufferSize对GridFS无效,它只管网络协议层压缩。
真正该调的是chunkSizeBytes。PHP驱动支持在uploadFromFile或openUploadStream时传参:
$fileId = $bucket->uploadFromFile(
'video.mp4',
[
'source' => '/tmp/video.mp4',
'chunkSizeBytes' => 4 * 1024 * 1024 // 4MB
]
);- 设成1–4MB后,chunk数量减少75%以上,BSON解析开销直降,机械硬盘随机I/O压力也小
- 别设超过8MB:PHP的
memory_limit可能不够单次读取一个chunk,尤其开启opcache时 - 改完chunkSize后,旧文件不会自动重分块,要用
mongofiles命令或脚本迁移
文件删除后fs.chunks残留:孤儿块得手动清理
$bucket->delete($fileId)只删fs.files文档和对应fs.chunks,但如果之前上传中断过,或者用mongofiles delete按名删,很容易留下fs.chunks里files_id指向不存在_id的“孤儿块”。这些块占磁盘、拖慢fs.chunks查询速度。
PHP里没法直接执行聚合删除,得用executeCommand跑原子操作:
$orphanQuery = [
'find' => 'fs.chunks',
'filter' => [
'files_id' => ['$nin' => ['$in' => []]] // 占位,实际要先查出所有有效files._id
],
'projection' => ['_id' => 1]
];
// 更稳妥的做法:先取全部有效files._id,再构造$orphanQuery的$nin数组,最后用deleteMany- 生产环境建议每周定时跑一次孤儿块清理,用
mongosh脚本比PHP更稳 - 删除前务必备份
fs.chunks,误删会导致多个文件损坏 - 如果业务允许,优先用TTL索引自动过期旧文件,比手动删更可靠
GridFS在PHP 8里最易被忽略的其实是元数据一致性——uploadFromFile成功但没捕获返回值,或delete后没清业务表里的引用字段,都会让系统进入“文件存在但找不到”或“文件已删但业务还在用ID”的撕裂状态。这类问题往往要查fs.files和业务集合的JOIN结果才能定位。



















