php-fpm和cgi,并发响应的理解以及高并发和多线程的关系、 为高性能优化 PHP-FPM

 

首先搞清楚php-fpm与cgi的关系

  • cgi

  cgi是一个web server与cgi程序(这里可以理解为是php解释器)之间进行数据传输的协议,保证了传递的是标准数据。

  • php-cgi

  php-cgi是php解释器,就是上文提到的cgi程序。

  • Fastcgi

  Fastcgi是用来提高cgi程序(php-cgi)性能的方案/协议。

  cgi程序的性能问题在哪呢?"PHP解析器会解析php.ini文件,初始化执行环境",就是这里了。标准的CGI对每个请求都会执行这些步骤,所以处理的时间会比较长。

  Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复劳动,效率自然提高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是Fastcgi的对进程的管理。

  • php-fpm

  上文提到了Fastcgi只是一个方案或者协议,那么php-fpm就是这个实现了Fastcgi的程序,也就是说,上文所描述的进程分配和管理是FPM来做的。官方对FPM的解释是 Fastcgi Process Manager(Fastcgi 进程管理器) 。

  

PHP对并发访问的处理

  • 进程和线程

  PHP从代码级别来讲不支持多线程操作,不能像Java、C#等语言一样可以编写多线程代码。但多线程和并发没有直接关系,多线程只是代码被运行时在同一时间同时执行多个线程任务,来提高服务器CPU的利用率,提高代码效率。但php是可以多进程执行的,上文所述的FPM进程管理机制就是多进程单线程的,有效提高了并发访问的响应效率。

  • 简单的web server + php-fpm 模式

  1. 当客户端发送一个请求时,web server会通过一个php-fpm进程(这里和下文所说指的fpm进程都是fpm开启的worker进程,关于fpm的工作原理这里不再累述)去执行php代码,php代码的执行是单线程的。

  2. 那么,当有多个客户端同时发送请求时(并发),web server会通过php-fpm为每个请求开启一个单独进程去执行php代码。

  3. 请求执行过后,空闲的php-fpm进程被销毁,内存得以释放。

  4. 但并发的问题在于,在某一时间,客户端请求让php-fpm进程数量达到了最大限制数,这个时候,新来的请求只能等待空闲的php-fpm进程来处理,这就是多进程同步阻塞模式的弊端,当然还有进程过多所带来的内存占用问题。

高并发和多线程

“高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程

  多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。

   高并发是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……而多线程只是其中解决方法之一

   实现高并发需要考虑:
                系统的架构设计,如何在架构层面减少不必要的处理(网络请求,数据库操作等)
                网络拓扑优化减少网络请求时间、如何设计拓扑结构,分布式如何实现?
                系统代码级别的代码优化,使用什么设计模式来进行工作?哪些类需要使用单例,哪些需要尽量减少new操作?
                提高代码层面的运行效率、如何选取合适的数据结构进行数据存取?如何设计合适的算法?
                任务执行方式级别的同异步操作,在哪里使用同步,哪里使用异步?
                JVM调优,是以server模式还是以clien模式运行,如何设置Heap、Stack、Eden的大小,如何选择GC策略,控制Full GC的频率?
                数据库优化减少查询修改时间。数据库的选取?数据库引擎的选取?数据库表结构的设计?数据库索引、触发器等设计?是否使用读写分离?还是需要考虑使用数据仓库?
                缓存数据库的使用,如何选择缓存数据库?是Redis还是Memcache? 如何设计缓存机制?
                数据通信问题,如何选择通信方式?是使用TCP还是UDP,是使用长连接还是短连接?NIO还是BIO?netty、mina还是原生socket?
                操作系统选取,是使用winserver还是Linux?或者Unix?
                硬件配置?是8G内存还是32G,网卡10G还是1G?
                ……
                ……

以上的这些问题在高并发中都是必须要深入考虑的,就像木桶原理一样,只要其中的某一方面没有考虑到,都会造成系统瓶颈,影响整个系统的运行。而高并发问题不仅仅涉及面之广,同时又要求有足够的深度!!!

   而多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。

多线程在解决高并发问题中所起到的作用就是使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。

