使用Flysystem处理S3大文件的性能问题及优化咨询
我之前也碰到过一模一样的问题——本来以为用流就能实现边读边传,结果大文件还是会先全加载到内存里才开始传输,折腾了好一阵才找到解决办法,下面是几个亲测有效的优化方案:
1. 确保Flysystem S3适配器启用真正的流式读取
Flysystem的AWS S3适配器默认可能没有启用流式处理,导致readStream方法实际上还是把文件拉到本地内存后再返回流。你需要在配置适配器的时候明确开启流式相关选项:
如果是Symfony的services.yaml配置:
services: Flysystem\Adapter\AwsS3v3: arguments: - '@aws.s3.client' - 'your-bucket-name' - '' # 文件前缀(可选) - { stream_read: true } # 启用流式读取 - true # 是否使用路径式端点(根据你的S3配置调整)
如果是手动创建S3客户端,要在HTTP选项里开启流:
$s3Client = new Aws\S3\S3Client([ 'version' => 'latest', 'region' => 'your-region', 'credentials' => [ 'key' => 'your-access-key', 'secret' => 'your-secret-key', ], 'http' => [ 'stream' => true, // 强制使用流式HTTP请求 ], ]);
2. 优化StreamedResponse的实现
你当前的代码里有两个可能导致预加载的问题:
- 用
fstat($downloadableFileStream)['size']获取文件大小,S3的流对象并不支持fstat,这会导致PHP被迫读取整个流来计算大小; fpassthru虽然能输出流,但缓冲区设置可能导致内容积累后才传输。
改成下面的写法:
// 直接从Flysystem获取文件元数据,不用读取流 $fileKey = $document->getDocument(); $downloadableFileStream = $this->get("filesystem")->readStream($fileKey); $mimeType = $this->get("filesystem")->getMimetype($fileKey); $filename = $fileKey; // 可以根据需要重命名 $fileSize = $this->get("filesystem")->getSize($fileKey); // 清理输出缓冲区 if (ob_get_level()) { ob_end_clean(); } return new StreamedResponse(function () use ($downloadableFileStream) { $chunkSize = 1024 * 1024; // 每次读取1MB分块 while (!feof($downloadableFileStream)) { echo fread($downloadableFileStream, $chunkSize); flush(); // 强制输出当前缓冲区内容,确保即时传输 ob_flush(); // 配合flush使用,适配不同服务器环境 } fclose($downloadableFileStream); }, 200, [ 'Content-Transfer-Encoding' => 'binary', 'Content-Type' => $mimeType, 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), 'Content-Length' => $fileSize, 'Cache-Control' => 'no-cache, no-store, must-revalidate', // 避免缓存大文件 ]);
3. 大文件上传的分块优化
对于上传,Flysystem支持自动分块上传,只要配置适配器时设置分块阈值,当文件大小超过阈值时,自动分成多个部分上传,不会占用大量内存:
services: Flysystem\Adapter\AwsS3v3: arguments: - '@aws.s3.client' - 'your-bucket-name' - '' - { multipart_upload_threshold: 5242880, // 5MB,超过则分块 multipart_upload_part_size: 10485760 // 每个分块10MB(可选) }
4. 最优方案:返回S3预签名URL(强烈推荐)
如果你的业务场景允许(比如不需要在服务器层面做额外的权限校验,或者可以通过预签名URL的过期时间控制访问),直接返回S3的预签名URL是效率最高的方式——文件直接从S3传输到客户端,完全绕过你的Symfony服务器,彻底解决内存和带宽压力:
$s3Client = $this->get('aws.s3.client'); $fileKey = $document->getDocument(); // 构建GetObject命令,指定下载时的文件名和MIME类型 $command = $s3Client->getCommand('GetObject', [ 'Bucket' => 'your-bucket-name', 'Key' => $fileKey, 'ResponseContentDisposition' => sprintf('attachment; filename="%s"', $filename), 'ResponseContentType' => $mimeType, ]); // 生成15分钟内有效的预签名URL $presignedRequest = $s3Client->createPresignedRequest($command, '+15 minutes'); $presignedUrl = $presignedRequest->getUri(); return new RedirectResponse((string)$presignedUrl);
这个方案不仅解决了大文件传输慢的问题,还能大幅降低你的服务器负载,是处理S3大文件传输的首选方式。
内容的提问来源于stack exchange,提问作者Benjamin B.
相关产品推荐
相关产品推荐

