php下数据库持久连接,及apache模块下“数据库并发连接数超限”的潜在风险

php下的多个数据库引擎都提供持久连接的特性,实现了“连接池”的作用,让数据库连接“复用”,目的是减少php引擎连接数据库的消耗。这有类似fastCGI协议的设计初衷:让后端进程复用,节省启动关闭CGI进程的性能开锁。

数据库持久连接的实现方式

这需要从php的运行模式说起。典型的php运行模式是传统CGI、fastCGI、web模块三种。

其中CGI模式不支持持久连接,因为php每次处理请求,都是由一个独立的进程(操作系统的进程)处理,请求处理完毕,进程就销毁了,相应的数据连接之类的资源当然也已不存在,所以CGI本身是不支持持久连接的。

fastCGI模式下,php进程由进程管理器所管理。(apache下实现实例,参看这里)。不管哪种fastCGI方案,其背后都是一系列长期运行的进程(操作系统下的进程),进程本身可以保持资源,因此,php脚本引擎可以提供应用的接口,允许程序员将数据库连接保持下来,供下次php处理请求,可以直接复用这个连接。

web模块下,类似fastCGI模式。linux下apache默认prefork下,每个httpd进程在同一时刻只响应一个http请求,每个httpd进程可以看做一个fastCGI进程。

多数据库账号的持久连接

假设一web服务器下的所有应用,都是持久连接,并且使用了惟一的数据库连接账号。假设共开了10个fastCGI进程在运行,每个进程都保持了一个持久连接,如果当前处理请求需要连接数据库,直接使用该持久连接即可,不需要新连接数据库。事实上,运行一段时间后,就是这样状态。

假设该web服务器下的应用,共有10个数据库连接账号。每fastCGI进程,从启动开始,每处理一个新的数据库账号相关的请求,就要多保持一个持久连接。因为不同数据库账号的连接,肯定不能复用的。这样,在运行一段时间后,每个fastCGI进程都要维持10个持久连接,分别对应每个数据库账号。

以apache模块模式下运行的httpd进程,可以等同于一个fastCGI进程,上面讨论同样适用。

进程数及连接数讨论,及apache下的潜在风险!

如果web服务器下的的php应用,分别使用了多个数据库账号,而且全部连到同一台数据库服务器。这样,

该数据库的并发连接数 = fastCGI进程数 * 数据库账号数
如果php在apache模块下运行,使用httpd进程数代替fastCGI进程数

通常,fastCGI进程数量是比较有限的,对于一台web服务器,它接受的请求里,大多数请求都是静态的(想像一下,一个页面里,通常只有主html文档是动态,而里面的js,css,图片等等元素都是静态;这里不考虑“静态内容全部移到CDN上”的极端情况)。fastCGI进程数数,通常会远比http并发数小。

在apache模块运行的php下,所有请求都是由httpd进程处理的,每个httpd进程都有可能维护每个数据库用户相关的持久连接,如果数据库用户量较大,这个对mysql服务器并发的连接数影响非常大。通常mysql服务器都会设置一个最大并发数据,超过限制后,就不再受新连接!

php下持久连接的更多信息,参考官方文档 http://php.net/manual/zh/features.persistent-connections.php

 

CGI原理示例,及CGI,FastCGI,php-cgi,php-fpm等的总结

CGI

CGI全名“通用网关接口”(Common Gateway Interface),是一个技术规范,用来动态生成网页html。理论上可以使用任意语言写,只要支持标准输入输出及环境变量即可(标准输入输出概念参考C语言中stdio库的printf函数)。

举例简述一下实现细节,以类C语言伪代码演示(不想了解CGI细节可以跳过)

CGI程序 /usr/local/cgi/hello

printf("Content-type: text/html; charset=UTF-8")
printf("\r\n\r\n")
printf("hello world")

注意两个连续换行符号,它们是http头与内容的分隔。

