Apache httpd 2.4.x 使用 mod_proxy_fcgi 和 PHP-FPM 的方式

PHP-FPM 是一个简单可靠的 FastCGI 进程管理器(FastCGI Process Manager),从 PHP 5.3.3 开始就成为了 PHP 的内置管理器。Apache 官方网站也提供了配置 Apache httpd 2.4.x 使用 mod_proxy_fcgi 和 PHP-FPM 运行 php 程序的基本方法和设置运行方式的简单介绍。可是折腾了一圈之后发现实际上这个东西还挺麻烦的,所以汇总记录下查找的资料以备查阅。

为了省事,就以官方文档做模子了。基本的安装和配置已经在“更改 LAMP 执行方式为 PHP-FPM”一文中有记录。这里仅仅记录设置代理方式的问题。

使用 PHP-FPM 就意味着不用 Apache 内置的 mod_php,也就是要在 Apache 之外处理 php 程序的解释运行问题。看起来是多引入了一个额外的程序 PHP-FPM,既占 CPU 又占内存。但是这样一来,因为 Apache 可以专心处理除 php 之外的静态网页及元素,反而 httpd 进程本身占用的 CPU 和内存可以显著降低,从而从整体上降低资源消耗。

1. PHP-FPM 监听方式

官方文档提到的 PHP-FPM 监听方式(接收 Apache 转过去的处理 PHP 的请求的方式)有 2 个。这是在 PHP-FPM 的 pool 配置文件,如 /etc/php-fpm.d/www.conf 中设置的监听方式。分别如下,

  • TCP socket (IP 和 port)
    listen = 127.0.0.1:9000

    就表示使用 TCP socket 方式。

  • Unix Domain Socket (UDS)(Apache 2.4.9 及以上版本才支持此方式),通过路径指明 socket 的位置 /path/to/unix/socket,例如,
    listen = /var/run/php-fpm/php-fpm.sock

    就表示使用 UDS 方式。
    因为暂时 CentOS 7 中默认的还是 Apache 2.4.6,未曾测试这一方式,所以暂时就没有特别的记录了

2. Apache 发送 PHP 处理请求的方式

原来的 mod_php 采用 SetHandler 的方式处理 php 文件并不需要特别的设置,因为在安装 PHP 的时候会自动在 Apache 的配置文件目录写入一个 php.conf 的配置文件,里面有告诉 Apache 处理 php 需要的操作:

<FilesMatch \.php$>
     SetHandler application/x-httpd-php
</FilesMatch>

这是很方便的。因为只要 Apache 遇到 .php 类型的文件就会知道要让 mod_php 来解释运行。这种方式是在收到每个请求之后才处理的,所以可以用于全局。

而对 PHP-FPM 来说,这种简单的、全局的处理方式,SetHandler 的方式,要到 Apache 2.4.9 中才会引入,参见 Les RPM de Remi – Blog。下面也会对此举例,且会放在第一,因为这正是水景一页想要使用的方式。

进一步的,到了 Apache 2.4.10 之后,关于 mod_php 以及 PHP-FPM 的配置和冲突规避的逻辑判断,都会在默认配置里设置好,参见 Les RPM de Remi – Blog。不过逻辑判断什么的比较简单,现在水景一页都在用,就图个在 prefork 和 event 之间切换方便。关键在于 2.4.9 里引入的 SetHandler 处理方式。

目前 Apache 转发代理的方式,也就是 Apache 发送 php 处理请求给 PHP-FPM 的方式,有 3 种:

这样一来情况就变复杂了,2 x 3 的组合方式就是 6 种。也正是因为这个,如果要一一详细解释就会很麻烦,也很容易让人迷惑,所以官网上才给出了一个复杂度和灵活性都居中的解决方案作为例子(ProxyPassMatch)。

下面分别针对 SetHandler、ProxyPassMatch、ProxyPass、mod_rewrite 举例。

3. 四种处理方式举例

3.1 SetHandler

前面说了,这种处理方式的适应性最强:在 Apache 范围内部署一次之后,所有的虚拟主机 <VirtualHost > 里的 PHP 文件都会由它来处理。可谓一劳永逸,跟 mod_php 的便利程度是一样的。当然,也就没法针对每个 VirtualHost 来控制 PHP-FPM 的运行用户和资源分配了。可惜需要到 Apache 2.4.9 才能用上,当然,或者自己打补丁也行 :P

首先,设置 1 中的 IP:Port 监听方式,在 /etc/php-fpm.d/www.conf 中设置,

listen = 127.0.0.1:9000

然后,在前例的基础上,设置一个全局的,比如在 vhost.conf 中所有 <VirtualHost > 之前,或者干脆放到 php.conf 里面,

<FilesMatch \.php$>
         SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>

Ok,完成 :) 就是这么简单,所以水景一页很喜欢。

另外,如果希望采用 UDS 方式监听的话,例如,

listen = /var/run/php-fpm/php-fpm.sock

可以照下面这么做,

<Proxy "unix:/var/run/php-fpm/php-fpm.sock|fcgi://php-fpm">
    # we must declare a parameter in here (doesn't matter which) or it'll not register the proxy ahead of time
    ProxySet disablereuse=off
</Proxy>

# Redirect to the proxy
<FilesMatch \.php$>
    SetHandler proxy:fcgi://php-fpm
</FilesMatch>

但是,水景一页测试了一下,首先,Apache 启动错误,提示 ProxySet URL must be absolute! 而如果是,

unix:///var/run/php-fpm/php-fpm.sock|fcgi://php-fpm

就没有问题。其次,网页出现白板,页面源文件显示 php 没有运行而是直接显示源代码。

如果是 Apache 2.4.10,还可以这么设置,

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

