# php-fpm 运行模式
## 多进程同步阻塞模式
PHP-FPM(FastCGI Process Manager)是一个PHP FastCGI 进程管理器
FastCGI 可以理解为一种协议,用于web服务器(nginx、Apache)和处理程序间进行通信,是一种应用层通信协议。
客户端发送一个请求到web服务器,web服务器会将请求通过TCP或/UDP的方式转发到php-fpm进程管理器。
php-fpm 在master进程中创建多个worker,调用一个worker进程处理php代码。有请求到达后worker开始读取请求数据。这些work在启动后阻塞fcgi_accept_request()上,各自accept请求。
读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说fpm的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求。
**php代码是单线程的。**
fpm的master与work进程间不会直接通讯,master通过共享内存获取worker进程的信息,比如worker进程当前状态、已处理请求数等,master要杀死worker进程也是通过发送信号。
**pm 可以同时监听多个端口,每个端口对应一个worker pool**,而每个pool下对应多个worker进程,类似nginx中server概念, 在php-fpm.conf中可以配置多个,例如:
```
[web1]
listen:127.0.0.1:9000
[web2]
listen:127.0.0.1:9001
```

如果存在并发请求时,php-fpm会为每个请求开启一个单独的进程去处理php代码。
请求执行完成后,空闲的php-fpm进程被销毁,内存释放。
但是并发的问题是在某个时间段突然来了大量请求,php-fpm 进程已经达到了最大限制数。此时,新来的请求只能等待空闲的php-fpm进程来处理,这就是多进程同步阻塞模式的弊端,期间还会存在多进程带来的内存占用问题。
php-fpm 是一种 master/worker 多进程架构。php-fpm 进程主要负责CGI及php环境初始化、监听事件、监听子进程状态等。worker负责具体的php请求。
# php-fpm 三种对子进程的管理方式
### pm = static
worker进程数是固定的,具体大小有pm.max_children定义。
```
pm.max_children:静态方式下开启的php-fpm进程数量
```
### pm = dynamic (默认)
子进程数量由`pm.max_children\pm.start_servers\pm.min_spare_servers\pm.max_spare_servers`这些参数决定。
启动的时候,会产生固定数量的子进程(pm.start_servers控制)也就是最小子进程,最大子进程由(pm.max_children)控制。
子进程数量会在最大和最小范围中变化。
真正的worker进程启动后还会启动一定数量的空闲进程。
pm.min_spare_servers 和 pm.max_spare_servers 控制空闲进程的最大最小数。如果空闲的子进程超过了max_spare_servers 就会被杀死。
优点:灵活
缺点:dynamic模式为了最大化服务器性能,会造成更多内存使用。
因为这种模式只会杀掉超出最大闲置进程数(pm.max_spare_servers)的闲置进程,比如最大闲置进程数是30,最大进程数是50,然后网站经历了一次访问高峰,此时50个进程全部忙碌,0个闲置进程数,接着过了高峰期,可能没有一个请求,于是会有50个闲置进程,但是此时php-fpm只会杀掉20个子进程,始终剩下30个进程继续作为闲置进程来等待请求,这可能就是为什么过了高峰期后即便请求数大量减少,服务器内存使用却也没有大量减少,也可能是为什么有些时候重启下服务器情况就会好很多,因为重启后,php-fpm的子进程数会变成最小闲置进程数,而不是之前的最大闲置进程数。
```
pm.start_servers 起始php-fpm进程数量
# 必须是介于 pm.min_spare_servers 和 pm.max_spare_servers 这个值之间
pm.max_requests int 空闲服务进程的最大数(必须)
pm.max_spare_servers int 空闲服务进程的最低数(必须)
pm.min_spare_servers int 启动时创建的子进程数
# 默认值:min_spare_servers + (max_spare_servers - min_spare_servers) / 2
```
### pm = ondemand
这种模式把内存放在第一位,他的工作模式很简单,每个闲置进程,在持续闲置了pm.process_idle_timeout秒后就会被杀掉,有了这个模式,到了服务器低峰期内存自然会降下来,如果服务器长时间没有请求,就只会有一个php-fpm主进程,当然弊端是,遇到高峰期或者如果pm.process_idle_timeout的值太短的话,无法避免服务器频繁创建进程的问题,因此pm = dynamic和pm = ondemand谁更适合视实际情况而定。
# FPM 具体执行流程
fpm通过sapi接口与php进程交互
1.fpm启动会调用各扩展的MINT方法,进行一些数据初始化(长驻内存)
2.每个请求过来,先会执行RINT对单个请求行一个初始化
3.执行php脚本(在没有缓存opcode的情况下,这里的php脚本是动态执行的,所以更新php脚本后,会执行新的php脚本,详情不在这里叙述)
4.执行RSHUTDOWN方法
5.如果你要停止fpm了,才会执行MSHUTDOWN。
fpm对每个请求的处理都是一直在在重复执行 2~4步,在第三步中,php的脚本是动态执行的,由于每次都要执行一次php脚本,而每次php脚本都要有一个把php文件翻译成opcode的流程(比较耗时), 于是就产生的opcache工具。
## opcache
直接把php翻译后的opcode代码树保存到共享内存中,下次请求过来就不用再编译了。
opcache的问题: 按照他的描述,修改了php文件,并不能立即被更新。
opcache的解决方案: 有一个配置来设置隔多长时间检测文件是否更新了,从而有机会在第二步重新来reload相关的文件。
当然,直接reload fpm,从而达到php热更新的效果(opcache扩展可以在第四步把相关的opcode cache给清空)。
## work 工作流程
等待请求:fcgi_accept_request()阻塞等待请求
接收请求:fastcgi请求到达后被worker接收并解析,一直到完全接收,然后将method、query、uri等信息保存到worker进程的fpm_scoreboard_proc_s结构中
初始化请求:php_request_startup()执行,此步骤会调用每个扩展的PHP_RINIT_FUNCTION方法,初始化一些操作
处理请求(编译、执行):php代码编译执行阶段,由 php_execute_script方法完成
关闭请求:返回响应,执行php_request_shutdown方法关闭请求,然后进入第一步继续等待请求,此步骤会执行每个扩展的PHP_RSHUTDOWN_FUNCTION进行一些收尾工作
```
int main(int argc, char *argv[])
{
...变量定义,参数初始化
//注册SAPI
sapi_startup(&cgi_sapi_module);
...
//执行php_module_starup()
if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
return FPM_EXIT_SOFTWARE;
}
//初始化
if(0 > fpm_init(...)){
//记录日志并退出
return FPM_EXIT_CONFIG;
}
...
fpm_is_running = 1;//fpm运行状态标识
fcgi_fd = fpm_run(&max_requests);//进程初始化,调用fork()创建work进程
...
fcgi_init_request(&request, fcgi_fd); //初始化请求;
//此阶段的php_request_startup()会调用每个扩展的:PHP_RINIT_FUNCTION();
if (UNEXPECTED(php_request_startup() == FAILURE)) {
...
}
...
php_fopen_primary_script(&file_handle TSRMLS_CC); //打开脚本;
...
php_execute_script(&file_handle TSRMLS_CC); //执行脚本;
...
//worker进程退出
php_module_shutdown();
...
}
```
# cgi php-cgi fastcgi php-fpm
cgi是web服务器和php解释器之间的通信协议,用来保证数据正确传输。
php-cgi只能解析请求,返回结果,不会管理进程。
Fastcgi是用来提高cgi程序(php-cgi)性能的方案/协议。
**cgi程序的性能问题在哪呢?**
"PHP解析器会解析php.ini文件,初始化执行环境"。标准的CGI对每个请求都会执行这些步骤,所以处理的时间会比较长。
Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复劳动,效率自然提高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是Fastcgi的对进程的管理。
FastCGI是一个方案或者协议,php-fpm就是FastCGI的一个管理工具,也就是说,进程的分配和管理都是php-fpm来做的。
php-fpm的管理对象是php-cgi,他负责管理一个进程池,处理来自Web服务器的请求。
对于php.ini文件的修改,php-cgi进程是没办法平滑重启的,有了php-fpm后,就可以平滑重启了。
原理是php-fpm会用新的配置文件启用新的worker进程,已经存在的老的worker处理完手上的活就可以歇着了,老的进程在请求处理完成就会被回收。php-fpm就是通过这种机制来平滑过度的。
# php-fpm的配置和优化
**避免程序跑死(hang)**
在负载较高的服务器上定时重载php-fpm,reload可以平滑重启而不影响生产系统的php脚本运行,每15分钟reload一次:
```
0-59/15 * * * * /usr/local/php/sbin/php-fpm reload
```
**增加最大处理请求数**
最大处理请求数是指一个php-fpm的worker进程在处理多少个请求后就终止掉,master进程会重新拉起新的worker。可以避免php解释器自身或程序引起的memory leaks。默认值是500,当一个 PHP-CGI 进程处理的请求数累积到 500 个后,自动重启该进程。
```
pm.max_requests = 1024
```
**为什么要重启进程呢?**
项目中,我们多多少少都会用到一些 PHP 的第三方库,这些第三方库经常存在内存泄漏问题,如果不重启 PHP-CGI 进程,肯定会造成内存使用量不断增长。因此 PHP-FPM 作为 PHP-CGI 的管理器,提供了这么一项监控功能,对请求达到指定次数的 PHP-CGI 进程进行重启,保证内存使用量不增长。
## 优化动态fpm进程数
```
pm.max_children = 100
pm.start_servers = 30
pm.min_spare_servers = 20
pm.max_spare_servers = 100
pm.max_requests = 500
```
运行的PHP程序在执行完成后,或多或少会有内存泄露的问题。
这也是为什么开始的时候一个php-fpm进程只占用3M左右内存,运行一段时间后就会上升到20-30M的原因了。
对于内存大的服务器(比如8G以上)来说,指定静态的max_children实际上更为妥当,因为这样不需要进行额外的进程数目控制,会提高效率。
因为频繁开关php-fpm进程也会有时滞,所以内存够大的情况下开静态效果会更好。数量也可以根据 内存/30M 得到,比如8GB内存可以设置为100,
那么php-fpm耗费的内存就能控制在 2G-3G的样子。如果内存稍微小点,比如1G,那么指定静态的进程数量更加有利于服务器的稳定。
这样可以保证php-fpm只获取够用的内存,将不多的内存分配给其他应用去使用,会使系统的运行更加畅通。
# php-fpm.config 参数配置
fpm启动后会先读php.ini,然后再读相应的conf配置文件,conf配置可以覆盖php.ini的配置。
启动fpm之后,会创建一个master进程,监听9000端口(可配置),master进程又会根据fpm.conf/www.conf去创建若干子进程,子进程用于处理实际的业务。
当有客户端(比如nginx)来连接9000端口时,空闲子进程会自己去accept,如果子进程全部处于忙碌状态,新进的待accept的连接会被master放进队列里,等待fpm子进程空闲;这个存放待accept的半连接的队列有多长,由listen.backlog配置。
**进程池**
除了有php-fpm.conf配置文件外,通常还有其他的*.conf配置文件(也可以不要,直接在php-fpm.conf配置)用于配置进程池,不同的进程池可以用不同的用户执行,监听不同的端口,处理不同的任务;多个进程池共用一个全局配置。
include=/opt/remi/php56/root/etc/php-fpm.d/*.conf 载入其他的配置文件。
**php-fpm.conf 全局配置段**
```
;pid = run/php-fpm.pid 设置pid文件的位置,默认目录路径 /usr/local/php/var
;error_log = log/php-fpm.log 记录错误日志的文件,默认目录路径 /usr/local/php/var
;syslog.facility = daemon 用于指定什么类型的程序日志消息。
;syslog.ident = php-fpm 用于FPM多实例甄别
;log_level = notice 记录日志的等级,默认notice,可取值alert, error, warning, notice, debug
;emergency_restart_threshold = 0 如果子进程在这个时间段内带有IGSEGV或SIGBUS退出,则重启fpm,默认0表示关闭这个功能
;emergency_restart_interval = 0 设置时间间隔来决定服务的初始化时间(默认单位:s秒),可选s秒,m分,h时,d天
;process_control_timeout = 0 子进程等待master进程对信号的回应(默认单位:s秒),可选s秒,m分,h时,d天
;process.max = 128 控制最大进程数,使用时需谨慎
;process.priority = -19 处理nice(2)的进程优先级别-19(最高)到20(最低)
;rlimit_files = 1024 设置主进程文件描述符rlimit的数量
;rlimit_core = 0 设置主进程rlimit最大核数
;events.mechanism = epoll 使用处理event事件的机制
; - select (any POSIX os)
; - poll (any POSIX os)
; - epoll (linux >= 2.5.44)
; - kqueue (FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0)
; - /dev/poll (Solaris >= 7)
; - port (Solaris >= 10)
;daemonize = yes 将fpm转至后台运行,如果设置为"no",那么fpm会运行在前台
;systemd_interval = 10 使用 systemd 集成的 FPM 时,设置间歇秒数,报告健在通知给 systemd。 设置为 0 表示禁用。默认值:10。
```
**运行配置区段**
```
listen string
设置接受 FastCGI 请求的地址。可用格式为:'ip:port','port','/path/to/unix/socket'。每个进程池都需要设置。
user string
FPM 进程运行的Unix用户。必须设置。
group string
FPM 进程运行的 Unix 用户组。如果不设置,就使用默认用户的用户组。
pm string
设置进程管理器如何管理子进程。可用值:static,ondemand,dynamic。必须设置。
pm.max_spare_servers。
pm.max_children int
pm 设置为 static 时表示创建的子进程的数量,pm 设置为 dynamic 时表示最大可创建的子进程的数量。必须设置。
该选项设置可以同时提供服务的请求数限制。类似 Apache 的 mpm_prefork 中 MaxClients 的设置和 普通PHP FastCGI中的 PHP_FCGI_CHILDREN 环境变量。
pm.start_servers in
设置启动时创建的子进程数目。仅在 pm 设置为 dynamic 时使用。默认值:min_spare_servers + (max_spare_servers - min_spare_servers) / 2。
pm.min_spare_servers int
设置空闲服务进程的最低数目。仅在 pm 设置为 dynamic 时使用。必须设置。
pm.max_spare_servers int
设置空闲服务进程的最大数目。仅在 pm 设置为 dynamic 时使用。必须设置。
pm.process_idle_timeout mixed
秒数,多久之后结束空闲进程。 仅当设置 pm 为 ondemand。 可用单位:s(秒),m(分),h(小时)或者 d(天)。默认单位:10s。
pm.max_requests int
设置每个子进程重生之前服务的请求数。对于可能存在内存泄漏的第三方模块来说是非常有用的。如果设置为 '0' 则一直接受请求,等同于 PHP_FCGI_MAX_REQUESTS 环境变量。默认值:0。
pm.status_path string
FPM 状态页面的网址。如果没有设置,则无法访问状态页面,默认值:无。
request_slowlog_timeout mixed
当一个请求到达该超时时间后,就会将对应的 PHP 调用堆栈信息完整写入到慢日志中。设置为 '0' 表示 'Off'。可用单位:s(秒),m(分),h(小时)或者 d(天)。默认单位:s(秒)。默认值:0(关闭)。
slowlog string
慢请求的记录日志。默认值:#INSTALL_PREFIX#/log/php-fpm.log.slow。
rlimit_files int
设置文件打开描述符的 rlimit 限制。默认值:系统定义值。
```
自 5.3.3 起,也可以通过 web 服务器设置 PHP 的设定。
**在 nginx.conf 中设定 PHP**
```
set $php_value "pcre.backtrack_limit=424242";
set $php_value "$php_value \n pcre.recursion_limit=99999";
fastcgi_param PHP_VALUE $php_value;
fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/htdocs";
```
## PHP-FPM与Nginx的通信机制
```
location ~ .php$ {
root /home/wwwroot;
fastcgi_pass 127.0.0.1:9000;
#fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
#fastcgi_pass unix:/tmp/php-cgi.sock;
try_files $uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
```
**两种方式**
Nginx和PHP-FPM的进程间通信有两种方式,一种是TCP,一种是UNIX Domain Socket.
**TCP**
允许通过网络进程之间的通信,也可以通过loopback进行本地进程之间通信。
**UNIX Domain Socket**
允许在本地运行的进程之间进行通信。
其中TCP是IP加端口,可以跨服务器.而UNIX Domain Socket不经过网络,只能用于Nginx跟PHP-FPM都在同一服务器的场景。可以节约创建连接的时间,从而提高性能。
**PHP-FPM配置**
```
方式1:
php-fpm.conf: listen = 127.0.0.1:9000
nginx.conf: fastcgi_pass 127.0.0.1:9000;
方式2:
php-fpm.conf: listen = /tmp/php-fpm.sock
nginx.conf: fastcgi_pass unix:/tmp/php-fpm.sock;
其中php-fpm.sock是一个文件,由php-fpm生成,类型是srw-rw—-
```
**UNIX Domain Socket和tcp链接的区别**
一方面:
UNIX Domain Socket可用于两个没有亲缘关系的进程,是目前广泛使用的IPC机制。
UNIX Domain Socket减少了不必要的tcp开销,而tcp需要经过loopback,还要申请临时端口和tcp相关资源。
另一方面:
unix socket高并发时候不稳定,连接数爆发时,会产生大量的长时缓存,在没有面向连接协议的支撑下,大数据包可能会直接出错不返回异常。tcp这样的面向连接的协议,多少可以保证通信的正确性和完整性。
**tcp socket**
允许通过网络进程之间的通信,也可以通过loopback进行本地进程之间通信。
**unix socket**
允许在本地运行的进程之间进行通信。
```
UNIX Domain Socket(只能是本地):
Nginx <=> socket <=> PHP-FPM
TCP Socket(本地回环):
Nginx <=> socket <=> TCP/IP <=> socket <=> PHP-FPM
TCP Socket(Nginx和PHP-FPM位于不同服务器):
Nginx <=> socket <=> TCP/IP <=> 物理层 <=> 路由器 <=> 物理层 <=> TCP/IP <=> socket <=> PHP-FPM
```
## 类似的软件
像mysql命令行客户端连接mysqld服务也类似有这两种方式:
**使用Unix Socket连接(默认):**
```
mysql -uroot -p --protocol=socket --socket=/tmp/mysql.sock
```
**使用TCP连接:**
```
mysql -uroot -p --protocol=tcp --host=127.0.0.1 --port=3306
```
**总结**
此处,用unix domain socker的方式,比tcp方式速度更快,但是tcp是面向连接的协议,稳定性更高,这是一个区别点。
# 优化 Nginx unix socke 稳定性
1.修改内核参数
```
net.unix.max_dgram_qlen = 4096
net.core.netdev_max_backlog = 4096
net.core.somaxconn = 4096
```
2.提高backlog
```
backlog默认位128,1024这个值最好换算成自己正常的QPS。
nginx.conf
server{
listen 80 default backlog=1024;
}
php-fpm.conf
listen.backlog = 1024
```
3.增加sock文件和php-fpm实例
在/dev/shm新建一个sock文件,在nginx中通过upstream魔抗将请求负载均衡到两个sock文件,
并且将两个sock文件分别对应到两套php-fpm实例上。
## Nginx php-fpm请求链路
```
www.example.com
|
|
Nginx
|
|
路由到www.example.com/index.php
|
|
加载nginx的fast-cgi模块
|
|
fast-cgi监听127.0.0.1:9000地址
|
|
www.example.com/index.php请求到达127.0.0.1:9000
|
|
php-fpm 监听127.0.0.1:9000
|
|
php-fpm 接收到请求,启用worker进程处理请求
|
|
php-fpm 处理完请求,返回给nginx
|
|
nginx将结果通过http返回给浏览器
```
## fpm进程状态监控
1,nginx配置:遇到status的请求,直接转发给php
```
location ~^/status$ {
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
}
```
2,fpm配置:pm.status_path = /status
3, 然后重新fpm和nginx,在浏览器里访问就能看到了:
默认以text/plain展示结果,可以传参数?json/html/xml分别得到json等格式的结果;参数full可以查看每个子进程的明细。
```
pool 进程池名称
process manager 进程管理方式
start time 进程什么时候启动的
start since 进程已经运行了多少秒
accepted conn 该池总共accept了多少连接
listen queue 等待accept的连接的数量
max listen queue fpm启动后,历史最高等待accept的连接的数量
listen queue len 配置的监听队列最大长度 受限于`listen.backlog`和系统`cat /proc/sys/net/core/somaxconn`,两者中取最小值
idle processes 闲置的进程数
active process 正在工作的进程数(加上限制的,就是总的子进程数)
total processes 总的子进程数量
max active processes fpm启动后,历史最多同时工作的进程数
max children reached 进程管理模式为 'dynamic'和 'ondemand'时,此数值是当子进程不够用时,master创建更多子进程的次数
slow requests 慢请求个数
full 参数下
pid 子进程ID;
state 子进程状态(Idle, Running, ...);
start time 子进程启动的时间;
start since 子进程启动后运行了多少秒;
requests 当前子进程一共处理了多少个请求;
request duration 请求耗费的纳秒数;
request method 请求方法 (GET, POST, ...);
request URI 请求参数;
content length POST请求时,请求的内容长度;
user - the user (PHP_AUTH_USER) (or '-' if not set);
script 请求的哪个php文件;
last request cpu 上次请求耗费的cpu资源
last request memory 上次请求耗费的内存峰值
如果进程是闲置状态,那这些信息记录的就是上次请求的相关数据,否则就是当前本次请求的相关数据。
```
# backlog配置问题
TCP 3次握手
可分为4步
1 客户端发起connect(),发送SYN j
2 服务器从SYN queue中建立条目,响应SYN k, ACK J+1
3 客户端connect()成功返回,响应ACK K+1
4 服务器将socket从SYN queue移入accept queue,accept()成功返回
**SYN/FIN各占一个序列号,ACK/RST不占序列号**
## Nginx 配置
```
listen address[:port] [backlog=number]
```
参数backlog 限制了用于存放处于挂起状态连接的队列最大长度, 已连接但未进行accept处理的SOCKET队列大小,即这些连接已经完全建立了,但还没有被处理,其默认值是 -1。当一个连接请求到达时,如果此时队列满了,客户端会收到连接拒绝(“Connection refused”)。
php-fpm的backlog大小设置跟php-fpm的处理能力有关,一个fpm子进程在同一时间只能处理一个请求。
backlog太大了,导致php-fpm处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时php-fpm处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。
php-fpm的backlog太小的话,nginx之类的client请求,根本进入不了php-fpm的accept queue,报“502 Bad Gateway”错。所以,这还得去根据php-fpm的QPS来决定backlog的大小。计算方式最好为QPS=backlog。
所以需要合理的设置backlog参数。
**如何查看accept queue溢出**
```
[root@Server-i-9ernh8hkz8 ~]# netstat -s | grep LISTEN
349 SYNs to LISTEN sockets dropped
```
