通过mod_proxy禁用Apache和PHP-FPM的输出缓冲区

时间:2015-11-10 11:27:04

标签: php apache

当PHP使用Apache作为模块时,只要PHP生成它就输出内容很好,因为您可以在PHP中禁用output_buffering并使用flush()或implicit_flush(1)。这是我以前使用的,它工作正常。

我已经遇到问题,因为我已经切换到PHP-FPM,其中我无法让Apache(2.4)输出PHP的内容直到整个脚本完成。我仍然关闭output_buffering和刷新,但这还不够。 Apache没有使用mod_gzip(无论如何都会影响PHP模块。)

Nginx有一个禁用proxy_buffering的选项,通过阅读其他人的评论修复了这个问题,但是我找不到任何在Apache中这样做的方法。

以下是如何在Apache中调用PHP:

<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php-fpm/php-fpm.sock|fcgi://localhost/"
</FilesMatch>

<Proxy fcgi://localhost/ enablereuse=on retry=0 timeout=7200 max=500 flushpackets=on>
</Proxy>

Apache文档提到了似乎需要的flushpackets(上面使用过的),但是接下来它还说它现在只适用于AJS,而不是所有代理内容所以它不会做任何事情在这种情况下。

回应足够的空格来填充缓冲区可能会有效,但这是一个混乱的解决方法,远非理想。

简而言之:有没有人知道让Apache发送PHP内容的正确方法,而不是等到脚本完成之后?

5 个答案:

答案 0 :(得分:3)

重新发布我刚刚在这里发布的一个非常相似的问题的答案:How to disable buffering with apache2 and mod_proxy_fcgi?

一些注意事项,因为我在过去几个小时里试图找到这个问题的答案:

  1. 使用mod_proxy / mod_proxy_fcgi时,完全禁用输出缓冲是不可能的,但是,您仍然可以在块中传输响应。
  2. 根据我的实验,似乎在将输出刷新到浏览器之前,块必须至少为4096个字节。
  3. 可以使用mod_fastcgimod_fcgi模块禁用输出缓冲,但这些mod并不像Apache 2.4那样受欢迎/广泛使用。
  4. 如果您已启用mod_deflate并且未为虚拟主机/目录/等设置SetEnv no-gzip 1。那个流数据,然后gzip将不允许缓冲区刷新,直到请求完成。
  5. 我正在测试一些东西,看看如何最好地使用Drupal 8的新BigPipe功能来向客户端发送流请求,我在this GitHub issue中发布了更多注释。

答案 1 :(得分:2)

在我的环境(Apache 2.4,php-fpm)中,它在关闭压缩并将输出填充到output_buffering时有效,请参见脚本:

header('Content-Encoding: none;');
$padSize = ini_get('output_buffering');

for($i=0;$i<10;$i++) {
  echo str_pad("$i<br>", $padSize);
  flush();
  sleep(1);
}

答案 2 :(得分:0)

我通过重写Proxy部分(基于this answer)成功禁用了输出缓冲:

<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php-fpm/php-fpm.sock|fcgi://localhost"
</FilesMatch>

<Proxy fcgi://localhost>
    ProxySet enablereuse=on flushpackets=on
</Proxy>

答案 3 :(得分:0)

https://www.php.net/manual/en/function.fastcgi-finish-request.php是拯救我理智的原因。我尝试了各种技巧和技术来使Apache和php-fpm(7.4)在浏览器中显示长时间运行的进程的进度,包括Server-Sent Events,将进度写入文本文件并使用xhr对其进行轮询, flush()像疯了似的,等等。直到我做了这样的事情(在我的MVC动作控制器中),一切都没有起作用

public function longRunningProcessAction()
{

    $path = \realpath('./data/progress.sqlite');
    $db = new \PDO("sqlite:$path");
    $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    $stmt = $db->prepare("UPDATE progress SET status = :status");
    $stmt->execute([':status' => "starting"]);
    header("content-type: application/json");
    echo json_encode(['status'=>'started']);
    // this here is critical ...
    session_write_close();
    fastcgi_finish_request();
    // otherwise it will NOT work
    for ($i = 0; $i <= 150; $i++) {
        usleep(250*1000);
        $stmt->execute([':status' => "$i of 150"]);
        // this also works
        file_put_contents('./data/progress.txt',"$i of 150");
    }
    $stmt->execute([':status' => "done"]);        
}
// and...
public function progressAction()
{
    $path = \realpath('./data/progress.sqlite');
    $db = new \PDO("sqlite:$path");
    $query = 'SELECT status FROM progress';
    $stmt = $db->query($query);
    // and this is working as well..
    $text = file_get_contents('./data/progress.txt');
    return new JsonModel(['status'=>$stmt->fetchColumn(),'text'=>$text]);
}

然后是一些Javascript(jQuery)

    var check_progress = function() {
        $.get("/my/job/progress").then(r=>{
        $("#progress").text(r.status);
        if (r.status === "done") { return; }
        window.setTimeout(check_progress,300);
    });

    $.post("/long/running/process",data).then(check_progress);

Voilà!

答案 4 :(得分:-2)

使用Apache 2.4 mod_proxy进行PHP FPM的工作:

  • 在PHP脚本的开头调用ob_end_clean()
  • 调用flush()至少21次以刷新输出而不是调用一次;总是在调用flush()
  • 之间发送至少一个字符

使用没有ob_start()的ob_end_clean()对我没有意义,但它似乎有所帮助 - 它返回true(=成功!)