PHP 是无处不在的,可以说是互联网 Web 应用上使用最广泛的语言。

然而,它的高性能并不为人所知,尤其是在涉及到高并发系统时。这就是为什么对于这样特殊的用例,正在被 Node (是的,我知道,它不是一种语言)、Go 和 Elixir 等语言接管。

也就是说,您可以做很多事情来改进服务器上的 PHP 性能。本文主要关注 php-fpm 方面的内容,如果您使用 Nginx,这是在服务器上的默认配置。

如果你知道 php-fpm 是什么,请直接跳到优化部分。

什么是 php-fpm?
许多开发人员对 DevOps 方面的知识不太感兴趣,即使是那些对此感兴趣的开发人员,也极少有人知道它的底层原理。有趣的是,当浏览器发送一个请求到运行 PHP 的服务器上时,PHP 也不是最先进行处理请求的服务;而是,HTTP 服务器,Apache 和 Nginx 是其中最主要的两个。「web 服务器」决定如何与 PHP 进行通信,然后传递请求的类型,数据和头部信息到 PHP 进程。

上图是 PHP 项目的请求 - 响应生命周期(图片来源: ProinerTech)

在现代 PHP 应用中,「find file」部分即为 index.php 文件,它是在服务器配置文件中配置的用于处理所有请求的代理。

如今,Web 服务器究竟如何连接 PHP 正在进化,如果我们要深入研究所有细节,这篇文章的长度将激增。但粗略来说,在 Apache 作为 Web 服务器首选的时间段,PHP 是作为包含在服务器内部的模块。

所以每当一个请求被接收,服务器将开启一个新的进程,它将自动包含 PHP 和执行请求。这个方法被称作 mod_php,“PHP 作为一个模块” 的缩写。这种方法有其局限性,而 Nginx 和 php-fpm 克服了它。

在 php-fpm 中,管理 PHP 的责任在于服务器内部的 PHP 程序。换言之,Web 服务器 (Nginx, 在本例中), 不在乎 PHP 在哪和怎样运行的,只要它知道如何发送和接收数据即可。如果需要,在这种情况下,您可以将 PHP 视为另一台服务器,它管理传入请求的某些子 PHP 进程(因此,我们将请求送到服务器,该请求由服务器接收并传递到服务器 — — 太疯狂了!:-P)。

如果你用过 Nginx, 你会看到这些代码:

     location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
对于这一行:fastcgi_pass unix:/run/php/php7.2-fpm.sock;,它告诉 Nginx 通过 php7.2-fpm.sock 的 socket 与 php 进程通信。因此,对于每个传入的请求,Nginx 都通过这个文件写入数据,在接收到输出后,将其发送回浏览器。

我必须再次强调,对于如何运行这不是最完整或者最准确的,但对于大多数 DevOps 任务是完全准确的。

除此之外,让我们回顾一下到目前为止所学到的东西:

PHP 不会直接接收浏览器发送的请求。像 Nginx 这种 Web 服务器首先会拦截它。
Web 服务器知道如何连接到 PHP 进程,并将所有请求数据(粘贴所有内容)传递到 PHP 上。
PHP 完成其职责后,会将响应发送回 Web 服务器,然后将其发送回客户端(在大多数情况下为浏览器)。
流程图如下:

PHP 和 Nginx 如何协同工作? (图片来源:数据狗)

到目前为止都不错,那么关键问题来了:PHP-FPM 到底是什么呢?

PHP 中的 FPM 代表 「快速进程管理器」, 花式解释就是说,在服务器上运行的 PHP 并不是单个进程,而是由这个 FPM 进程管理器派生、控制和终止的一些 PHP 进程。web 服务器将请求传递给的就是这个进程管理器。

PHP-FPM 本身就是一个完整的兔子洞,所以如果您愿意,可以随意探索,但是对于我们的目的,这些解释就足够啦。 ?

为什么要优化 php-fpm?
一般在正常运行的情况下,为什么要考虑优化呢? 为什么不将事物保持原样。

