LAMP:如何创建.为用户即时压缩大文件,无需磁盘/CPU 抖动

2022-08-30 11:12:36

通常,Web 服务需要压缩多个大文件以供客户端下载。最明显的方法是创建一个临时zip文件,然后将其发送给用户或将其保存到磁盘并重定向(在将来的某个时候删除它)。echo

但是,以这种方式做事有缺点:

  • 密集 CPU 和磁盘抖动的初始阶段,导致...
  • 在准备存档时,用户会出现相当大的初始延迟
  • 每个请求的内存占用非常高
  • 使用大量临时磁盘空间
  • 如果用户中途取消下载,则在初始阶段使用的所有资源(CPU,内存,磁盘)都将被浪费

ZipStream-PHP这样的解决方案通过逐个文件地将数据铲入Apache来改进这一点。但是,结果仍然是内存使用率很高(文件完全加载到内存中),并且磁盘和 CPU 使用率出现大而剧烈的峰值。

相比之下,请考虑以下 bash 代码段:

ls -1 | zip -@ - | cat > file.zip
  # Note -@ is not supported on MacOS

在这里,在流模式下运行,从而降低了内存占用量。管道具有完整的缓冲区 - 当缓冲区已满时,操作系统会挂起写入程序(管道左侧的程序)。这在这里确保其工作速度与它的输出可以由 写入的速度一样快。zipzipcat

那么,最好的方法是做同样的事情:替换为Web服务器进程,将zip文件流式传输给用户,并动态创建它。与仅流式传输文件相比,这将产生很少的开销,并且将具有没有问题的,非峰值的资源配置文件。cat

如何在 LAMP 堆栈上实现这一点?


答案 1

您可以使用(docs)(docs)来执行unix命令(例如.zip或gzip),并将stdout作为php流取回。 (docs)将尽最大努力将php输出缓冲区的内容推送到浏览器。popen()proc_open()flush()

结合所有这些将为您提供所需的内容(前提是没有其他任何阻碍 - 请参阅文档页面上的警告)。flush()

(注意:不要使用 .有关详细信息,请参阅下面的更新。flush()

像下面这样的东西可以做到这一点:

<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/x-gzip');

// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to 
// control the input of the pipeline too)
//
$fp = popen('tar cf - file1 file2 file3 | gzip -c', 'r');

// pick a bufsize that makes you happy (64k may be a bit too big).
$bufsize = 65535;
$buff = '';
while( !feof($fp) ) {
   $buff = fread($fp, $bufsize);
   echo $buff;
}
pclose($fp);

你问到“其他技术”:我会说,“任何支持在请求的整个生命周期内无阻塞i / o的东西”。如果你愿意进入非阻塞文件访问的“宕机和肮脏”状态,你可以用Java或C / C++(或任何其他可用语言中的任何一种)构建这样的组件作为独立服务器。

如果你想要一个非阻塞的实现,但你宁愿避免“向下和肮脏”,最简单的路径(恕我直言)是使用nodeJS。在现有的nodejs版本中,有很多对你需要的所有功能的支持:使用http服务器的模块(当然);并使用模块生成tar/zip/任何管道。httpchild_process

最后,如果(并且仅当)您正在运行多处理器(或多核)服务器,并且您希望从 nodejs 中获得最大收益,则可以使用 Spark2 在同一端口上运行多个实例。不要为每个处理器核心运行多个 nodejs 实例。


更新(来自Benji在评论部分对此答案的出色反馈)

1. 的文档指示该函数一次只能从任何非常规文件中读取最多 8192 字节的数据。因此,8192 可能是缓冲区大小的不错选择。fread()

[编辑注] 8192几乎可以肯定是一个依赖于平台的值 - 在大多数平台上,将读取数据,直到操作系统的内部缓冲区为空,此时它将返回,允许操作系统再次异步填充缓冲区。8192 是许多流行操作系统上默认缓冲区的大小。fread()

还有其他情况可能导致 fread 返回甚至小于 8192 字节 - 例如,“远程”客户端(或进程)填充缓冲区的速度很慢 - 在大多数情况下,将按原样返回输入缓冲区的内容,而无需等待它变满。这可能意味着返回0..os_buffer_size字节。fread()

寓意是:你传递到的值应该被认为是一个“最大”大小 - 永远不要假设你已经收到了你要求的字节数(或任何其他数字)。fread()buffsize

2.根据对fread文档的评论,一些警告:魔术引号可能会干扰,必须关闭

3. 设置(文档)可能是一个好主意。虽然已经是默认设置,但如果您的代码或配置之前已将其更改为其他设置,则可能需要显式指定它。mb_http_output('pass')'pass'

4. 如果要创建 zip(而不是 gzip),则需要使用内容类型标头:

Content-type: application/zip

或。。。可以改用“应用程序/八位字节流”。(它是用于所有不同种类的二进制下载的通用内容类型):

Content-type: application/octet-stream

如果您希望系统提示用户下载文件并将其保存到磁盘(而不是让浏览器尝试将文件显示为文本),则需要内容处置标头。(其中文件名表示应在保存对话框中建议的名称):

Content-disposition: attachment; filename="file.zip"

人们还应该发送内容长度标题,但是使用这种技术很难,因为您事先不知道zip的确切大小。是否可以设置标头以指示内容是“流式传输”或长度未知?有人知道吗?


最后,这是一个修订后的示例,它使用了 @Benji 的所有建议(并创建一个 ZIP 文件而不是 TAR。GZIP 文件):

<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="file.zip"');

// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to 
// control the input of the pipeline too)
//
$fp = popen('zip -r - file1 file2 file3', 'r');

// pick a bufsize that makes you happy (8192 has been suggested).
$bufsize = 8192;
$buff = '';
while( !feof($fp) ) {
   $buff = fread($fp, $bufsize);
   echo $buff;
}
pclose($fp);

更新:(2012-11-23)我发现在处理非常大的文件和/或非常慢的网络时,在读取/回显循环中调用可能会导致问题。至少,当在Apache后面以cgi / fastcgi的形式运行PHP时,情况确实如此,并且在其他配置中运行时似乎也可能会出现同样的问题。当PHP将输出刷新到Apache的速度比Apache通过套接字实际发送它的速度更快时,问题似乎就出现了。对于非常大的文件(或慢速连接),这最终会导致Apache的内部输出缓冲区溢出。这会导致Apache终止PHP进程,这当然会导致下载挂起或过早完成,只有部分传输发生。flush()

解决方案是根本不打电话。我已经更新了上面的代码示例以反映这一点,并在答案顶部的文本中放置了一个注释。flush()


答案 2

另一个解决方案是我为Nginx编写的mod_zip模块,专门为此目的编写:

https://github.com/evanmiller/mod_zip

它非常轻巧,不会调用单独的“zip”过程或通过管道进行通信。您只需指向一个脚本,该脚本列出了要包含的文件的位置,mod_zip完成其余工作。


推荐