Nginx与PHP相关交互参数详解

我们知道,在传统的Apache Httpd中,php是作为Apache的一个mod直接成为Apache的一部分的。好处是Apache和PHP的交互配置简单,无需过多操心。但弊端非常明显,Apache在处理不需要运行php mod的静态资源时,由于php mod已经启用了,所以仍然会加载,使得性能下降。近年来流行起来的Nginx在短短几年,市场份额便与Apache平分秋色,但Nginx与PHP交互的方式FastCGI在配置时可能会使得习惯了Apache的人略感繁琐,在这里详细分析一下。

1. 环境

  • Debian 9
  • PHP 7.0 (Apt-Installed From Debian Packages)
  • Nginx 1.14.1 (From Debian stretch-backports)
  • Nginx 1.14.2 (Deb Package From nginx.org)

2. PHP运行方式的简单说明

在传统的Apache Httpd中,php是作为Apache的一个mod直接成为Apache的一部分的。好处是Apache和PHP的交互配置简单,无需过多操心。但弊端非常明显,Apache在处理不需要运行php mod的静态资源时,由于php mod已经启用了,所以仍然会加载,使得性能下降。外加上Nginx事件驱动模型的优异性,Nginx在性能上严重优于Apache的事实毋庸置疑。Nginx大行其道绝非毫无道理。而Nginx并不支持PHP作为Nginx的一个module来运行。众所周知,Web服务器运行外部程序实时处理生成网页并展示的规范叫做CGI(通用网关接口)。按照CGI的标准,每当一个页面需要外部处理时(php即扩展名为php时),Nginx将这个页面送入PHP-CGI的监听端口,由PHP处理返回输出再由Nginx传输给访问用户,每个CGI对应一个进程,每一个Web请求PHP都必须重新解析php.ini、重新载入全部扩展并重初始化全部数据结构,执行代码,返回输出,然后退出。CGI这种交互方式显然从字面上就会让人觉得效率低下,容易发生死锁。但是Nginx支持了FastCGI标准,所谓FastCGI,就是运行了一个起Daemon作用的Master进程和若干个FastCGI Worker进程,Nginx交互时直接将PHP部分送入Master进程,由Master决定使用哪个Worker来处理代码返回输出,最终Master将输出返回给Nginx。使用FastCGI,nginx对PHP的处理效率大大提升。而PHP官方的FastCGI实现叫做PHP-FPM,在各大发行版中均可直接安装。

PHP-FPM的Master进程通常会运行在9000端口(如果使用Socket通信),或直接使用Unix文件描述符通信。后者的代价显然比前者要小。所以如果Nginx在PHP在同一台服务器上安装,我们通常选择后者。

3. Nginx官方包自带配置分析