具有讽刺意味的是,一般我为大多数用例提供建议的话。 如果您的设置运行良好,并且没有特殊用例,请使用默认设置。 但是,如果您希望扩展一台机器之外的能力,那么从一台机器中挤出最大的处理能力是必不可少的,因为它可以将您服务器的花费减少一半(甚至更多!)。

要说明的另一件事情是,Nginx 是为处理巨大的工作负载而构建的。 它能够同时处理成千上万的连接,但是如果您的 PHP 设置不合理,那么您将浪费很多资源,因为 Nginx 必须等待 PHP 完成当前处理之后才可以接受下一个请求,最终 Nginx 不能为您的服务提供任何优势!

所以,接下来让我们看看尝试优化 php-fpm 时我们到底要优化什么。

如何优化 PHP-FPM ?
php-fpm 的配置文件在不同服务器上的位置可能不同,因此您需要做一些调查来确定它的位置。在 UNIX 上,你可以使用 find 命令。在我的 Ubuntu 上,它的路径是 /etc/php/7.2/fpm/php-fpm.conf 。当然,7.2 是我正在运行的 PHP 版本。

下面是这个文件的前几行代码:

;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

; All relative paths in this configuration file are relative to PHP's install
; prefix (/usr). This prefix can be dynamically changed by using the
; '-p' argument from the command line.

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
; Pid file
; Note: the default prefix is /var
; Default Value: none
pid = /run/php/php7.2-fpm.pid

; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; into a local file.
; Note: the default prefix is /var
; Default Value: log/php-fpm.log
error_log = /var/log/php7.2-fpm.log
很明显:这一行 pid = /run/php/php7.2-fpm.pid 告诉我们哪个文件包含了 php-fpm 进程的进程 id。

我们还看到 /var/log/php7.2-fpm.log 是 php-fpm 存储日志的地方。

在这个文件中,像下面这样添加三个变量:

emergency_restart_threshold 10
emergency_restart_interval 1m
process_control_timeout 10s
前两个设置是警告性的,它们告诉 php-fpm 进程,如果 10 个子进程在一分钟内失败,主 php-fpm 进程应该重新启动自己。

这听起来可能不够稳健,但是 PHP 是一个短暂的进程,它会泄漏内存,所以在出现高故障时重新启动主进程可以解决很多问题。

第三个选项是 process_control_timeout,它告诉子进程在执行从父进程接收到的信号之前需要等待这么长的时间。这个设置是非常有用的。例如,当父进程发送终止信号时,子进程正在处理某些事情的时候。十秒的时间,他们会有一个更好的机会完成任务并且优雅地退出。

令人惊讶的是,这 不是 php-fpm 的核心配置!这是因为,为了 web 请求服务,php-fpm 创建了一个新的进程池,它将具有一个单独的配置。在我的例子中,进程池的名称是 www,我想编辑的文件是 /etc/php/7.2/fpm/pool.d/www.conf。

让我们来看看文件的内容:

; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]

; Per pool prefix
; It only applies on the following directives:
; - 'access.log'
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values'
; - 'php_admin_values'
; When not set, the global prefix (or /usr) applies instead.
; Note: This directive can also be relative to the global prefix.
; Default Value: none
;prefix = /path/to/pools/$pool

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
user = www-data
group = www-data
快速浏览一下上面代码片段的末尾,您就会明白为什么服务器进程以 www-data 的形式运行了。如果您在设置网站时遇到文件权限问题,您可能要将目录的所有者或组更改为 www-data,从而允许 PHP 进程写入日志文件和上传文档等。

最后,我们到达了问题的根源,流程管理器 (pm) 设置。一般情况下,默认值是这样的:

pm = dynamic
pm.max_children = 5
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 200
那么,这里的 「dynamic(动态)」是什么意思呢?我认为官方文档最好地解释了这一点 (我的意思是,这应该已经是您正在编辑的文件的一部分,但是我在这里复制了它,以防它不是):

