PHP输出缓冲区未刷新

16

我有一些脚本,在执行过程中会输出进度信息,因为它们需要长时间运行。每个脚本基本上在处理完每一行数据的循环后都会执行以下操作:

echo '.';
@ob_flush();
flush();

多年来一直正常工作,然后我在几个服务器上升级到了PHP 5.3.x和Apache 2.2.x。现在,即使我用空格填充缓冲区或设置“ob_implicit_flush(1)”,也无法在命令中显示输出。

其中一个服务器仍然显示输出,但是以块的形式出现。可能需要近5分钟,然后突然屏幕上出现一串点。对于其他服务器,除非脚本完全执行完成,否则我将看不到任何输出。

我尝试浏览php.ini和httpd.conf文件,以查看我是否能够弄清楚不同服务器之间发生了什么变化,但很明显我漏掉了某些东西。

我还尝试在受影响的脚本中在.htaccess中禁用mod_deflate,但也没有帮助(禁用mod_gzip曾经可以立即解决问题)。

请问有人能指导我正确的方向吗?无法实时监视脚本执行正在引起各种问题,但我们不能再继续使用这些旧的PHP版本了。

更奇怪的是,我尝试将一个服务器降级到PHP 5.2.17,但降级后输出缓冲区问题仍然存在。这让我怀疑与Apache处理PHP输出的方式有关,因为Apache 2仍保留在原地。


一种替代方法 - 谁真的想坐在那里看脚本运行,为什么不在后台运行它,让“用户”知道它何时完成。对于任何需要超过几秒钟的东西,这是我的策略。 - user557846
我对其中一些脚本可以这样做,但对其他脚本则不行。其中一些脚本需要在处理过程中检查输出,以确定是否需要在完成前停止脚本。其中一些脚本会处理一些相当复杂且经常变化的数据。 - eComEvo
可以在命令行中运行,这样就不会有浏览器/服务器缓冲问题与cli。 - user557846
当您想要它正常工作时,您运行的是哪个Apache版本? - light
Apache 2.2在某些配置下存在问题,我想是这样,但不确定。这个问题太久了,我已经放弃并迁移到使用Laravel命令和后台Beanstalkd队列以及配合AJAX进度条的Nginx。这些技术都没有刷新问题,并且更容易维护。 - eComEvo
5个回答

13

ob_flush()(与flush())仅刷新PHP缓冲区 - Web服务器会自己维护缓冲区。尽管听起来很奇怪,但早期刷新缓冲区实际上会降低服务器的吞吐量,因此最近的Apache版本更积极地使用缓冲区。在使用HTTP分块编码时,还存在与压缩和部分呈现相关的可怕问题。

如果要逐步添加内容到页面,则可以使用ajax或websockets逐步添加内容。


因此,我所有最新的脚本都使用这些方法。然而,我正在处理大量的旧代码,因此由于时间不足,我非常需要快速解决方法。如果我真的不得不重写其中一些代码,编码神请怜悯我:( - eComEvo
我会接受这个答案,因为它很好地解释了问题和缺点(现在我已经比之前更有经验了),但似乎有人正在寻求一个真正的方法来避免这个问题,我不想得罪任何人。不过还是要点赞 :) - eComEvo
我写这篇文章已经有一段时间了。你可能想尝试减小DeflateBufferSize(对于mod_deflate,但在发送之前仍需要填充缓冲区),并从你的php代码中使用 while (ob_get_level()) ob_end_flush(); flush(); - symcbean

7
这个问题更多与你的服务器(Apache)有关,而不是PHP版本。
一种选择是禁用输出缓冲,尽管网站的其他部分可能会受到性能影响。
在Apache上,可以通过设置php ini指令(output_buffering=off)来解决,包括在服务器配置中和.htaccess文件中。所以我在一个.htaccess文件中使用了以下内容,只为该文件禁用输出缓冲:
<Files "q.php">
    php_value output_buffering Off
</Files>

在我的静态服务器配置中,我只需要使用AllowOverride Options=php_value(或者更大的锤子,如AllowOverride All),才能允许在.htaccess文件中使用它。
在Nginx上,要禁用缓冲(在配置文件中添加“proxy_buffering off;”并重新启动Nginx)。

这个解决方案可以解决问题,应该成为被接受的答案的一部分。 - Danielzt
如果你使用的是Nginx+Apache配置,那么"proxy_buffering off;"就是关键。而在仅使用Nginx的配置中,则需要使用"fastcgi_buffering off;"。对于mod_fcgid来说也是如此。 - Marco Marsala
我在.htaccess文件中使用这个时,出现了“内部服务器错误”。 - Anton Duzenko

