💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
# 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 ``` ![image](https://img-blog.csdnimg.cn/20200116190902347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5Nzg3MzY3,size_16,color_FFFFFF,t_70) 如果存在并发请求时,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 ```