Fpm(FastCGI Process Manage)是PHP FastCGI运行模式的一个进程管理器。从它的定义可以看出,Fpm的核心功能是进程管理。那么用它来管理什么进程呢?这个问题需要从FastCGI说起。
FastCGI是Web服务器(nginx,apache)和处理程序之间的一种通信协议,它是与HTTP类似的一种应用层通信协议。注意:它只是一种协议。
PHP是一个脚本解析器,可以简单地把它理解为一个黑盒函数,输入是PHP脚本,输出是脚本的执行结果。
除了在命令行下执行脚本,能不能让PHP处理HTTP请求呢?这种场景下就涉及到网络处理,需要接收请求,解析协议,处理完成后再返回处理结果。
在网络应用场景下,PHP并没有像Golang那样实现HTTP网络库,而是实现了FastCGI协议,然后与Web服务器配合实现了HTTP的处理。
Web服务器来处理HTTP请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给Web服务器,Web服务器再返回给用户,如下图所示。
网络处理模型
PHP实现了FastCGI协议的处理,但是并没有实现具体的网络连接。比较常见的网络处理模型有以下两种。
1:多进程模型:
由一个主进程和多个子进程组成,主进程负责管理子进程,基本的网络事件由各个子进程处理,Nginx采用的就是这种模型。
2:多线程模型:
与多进程类似,只是它是线程粒度,这种模型通常会由主进程监听,接收请求,然后交由子线程处理。memcached就是这种模式,有的也是采用多进程那种模式—-主线程只负责管理子线程不处理网络事件,各个子进程监听,接收,处理请求。memcached使用UDP协议时采用的就是这种模式。
进程拥有独立的地址空间和资源,而线程并没有,线程之间共享进程的地址空间及资源,所以在资源管理上多进程模型比较简单,而多线程模型则需要考虑不同线程之间的资源冲突,也就是线程安全。
worker pool
Fpm可以同时监听多个端口,每个端口对应一个worker pool。每个pool下面对应多个worker进程,类似Nginx中server的概念。
在php-fpm.conf中通过[pool name]声明一个worker pool,每个pool各自配置监听的地址,进程管理方式,worker进程数等。
上面这个例子配置了两个worker pool,分别监听9000,9001端口。
基本实现
Fpm是一种多进程模型,它由一个master和多个worker进程组成。master进程启动时会创建一个socket,但是不会接收,处理请求,而是由fork出来的worker子进程完成请求的接收以及处理。
master进程的主要工作是管理worker进程,负责fork或杀掉worker进程,比如当请求比较多,worker进程处理不过来时,master进程会尝试fork新的worker进程进行处理,而当空闲worker进程比较多的时候则会杀掉部分子进程,避免占用,浪费系统资源。
worker进程的主要工作是处理请求,每个worker进程会竞争地Accept请求,接收成功后解析FastCGI,然后执行相应的脚本,处理完成后关闭连接,继续等待新的连接,这就是一个worker进程的生命周期。
从worker进程的生命周期可以看到:一个worker进程只能处理一个请求,只有将一个请求处理完成后才会处理下一个请求。
这与Nginx的事件模型有很大不同。Nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。
Fpm的这种处理方式大大简化了PHP的资源管理,使得在Fpm模型下不需要考虑并发导致的资源冲突。
master进程与worker进程不会直接通信,master通过共享内存获取worker进程的信息,比如worker进程当前状态,已处理请求数等。master进程通过发送信号的方式杀掉worker进程。
三种进程管理方式
1:静态模式(static)
这种方式比较简单,在启动时master根据pm.max_chidlren配置fork出相应数量的worker进程,也就是worker进程数是固定不变的。
2:动态模式(dynamic)
这种模式比较常用,在Fpm启动时会根据pm.start_servers配置初始化一定量的worker。
如果运行期间master发现空闲worker低于pm.min_spare_servers配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过pm.max_children。
如果master发现空闲worker数超过了pm.max_spare_servers(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源。
master通过这4个值来动态控制worker的数量
3:按需模式(ondemand)
这种模式很像传统的cgi,在启动时不分配worker进程,等到有通知了之后再通知master进程fork worker进程,也就是来了请求以后再fork子进程进行处理,总的worker数量不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出。
Web服务器配置
在Linux中,Nginx服务器和PHP-FPM可以通过TCP Socket和UNIX Socket两种方式实现。
其中,UNIX Socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。这种方式需要在Nginx配置文件中填写PHP-FPM的pid文件位置,效率要比TCP Socket高。
TCP Socket的优点是可以跨服务器,当Nginx和PHP-FPM不在同一台机器上时,只能使用这种方式,配置方式如下:
master进程会创建Socket,而worker进程会通过创建的fd来accept请求。
FastCGI协议
FastCGI是一种协议,它是建立在CGI/1.1基础之上的,把CGI/1.1立面要传递的数据通过FastCGI协议定义的顺序和格式进行传递,为了更好理解FPM的工作,下面具体阐述一下FastCGI的内容。
1:消息类型
FastCGI协议分为10种类型,具体定义如下:
整个FastCGI是二进制连续传递的,定义了一个统一结构的的消息头,用来读取每个消息的消息体,方便消息包的切割。
一般情况下,最先发送的是FCGI_BEGIN_REQUEST类型的消息,然后是FCGI_PARAMS和FCGI_STDIN类型的消息。
当FastCGI响应处理完后,将发送FCG_STDOUT和FCGI_STDERR类型的消息,最后以FCGI_END_REQUEST表示请求的结束。
FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分别表示请求的开始和结束,与整个协议无关。
2:消息头
以上10种类型的消息都是以一个消息头开始的,其结构体定义如下:
其中:
- version表示FastCGI协议版本
- type标识FastCGI记录类型
- requestId标识消息所属的FastCGI请求,计算方式如下:(requestIdB1 << 8) + requestIdB0,所以requestId的范围是0~65535。
- contentLength是标识消息的contentData组件的字节数,计算方式跟requestId类似,范围同样是0~65535。 (contentLengthB1 << 8) | contentLengthB0
- paddingLength是标识消息的paddingData组件的字节数,范围是0~255;协议通过paddingData提供给发送者填充发送的记录的功能,并且方便接受者通过paddingLength快速地跳过paddingData。填充的目的是允许发送者更有效地处理保持对齐的数据。如果内容的长度超过65535字节怎么办?答案是可以分成多个消息发送。
3:FCGI_BEGIN_REQUEST
FCGI_BEGIN_REQUEST的结构体定义如下:
其中,role代表的是Web服务器期望应用扮演的角色,计算方式如下:
(roleB1 << 8) + roleB0
PHP 7处理了3种角色,分别是FCGI_RESPONDER、FCGI_AUTHORIZER和FCGI_FILTER。
flags和FCGI_KEEP_CONN如果为0,则在对本次请求响应后关闭连接;如果非0,则在对本次请求响应后不会关闭连接。
4:名-值对
对于type为FCGI_PARAMS类型,FastCGI协议提供了名-值对来很好地满足读写可变长度的name和value,格式如下:
nameLength+valueLength+name+value
为了节省空间,对于0~127长度的值,Length使用了一个char来表示,第一位为0,对于大于127的长度的值,Length使用了4个char来表示,第一位为1。具体如下图所示。
长度计算代码如下:
这样可以表达0~2的31次方的长度。
5:请求协议
FastCGI协议的定义结构体如下:
分析完FastCGI的协议,我们整体掌握了请求的FastCGI消息的内容。
PHP-FPM全局配置
在Ubuntu中,PHP-FPM的主配置文件是/etc/php7/fpm/php-fpm.conf。在CentOS中,PHP-FPM的主配置文件在/etc/php-fpm.conf。
常见的配置参数
1:user=www
拥有这个PHP-FPM进程池中子进程的系统用户,要把这个设置的值设为运行PHP应用的非根用户的用户名
2:group=www
拥有这个PHP-FPM进程池中子进程的系统用户组,要把这个设置的值设为运行PHP应用的非根用户的用户名
3:listen=127.0.0.1:9000
PHP-FPM进程池监听的IP地址和端口号,让PHP-FPM只接受nginx从这里传入的请求。127.0.0.1:9000让指定的PHP-FPM进程池监听从本地端口9000进入的连接。
4:listen_allow_clients=127.0.0.1
可以向这个PHP-FPM进程池发送请求的IP地址(一个或多个)。为了安全,把这设置设置127.0.0.1,即只有当前设备能把请求转发给这个PHP-FPM进程池。默认情况下,这个设置可能被注释掉了,如果需要,去掉这个设置的注释。
5:pm.max_children=51
这个设置设定任何时间点PHP-FPM进程池中最多能有多少个进程,这个设置没有绝对正确的值。
你应该测试你的PHP应用,确定每个PHP进程需要使用多少内存,然后把这个设置设为设备可用内存容纳的PHP进程总数。
可以使用memory_get_peak_usage()方法来获取分配给你的 PHP 脚本的内存峰值字节数。
对大多数中小型PHP应用来说,每个PHP进程要使用5-15MB内存。
假设我们使用的设备未这个PHP-FPM进程池分配了512MB可用内存,那么我们可以把这个设置的值设置(512MB总内存)/(每个进程使用10MB)=51个进程。
6:pm.start_servers=3
PHP-FPM启动时PHP-FPM进程池中立即可用的进程数,同样的,这个设置也没有绝对正确的值,对大多是中小型PHP应用来说,建议设置为2或3,这么做是为了先准备好两到三个进程,等待请求进入,不让PHP应用的头几个HTTP请求等待PHP-FPM初始化进程池中的进程。
7:pm.min_spare_servers=2
PHP应用空闲时PHP-FPM进程池中可以存在的进程数量最小值,这个设置的值一般与pm.start_servers设置的值一样,用于确保新进入的HTTP请求无需等待PHP-FPM在进程池中初始化进程。
8:pm.max_spare_servers=5
PHP应用进程空间时PHP-FPM进程池中可以存在的进程数量最大值。这个设置的值一遍比pm.start_servers设置的值要大一些,用于确保新进入的HTTP请求无需等待PHP-FPM在进程池中重新初始化阶段
9:pm.max_requests=1000
回收进程之前,PHP-FPM进程池中各个进程最多能处理的HTTP请求数量。这个设置有助于避免PHP扩展或库编写拙劣而导致不断泄露内存。
10:slowlog=/path/to/slowlog.log
这个设置的值是一个日志文件在文件系统中的绝对路径。这个日志文件用于记录处理时间超过m秒的HTTP请求信息,以便找出PHP应用的瓶颈,进行调试。记住,PHP-FPM进程池所属的用户和用户组必须有这个文件的写权限。
11:request_slowlog_timeout=5s
如果当前HTTP请求的处理时间超过指定的值,就把请求的回溯信息写入slowlog设置制定的日志文件。把这个设置的值设为多少,取决于你认为多长时间算久,一开始可以设定为5s
内存
运行PHP的时候,大家最关心的是每个PHP进程要使用多少内存。php.ini文件中的memory_limit设置用于设定单个PHP进程可以使用的系统内存最大值。
这个设置的默认值一般是128M,这对于大多数中小型PHP应用来说或许合适,如果运行的是微型PHP应用,可以降低这个值到64M,可以更节省系统资源。
如果运行的事内存集中型PHP应用,可以增加这个值,例如设置为512M,用以提高性能。
这个设置的值由可用的系统内存决定,决定给PHP分配多少内存,以及能负担得起多少个PHP-FPM进程时,需要考虑以下几个问题。
1:一共能分配给PHP多少内存?
首先,我们要确定能分配给PHP多少系统内存。例如,我们可能会使用一个Linode虚拟设备,这个设备一共有2GB内存。可是,这台设备中可能还有其他进程(例如,nginx,mysql,memcache),而这些进程也要消耗内存,那么我们可能觉得留512MB给PHP就足够了。
2:单个PHP进程平均消耗多少内存
然后,我们需要确定单个PHP进程平均消耗多少内存。为此,我们需要监控进程的内存使用量。
如果使用命令行,可以执行top命令,查看运行中的进程的实时统计数据。除此之外,还可以在PHP脚本的最后调用memory_get_peak_usage()函数,输出当前脚本消耗的最大内存量。
不管使用哪种方式,都要多次运行同一个PHP脚本,然后取内存消耗量的平均值。
3:能负担得起多少个PHP-FPM进程
假设我们给PHP-FPM分配了512MB内存,每个PHP进程平均消耗了15MB内存,那我们拿内存总量除以每个PHP进程消耗的内存量,从而确定能负担得起34个PHP-FPM进程。这是个估值,应该再做实验,得到精确值。
4:有足够的系统资源吗?
最后我们需要问自己,确信有足够的系统资源运行PHP应用并处理预期的流量吗?如果答案是肯定的,那太好了。如果答案是否定的,就需要升级服务器,添加更多的内存,然后再回到第一个问题。
参考资料
- 《Modern PHP》
- 《PHP7底层设计与源码实现》
- 《PHP7内核剖析》