LAMP:如何为用户快速创build.zip大文件,无需磁盘/ CPU抖动
Web服务通常需要压缩几个大文件供客户端下载。 最明显的方法是创build一个临时的zip文件,然后或者将其echo
给用户,或者将其保存到磁盘并redirect(将来有一段时间将其删除)。
但是,这样做有缺点:
- 密集的CPU和磁盘颠簸的初始阶段,导致…
- 在档案准备的同时,对用户的相当大的初始延迟
- 每个请求的内存占用量非常高
- 使用大量的临时磁盘空间
- 如果用户取消下载的一半,初始阶段(CPU,内存,磁盘)使用的所有资源将被浪费
像ZipStream-PHP这样的解决scheme通过将数据按文件铲入Apache文件来改善这一点。 但是,结果仍然是高内存使用率(文件全部加载到内存中),而且磁盘和CPU使用率也很高。
相比之下,请考虑下面的bash代码片段:
ls -1 | zip -@ - | cat > file.zip # Note -@ is not supported on MacOS
在这里, zip
以stream模式运行,导致内存占用less。 一个pipe道有一个整体的缓冲区 – 当缓冲区满时,操作系统暂停写入程序(pipe道左侧的程序)。 这里确保zip
运行速度与cat
输出速度一样快。
那么最好的方法就是做同样的事情:用web服务器进程replacecat
,将zip文件stream式传输给用户。 与仅传输文件相比,这会产生很小的开销,并且会有一个没有问题的,非尖锐的资源configuration文件。
你怎么能在LAMP栈上实现这个function?
您可以使用popen()
(docs)或proc_open()
(docs)来执行一个unix命令(例如.zip或gzip),然后以stdout的forms返回。 flush()
(docs)将尽其最大的努力推动PHP的输出缓冲区的内容到浏览器。
将所有这些结合在一起会给你想要的东西(假设没有其他的东西在这里获得 – 尤其是看看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 ++(或任何其他可用的语言)构build一个独立的服务器组件。
如果你想要一个非阻塞的实现,但你宁愿避免“沮丧和肮脏”,最简单的path(恕我直言)将使用nodeJS 。 在现有的nodejs发行版中,对于你需要的所有function都有很多的支持:使用http
模块(当然是)http服务器; 并使用child_process
模块来产生tar / zip /任何pipe道。
最后,如果(且仅当)您运行的是多处理器(或多核)服务器,并且您希望从nodejs得到最多,则可以使用Spark2在同一端口上运行多个实例。 每个处理器内核不要运行多个nodejs实例。
更新 (来自Benji在这个答案的意见部分的优秀反馈)
1. fread()
的文档表明,函数一次只能读取最多8192个字节的数据,而不是普通文件。 因此,8192可能是一个很好的缓冲区大小的select。
8192几乎肯定是一个平台相关的值 – 在大多数平台上, fread()
将读取数据,直到操作系统的内部缓冲区为空,此时它将返回,允许os以asynchronous方式再次填充缓冲区。 8192是许多stream行操作系统上默认缓冲区的大小。
还有其他一些情况可能导致fread返回甚至less于8192字节 – 例如,“远程”客户端(或进程)缓慢地填充缓冲区 – 在大多数情况下, fread()
将返回input的内容现在缓冲区没有等待它充满。 这可能意味着任何从0..os_buffer_size字节得到返回。
道德是:你传递给fread()
的价值作为buffsize
应该被认为是一个“最大”的大小 – 永远不要假设你已经收到你要求的字节数(或者其他任何数字)。
2.根据fread文档的评论,一些注意事项: 魔术引号可能会干扰,必须closures 。
3.设置mb_http_output('pass')
(docs)可能是一个好主意。 虽然'pass'
已经是默认设置,但是如果您的代码或configuration先前已将其更改为其他设置,则可能需要明确指定它。
4.如果你正在创build一个zip(而不是gzip),你会想要使用内容types头:
Content-type: application/zip
或者…'application / octet-stream'可以用来代替。 (这是用于所有不同种类的二进制下载的通用内容types):
Content-type: application/octet-stream
如果您希望提示用户下载文件并将其保存到磁盘(而不是让浏览器尝试将文件显示为文本),那么您将需要内容处置标题。 (其中文件名指示应在保存对话框中build议的名称):
Content-disposition: attachment; filename="file.zip"
还应该发送内容长度标题,但这是很难用这种技术,因为你事先不知道拉链的确切大小。 是否有一个标题可以设置为表示内容是“stream”还是未知的长度? 有人知道吗?
最后,这是一个修改后的例子,它使用了@ Benji的所有build议(并创build了一个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)我发现在read / echo循环中调用flush()
会在处理非常大的文件和/或非常慢的networking时导致问题。 至less,在Apache中运行PHP作为cgi / fastcgi时,情况就是这样,在其他configuration中运行时也会出现同样的问题。 当PHP刷新输出到Apache的速度比Apache实际上通过套接字发送的时候要快。 对于非常大的文件(或慢速连接),这最终会导致Apache的内部输出缓冲区溢出。 这会导致Apache杀死PHP进程,这当然会导致下载挂起,或者只是发生部分传输而提早完成。
解决scheme是不要调用flush()
。 我已经更新了上面的代码示例来反映这一点,我在答案顶部的文本中放了一个注释。
另一个解决scheme是我的mod_zip模块Nginx,专门为此编写的:
https://github.com/evanmiller/mod_zip
它非常轻便,不会调用单独的“zip”进程或通过pipe道进行通信。 您只需指向一个脚本,列出要包含的文件的位置,其余部分则由mod_zip完成。
我上周末写了这个s3蒸汽文件拉链微服务 – 可能是有用的: http : //engineroom.teamwork.com/how-to-securely-provide-a-zip-download-of-a-s3-file-bundle/
根据PHP手册 , ZIP扩展提供了zip:wrapper。
我从来没有使用它,我不知道它的内部,但从逻辑上来说,它应该能够做你想要的,假设ZIP档案可以stream式传输,我不完全确定。
至于你关于“LAMP堆栈”的问题,只要PHP 没有 configuration为缓冲输出 就不应该是一个问题。
编辑:我试图把一个概念validation,但似乎不是微不足道的。 如果你对PHP的stream没有经验,如果甚至可能的话,它可能太复杂了。
编辑(2):看看ZipStream后重读你的问题,我发现当你说(重点增加)时,这里将会是你的主要问题,
操作中的压缩应该以stream模式操作,即处理文件和以下载速率提供数据。
这部分将非常难以实现,因为我不认为PHP提供了一种方法来确定Apache的缓冲区是多么的完整。 所以,你的问题的答案是否定的,你可能无法在PHP中做到这一点。
试图实现一个dynamic生成的下载与大量不同大小的文件我遇到了这个解决scheme,但我遇到了各种内存错误,如“允许内存大小134217728字节用尽……”。
添加ob_flush();
flush();
内存错误消失。
与发送标题一起,我的最终解决scheme如下所示(只需将文件存储在不带目录结构的zip中):
<?php // Sending headers header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="download.zip"'); header('Content-Transfer-Encoding: binary'); ob_clean(); flush(); // On the fly zip creation $fp = popen('zip -0 -j -q -r - file1 file2 file3', 'r'); while (!feof($fp)) { echo fread($fp, 8192); ob_flush(); flush(); } pclose($fp);
看来,你可以通过使用fpassthru()来消除任何与输出缓冲相关的问题。 我也用-0
来节省CPU时间,因为我的数据已经很小。 我使用这个代码来提供一个完整的文件夹,即时进行压缩:
chdir($folder); $fp = popen('zip -0 -r - .', 'r'); header('Content-Type: application/octet-stream'); header('Content-disposition: attachment; filename="'.basename($folder).'.zip"'); fpassthru($fp);