Skip to content

Menu
Menu

FPM

Posted on 2021年10月2日2023年1月11日 by zhezimi

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种类型的消息都是以一个消息头开始的,其结构体定义如下:

其中:

  1. version表示FastCGI协议版本
  2. type标识FastCGI记录类型
  3. requestId标识消息所属的FastCGI请求,计算方式如下:(requestIdB1 << 8) + requestIdB0,所以requestId的范围是0~65535。
  4. contentLength是标识消息的contentData组件的字节数,计算方式跟requestId类似,范围同样是0~65535。 (contentLengthB1 << 8) | contentLengthB0
  5. 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内核剖析》

相关文章

  • PHP-FPM异常问题

  • 依赖注入容器

  • PHP7新特性

  • 生成器

  • 错误与异常

发表评论 取消回复

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

近期文章

  • 排查网络故障常用命令
  • PHP-FPM异常问题
  • RabbitMQ 1:介绍
  • 观察者模式
  • 装饰者模式

近期评论

没有评论可显示。

分类

  • cdn
  • css
  • docker
  • git
  • http
  • javascript
  • linux
  • mysql
  • nginx
  • php
  • RabbitMQ
  • 代码规范
  • 性能
  • 正则表达式
  • 网络协议
  • 设计模式
© 2025 | Powered by Minimalist Blog WordPress Theme