这个水景一页当然就还没有测试了。

3.2 ProxyPassMatch

在前面切换 mod_php 到 PHP-FPM 一文中已经采用了这种方式。关键点在于:

  • 需要在每个 <VirtualHost > 中分别设置,即使使用相同的 pool,也需要在设置的时候修改 pool 后面的 webroot 路径;
  • 有些特殊的网站网页地址 url 与系统文件路径 dir 之间匹配的问题,简单的一条 ProxyPassMatch 可能还不够,比如那个设置 phpMyAdmin 的例子(见上文)。

还是假设采用 IP:Port 监听方式,同 3.1

然后,在每个 <VirtualHost > 里面加入,

<IfModule mpm_event_module>
    ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/path/to/webroot/$1
</IfModule>

特别注意的是,红色字体部分需要与每个 <VirtualHost > 中的 DocumentRoot 后的路径一致!

  • ProxyPassMatch
    只有满足特定正则模式的内容才会匹配并执行此规则,这里的模式是,
    ^/(.*\.php(/.*)?)$
    从网站(虚拟主机 <VirtualHost > 的根目录开始,匹配任何以 .php 结尾,或者在 .php 之后紧跟一个 / 再跟别的内容的路径。
  • ^ (caret) 和 $ (dollar)
    标志要匹配的路径的开始和结束
  • ( )
    括号里的内容可以用 $1 来表示,以方便后面引用它。
  • fcgi://127.0.0.1:9000
    通过 mod_proxy_fcgi 来转发的代理,使用 fastCGI 协议,转到 PHP-FPM 监听的端口。
    改变 IP 地址和/或端口号就可以要转到的不同的 pool。用这个可以实现服务器分流、均衡等。
  • /path/to/your/documentroot/
    非常重要!必须与虚拟主机的路径匹配,且必须是对应 php 文件在操作系统中的绝对路径。否则会找不到文件(PHP Script File)。这也是 URL 地址重写的目的所在:将 URL 地址转换成 fcgi://127.0.0.1:9000 后面紧跟着的 php 文件绝对路径
  • $1
    可以从原始请求扩展成整个请求路径的变量,这里指代前面外围 ( ) 里面匹配的那个路径(uri)

这里之所以没有官网例子中的 DirectoryIndex /index.php,是因为我们已经设置过 DirectoryIndex index.php 了。所以,如果有的服务器没有这么配置,可能要像官网示例那么配置:

ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/path/to/your/documentroot/$1
DirectoryIndex /index.php

当然,也可以用 UDS 方式,如果支持的话。这里就不再费事了。

3.3 ProxyPass

仅以采用 IP:Port 监听方式,同 3.1,来给个例子

<LocationMatch ^(.*\.php)$> 
    ProxyPass fcgi://127.0.0.1:9000/path/to/webroot/
    ProxyErrorOverride on 
</LocationMatch>

仍然需要在每个 <VirtualHost > 中分别设置。可以放在 </Directory> 之后。可以看到,这个方法与 ProxyPassMatch 是一回事儿。

简单情况下,ProxyPass 那一行最后没有那个 $1 无关紧要,ProxyPassMatch 也一样。注意是简单情况下

3.4 Mod_Rewrite

这个方法,利用 mod_rewrite 的 P 标志将地址重写并传递给 mod_proxy。本来应该是挺好的,还可以用来解决这里提到的一个严重警告。可惜:

  • mod_rewrite 页面写得很清楚,这个方法不支持持久连接(persistent connections);
  • 水景一页没办法使找到的几个例子正常工作,总是会导致 php 源代码直接展示在网页,或者网页的源文件里。

既然不好,就不浪费版面了。如果大家有兴趣,可以看下面几个例子:

  • http://serverfault.com/questions/398834/understanding-apache-2-4-mod-proxy-fcgi-and-rewriterules-in-htaccess
  • http://serverfault.com/questions/450628/apache-2-4-php-fpm-proxypassmatch
  • http://www.gossamer-threads.com/lists/apache/users/409168#409168

4. 总结

捣鼓了这么多,来个推荐吧,免得大家跟我一样走弯路。

从性能、方便程度而言,目前还是直接用 ProxyPassMatch 吧,直到 Apache 2.4.10,一次性切换到 SetHandler。这会是个比较好的路线。但是呢,考虑到 CentOS 上游 RedHat 官方的 backporting 政策,Apache 2.4.10 很有可能需要到下一次大的系统升级,比如 CentOS 8 中才会加入。

另外,关于 PHP-FPM 处理方式中的一个安全警告,值得一看。©

本文发表于水景一页。永久链接:<https://cnzhx.net/blog/apache-httpd-mod_proxy_fcgi-php-fpm/>。转载请保留此信息及相应链接。

5 条关于 “Apache httpd 2.4.x 使用 mod_proxy_fcgi 和 PHP-FPM 的方式” 的评论

  1. Apache 2.4.10 这个配置是错的,apache 手册后面的留言就写错 fgci://localhost ,应该是 fcgi://localhost。很多人直接 copy,结果都出问题了,呵呵。

    SetHandler “proxy:unix:/var/run/php-fpm/php-fpm.sock|fgci://localhost”

    • 非常感谢!
      因为一直还在 2.4.6 所以都没有机会测试,文中的设置也是直接复制自手册,都没有注意到这个拼写错误。

雁过留声,人过留名

您的电子邮箱地址不会被公开。 必填项已用 * 标注

特别提示:与当前文章主题无关的讨论相关但需要较多讨论求助信息请发布到水景一页讨论区的相应版块,谢谢您的理解与合作!请参考本站互助指南
您可以在评论中使用如下的 HTML 标记来辅助表达: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>