zoco

php output buffer

2016-03-23


LNMP架构中的输出缓冲区

首先来看一下LNMP架构中涉及输出缓冲区的部分,后面会依次验证:

NneoJH.png

从图中,可以看出 Nginx层,SAPI层(PHP-FPM)和PHP内核中均有输出缓冲区,由此可见缓冲区是多么重要的一部分。

PHP-FPM 输出缓冲区

探讨下PHP-FPM 中的输出缓冲区,先来设置一个ini配置: output_buffering,把他设置为 0:

; Development Value: 4096
; Production Value: 4096
; http://php.net/output-buffering
output_buffering = 0

设置完这个参数后,便关闭了PHP内核中的默认缓冲区。 来看一段代码,因为nginx中设置的fastcgi buffer的大小是4k,而每次输出的数据都是大于4k的,因此输出不会受到nginx输出缓冲区的影响。

未使用任何flush函数

<?php
    /**
     * 默认页面
     */
    class IndexController extends AbstractController {
 
        public function process() {
 
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                sleep(1);
            }   
            exit;
        }   
    }   

运行这段代码,预期是每次输出1个数字,可是结果却是每次输出2个数字,即0,1一起输出,2,3一起输出,这是为什么呢? 由于关闭了PHP内核中的输出缓冲,也没有使用ob_start系列的函数,而且数据量也大于了nginx中的输出缓冲,所以我猜测PHP-FPM中还有一个输出缓冲区,通过查阅fpm的源代码(php-5.6.10/sapi/fpm/fpm/fastcgi.h文件),我发现在fcgi_request结构中有一个out_buf参数,大小是8k:

typedef struct _fcgi_request {
    int            listen_socket;
#ifdef _WIN32
    int            tcp;
#endif
    int            fd;
    int            id;
    int            keep;
    int            closed;
 
    int            in_len;
    int            in_pad;
 
    fcgi_header   *out_hdr;
    unsigned char *out_pos;
    unsigned char  out_buf[1024*8];
    unsigned char  reserved[sizeof(fcgi_end_request_rec)];
 
    HashTable     *env;
} fcgi_request;

于是我觉得这个参数应该就是PHP-FPM中的输出缓冲区大小。 OK,来验证一下,将上面代码中空格的数目改为8192试试,即:

   public function process() {
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 8192) . "<br />";
                sleep(1);
            }
            exit;
        }

重新请求该页面,果然,现在是每次输出1个数字了,和我预料的一样。

使用ob_flush函数:

        public function process() {
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                ob_flush();
                sleep(1);
            }
            exit;
        }

重新请求上面的页面,发现还是每次输出2个数字,说明ob_flush不能刷新PHP-FPM缓冲区。

使用flush函数:

       public function process() {
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                flush();
                sleep(1);
            }
            exit;
        }

重新请求上面的页面,每次输出1个数字,说明flush函数可以刷新PHP-FPM缓冲区。

PHP内核默认缓冲区

了解了PHP-FPM中的缓冲区后,来看看PHP内核中的输出缓冲区。 将ini配置中output_buffering设置为16384,即16k,这主要是为了避开PHP-FPM中缓冲区的影响,毕竟PHP-FPM中的缓冲区大小是硬编码的,没法修改配置,只能规避。

未使用任何flush函数

重新请求二(1)中的代码, 页面中每次输出4个数字,说明output_buffering的设置起了作用,4个数字的输出结果就是16k+。

使用ob_flush函数

重新请求二(2)中的代码,页面中每2个数字输出一次,可见output_buffering已经不起作用了,现在只有PHP-FPM中的缓冲区起作用,由此说明ob_flush函数可以刷新PHP内核中的默认输出缓冲区。

使用flush函数

重新请求二(3)中的代码,页面中每4个数字输出一次,这说明flush函数不能刷新PHP内核中默认的输出缓冲区。

同时使用flush和ob_flush函数,即:

        public function process() {
 
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                ob_flush();
                flush();
                sleep(1);
            }
            exit;

