CGI/FastCGI/php-cgi/php-fpm的区别

一、最早的Web服务器

最早的Web服务器简单地响应浏览器发来的HTTP静态文件请求,并将存储在服务器上的静态文件(例如: jpg、htm、html)返回给浏览器。如图是处理流程

比如我访问:http://www.example.com/index.html,那么网络服务器就会去对应目录中找到 index.html这个文件,并返回给浏览器。

二、CGI的出现

首先说明:CGI是一种协议

事物总是不断发展,网站也越来越复杂,所以出现动态技术。但是Web服务器并不能直接运行 php/asp 这样的文件,自己不能做,外包给别人吧,但是要与第三做个约定,我给你什么,然后你给我什么,就是我把请求参数发送给你,然后我接收你的处理结果再给客户端。这个约定就是 CGI协议(Common Gateway Interface),协议只是一个“规定、规则”,理论上用什么语言都能实现,比如用 vb/c/perl/php/python 来实现。

在2000年或更早的时候,CGI 比较盛行。那时,Perl 是编写 CGI 的主流语言,以至于一般的 CGI 程序(遵循 CGI 协议的程序)就是 Perl 程序(例如世界上80%的网站所采用的编程语言 php 语言刚开始的版本就是用Perl语言写的)。

CGI 是 “Common Gateway Interface” 的缩写,翻成中文叫“公共网关接口”,它是 web 服务器与外部应用程序(CGI 程序)之间传递信息的接口标准。通过 CGI 接口,web 服务器就能够获取客户端提交的信息,并转交给服务器端的 CGI 程序处理,最后返回结果给客户端。也就是说,CGI 实际上是一个接口标准。我们通常所说的 CGI 是指 CGI 程序,即实现了 CGI 接口标准的程序。只要某种语言具有标准输入、输出和环境变量,如 perl/PHP/C 等,就可以用来编写 CGI 程序。CGI 只是接口协议,根本不是什么语言。

CGI程序的工作方式

web 服务器一般只处理静态文件请求(如 jpg、htm、html),如果碰到一个动态脚本请求(如 php),web 服务器主进程,就 fork 出一个新的进程来启动 CGI 程序,也就是说将动态脚本请求交给 CGI 程序来处理。启动 CGI 程序需要一个过程,比如,读取配置文件,加载扩展等。CGI 程序启动后,就会解析动态脚本,然后将结果返回给 web 服务器,最后 web 服务器再将结果返回给客户端,刚才 fork 的进程也会随之关闭。这样,每次用户请求动态脚本,web 服务器都要重新 fork 一个新进程,去启动 CGI 程序,由 CGI 程序来处理动态脚本,处理完后进程随之关闭。毫无疑问,这种工作方式的效率是非常低下的。运行示意图如下:

CGI程序与web服务器传递数据:

CGI 程序通过标准输入(STDIN)和标准输出(STDOUT)来进行输入输出。此外 CGI 程序还通过环境变量来得到输入,操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取这些环境变量。web 服务器和 CGI 接口又另外设置了一些环境变量,用来向CGI程序传递一些重要的参 数。CGI 的 GET 方法还通过环境变量 QUERY-STRING 向 CGI 程序传递 Form 中的数据。 下面是一些常用的 CGI 环境变量:

