PHP 大文件下载不占内存:fread 分段与 X-Accel-Redirect 方案

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-16 04:20 ·3 浏览 ·0 回复

结论:PHP 下载大文件不占内存,靠的不是调大 memory_limit,而是三件事——不用整文件读取函数、用固定大小的 `fread` 循环分段输出并逐段 `flush()`、有条件就把"发送文件"这个动作交给 Nginx 的 X-Accel-Redirect。前者把内存压到几十 KB 的常量级,后者连 PHP 进程的占用都省了。

先找出内存被谁吃掉的

结论:PHP 下载耗内存,99% 是用了 `file_get_contents()` 这类一次性读入的函数,而不是下载本身有问题。

`file_get_contents()` 会把整个文件塞进一个字符串变量,一个 500MB 的附件就要 500MB 以上内存,并发几个人服务器直接 OOM。换成 `readfile()` 其实是流式输出的,不会一次性占内存,但它只负责"吐数据",你没法控制分段、断点续传和输出缓冲。

真正容易被忽略的是输出缓冲:PHP 的 `output_buffering`、`zlib.output_compression`、以及框架自带的缓冲层,会把你 `echo` 出来的数据先攒在内存里不发给浏览器。所以分段下载的第一行代码永远是清缓冲:

while (ob_get_level()) { ob_end_clean(); }
ini_set('zlib.output_compression', 'Off');

fread 分段下载的标准写法

结论:以 512KB 左右为单位循环 `fread` + `echo` + `flush()`,配合 `set_time_limit(0)` 和 `session_write_close()`,是纯 PHP 方案里最稳的组合。

分块大小建议 256KB~1MB:太小系统调用次数多,太大单次仍会占内存。循环要用 `connection_status()` 判断客户端是否断开,避免用户关掉页面后 PHP 还在傻读硬盘。

set_time_limit(0);
ignore_user_abort(true);
session_write_close();          // 关键:提前释放 session 锁

$fp = fopen($file, 'rb');
$chunk = 512 * 1024;
while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
    echo fread($fp, $chunk);
    flush();
}
fclose($fp);

`session_write_close()` 是很多人的坑点:PHP 的 session 文件默认独占锁,下载脚本跑 10 分钟,同一个用户的其他请求就全部卡住,表现成"下载时网站打不开"。

断点续传必须自己实现

结论:要支持断点续传,必须解析 `Range` 请求头,返回 206 状态码和 `Content-Range`,否则浏览器只能从头重下。

基础 header 是 `Accept-Ranges: bytes`、`Content-Length`、`Content-Disposition`。收到 `Range: bytes=1000-` 时,用 `fseek()` 定位起点,把状态码改成 206,并补上 `Content-Range: bytes 1000-4999999/5000000`,`Content-Length` 也要改成实际要发的字节数。这里千万别同时开 gzip 压缩——压缩后长度和 `Content-Length` 对不上,浏览器会报"网络错误"。

X-Accel-Redirect:让 Nginx 干活

结论:如果有 Nginx,最优解是 PHP 只做鉴权和算路径,然后发一个 `X-Accel-Redirect` 头,由 Nginx 以 `sendfile` 零拷贝方式把文件发出去,PHP 进程立刻释放。

Nginx 侧配置一个 internal 目录,`internal` 保证外部无法直接访问:

location /protected/ {
    internal;
    alias /data/attach/;
}

PHP 侧只需要:

// 前面照常做权限校验、扣积分、记录下载日志
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="big.zip"');
header('X-Accel-Redirect: /protected/big.zip');

PHP 一执行完就结束,不占用进程数,Range、断点续传、限速全部由 Nginx 处理。注意文件路径必须是 Nginx 能看到的本地路径,且不要在 PHP 里再 `echo` 任何内容。Apache 环境对应的是 `X-Sendfile`(需要 mod_xsendfile 模块)。

如果你的站点是 Clara BBS 这类带下载权限、附件定价的系统,下载前必须先过鉴权(校验用户组、是否购买),这正是"PHP 鉴权 + Nginx 发送"组合最典型的用武之地。

选型与省事的替代路径

结论:访问量不大时 fread 方案足够;有 Nginx 优先 X-Accel-Redirect;超大文件、高并发、带宽敏感的场景,直接让文件不落在自己服务器上最省心。

Clara BBS 的附件面板支持"网盘链接模式"——填链接和提取码,附件不占服务器存储,几个 GB 的素材走这条路径成本最低。至于服务器本地上传,记得后台「系统设置→上传」里"允许的扩展名"和"附件允许的扩展名"是两个独立字段,PHP 的 `upload_max_filesize`、`post_max_size` 也要相应调大,否则文件根本传不上来,后面下载优化再漂亮也没用。

一句话收尾:小站用 fread 分段 + 清缓冲 + 断点续传足够撑住;上了 Nginx 就换成 X-Accel-Redirect,把内存和进程占用一起省掉;文件大到一定程度,就别自己扛了。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-391.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~