月度归档:2016年06月

CGI – FastCGI – FPM PHP-FPM

CGI – Common Gateway Interface的简写,一般叫做通用网关接口。CGI程序就是指可以使用某某解释器解释的脚本程序,比如Perl、PHP、Shell脚本等。工作流程大体是这样:HTTP服务器根据请求调用相应的CGI脚本,这个时候会从HTTP服务器fork一个进程,在这个进程内用脚本声明的解释器执行脚本,HTTP服务器接收标准输出,然后CGI进程退出。当有新的请求过来,继续这样的流程。在Apache Httpd服务器中,提供了CGI模块,为了加深认识,这里配置一下:

yum install httpd

vi /etc/httpd/conf/httpd.conf
#修改下端口,避免冲突
Listen *:8080
#明确指定服务器名称
ServerName localhost:8080

#修改cgi-bin目录下的文件都是cgi脚本,都可以执行
<Directory "/var/www/cgi-bin">
    AllowOverride All
    Options +ExecCGI
    #AddHandler cgi-script .cgi
    SetHandler cgi-script
    Require all granted
</Directory>

#由于是演示,少开几个进程
<IfModule mpm_prefork_module>
    StartServers 2
    MinSpareServers 2
    MaxSpareServers 6
    ServerLimit 200
    MaxClients 150
    MaxRequestsPerChild 30
</IfModule>

#启动服务器器
systemctl start httpd.service
# - or -
/usr/sbin/httpd

然后分别建立三种类型的脚本(shell, perl, php)

cd /var/www/cgi-bin

## 1 Shell 
vi cgi.sh
 
#!/bin/sh
set -f
echo "Content-type: text/plain; charset=utf-8"
echo
echo CGI Shell Test----------------------------:
echo
echo argc is $#. argv is "$*".
echo
echo SERVER_SOFTWARE = $SERVER_SOFTWARE
echo SERVER_NAME = $SERVER_NAME
echo GATEWAY_INTERFACE = $GATEWAY_INTERFACE
echo SERVER_PROTOCOL = $SERVER_PROTOCOL
echo SERVER_PORT = $SERVER_PORT
echo REQUEST_METHOD = $REQUEST_METHOD
echo HTTP_ACCEPT = "$HTTP_ACCEPT"
echo PATH_INFO = "$PATH_INFO"
echo PATH_TRANSLATED = "$PATH_TRANSLATED"
echo SCRIPT_NAME = "$SCRIPT_NAME"
echo QUERY_STRING = "$QUERY_STRING"
echo REMOTE_HOST = $REMOTE_HOST
echo REMOTE_ADDR = $REMOTE_ADDR
echo REMOTE_USER = $REMOTE_USER
echo AUTH_TYPE = $AUTH_TYPE
echo CONTENT_TYPE = $CONTENT_TYPE
echo CONTENT_LENGTH = $CONTENT_LENGTH

## 2 Perl
vi cgi.perl

#!/usr/bin/perl

print "Content-type: text/plain; charset=utf-8\n\n";

foreach $var (sort(keys(%ENV))) {
    $val = $ENV{$var};
    $val =~ s|\n|\\n|g;
    $val =~ s|"|\\"|g;
    print "${var}=\"${val}\"\n";
}

## 3 PHP
#!/usr/bin/php
<?php
echo "Content-type: text/plain; charset=utf-8\n\n";

print_r($_SERVER);

确保这些程序是可以执行的(chmod 777 *), 然后通过浏览器访问:
1 Shell的输出
cgi-sh

2 Perl的输出
cgi-perl

3 PHP的输出
cig-php

看到以上的输出,应该对CGI会有一个比较直观的了解。 然后把cgi.php修改一下:

#!/usr/bin/php
<?php
echo "Content-type: text/plain; charset=utf-8\n\n";

$i = 20;
while($i > 0) {
	echo $i." sleep...<br />";
	sleep(2);
	$i--;
}

在进程没有退出前,查看进程:
cgi-ps
很明显,从httpd派生了一个进程,然后把脚本传递给php解释器。

HTTP服务器会把环境变量(比如请求参数等)传递给CGI解释器,比如PHP解释器的$_SERVER输出就能取到传递过来的参数。而CGI程序需要输出一个MIME类型的头(”Content-type: text/plain; charset=utf-8\n\n”),之后跟一个空行(Shell中无法解释转义,需要明确输出空行),这些几乎就是大部分内容了。

FastCGI是为了解决CGI的缺陷而引入的。CGI的缺陷是非常明显的,每一个请求都启动一个进程,用完了后退出。FastCGI要解决的问题是进程启动,转入解释器,解释脚本,完成后不退出,继续等待其它脚本的投递。比如启动10个FastCGI进程,形成一个进程池。PHP中提供了php-cgi(php-cgi.exe),可以非常便利实现这个进程池:

#启动三个php-cgi
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000

这样在Nginx中,可以直接使用:

    location ~ \.php$ {
        client_max_body_size    100M;
        
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout    300;
        fastcgi_read_timeout    300;
        fastcgi_buffer_size     32k;
        fastcgi_buffers     256 32k;
 
        fastcgi_pass        127.0.0.1:9000;
        fastcgi_index       index.php;
 
        fastcgi_param       HTTPS $https if_not_empty;
 
        fastcgi_param       SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include             fastcgi_params;
   }