5
很可能原问题描述的更改是新设置使用了FastCgi (http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html), 而且默认开启缓冲。但是还有其他要检查的因素:
如果您正在使用Fcgid,则此也具有缓冲:http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html#fcgidoutputbuffersize 如果您的PHP和Apache之间的字符集不匹配-这可能会延迟处理时间。
mod_deflate和mod_gzip也会缓冲(如原始问题中所述)
因此,检查步骤如下:
1. 刷新PHP缓冲区- (如问题所述)
2. 关闭Apache缓冲- 在.htaccess中添加php_value output_buffering off 3. 禁用缓冲模块- 禁用mod_deflate和mod_gzip 4. 确保PHP和Apache之间的字符编码匹配- 在php.ini中添加default_charset = "utf-8";和在httpd.conf中添加AddDefaultCharset utf-8 5. 在FastCgi或Fcgid中禁用缓冲- 您可以通过添加-flush选项来禁用FastCgi中的缓冲。有关详细信息,请参见上面的链接。Fcgid的选项也在上面列出。
据我所知,这些是服务器上唯一的缓冲区;显然,在服务器和浏览器之间的其他设备可能也会缓冲,例如代理可能会等待提供完整输出后再将其传递。Fiddler (https://www.telerik.com/fiddler)就是这样做的,直到我想起来为止。

如果您有一个 Nginx 前端到 Apache 的配置,您应该在 nginx.conf 中添加 proxy_buffering off;。顺便说一句,这个解决方案完美地运作。仍在寻找使用 mod_proxy_fcgid.c 的解决方案。 - Marco Marsala

4

有一种可能的解决方法可以避免您编辑现有脚本或修改服务器配置来停止缓冲输出。通过使用包装脚本,您可以从提供Web请求的php脚本中以后台方式启动长时间运行的进程。然后,您可以将长时间运行的进程的输出导入到文本文件中,通过轮询可以轻松地读取该文件以查找脚本的当前进度。以下是示例:

长时间运行的进程脚本

<?php
// long_process.php
echo "I am a long running process ";
for ($i = 0; $i < 10; $i++) {
    echo ".";
    sleep(1);
}
echo " Processing complete";
?>

初始化长时间运行的进程并监控输出的脚本

<?php
    // proc_watcher.php
    $output = './output.txt';
    if ($_GET['action'] == 'start') {
        echo 'starting running long process<br>';
        $handle = popen("nohup php ./long_process.php > $output &", 'r');
        pclose($handle);
    } else {
        echo 'Progress at ' . date('H:i:s') . '<br>';
        echo file_get_contents($output);
    }
    $url = 'proc_watcher.php';
?>
<script>
    window.setTimeout(function() {
         window.location = '<?php echo $url;?>';
    }, 1000);
</script>

如果您向proc_watcher.php?action=start发送Web请求,则脚本应在后台启动长时间运行的进程,然后每秒将输出文件的内容返回给Web浏览器。
这里的诀窍是命令行nohup php ./long_process.php > ./output.txt &,它在后台运行该进程,并将输出发送到文件而不是STDOUT。

3

我尝试了所有已知的设置,包括上面列出的所有设置,但仍无法实现目标。我一直在尝试使用PHP通过HTTP_RANGE来提供分块视频文件,但一直不起作用。

在几乎绝望之际,我找到了答案:您必须输出比缓冲区大小多至少一个字节才能将其输出到浏览器中。这是最终为我工作的脚本:

<?php 
// Close open sessions
session_write_close();

// Turn off apache-level compression
@apache_setenv('no-gzip', 1);

// Turn off compression
@ini_set('zlib.output_compression', 0);

// Turn error reporting off
@ini_set('error_reporting', E_ALL & ~ E_NOTICE);

// Tell browser not to cache this
header("Cache-Control: no-cache, must-revalidate");

// close any existing buffers
while (ob_get_level()) ob_end_clean();

// Set this to whatever you like
$buffer = 8096;

for($i = 1; $i <= 8; $i++) 
{
    // Start a output buffer with specified size
    ob_start(null,$buffer,PHP_OUTPUT_HANDLER_FLUSHABLE);
    // Output exactly one byte more than that size 
    // \n == 2 bytes, so 8096-1+2 = 8097
    echo str_repeat('=', $buffer-1)."\n";
    // 0.25s nap
    usleep(250000);
    // End output buffering and flush it
    ob_end_flush();
    flush();
}

希望这对某些人有帮助!

非常感谢您发布这个!我一直在苦苦思索这个问题,本地可以运行但在多个远程服务器上却无法正常工作。我尝试了各种答案,从每篇帖子中都能找到的答案,而这是唯一有效的方法! - shiftyllama

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接