正如前面已经得到的结论,ob_flush可以刷新PHP内核中的默认缓冲区,flush可以刷新PHP-FPM中的缓冲区,那么这2个函数一起使用,就可以得到预期的效果了:每次输出1个数字。

至此,已经了解了LNMP输出缓冲区架构图中default Ob到PHP-FPM的部分了。

PHP内核用户自定义缓冲区

PHP内核中,除了默认的输出缓冲区之外,用户也可以自己定义输出缓冲区,也就是ob_系列函数了。 (1)、可以通过ob_start函数激活一个缓冲区,激活的缓存区被压入栈中(OG(handlers)),多次调用ob_start函数会生成一个缓冲区栈。 开启output_buffering配置后,PHP内核默认输出缓冲区处于OG(handlers)的栈底。 (2)、通过ob_get_contents函数可以获取OG(handlers)栈顶部缓冲区(称之为active)的内容。 (3)、通过ob_flush函数可以将active缓冲区的内容冲刷到active缓冲区的上一级缓冲区(栈顶的下一位),如果active已经处于栈底,那么冲刷active缓冲区会将其内容输出到SAPI(正如在三中所看到的)。 (4)、通过ob_end_flush函数除了将active缓冲区的内容冲刷出去外,还会将当前缓冲区从栈顶弹出。

从(3)(4)中可以看出,如果在代码中使用了嵌套的ob_start,如果还希望,调用完ob_flush和flush的组合之后,就能将PHP中缓冲区的内容输出给Nginx,那么代码中的ob_start函数的数目和ob_end_相关函数的数目应该是一致的,如下简单示例:

    public function process() {
            for($i = 0; $i < 10; ++$i) {
                ob_start();
                echo $i;
                ob_start();
                echo str_repeat(' ', 4096) . "<br />";
                ob_end_flush();
                ob_end_flush();
                ob_flush();
                flush();
                sleep(1);
            }   
            exit;
        }

函数仅用于示例,没有实际意义…. 至此,已经了解了LNMP输出缓冲区架构图中User Ob的部分。

Nginx fastcgi 缓冲区

之前都是关闭了gzip,因为gzip后,数据小于fastcgi的buffer大小,这样的话就没法达到想要的效果,但是关闭gzip就起不到压缩数据的目的了,而且在整个数据的生成过程中,已经有多个buffer了,因此可以关闭nginx的fastcgi buffer了,如下:

fastcgi_buffering off;

运行如下的一段代码:

        public function process() {
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                ob_flush();
                flush();
                sleep(1);
            }
            exit;
        }

这段代码是可以达到的效果的: 每s输出一个数字! 当然,直接关闭fastcgi_buffer是很不灵活的,因为毕竟有很多请求是不使用bigpipe模式的,还好nginx为提供了一个header头: X-Accel-Buffering,使用这个header头可以不使用buffer,如下:

   public function process() {
            header('X-Accel-Buffering: no');
            for($i = 0; $i < 10; ++$i) {
                echo $i;
                echo str_repeat(' ', 4096) . "<br />";
                ob_flush();
                flush();
                sleep(1);
            }   
            exit;
        }

这样在开启fastcgi_buffer和gzip的情况下面也可以每s输出一个数字。 在代码中始终有补全4k个空格的操作,这是因为有些浏览器接收到这么多数据的时候才会渲染,我在mac中的chrome中测试,当使用了X-Accel-Buffering头的时候,即使不补全空格,也是可以实现的效果的,但是在windows中的IE浏览器中不行。

implicit_flush配置和ob_implicit_flush函数

其中,如果将php.ini配置中的implicit_flush设置为1, 或者调用ob_implicit_flush(true),那么就不需要调用flush函数了,他们都会刷新PHP-FPM缓冲区中的内容。

本文中主要讨论了SAPI使用PHP-FPM时的输出缓冲区情况,对于别的SAPI,需要区别对待,比如CLI,他的implicit_flush硬编码为1,他的output_buffering也是设置为0。