PHP:php+nginx实时输出的问题

PHP:php+nginx实时输出的问题

今天帮朋友看一个PHP实时输出的问题,特此总结一下。测试环境为: php 5.6.28,nginx 1.10.0 。

首先我们要了解,无论我们是通过echo语句还是printf函数输出一些信息到最终在终端显示出来会,这个过程会经历好几个不同的缓冲区。例如对于 php + nginx 的环境该过程会依次经历:output_buffering, fastcgi_buffer 和 proxy_buffering 。其次我们要了解缓冲区的行为和具体的SAPI的实现有关。例如在 CLI 中会强制将php.ini中的output_buffering设置为关闭状态,即无论php的配置文件是开启了缓冲区,还是关闭了缓冲区,CLI 默认都不会使用output_buffering。除非我们在代码中显示的调用ob_start()来使用该缓冲区。CLI 同时还会强制将implicit_flush设置为开启状态。即当有任何数据写入到 CLI SAPI中的时候,CLI SPAI都会立即将该数据扔给下一级,不会做任何缓冲。而其它的 SAPI 一般都会使用缓冲,像fast_cgi SAPI 就会使用fastcgi_buffer。下面我们看一个简单的例子:


<?php
// V_1.0
echo "<br>".'Java'."<br>";
sleep(1);

echo "<br>".'PHP'."<br>";
sleep(1);

echo "<br>".'JavaScript'."<br>";
sleep(1);

echo "<br>".'C#'."<br>";
sleep(1);
在命令行中直接用 PHP执行该文件,无论我们是否设置了output_buffering结果都是每隔一秒输出一串。如果我们在代码的第一行加上ob_start();那么就会在4秒后一次性输出该4串数据。
如果这时我们还想每隔一秒输出一串,则可以在echo后面调用ob_flush()函数即可,该函数会将缓冲区的内容取出,并清空缓冲区,注意这时候内容并不会输出到客户端,我们需要调用flush()函数将这些内容输出到客户端。在 CLI 中不需要显示调用flush()函数,是因为CLI强制将implicit_flush设置为开启状态,就是在每一次输出后会自动执行 flush() 。

现在我们通过浏览器访问这个文件,如果我们用的是appache那么结果是正常的每隔一秒输出一串,但如果我们用的是nginx,结果并没有每隔一秒输出一串,而是在4秒后一次性输出该4串数据,这是什么原因呢?


<?php
// V_1.1
ob_start();
echo "<br>".'Java'."<br>";
ob_flush();
flush();
sleep(1);

echo "<br>".'PHP'."<br>";
ob_flush();
flush();
sleep(1);

echo "<br>".'JavaScript'."<br>";
ob_flush();
flush();
sleep(1);

echo "<br>".'C#'."<br>";
ob_flush();
flush();
sleep(1);

这是因为无论是appache还是nginx都有自己的缓冲区,只有脚本执行结束了,或者使用flush()函数强制刷新缓冲区时数据才会输出到客户端。但是fastcgi_buffer是强制打开的,我们无法通过flush()函数强制刷新缓冲区,即在nginx下flush()函数对fastcgi_buffer是不起作用的,但将等待输出的内容立即发送到客户端的功能还是有效的。所以我们想到的一个解决方案是:在我们真正 echo 之前,我们先将缓冲区填满不就行了。查看nginx中fastcgi_buffer的相关配置,现在fastcgi_buffer的大小是4kb,所以我们第二版的代码是:


fastcgi_buffer_size 4k;
fastcgi_buffers   8 4k;
fastcgi_busy_buffers_size 4k;
<?php
// V_1.2
ob_start();
echo str_repeat('a', 1024*4); 
echo "<br>".'Java'."<br>";
ob_flush();
flush();
sleep(1);

echo str_repeat('b', 1024*4);
echo "<br>".'PHP'."<br>";
ob_flush();
flush();
sleep(1);

