CGI

CGI 全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端,从网页浏览器向执行在 Web 服务器上的程序请求数据。 CGI 描述了客户端和这个程序之间传输数据的一种标准。 CGI 的一个目的是要独立于任何语言的,所以 CGI 可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。 如php,perl,tcl等。

CGI 的运行原理

  1. 客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。
  2. 服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。
  3. 再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端。

一个 GET 请求的示例如下:

CGI 运行原理示举例示意图

如图所示,本次请求的流程如下:

  1. 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
  2. 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
  3. 通过解析 HTTP 头信息,得知是 GET 请求,并且请求的是 /cgi-bin/ 目录下的 user 文件。
  4. 将 uri 里的 id=1 通过存入 QUERY_STRING 环境变量。
  5. Web 守护进程 fork 一个子进程,然后在子进程中执行 user 程序,通过环境变量获取到id。
  6. 执行完毕之后,将结果通过标准输出返回到子进程。
  7. 子进程将结果返回给客户端。

FastCGI

CGI 有一个致命的缺点,那就是每处理一个请求都需要 fork 一个全新的进程,随着 Web 的兴起,高并发越来越成为常态,这样低效的方式明显不能满足需求。就这样,FastCGI 诞生了,CGI 很快就退出了历史的舞台。FastCGI,顾名思义为更快的 CGI,它允许在一个进程内处理多个请求,而不是一个请求处理完毕就直接结束进程,性能上有了很大的提高。

FastCGI 是 Web 服务器和处理程序之间通信的一种协议, 是 CGI 的一种改进方案,FastCGI 像是一个常驻(long-lived)型的 CGI, 它可以一直执行,在请求到达时不会花费时间去 fork 一个进程来处理(这是 CGI 最为人诟病的 fork-and-execute 模式)。 正是因为他只是一个通信协议,它还支持分布式的运算,所以 FastCGI 程序可以在网站服务器以外的主机上执行,并且可以接受来自其它网站服务器的请求。

FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等。

FastCGI 工作流程

工作流程如下:

  1. FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的连接。
  2. Web 服务器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
  3. CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。
  4. CGI 解释器进程接着等待并处理来自 Web Server 的下一个连接。

FastCGI 运行原理示举例示意图

FastCGI 与传统 CGI 模式的区别之一则是 Web 服务器不是直接执行 CGI 程序了,而是通过 Socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,也正是由于 FastCGI 进程管理器是基于 Socket 通信的,所以也是分布式的,Web 服务器可以和 CGI 响应器服务器分开部署。Web 服务器需要将数据 CGI/1.1 的规范封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。

FPM

FPM (FastCGI Process Manager),它是 FastCGI 的实现,任何实现了 FastCGI 协议的 Web Server 都能够与之通信。FPM 之于标准的 FastCGI,也提供了一些增强功能。FPM 是PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,FPM的核心功能是进程管理。

能不能让PHP处理http请求呢?这时就涉及到了网络处理,PHP需要接收请求、解析协议,然后处理完成返回请求。在网络应用场景下,PHP并没有像Golang那样实现http网络库,而是实现了FastCGI协议,然后与web服务器配合实现了http的处理,web服务器来处理http请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给web服务器,web服务器再返回给用户。

PHP实现了FastCGI协议的解析,但是并没有具体实现网络处理,一般的处理模型:多进程、多线程,多进程模型通常是主进程只负责管理子进程,而基本的网络事件由各个子进程处理,nginx、fpm就是这种模式;另一种多线程模型与多进程类似,只是它是线程粒度,通常会由主线程监听、接收请求,然后交由子线程处理,memcached就是这种模式,有的也是采用多进程那种模式:主线程只负责管理子线程不处理网络事件,各个子线程监听、接收、处理请求,memcached使用udp协议时采用的是这种模式。

FPM 是一个 PHP 进程管理器,包含 master 进程和 worker 进程两种进程:master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个 (具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。

FPM 的实现就是创建一个 master 进程,在 master 进程中创建并监听 socket,然后 fork 出多个子进程,这些子进程各自 accept 请求,子进程的处理非常简单,它在启动后阻塞在 accept 上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说 FPM 的子进程同时只能响应一个请求,只有把这个请求处理完成后才会 accept 下一个请求,这一点与 nginx 的事件驱动有很大的区别,nginx 的子进程通过 epoll 管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。

FPM 的 master 进程与 worker 进程之间不会直接进行通信,master 通过共享内存获取 worker 进程的信息,比如 worker 进程当前状态、已处理请求数等,当 master 进程要杀掉一个 worker 进程时则通过发送信号的方式通知 worker 进程。

fpm可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程,类似nginx中server概念。

从 FPM 接收到请求,到处理完毕,其具体的流程如下:

  1. FPM 的 master 进程接收到请求
  2. master 进程根据配置指派特定的 worker 进程进行请求处理,如果没有可用进程,返回错误,这也是我们配合 Nginx 遇到502错误比较多的原因。
  3. worker 进程处理请求,如果超时,返回504错误
  4. 请求处理结束,返回结果

在 fork 后 worker 进程返回了监听的套接字继续main()后面的处理,而master将永远阻塞在fpm_event_loop()

worker 进程对请求的处理过程

fpm_run()执行后将 fork 出 worker 进程,worker 进程返回main()中继续向下执行,后面的流程就是 worker 进程不断 accept 请求,然后执行 PHP 脚本并返回。整体流程如下:

  1. 等待请求: worker 进程阻塞在fcgi_accept_request()等待请求;
  2. 解析请求: fastcgi 请求到达后被 worker 接收,然后开始接收并解析请求数据,直到 request 数据完全到达;
  3. 请求初始化: 执行php_request_startup(),此阶段会调用每个扩展的:PHP_RINIT_FUNCTION()
  4. 编译、执行: 由php_execute_script()完成 PHP 脚本的编译、执行;
  5. 关闭请求: 请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION(),然后进入步骤(1)等待下一个请求。

worker进程一次请求的处理被划分为5个阶段:

  1. FPM_REQUEST_ACCEPTING: 等待请求阶段
  2. FPM_REQUEST_READING_HEADERS: 读取fastcgi请求header阶段
  3. FPM_REQUEST_INFO: 获取请求信息阶段,此阶段是将请求的method、query stirng、request uri等信息保存到各worker进程的fpm_scoreboard_proc_s结构中,此操作需要加锁,因为master进程也会操作此结构
  4. FPM_REQUEST_EXECUTING: 执行请求阶段
  5. FPM_REQUEST_END: 没有使用
  6. FPM_REQUEST_FINISHED: 请求处理完成

worker处理到各个阶段时将会把当前阶段更新到fpm_scoreboard_proc_s->request_stage,master进程正是通过这个标识判断worker进程是否空闲的。

PHP 的生命周期

PHP的生命周期

参考资料

  1. The Common Gateway Interface (CGI) Version 1.1
  2. FastCGI 协议
  3. FastCGI Process Manager (FPM)
  4. TIPI 深入理解 PHP 内核
  5. php7-internal
  6. 深入理解PHP之:Nginx 与 FPM 的工作机制