变量名 描述
CONTENT_TYPE 这个环境变量的值指示所传递来的信息的MIME类型。目前,环境变量CONTENT_TYPE一般都是:application/x-www-form-urlencoded,他表示数据来自于HTML表单。
CONTENT_LENGTH 如果服务器与CGI程序信息的传递方式是POST,这个环境变量即使从标准输入STDIN中可以读到的有效数据的字节数。这个环境变量在读取所输入的数据时必须使用。
HTTP_COOKIE 客户机内的 COOKIE 内容。
HTTP_USER_AGENT 提供包含了版本数或其他专有数据的客户浏览器信息。
PATH_INFO 这个环境变量的值表示紧接在CGI程序名之后的其他路径信息。它常常作为CGI程序的参数出现。
QUERY_STRING 如果服务器与CGI程序信息的传递方式是GET,这个环境变量的值即是所传递的信息。这个信息经跟在CGI程序名的后面,两者中间用一个问号?分隔,多个参数用&号连接。
REMOTE_ADDR 这个环境变量的值是发送请求的客户机的IP地址,例如上面的192.168.1.67。这个值总是存在的。而且它是Web客户机需要提供给Web服务器的唯一标识,可以在CGI程序中用它来区分不同的Web客户机。
REMOTE_HOST 这个环境变量的值包含发送CGI请求的客户机的主机名。如果不支持你想查询,则无需定义此环境变量。
REQUEST_METHOD 提供脚本被调用的方法。对于使用 HTTP/1.0 协议的脚本,仅 GET 和 POST 有意义。
SCRIPT_FILENAME CGI脚本的完整路径。
SCRIPT_NAME CGI脚本的的名称。
SERVER_NAME 这是你的 WEB 服务器的主机名、别名或IP地址。
SERVER_SOFTWARE 这个环境变量的值包含了调用CGI程序的HTTP服务器的名称和版本号。例如,上面的值为Apache/2.2.14(Unix)。

web服务器内置模块

后来,出现了一种比较高效的方式:web 服务器内置模块。例如,apache 的 mod_php 模块。将 php 解释器做成模块,然后加载到 apache 服务器中。

这样,apache 服务器在启动的时候,就会同时启动 php 模块。当客户端请求 php 文件时,apache 服务器就不用再 fork 出一个新进程来启动 php 解释器,而是直接将 php 文件交给运行中的 php 模块处理。显然,这种方式下,效率会比较高。

由于在 apache 服务器启动时,才会读取 php 的配置文件,加载 php 模块,在 apache 的运行过程中 ,不会再重新读取 php 的配置文件。所以,每次我们修改了 php 的配置文件后,必须重启 apache,新的php配置文件才会生效。运行示意图如下:

FastCGI

FastCGI是一种协议,它是在CGI标准协议基础上发展出来的一个变种协议,它的主要目标是减轻 web 服务器与 CGI 程序之间交互时的负载,这样一台服务器就可以在同一时间处理更多的 web 请求。

FASTCGI 的定义相关文章:FastCGI Specificationthe FastCGI Interface

FastCGI进程管理器是遵循FastCGI协议的程序,只要你有能力就可以写出遵循 “FastCGI协议” 的 “FastCGI进程管理器”,毫无疑问,“FastCGI进程管理器” 并不是一个程序的名称,而是指遵循FastCGI协议的一类程序。

当客户端请求 Web 服务器上的动态脚本时,Web 服务器会将动态脚本通过 Unix 域套接字(Unix domain socket),或命名管道(named pipe),或 TCP 连接(TCP connection)交给 FastCGI 主进程,FastCGI 主进程根据情况,安排一个空闲的子进程来解析动态脚本,处理完成后将结果返回给 Web 服务器,Web 服务器再将结果返回给客户端。该客户端请求处理完毕后,FastCGI 子进程并不会随之关闭,而是继续等待主进程安排工作任务。由此可知,FastCGI 的工作效率是非常高的。运行示意图如下:

php-fpm

fpmFastCGI Process Manager的缩写,中文叫 “FastCGI进程管理器”,而 php-fpm 就是用于 php 语言的 FastCGI 进程管理器(前面说过,“FastCGI进程管理器”是一类程序,而 php-fpm 就属于这一类程序中的其中一个)。对于 php5.3 之前的版本来说,php-fpm 是一个第三方的补丁包,旨在将 FastCGI 进程管理整合进PHP包中。在 php5.3 之后的版本中,php-fpm 不再是第三方的包,它已经被集成到 php 的源码中了,因为 php-fpm 提供了更好的PHP进程管理方式,可以有效控制内存和进程、可以平滑重载 PHP 配置,比 spawn-fcgi 具有更多优点,所以 php-fpm 被 PHP 官方集成了。

php-cgi

PHP为什么叫PHP