假设web服务器,把对 http://localhost/hello的请求,转给 /usr/local/cgi/hello 处理,这就要启用一个进程支行hello程序,hello程序执行后标准输出结果如下

Content-type: text/html; charset=UTF-8

hello world

web服务器拿到结果,把并把hello程序的输出结果添加其它http头元素,如HTTP状态码、Connection: keep-alive等,这就是完整的http响应消息内容了,把它们一并发回给浏览器,一次CGI的http请求就完成。

这是个最简单的示例,我们忽略了http请求数据、本地目录文件等,事实上的程序会复杂得多。

通常是这样的,一个cgi-bin目录里,放置CGI可执行程序(类比上面的hello程序),web服务器通常不是把所有请求都通过CGI来处理的,而是把符合一定规则(比如url路径以.pl结尾)的请求,启动CGI进程,并将对应的web目录文件(比如hello.pl)传给CGI程序,CGI程序按hello.pl文件的内容执行,标准输出结果再由web服务器拿到并补充http头等,生成http影响数据,发回给浏览器。

这里的CGI程序的行为,与脚本语言解释器类似(本地目录的相关文件当脚本程序,被CGI程序解释执行)

CGI的具体细节参考rfc3875,或 英文维基CGI词条

FastCGI

传统的CGI程序,对每次http请求,都要启动一个操作系统层面的进程process,进程处理一次就结束。下次http请求还得再起动。启动进程对于操作系统来说是较耗费时间的操作,所以在CGI的负载能力比较有限,在启动关闭进程上浪费严重性能。

针对这一点,FastCGI协议被出了:让CGI进程长期驻守,有请求,就直接给它处理,没有就等待。

可以这样认为,FastCGI是CGI的改进版,虽然并不完全兼容。传统的CGI要改造成FastCGI,很多代码可以直接借用的。

因为CGI/FastCGI协议都是语言无关的,只要能按这个规范来,任何语言都可以。所以有很多具体的技术支持,比如php的CGI/FastCGI模式运行;这点后面讨论。

单个FastCGI进程的负载是有限的,所以通常需要多个进程同时工作,这就可以同时接受更多请求,提高并发数。

如果某个进程运行时间长了,占用了过多内存,或者该进程僵死了,需要结束它,并启动新进程;或者想在并发量大时,自动增加FastCGI进程数,而并发小时,自动减少;...这些就需要有有“人”来管理,就是FastCGI进程管理员的工作了。

php-cgi

编译后php的二进制文件里,在bin目录里有php-cgi文件(在windows版里是,php-cgi.exe),即是支持CGI/FastCGI的可执行程序。

可以这样执行一个php文件  /path/to/php-cgi /path/to/your-php-file.php

php-fpm

前面说过,FastCGI程序是驻守系统进程中的,该进程不可能凭空启动,那这个进程由谁管理呢?这就是FastCGI进程管理员 (FastCGI Process Manager)。对于PHP而言,在php 5.3以后的版本,自带的php-fpm即是php的fpm。(php 5.2及以前版本,可以通过补丁的形式将php-fpm整合到php中,或使用其它方案)。

不过,支持php的fpm并非只有php-fpm,还有其它方案,只要起到管理fastCGI进程的启动、关闭等 功能即可。apache下的mod_fcgid即是另一个可选方案。所以,在apache下,如果使用mod_fcgid整合php,就不需要php-fpm了。有更广泛支持的Spawn-FCGI是另一个备选项。

 

 

CentOS 6.x/apache 2.2下php多版本共存探索(模块及fastCGI)/mod_fcgi,mod_proxy_fcgi实现

在apache下整合fastCGI模式运行的php-fpm,似乎网上很少相关材料,就连英文版材料也少。只要是php-fpm,基本上都是与nginx搭配。查了一大批相关资料,写本文总结一下。