注:Nginx官方包来自:http://nginx.org/en/linux_packages.html#stable
我们先用Nginx官方打的包来举例,如果懒得安装可以直接在github上看:https://github.com/nginx/nginx/tree/master/conf
可以看到,在Nginx主配置文件nginx.conf(https://github.com/nginx/nginx/blob/master/conf/nginx.conf),Server段中与PHP交互相关部分在:

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

通过注释可以看到,上面一段是Nginx反代Apache+PHP的时候用的,下面这段使我们真正需要分析的。

第一句location ~ \.php$很简单,~表示大小写区分的正则匹配,如果对Nginx匹配规则不清楚的推荐阅读Nginx官方文档和这篇文章。后面的.php$匹配了所有结尾是.php的URI。
这里其实有个需要注意的地方。我们在使用很多PHP框架或自己写PHP的时候,经常会使用index.php做路由,便会出现类似于index.php/foo/bar这种URI,显然,这样的URI是匹配不到.php$的,我们可以将匹配规则改为:

location ~ .*\.php(\/.*)*$

第二行root html;其实与FastCGI无关,如果往上翻翻就可以发现他并没有把root写在server段,而是写在了每个location段,这里只是指定下根目录位置。在Debian的默认配置中,root参数是直接写在server段的,所以就不需要在这里再次指定root参数了。

第三行fastcgi_pass便是制定Nginx去哪找到FastCGI服务器的Master进程。套接字连接方式不用修改。Debian下安装好php-fpm后默认就是Unix文件描述符连接方式。这里写为

fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; # Debian 9 PHP 7.0

fastcgi_pass unix:/var/run/php5-fpm.sock; # Debian 7/8 PHP 5.x

从PHP7.*开始,文件描述符会放在/var/run/php下,并会显示出小数点后一位版本号,意味着Debian 10如果带的是PHP7.3版本(99%可能性),就可以写为

fastcgi_pass unix:/var/run/php/php7.3-fpm.sock; 

第四行fastcgi_index制定了fastcgi的主页是index.php,无须解释。

第五行就很有点意思了。我们连带第六行一起说。第六行引入了一个新文件fastcgi_params,我们将fastcgi_params的内容也贴在下面:

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

可以看到,第六行引入的这个文件和第五行一样都是以fastcgi_param打头。fastcgi_param用于指定Nginx将文件传入FastCGI Server时的各种参数使用的。如第五行

fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;

说明了要传入的文件是/scripts/下的具体php文件名,这就使得FastCGI Server会去/scripts目录下找这个php文件。如果你的php文件其实不在这个目录下(比如网站根目录下),FastCGI Server其实根本找不到。所以我们必须把/scripts改成和和root参数一样的实际网站根目录。一段时间后Nginx官方也发现这个参数太不方便了,提供了一个$document_root参数指向当前网站根目录,所以我们可以改成

fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;

Nginx顺便还提供了一个包括了这个参数的新的fastcgi_params文件重命名为fastcgi.conf:https://github.com/nginx/nginx/blob/master/conf/fastcgi.conf。但由于兼容性原因和历史包袱,保留了nginx.conf的写法和fastcgi_params文件,实际上,我们大可以将第五六行

        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;

直接改成

            include        fastcgi.conf;

就可以做到开箱即用。

至于fastcgi.conf中其他的内容(也就是fastcgi_params的内容),其实不用分析,我们通过参数名和值就可以很清晰的看到他们是做什么的,这些参数可以在CGI 1.1版本规范中找到。Nginx做的就是生成出参数值赋给对应的参数。在网站根目录新建一个cgiparams.php,内容:

<pre><?php var_export($_SERVER)?></pre> 

就可以看到所有的CGI参数信息。
如图所示:

4. Debian的Nginx包配置文件组织形式

我们知道,Debian经常会重新组织上游软件的配合文件格式值其更加对用户友好更加统一。这种做法当然有利有弊,但我觉得还是利大于弊的。很多软件配置方式五花八门,Debian组织后相当清晰,很好理解。在Debian中,有3个打包好Nginx的版本,分别是nginx-light,nginx-full,nginx-extra,直接安装nginx安装的是nginx-full,他们的区别是带的module不同,具体区别见Debian Wiki的Nginx页面总体来说就是nginx-light是轻量版,足够我们使用。

$ apt install nginx-light

在/etc/nginx下认真看一下主配置文件nginx.conf后可以发现,nginx将具体每个Server段相关配置移至sites-enabled目录,并通过将每个Server段配置写在sites-available,然后软连接/删除软连接至sites-enabled的方式使得配置文件组织的更加井井有条。多说一句,这和Debian提供的Apache配置方式完全相同。Debian非常严谨,我强烈建议跟着Debian走,这样以后哪怕迁移到别的软件,你也会发现配置形式大同小异,而不用完全重新去熟悉另一整套写法。

我们去sites-available目录下找到Debian写好的默认参考配置default文件,我将php相关部分贴在下面:

    # pass PHP scripts to FastCGI server
    #
    #location ~ \.php$ {
    #    include snippets/fastcgi-php.conf;
    #
    #    # With php-fpm (or other unix sockets):
    #    fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
    #    # With php-cgi (or other tcp sockets):
    #    fastcgi_pass 127.0.0.1:9000;
    #}

可以看到,Debian将Nginx官方配置文件中最有意义最需要制定的fastcgi_pass独立了出来,其他配置全扔进了自己创建的一个snippet(翻译过来是代码片段)文件中,我们只需要取消几行注释即可。接下来我们单独把Debian提供的snippets/fastcgi-php.conf拉出来看一下。

# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+\.php)(/.+)$;

# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;

# Bypass the fact that try_files resets $fastcgi_path_info
# see: http://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;

fastcgi_index index.php;
include fastcgi.conf;

可以看到,Debian在这个文件中直接包括了fastcgi.conf这个“改良版”的fastcgi_params和fastcgi_index参数。接下来我们来讨论下这个文件的其他部分。
我们先来看下上面那张CGI参数截图,可以看到是没有PATH_INFO这个参数的。PATH_INFO是什么呢,简单来说,PHP经常会用到使用一个入口php文件进行路由,如index.php/foo/bar的URI,在这时,/foo/bar就是PATH_INFO。我上面那张截图便是在使用nginx官方配置访问index.php/foo/bar访问得到的。可以看到默认Nginx并不会把PATH_INFO的参数值传进PHP。Debian这几行代码就是完成PATH_INFO的设置的。通过Nginx的FastCGI模块官方文档可以看到,fastcgi_split_path会通过给定的正则表达式生成$fastcgi_path_info和$fastcgi_script_name。这样就可以传入PHP了。try_files就很好理解了,先去验证一下该php是否存在。同时Debian注释告诉我们Nginx有一个won't fix的bug,try_files会将$fastcgi_path_info置空。可以通过注释中的网页看到。

使用Debian配置后可以看到PATH_INFO已经有了:

以上就是Nginx和PHP-FPM的交互过程配置大概。

5. 参考文档

以上外部链接,懒得复制了!


Update 18.12.29 需要注意的是,path_info设置中的

try_files $fastcgi_script_name =404;

是有可能和一些框架和博客系统需要设置的rewrite定义冲突的。这些框架一般只有index.php一个入口,一切通过index.php路由。所以会要求配置文件中出现一旦发现这个文件没有则立即在URI前加上index.php/的配置。比如Typecho,在Nginx下推荐设置中使用rewrite这一特性在软件路由定义中实现伪静态,该推荐配置关键处如下

        if (!-e $request_filename) {
            rewrite ^(.*)$ /index.php$1 last;
        }

这样举例如果我们要访问http://xuchen.wang/index.php/1这一实际不存在的URI时,Nginx首先rewrite成http://xuchen.wang/index.php/index.php/1,再正则匹配到php部分,使得

try_files $fastcgi_script_name =404;

$fastcgi_script_name赋值为index.php/index.php从而直接返回404。所以配置使用typecho时需要像推荐配置那样直接引入fastcgi.conf而不使用debian的snippets/fastcgi-php.conf。

title: Nginx与PHP相关交互参数详解
time: 2018-12-21 00:24
tags: PHP,Nginx,Debian
category: Study

标签: PHP Nginx Debian

发表评论: