php - LAMP:如何为用户动态创建 .Zip 大文件,而不会出现磁盘/CPU 抖动

标签 php bash zip pipe lamp

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

但是,这样做有缺点:

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

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

    相比之下,请考虑以下 bash 代码段:
    ls -1 | zip -@ - | cat > file.zip
      # Note -@ is not supported on MacOS
    

    在这里,zip在流模式下运行,从而减少内存占用。管道有一个完整的缓冲区——当缓冲区已满时,操作系统会暂停写入程序(管道左侧的程序)。这在这里确保 zip仅在其输出可以由 cat 写入时才起作用.

    那么,最佳方法是执行相同的操作:替换 cat使用 Web 服务器进程,将 zip 文件流式传输给用户,并即时创建。与仅流式传输文件相比,这将产生很少的开销,并且具有无问题、无尖峰的资源配置文件。

    您如何在 LAMP 堆栈上实现这一目标?

    最佳答案

    您可以使用 popen() (docs)proc_open() (docs)执行 unix 命令(例如 zip 或 gzip),并将标准输出作为 php 流返回。 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++(或许多其他可用语言中的任何一种)构建这样一个组件作为独立服务器,如果您愿意进入非阻塞文件访问等的“低级和肮脏”。

    如果你想要一个非阻塞的实现,但你宁愿避免“低落和肮脏”,最简单的路径(恕我直言)是使用 nodeJS .在现有的 nodejs 版本中,您需要的所有功能都有大量支持:使用 http模块(当然)用于 http 服务器;并使用 child_process生成 tar/zip/whatever 管道的模块。

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

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

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

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

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

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

    2. 根据对 fread 文档的评论,一些警告:magic quotes可能会干扰,必须是 turned off .

    3. 设置 mb_http_output('pass') (docs)可能是个好主意。虽然 'pass'已经是默认设置,如果您的代码或配置之前已将其更改为其他内容,您可能需要明确指定它。

    4. 如果您要创建 zip(而不是 gzip),则需要使用内容类型 header :
    Content-type: application/zip
    

    或者...可以使用“application/octet-stream”代替。 (它是用于所有不同类型的二进制下载的通用内容类型):
    Content-type: application/octet-stream
    

    如果您希望提示用户下载文件并将其保存到磁盘(而不是可能让浏览器尝试将文件显示为文本),那么您将需要 content-disposition header 。 (其中文件名表示应在保存对话框中建议的名称):
    Content-disposition: attachment; filename="file.zip"
    

    还应该发送 Content-length header ,但是使用这种技术很难做到这一点,因为您事先不知道 zip 的确切大小。 是否可以设置一个 header 来指示内容是“流式传输”或长度未知?有人知道吗?

    最后,这是一个使用所有@ Benji's 的修改示例。建议(并创建一个 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) 我发现打电话flush()在处理非常大的文件和/或非常慢的网络时,在读取/echo 循环中可能会导致问题。至少,在 Apache 后面将 PHP 作为 cgi/fastcgi 运行时确实如此,并且在其他配置中运行时似乎也会出现同样的问题。当 PHP 将输出刷新到 Apache 的速度比 Apache 通过套接字实际发送它的速度快时,就会出现这个问题。对于非常大的文件(或慢速连接),这最终会导致 Apache 的内部输出缓冲区溢出。这会导致 Apache 终止 PHP 进程,这当然会导致下载挂起或过早完成,只进行了部分传输。

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

    关于php - LAMP:如何为用户动态创建 .Zip 大文件,而不会出现磁盘/CPU 抖动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4357073/

    相关文章:

    php - Laravel Spark 无法正确引入 css

    python - 初始性能的脚本语言选择

    java - 如何用 Java 密码保护压缩的 Excel 文件?

    Android 3.x+ Java ZipFile 类 - 无法从大文件中读取 ZipEntries

    php - Jquery提交和发布发布无法返回数据的表单

    javascript - 在发送数据之前验证ajax提交post方法

    bash - 如何在 cygwin bash 脚本中访问共享驱动器?

    bash - 使用 Bash 在名称中间重命名多个文件的一小部分?

    java - 如何在 Java 中创建一个 zip 文件

    javascript - 第二个下拉菜单自动更改其值