PHP于1994年由Rasmus Lerdorf创建,刚刚开始是Rasmus Lerdorf为了要维护个人网页而制作的一个简单的用Perl语言编写的程序。这些工具程序用来显示 Rasmus Lerdorf 的个人履历,以及统计网页流量。后来又用C语言重新编写,包括可以访问数据库。他将这些程序和一些表单直译器整合起来,称为 PHP/FI。

而PHP/FI,是“Personal Home Page/Form Interpreter”的缩写,意思是“个人主页/表单解释器”,也就是说,PHP最初还不是一门“语言”,而是“Rasmus Lerdorf”为了维护他自己的个人主页而写的一个简单的“表单解释器”。

不过后来,PHP被重新定义为“PHP: HyperText Preprocessor”的缩写,注意不是“HyperText Preprocessor”而是“PHP: HyperText Preprocessor”,这种将名称放到定义中的写法被称作递归缩写

所以,PHP现在的定义就是一个“超文本预处理器”,用于“把php语言写的程序解释成超文本”(说白了就是把你写的php代码转换成html,当然现在的能力不止是解释成html),安装好php后,对于Linux/Mac会在安装目录下有一个 php 和一个php-cgi,对于Win则是php.exephp-cgi.exe

php-cgi与php的区别

php-cgi与php的区别(在win下就是php-cgi.exe与php.exe)在于,php/php.exe是命令模式的php解释器,而php-cgi/php-cgi.exe是支持“通用网关接口”的php解释器,而通用网关接口就是我们前面说的“CGI”(从它的名称就能看出来啦,它都标明了“-cgi”了),不过现在的php-cgi是即支持“CGI”协议,也支持“CGI协议”的改进版——“fastCGI协议”的。

举例说明php与php-cgi都是php解释器: 运行php -i是“查看php的配置信息(i是info的缩写)”,而运行php-cgi -i同样是“查看php的配置信息”,只不过php -i是以命令版的格式返回(说白了就是纯字符串,最多加上换行),而php-cgi -i返回的格式,却是html格式的,你可以用 php-cgi -i > /path/to/php-cgi.html保存成html文件再来打开,可以发现跟你在php文件中用phpinfo();函数是一样的。

php-cgi支持fastCGI协议