echo str_repeat('c', 1024*4);
echo "<br>".'JavaScript'."<br>";
ob_flush();
flush();
sleep(1);

echo str_repeat('d', 1024*4);
echo "<br>".'C#'."<br>";
ob_flush();
flush();
sleep(1);

再次运行,发现还是没有按我们预期的来输出,为什么呢?打开chrome的调试模式,切换到network那一栏,发现传输的数据只有 387B,而我们填充的数据就有16KB,这是为什么呢?

原来nginx为了提高传输效率及速度,会开启 gzip 压缩,于是我们再次修改 nginx的配置文件来关闭 gzip 压缩, gzip off;

重启nginx,再次运行我们看到传输的数据有 16.3KB了。

这次数据虽然填充对了,但输出结果和我们预想的还是不太一样,是先输出了 aaa... 再输出 Java和bbb...... 再输出 PHP和ccc..... 再输出 JavaScript和ddd.... 最后输出 C# 。这是为什么呢?我们再来做一个实验,关闭 output_buffering,并且在代码中也不开启output_buffering:
<?php
// V_1.3
echo str_repeat('a', 1024*4);
echo "<br>".'Java'."<br>";
flush();
sleep(1);

echo str_repeat('b', 1024*4);
echo "<br>".'PHP'."<br>";
flush();
sleep(1);

echo str_repeat('c', 1024*4);
echo "<br>".'JavaScript'."<br>";
flush();
sleep(1);

echo str_repeat('d', 1024*4);
echo "<br>".'C#'."<br>";
flush();
sleep(1);

输出的顺序和上面一模一样。我们再一次调整我们的代码测试一下:

<?php
// V_1.4
echo "<br>".'Java'."<br>";
echo str_repeat('a', 1024*4);
flush();
sleep(1);

echo "<br>".'PHP'."<br>";
echo str_repeat('b', 1024*4);
flush();
sleep(1);

echo "<br>".'JavaScript'."<br>";
echo str_repeat('c', 1024*4);
flush();
sleep(1);

echo "<br>".'C#'."<br>";
echo str_repeat('d', 1024*4);
flush();
sleep(1);

注意这次的代码和上面一样,只是调整了echo语句的顺序,我们发现这次的输出结果和我们预想的一样,是先输出了Java和aaa... 再输出 PHP和bbb...... 再输出 JavaScript和ccc..... 最后输出 C# 和ddd....

再做最后一个实验,在1.2和1.3的版本中,最开头加上下面这行代码,测试一下:

//关闭 proxy_buffering, 当然你要确保你的nginx中没有使用proxy_ignore_headers来忽略X-Accel-Buffering
header('X-Accel-Buffering: no'); 

我们发现输出的结果和我们预期的一模一样。咋们做这几个实验是为了说明:当前buffer如果满了,并且还有下一层buffer时,则当前buffer会被清空,数据会转到下一层buff中,如果没有下一层buffer了,则数据会转存到我们在nginx.conf中设置的*_temp_file_*,直到脚本执行完成,或我们调用flush(),这时会将数据输出到客户端。当然如果*_temp_file_*也写满了,这时这些数据也会先输出到客户端,你可以跑一下下面这个代码来测试这个,在调用flush()之前就会有数据输出到浏览器:

<?php
// V_1.5
echo "<br>".'Java'."<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
echo "<br>";
echo str_repeat('a', 1024*4);
flush();
sleep(1);

echo "<br>".'PHP'."<br>";
echo str_repeat('b', 1024*4);
flush();
sleep(1);

echo "<br>".'JavaScript'."<br>";
echo str_repeat('c', 1024*4);
flush();
sleep(1);

echo "<br>".'C#'."<br>";
echo str_repeat('d', 1024*4);
flush();
sleep(1);

现在对于中途各个版本的代码的输出结果大家是不是明白了呢,自己画一下整个流程分析一下吧。

编辑于 2017-02-13