apache下有多个fastCGI的支持方案:至少有mod_fcgimod_fastcgigit)、mod_proxy_fcgi等。这两个模块都有点老,尤其mod_fastcgi自从2007年以来就没有更新,略掉不谈,事实上没用过用。mod_proxy_fcgi模块是httpd 2.4+的版本正式引入,通过简洁的一行 ProxyPassMatch 指令即可。

mod_fcgi

mod_fcgi模块本身是做fastCGI进程管理的,使用它就不需要使用php-fpm管理进程了。核心配置参数

LoadModule fcgid_module modules/mod_fcgid.so
<VirtualHost *:80>
    DocumentRoot "/var/www/html/site_1"
    ServerName "www.yourhost.com"
    DirectoryIndex index.html index.php
    #php.ini的存放目录,Linux下通常不需要
    #FcgidInitialEnv PHPRC "D:/php"
    # 设置PHP_FCGI_MAX_REQUESTS大于或等于FcgidMaxRequestsPerProcess,防止php-cgi进程在处理完所有请求前退出
    FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000
    #php-cgi每个进程的最大请求数
    FcgidMaxRequestsPerProcess 1000
    #php-cgi最大的进程数
    FcgidMaxProcesses 3
    #最大执行时间
    FcgidIOTimeout 600
    FcgidIdleTimeout 600
    #php-cgi的路径
    FcgidWrapper /usr/local/php7/bin/php-cgi .php
    AddHandler fcgid-script .php
    FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000
    <Directory "/var/www/html/site_1">
        Options +ExecCGI
    </Directory>
</VirtualHost>

其中粗体着色是必须参数,其它几个Fcgid*指令,是优化之用,这里仅示例,要按实际情况调整数值。具体参看mod_fcgi官方文档

使用mod_fcgid的几个特点

  1. php-fgi进程是由apache模块启动并管理,不需要配置php-fpm
  2. 在php-cig进程以apache用户身份运行,php程序写的文件,其权限为apache用户(而不像php-fpm下写文件为php-fpm用户所有,默认是nobody),这样在目录权限管理方面一致性高些。

mod_fastcgi

虽然CentOS 6.x下是apache 2.2,但所幸已经有人成功移植: https://github.com/ceph/mod-proxy-fcgi 我们可以直接使用;更幸运的是它已经进入epel源,直接yum安装即可;不想匹配epel源的,直接下载rpm包安装也可以(示例 http://mirrors.ustc.edu.cn/epel/6/x86_64/

当然可以重新编译安装apache 2.4, 这样直接有mod_proxy_fcgi可以使用,但这里还是保持原版本不变,省掉编译的工作量。

参考mod_proxy_fcgi官方文档,整合php-fpm的配置指令

ProxyPassMatch "^/myapp/.*\.php(/.*)?$" "fcgi://localhost:9000/var/www/"

语法很简单,跟配置反向代理类似,可以按实际需要做修改。事实上与mod_proxy模块语法一致的,不同处是将http协议改成fcig协议。

以上是apache整合php-fpm模式运行的fastCGI,接下来要对yum安装的php做下配置修改。

yum安装的php配置文件 /etc/httpd/conf.d/php.conf ,其中有如下一行

AddHandler php5-script .php

我们要对不同的站点启用不同的php,上面一行是对全局的.php文件分配给php模块处理,我们把这一行注释掉。而是在每个站点启用不同的php运行模式。

以上即是处理方式。

[已知问题]:裸目录地址转发

有一个困扰的问题没有解决,感觉有点像模块bug:

对于配置了DirectoryIndex index.php的目录,如果其子目录没有index.php,上述ProxyPassMatch还是会做fastCGI转发,这时会看到php-fpm的404响应,而不是apache的响应403页面。但前面的规则并不转发这裸空目录的url,所以感觉像bug

再者就是,对于ProxyPassMatch匹配的目录,apache自动索引功能失效。(当然如果不开启autoindex就无所谓了。生产环境下通常不开启的)

其它,似乎也没有什么严重后果,或者我没还意识到(?)。

解决方法:每个目录下,都放置一个index.html,避免fpm-php处理空请求