指令fastcgi_param定义的参数,会传递给FastCGI进程。在Window下为了使用Nginx+PHP组合,就必须这么做了。

现在这个模式解决了CGI资源消耗问题,但是又有了新的问题:一次性启动的进程,如果因为某种原因异常退出了,如何重启未解决;多个进程监听同一个端口,那么Nginx投递请求过来时,必定引起进程争抢请求现象(这个是资源浪费);如果请求量加大,无法动态加大进程池的数量(PHP是单进程堵塞模式,每次只能处理一个请求)。

看起来,这个进程池里面的进程,需要一个主管。它负责把请求交给谁处理,监控进程池的进程是否异常退出然后重启,根据请求情况动态增加进程池进程数量等等。这个就是进程管理器的概念。一般称为Fast CGI Process Manager,简称就是FPM,而PHP实现的FPM就叫PHP-FPM。PHP中的PHP-FPM主进程目前应该使用的是Reactor模型,它负责hold住请求,socket监听绑定(没有FPM时是各个进程同时监听和绑定,被唤醒时存在争抢),FPM可以均衡控制子进程处理的请求数量,在到达一定数量后,可以重启该进程(防止内存泄露问题),监控进程异常退出,动态加大进程池等。总之,PHP-FPM是一个非常不错的实现(Window下没有这个FPM)。

接下来具体看看PHP-FPM模型:

安装了php-fpm模块后,会对应一个服务启动脚本:

cat /usr/lib/systemd/system/php-fpm.service
[Unit]
Description=The PHP FastCGI Process Manager
After=syslog.target network.target

[Service]
Type=notify
PIDFile=/var/run/php-fpm/php-fpm.pid
EnvironmentFile=/etc/sysconfig/php-fpm
ExecStart=/usr/sbin/php-fpm --nodaemonize --fpm-config /etc/php-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

可以看到,/usr/sbin/php-fpm就是进程管理器,它接收配置文件,根据配置启动进程池。进程池里面的进程不是调用php-cgi而来的,而是从php-fpm fork出来的,php-fpm进程是常驻内存的,解释器(也可以认为是zend引擎)在每个进程中都独立存在一份,它的所谓opcode缓存也是在进程没有退出前有效的,由于PHP的特点,脚本执行完毕,脚本相关的一切变量都会被销毁(可以看做是在一个沙箱中执行代码,完了后把整个沙箱干掉),基于这样的事实,什么垃圾回收之类的都是浮云,垃圾回收也只能在脚本运行中回收。另外,当脚本中大量include文件进来时,这些文件编译后可以被缓存起来(opcode),但是缓存也是有大小限制的,当新的请求进来时,如果include了相同的文件,那么就可以从缓存中取出来执行,如果没有就从文件系统读入文件,编译成字节码,放入缓存,执行字节码。

注:这里无法确定的是当有缓存时,是否需要搬动。

nginx-php-fpm

root       25711     1  nginx: master process /usr/sbin/nginx
www      25712 25711  nginx: worker process
www      25713 25711  nginx: worker process

root       25726     1  php-fpm: master process (/etc/php-fpm.conf)
www      25727 25726  php-fpm: pool www
www      25728 25726  php-fpm: pool www

/etc/php-fpm.conf

#################################
#全局配置
#################################
[global]
pid = /var/run/php-fpm/php-fpm.pid
error_log = /var/log/php-fpm/error.log
;syslog.facility = daemon
;syslog.ident = php-fpm
;log_level = notice
;emergency_restart_threshold = 0
;emergency_restart_interval = 0
;process_control_timeout = 0
;process.max = 128
;process.priority = -19
daemonize = yes
;rlimit_files = 1024
;rlimit_core = 0
;events.mechanism = epoll
;systemd_interval = 10
include=/etc/php-fpm.d/*.conf

/etc/php-fpm.d/www.conf

#################################
#针对池的配置
#################################
[www]
listen = 127.0.0.1:9000
;listen.backlog = -1
listen.allowed_clients = 127.0.0.1

;listen.owner = nobody
;listen.group = nobody
;listen.mode = 0666

user = www
group = www

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
;pm.max_requests = 500

;pm.status_path = /status
;ping.path = /ping
;ping.response = pong
;request_terminate_timeout = 0
;request_slowlog_timeout = 0
slowlog = /var/log/php-fpm/www-slow.log
 
rlimit_files = 10240
;rlimit_core = 0
;chroot = 
;chdir = /var/www
;catch_workers_output = yes
;security.limit_extensions = .php .php3 .php4 .php5

;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp

;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 10240M

; Set session path to a directory owned by process user
php_value[session.save_handler] = files
php_value[session.save_path]    = /var/lib/php/session
php_value[soap.wsdl_cache_dir]  = /var/lib/php/wsdlcache

当配置了多个池时,如果日志都写入/var/log/php-fpm/,那么需要确保不同池的用户都可以往这个文件夹写入内容。同样的/var/lib/php也是如此。

错误日志:
如果应用程序由捕捉异常,相关的日志信息由程序的自定义程序处理。如果没有就会上交到PHP,如果PHP中配置了记录日志的位置,那么日志就会被记录到这个文件中,如果没有就会上交给HTTP服务器,比如Nginx,在HTTP服务器这个级别,如果对不同域做了配置,那么就记录到这个域下的配置中,如果没有则统一记录到error_log指令指向的文件。

参考:http://blog.ifeeline.com/1901.html