gongjiang 2019-09-18
声明: 我这篇博客是手抄 理解 Nginx 与 PHP-FPM 通信的工作机制 ,目的是加强我的记忆。
还有一更详细的, 深入剖析 Web 服务器与 PHP 应用之间的通信机制 - 掌握 CGI 和 FastCGI 协议的运行原理
浏览器请求网页的过程
请求静态页面
Borwser 请求 index.html -》 Web server (nginx/apache)分发,找到index.html 文件直接返回给Browser。
请求动态脚本
Browser 请求 index.php -》 Web server(nginx/apache)分发-》 php解析器(php-CGI 程序)-》返回处理结果给Web server -》 返回给 Browser。
原理是,服务器根据配置文件,知道这是一个脚本文件,然后需要去找php解析器来处理,php解析器会根据解析php.ini文件初始化执行环境,然后处理请求,再以标准的数据格式返回处理结果,最后退出进程。
CGI到FPM进化史
CGI(Common Gateway Interface)
CGI是服务器与后台语言(php/python)交互的协议,有了这个协议,开发者可以使用任何语言来处理服务器转发过来的请求,动态地生成内容,保证了传递过来的数据是标准格式的,即规定了以什么样的格式传哪些数据,包括URL、查询字符串、POST数据、HTTP header等等,方便了开发者。
PHP-CGI
php语言对应与服务器交互的CGI程序就是PHP-CGI 。
CGI程序本身只能解析请求、返回结果,不会进程管理,所以有一个致命的缺点,那就是每处理一个请求都会fork一个全新的进程,随着web的兴起,高并发越来越成为常态,这样的方式显然不能满足需求,每一次web请求都会有启动和退出,也就是最为人们诟病的fork-and-execute模式,这样在大并发的情况下机器就死翘翘了。
于是,FastCGI诞生了,CGI程序很快退出历史舞台。
FastCGI
FastCGI,顾名思义就是更快的CGI程序,用来提高CGI程序性能,它允许在一个进程内处理多个请求,而不是一个请求处理完就直接结束进程,性能上有了很大的提高。
具体地,FastCGI是如何提高性能的呢?先看CGI程序的性能问题出在哪儿。
php解析器会解析php.ini文件,初始化执行环境,就是这里了。标准的CGI程序对每个请求都会执行这些步骤,所以每个请求的时间都会bijiaochang。
那么FastCGI是怎么做的呢?
首先,FastCGI会先启动一个master进程,解析php.ini配置文件,初始化执行环境,然后再启动多个worker进程。当有请求过来的时候,master会把请求传递给worker,然后立即可以处理下一个请求。这样避免了重复的劳动,效率自然提高了。
而且,当worker不够用时,master可以根据配置预先启动几个workder等着。当空闲的worker太多时,也会主动停掉一些,这样就提高了性能也节约了资源。这就是FastCGI对进程的管理。
补充: 也有一些能够调度PHP-CGI的程序,比如说说由lighthttpd分离出来的spawn-fcgi。当然,PHP-FPM也是这样的程序,在长期的发展后,逐渐得到了大家都认可,现在非常流行。
PHP-FPM(FastCGI Process Manager)
它是FastCGI的一个实现,任何实现了FastCGI协议的服务器都可以与它进行通信。FPM之于标准的FastCGI,也提供了一些增强功能,具体可以参考官方文档:PHP: FPM Installation。
FPM是一个PHP进程管理器,包含master和worker两种进程。
master进程只有一个,负责监听端口,接受来自服务器的请求,而worker进程则一般有多个(具体数量根据配置)。每个进程内部都嵌入一个PHP解释器,是PHP代码真正执行的地方,下面是我本机上FPM的进程情况,1个master进程,2个worker进程。
$ ps -ef | grep fpm root 130 1 0 01:37 ? 00:00:01 php-fpm: master process (/usr/local/php/etc/php-fpm.conf) php-fpm 131 130 0 01:37 ? 00:00:00 php-fpm: pool www php-fpm 133 130 0 01:43 ? 00:00:00 php-fpm: pool www
从FPM接收到请求,到处理完毕,具体流程如下:
FPM处理进程流程清楚了,那么Nginx是如何发送请求给FPM的呢?
这就需要从Nginx层面来讲了。
我们知道,Nginx不仅是一个web服务器,也是一个功能强大的Proxy服务器,除了能进行http请求的代理,还能进行许多其他协议的请求代理,包括本文讲的与FPM相关的FastCGI协议。为了能够使Nginx理解FastCGI协议,Nginx提供了FastCGI模块来将http请求映射为对应的FastCGI请求。
Nginx的FastCGI模块提供了fastcgi_param指令类处理这些映射关系,下面是Nginx的一个配置文件实例,其主要工作是将Nginx中的变量翻译成PHP能理解的变量。
$ cat /usr/local/nginx/conf/fastcgi.conf fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REQUEST_SCHEME $scheme; fastcgi_param HTTPS $https if_not_empty; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name;
除此之外,就是fastcgi_pass指令了,这个指令用于指定FPM进程监听的地址,Nginx会把所有的PHP请求翻译成FastCGI请求之后发送到这个地址,下面是配置文件:
server { listen 80; server_name test.me; root /usr/local/web/myproject/public; index index.php index.html index.htm; access_log /usr/local/nginx/logs/test-access.log; error_log /usr/local/nginx/logs/test-error.log; location / { try_files $uri $uri/ /index.php?$query_string; } location ~\.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/local/web/myproject/public/$fastcgi_script_name; fastcgi_pass unix:/usr/local/php/var/run/php-fpm.sock; fastcgi_index index.php; } }
上面这个配置文件中,我们新建立一个虚拟主机,监听80端口,项目跟目录指向/usr/local/web/myproject/public; 然后我们通过location指令将所有以.php结尾的请求都交给FastCGI模块处理,从而把所有的PHP请求都交给了FPM处理,从而完成Nginx到FPM的闭环。
如此以来,Nginx与FPM的通信流程就比较清晰了。
最后,修改了php.ini配置文件后。使用PHP-FPM为什么可以平滑重启呢?
修改了php.ini之后,PHP-CGI进程是没办法平滑重启的。PHP-FPM对此的处理机制是新的worker进程用新的配置,已经存在的worker进程处理完手上的活就可以歇着了,通过这汇总机制来平滑过渡。
最后的最后,我写这个的最初原因是想知道Nginx跟PHP-FPM有哪几种通信方式。
这个问题的答案是:两种。分别是 unix socket和TCP socket。
unix socket 配置如下:
location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;; fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; }
unix socket是一种终端,可以使同一台操作系统上两个或多个进程进行数据通信。这种方式需要在Nginx配置文件中填写PHP-FPM的pid文件位置,效率比TCP socket高。
TCP socket配置如下:
location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; }
tcp socket的优点是可以跨服务器,如果Nginx和FPM不在一台机器上只能通过这种方式。
优缺点比较:
先看通信图解,如下:
从这图中不难看出,unix socket 减少了不必要的TCP开销,TCP需要经过loopback,还要申请临时端口和TCP相关资源,但是 unix socket高并发时候并不稳定,连接数爆发时,会产生大量的长时缓存,在没有面向连接协议的支撑下,大数据包可能会直接出错不返回异常。TCP这样的面向连接的协议,多少可以保证通信的正确性和完整性。
建议: 如果是同一台机器上运行Nginx和FPM,并发量不超过1000,选择unix socket方式,这样可以避免一些检查操作、路由等。因此更快、更轻。 如果面临高并发业务,选择更可靠TCP socket,还可以做负载均衡,内核优化等运维手段提维持效率。
最最最最后,既然说到web server和PHP的通信,顺便提一下我的新发现。 在我上一篇博文 keepalived+nginx高可用web架构实践 中我需要搭建一台web server,灵机一动决定用docker配置apache和PHP,分别下载两个镜像并启动容器,我需要关联两个容器。我找到一个配置,居然是apache通过FastCGI方式调用PHP的。因为以前配置apache+PHP都是直接加载PHP模块,配置如下:
LoadModule php7_module modules/libphp7.so AddType application/x-httpd-php .php <IfModule dir_module> DirectoryIndex index.html index.php </IfModule
FastCGI配置如下:
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so <VirtualHost *:80> DocumentRoot "/www/mysite.com" ServerName mysite.com ServerAlias www.mysite.com ProxyRequests Off ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/www/mysite.com/$1 <Directory "/www/mysite.com"> Options none AllowOverride none Require all granted </Directory> </VirtualHost>