; Choose how the process manager will control the number of child processes.
; Possible Values:
;   static  - a fixed number (pm.max_children) of child processes;
;   dynamic - the number of child processes are set dynamically based on the
;             following directives. With this process management, there will be
;             always at least 1 children.
;             pm.max_children      - the maximum number of children that can
;                                    be alive at the same time.
;             pm.start_servers     - the number of children created on startup.
;             pm.min_spare_servers - the minimum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is less than this
;                                    number then some children will be created.
;             pm.max_spare_servers - the maximum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is greater than this
;                                    number then some children will be killed.
;  ondemand - no children are created at startup. Children will be forked when
;             new requests will connect. The following parameter are used:
;             pm.max_children           - the maximum number of children that
;                                         can be alive at the same time.
;             pm.process_idle_timeout   - The number of seconds after which
;                                         an idle process will be killed.
; Note: This value is mandatory.
由此可见,有三个可用值:

Static: 无论什么情况,都会保持一个固定的 PHP 进程数量。
Dynamic: 我们需要指定 php-fpm 在任何给定时间点会保持活动的最小以及最大进程数量。
ondemand: 按照需求创建和销毁进程。
那这些设置有什么影响呢?

简而言之,如果你有个小流量的网站,“dynamic” 设置在大多数时间内都是一种资源的浪费。假设你的 pm.min_spare_servers 设置成了 3,那会有三个 PHP 进程会被创建并保持运行,甚至是网站没有流量时。这种情况下,“ondemand” 就是个更好的选择,可以让系统决定何时启动新的进程。

另一方面,大流量 或者必须快速响应的网站将在这种情况下被惩罚。 最好避免创建新的 PHP 进程的额外开销,使其成为池的一部分并对其进行监控。

使用 pm = static 固定子进程的数量,使最大的系统资源用于服务请求而不是管理 PHP。假如你确定走这条路,注意它有其指导方针和陷阱。关于它的一篇相当密集但非常有用的文章是 这篇 。

写在最后

由于有关网络性能的文章可能会引发争论或使人们感到困惑,因此在结束本文之前,我觉得需要讲几句话。 性能调优既涉及系统知识,也涉及猜测和技巧。

即使您完全了解 php-fpm 的所有设置,也无法保证成功。 如果您不了解 php-fpm 的存在,那么您就不必浪费时间担心它。 继续做您已经在做的事情并继续下去。

同时,尽可能不让结果变得很戏剧性。 是的,您可以通过从头开始重新编译 PHP 并删除所有不需要的模块来获得更好的性能,但是这种方法在生产环境中不够明智。 优化某些内容的整个想法是查看您的需求是否与默认值不同(它们很少这样做!),并根据需要进行较小的更改。

并发

一般由单位内完成的请求数来衡量,如,每秒事务数(TPS),每秒HTTP请求数(HPS),每秒查询数(QPS)。通常情况下,我们说PHP的并发,都是指一秒内PHP完成的动态请求的次数。如某网站高峰期的动态请求并发为5000每秒,这个数字不算太高,但也不低。一般日活跃用户数在1000万-5000万的网站应用才能达到这个级别。

性能

一般是指应用程序的处理速度,如果php的应用程序,打开一个页面(执行一个脚本程序)通常需要在50-100ms完成,这对程序的性能要求还是比较高的。但是这还仅仅只是程序处理,php处理完成之后,还要交给web server,web server再将数据返回浏览器,这中间会有一个网络延迟,通常网络正常的情况下,需要大约100ms,最终一个动态网页的请求大约200ms(理想的情况下)可以到达用户浏览器端(仅仅是一个html结构)。

资源分配

php-fpm进程数

按照上面的描述,并发为5000每秒,每个请求完成大约200ms(具体页面要具体分析,这里只是一个理想值),如果只有5台PHP应用程序服务器,那么每台机器平均为并发1000每秒,如果是使用nginx+php-fpm的架构,php-fpm的php-cgi进程管理器的配置应该如何呢?我计算的结果为(具体的配置项说明在后文):

pm=static
pm.max_children=100

上面的100是如何得来的,由于机器平均并发为1000每秒,每个动态请求的处理时间为100ms,也就是说1个php-fpm的worker处理进程在1秒内可以处理10个请求,100个php-fpm的worker处理进程,就可以处理1000个请求。

当然需要结合服务器硬件资源来进行配置,如果配置不当,很容易在请求高峰期或者流量猛增导致服务器宕机。