为什么说php-cgi既支持普通的CGI协议,也支持“fastCGI”协议呢?运行php-cgi -h输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Usage: php [-q] [-h] [-s] [-v] [-i] [-f <file>]
php <file> [args...]
-a Run interactively
-b <address:port>|<port> Bind Path for external FASTCGI Server mode
-C Do not chdir to the script's directory
-c <path>|<file> Look for php.ini file in this directory
-n No php.ini file will be used
-d foo[=bar] Define INI entry foo with value 'bar'
-e Generate extended information for debugger/profiler
-f <file> Parse <file>. Implies `-q'
-h This help
-i PHP information
-l Syntax check only (lint)
-m Show compiled in modules
-q Quiet-mode. Suppress HTTP Header output.
-s Display colour syntax highlighted source.
-v Version number
-w Display source with stripped comments and whitespace.
-z <file> Load Zend extension <file>.
-T <count> Measure execution time of script repeated <count> times.

其中有这句“Bind Path for external FASTCGI Server mode”:

1
-b <address:port>|<port> Bind Path for external FASTCGI Server mode

意思是“绑定路径以作为外部FASTCGI服务器模式来使用”,那么不绑定呢?不绑定就是作为“非FASTCGI”模式使用呗。

当然,php-cgi支持“标准CGI”接口只是我的猜测,我无法用实际的例子来解释(而且就算可以,也没人会用这种方式了),但是php-cgi支持“FASTCGI”这是绝对绝对可以确定的,因为我有实例可以证明!!

相信现在绝大部分人都是用nginx+php-fpm模式来运行网站的,我这个例子就以这个来解释!下列配置,只要配置过nginx+php-fpm的童鞋应该都很熟悉,意思就是当nginx遇到.php结尾的文件,就“调用php-fpm”来解释这个php文件,此时php-fpm相当于“服务器”,“127.0.0.1:9000”就是php-fpm服务器监听的ip和端口,而nginx相当于“客户端”:

1
2
3
4
5
6
7
8
location ~ \.php$ {
include fastcgi.conf;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}

现在我们把上述配置修改一个地方,把9000改成9001(如果你9001被占用了那就用其他未占用的端口,只要下边对应即可),然后sudo nginx -s reload重载配置,毫无疑问,现在用浏览器访问你的php文件,比如http://localhost/index.php将会出现502 Bad Gateway,因为php-fpm监听的是9000端口,现在你修改成9001它当然找不到啊。

还记得刚才前面说到的“绑定路径以作为外部FASTCGI服务器模式来使用”吗?现在我们就来使用它,进入php-cgi所在目录,运行:

1
./php-cgi -b 127.0.0.1:9001

再次刷新你的http://localhost/index.php,怎样?是不是没有502了?是不是正常了?但你要知道,现在解释你的index.php文件的是php-cgi而不再是php-fpm了,这就证明了php-cgi确实是支持“FASTCGI”协议的,为什么?因为nginx里用的参数,都是fastcgi_开头的啊,这就是“FASTCGI”协议啊。

实验结束,ctrl+C就可以关闭你刚才运行的php-cgi了,因为刚才是直接在前台运行的,然后把9001改加9000,再执行sudo nginx -s reload就恢复到用php-fpm了。

另一个用于证明php-cgi即支持“CGI”又支持“fastCGI”的例子:

由于Windows不支持默认php-fpm(因为php-fpm是基于Linux的fork()创建子进程的,而Windows不支持这个,不过可以用Cygwin模拟),而像一些集成工具一般都是直接使用php-cgi.exe代替php-fpm,比如phpStudy,我们选择用php+nginx:

然后查看phpStudy的子进程,里面就有一个CGI / FastCGI(32 bit),这个就是“php-cgi.exe”,你可以右击它→点击“属性”→点击“安全”,就能看到它的路径:

证明例子3:https://php.net/manual/en/install.fpm.php#121725,这个老外说了,php-cgi是fastCGI接口但不是fpm,所以这又证明了“php-cgi”是支持fastCGI协议的。

前面说了php-cgi与php的相同点——它们都是php解释器,只是一个只支持使用“命令行方式调用”,一个支持“通用网关接口”方式调用。

php-cgi与php-fpm的区别

那么php-cgi与php-fpm又有什么不同?其实你可以认为“php-fpm”就是“php-cgi”的改进版,前面我说了php-cgi就是一个“遵循通用网关接口的php解释器”,而php-fpm是php-cgi的改进版,说明php-fpm同样也是一个“遵循通用网关接口的php解释器”,并且这里的“通用网关接口”指的是“FASTCGI”而不是“CGI”,因为前面都已经用实例证明了“php-cgi”是支持“FASTCGI”协议的,既然php-cgi都支持,那么它的改进版——php-fpm肯定就更支持了,从它的名字“fpm”里就能看出来,“fpm”是“Fastcgi Process Manager”,意思是“支持fastCGI协议的进程管理器”,那么“php-fpm”就是用于php的“支持fastCGI协议的进程管理器”,因为“支持fastCGI协议的进程管理器”不一定只有用于php,也许还有其他的呢?

好了,既然php-fpm也是“遵循通用网关接口的php解释器”,那么有一点可以确定的是,php-fpm不会调用php-cgi也不会依赖php-cgi,因为它本身就是“php-cgi”的改进版,没有理由去调用(去依赖php-cgi),确定方法很简单,先停掉你的php-fpm,然后把php-cgi改个名或者移动到另一个目录,再启动php-fpm,看一切是否正常?答案是肯定正常的,php-fpm根本不依赖php-cgi。

前面说,“php-fpm”是“php-cgi”的改进版(当然这个“改进版”是我自己的说法,实际上php-fpm是不是在php-cgi的基础上改进的,我也不知道,但说它是改进版是没有问题的),既然是改进版,那改进了什么呢?请看官方文档:FastCGI 进程管理器(fpm)

  • 支持平滑停止/启动的高级进程管理功能;
  • 可以工作于不同的 uid/gid/chroot 环境下,并监听不同的端口和使用不同的 php.ini 配置文件(可取代 safe_mode 的设置);
  • stdout 和 stderr 日志记录;
  • 在发生意外情况的时候能够重新启动并缓存被破坏的 opcode;
  • 文件上传优化支持;
  • “慢日志” – 记录脚本(不仅记录文件名,还记录 PHP backtrace 信息,可以使用 ptrace或者类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢;
  • fastcgi_finish_request() – 特殊功能:用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等);
  • 动态/静态子进程产生;
  • 基本 SAPI 运行状态信息(类似Apache的 mod_status);
  • 基于 php.ini 的配置文件。

由前面的phpStudy我们可以看到,运行php-cgi.exe(在Mac/Linux上是php-cgi)时,只有一个进程,当网站并发数大时,这个进程很容易就“挂掉”,挂掉就再也无法处理nginx的请求了。而php-fpm是一个进程管理器,启动它时,它除了有一个“主进程(master)”外,还会创建很多“子进程”,如下图:

这些子进程,才是真正的“遵循通用网关接口的php解释器”,而主进程只不过是把请求分配给这些子进程而已,所以php-fpm才叫“php fastCGI进程管理器”,当网站并发数大时,主进程会不断把请求分配给这些子进程,从而可以同时处理高并发而不“挂掉”。

另外,子进程的多少,主进程还会“自动分配”,比如并发数大时,主进程会多创建一些子进程,用于同时处理更多的请求,而当并发数小时,则会自动关闭一些进程,从而“减少这些子进程对服务器内存的占用”,当然这个取决于php-fpm配置的进程管理方式(static方式不会自动)。

注意:网上很多人说,php-fpm是用于管理php-cgi的,这个说法对也不对,说他对,是因为在php5.4以前确实是这样的,但php5.4以后,php-fpm已被php官方收编,本身已经自带了解析php的功能,不只是做进程管理,也不再依赖php-cgi来解析php了。

php-fpm管理进程的三种方式

php-fpm的进程管理方式有:

  • static:静态方式,即子进程数是固定的,不会随着并发数的多少而自动调整子进程数,有两个缺点,1、当并发数少时,如果子进程太多会浪费内存,2、当并发数大时也不会自动增加子进程,比如“死板”,我们一般不会用这种方式。
  • dynamic:动态分配,当空闲时,会自动缩小到最少子进程数(通过pm.min_spare_servers指定),当并发数大时,会按需求增加子进程数,当然这个增加并不是无止境的,而是有最大子进程数的(通过pm.max_children指定,另外也有空闲时的最大子进程数,通过pm.max_spare_servers指定),一般我们都采用这种进程管理方式。
  • ondemand:启动php-fpm时,只有主进程,没有子进程,当有请求过来时,才会创建子进程,并发数越多创建的子进程数就越多,但有个极限值(由pm.max_children指定),空闲的进程会在pm.process_idle_timeout秒内被关闭,这种方式无法及时的“响应并处理”nginx的请求,虽然这会让php-fpm在空闲时占用内存最小,但没有必要,因为服务器不缺这点内存,这种方式一般也不会使用。

php-fpm平滑重启原理

1
kill -SIGUSR2 主进程id

因为php-fpm本身并没有类似nginx的reload之类的命令,你用man php-fpm也能看出来,确实没有,如果你有在网上看到过php-fpm reloadservice php-fpm reload之类的命令或者你使用过,它本身并不是php-fpm,而是一个shell脚本而已,它所在的位置有在/etc/init.d/php-fpm,而这个reload的本质,其实就是给php-fpm主进程发送-SIGUSR2信号,而php-fpm规定了-SIGUSR2信号为平滑重启信号(参见php-fpm信号,你造么?)。

平滑重启步骤:

  • master通过给子进程发送SIGQUIT信号的方式,平滑关闭所有的子进程
  • 如果过一段时间,有些子进程还没退出,给子进程发送SIGTERM信号,强制关闭子进程
  • 如果还没关闭,给子进程发送SIGKILL信号,强制关闭
  • 等所有的子进程退出后,master重新启动