网络带宽

网络带宽也会是一个重要的因素,如果你的服务处理很强,但是用户的请求和响应不能及时到达也是白忙活,这个参数如何计算呢?

并发5000每秒,每个请求的输出为20K,则5000x20K=100000K=100M

这就要求你的公网负载均衡器外网出口带宽至少要达到100M

内存

上述中100个php-fpm的worker处理进程,理论上如果服务器只运行php-fpm,那么我们可以将服务器内存的一半分配给php-fpm,通常情况下,我们可以认为一个php-fpm的worker处理进程占用内存20M,那么100x20M=2G,也就是说明服务器的内存大约为4G

CPU

由于php-fpm是一个多进程的模型应用,CPU进程调度消耗也是很大的,并且PHP应用程序有问题也会导致CPU占用率高,这就没有量化的指标,需要具体情况具体分析了。但是有一个小建议,可以部署一个crontab每隔一分钟检测cpu占用率超过多少就kill掉相应的php-fpm的worker处理进程。

php-fpm Unix Socket

如果nginx与php在同一台机器,nginx与php-fpm使用unix域套接字代替tcp socke进行通信,这个配置挺关键的,纯echo的ab测试,采用unix域套接字每秒请求数提升10%-20%

即nginx中配置:

fastcgi_pass unix:/data/server/var/php/php-fpm.sock;

php-fpm.conf中配置:

listen = /data/server/var/php/php-fpm.sock

最后遇到很多同学对php-fpm的进程管理器的核心配置不太了解,下面是我翻译的配置说明:

php-fpm配置项

pm

进程管理器以控制子进程的数量,可能的值有

static                        一个固定的值,由pm.max_children指定
dynamic                       动态的(工作方式和Apache的prefork模式一致),但是保持至少一个,由
                              pm.max_children             在同一时间最大的进程数
                              pm.start_servers              php-fpm启动时开启的等待请求到来的进程数
                              pm.min_spare_servers    在空闲状态下,运行的最小进程数,如果小于此值,会创建新的进程
                              pm.max_spare_servers   在空闲状态下,运行的最大进程数,如果大于此值,会kill部分进程
ondemand                      启动时不会创建进程,当请求达到时创建子进程处理请求
                              pm.max_children 在同一时间最大的进程数
                              pm.process_idle_timeout  空闲多少秒之后进程会被kill
pm = static

pm.max_children

在同一时间最大的进程数

pm.max_children = 120

pm.start_servers

php-fpm启动时开启的等待请求到来的进程数,默认值为:min_spare_servers + (max_spare_servers - min_spare_servers) / 2

pm.start_servers = 80

pm.min_spare_servers

在空闲状态下,运行的最小进程数,如果小于此值,会创建新的进程

pm.min_spare_servers = 60

pm.max_spare_servers

在空闲状态下,运行的最大进程数,如果大于此值,会kill部分进程

pm.max_spare_servers = 120

pm.process_idle_timeout

空闲多少秒之后进程会被kill,默认为10s

pm.process_idle_timeout = 10s

pm.max_requests

每个进程处理多少个请求之后自动终止,可以有效防止内存溢出,如果为0则不会自动终止,默认为0

pm.max_requests = 5000

pm.status_path

注册的URI,以展示php-fpm状态的统计信息

pm.status_path = /status

其中统计页面信息有:

pool                         进程池名称
process manager              进程管理器名称(static, dynamic or ondemand)
start time                   php-fpm启动时间
start since                  php-fpm启动的总秒数
accepted conn                当前进程池接收的请求数
listen queue                 等待队列的请求数
max listen queue             自启动以来等待队列中最大的请求数
listen queue len             等待连接socket队列大小
idle processes               当前空闲的进程数
active processes             活动的进程数
total processes              总共的进程数(idle+active)
max active processes         自启动以来活动的进程数最大值
max children reached         达到最大进程数的次数

ping.path

ping url,可以用来测试php-fpm是否存活并可以响应

ping.path = /ping

ping.response

ping url的响应正文

ping.response = pong

猜你喜欢

转载自blog.csdn.net/weixin_46742102/article/details/109516360