PHP的MySQL持久化连接,美好的目标,却拥有糟糕的口碑,往往令人敬而远之。这到底是为啥么。近距离观察后发现,这家伙也不容易啊,要看Apache的脸色,还得听MySQL指挥。

对于作为Apache模块运行的PHP来说,要实现MySQL持久化连接,首先得取决于Apache这个web服务器是否支持Keep-Alive。

Keep-Alive

Keep-Alive是什么东西?它是http协议的一部分,让我们复习一下没有Keep-Alive的http请求,从客户在浏览器输入一个有 效url地址开始,浏览器就会利用socket向url对应的web服务器发送一条TCP请求,这个请求成功一次就得需要来回握三次手才能确定,成功以 后,浏览器利用socket TCP连接资源向web服务器请求http协议,发送以后就等着web服务器把http返回头和body发送回来,发回来后浏览器关闭socket连接, 然后做http返回头和body的解析工作,最后呈现在浏览器上的就是漂亮的页面了。这里面有什么问题呢?TCP连接需要三次握手,也就是来回请求三次方 能确定一个TCP请求是否成功,然后TCP关闭呢?来回需要4次请求才能完成!每次http请求就3次握手,4次拜拜,这来来回回的不嫌累啊,多少时间和 资源都被浪费在socket连接关闭上了,能不能一次socket TCP连接发送多次http请求呢?于是Keep-Alive就应运而生,http/1.0里需要客户端自己在请求头加入 Connection:Keep-alive方能实现,在这里我们只考虑http1.1了,只需要设置一下Apache,让它默认就是Keep- Alive持久连接模式(Apache必须1.2+才能支持Keep-Alive)。在httpd.conf里找到KeepAive配置项,果断设置为 On,MaxKeepAliveRequests果断为0(一个持久TCP最多允许的请求数,如果过小,很容易在TCP未过期的情况下,达到最大连接,那 下次连接就又是新的TCP连接了,这里设置0表示不限制),然后对于mysql_pconnect最重要的选项KeepAliveTimeout设置为 15(表示15秒)。

好了,重启Apache,测试一下,赶紧写行东西:

<?php
echo "Apache进程号:". getmypid();
?>

很简单,获取当前PHP执行者(Apache)的进程号,用浏览器浏览这个页面,看到什么?对,有看到一串进程号数字,15秒内,连续刷新页面, 看看进程号有无变化?木有吧?现在把手拿开,交叉在胸前,度好时间,1秒,2秒,3,...15,16。好,过了15秒了,再去刷新页面,进程号有没有变 化?变了!又是一个新的Apache进程了,为什么15秒后就变成新的进程了?记得我们在Apache里设置的KeepAliveTimeout吗?它的 值就是15秒。现在我们应该大致清楚了,在web服务器默认打开KeepAlive的情况下,客户端第一次http成功请求后,Apache不会立刻断开 socket,而是一直监听来自这一客户端的请求,监听多久?根据KeepAliveTimeout选项配置的时间决定,一旦超过这一时间,Apache 就会断开socket了,那么下次同一客户端再次请求,Apache就会新开一个进程来相应。所以我们之前15内不停的刷新页面,看到的进程号都是一致 的,表明是浏览器请求给了同一个Apache进程。

浏览器是怎么知道不需要重新进行TCP连接就可以直接发送http请求呢?因为http返回头里就会带上Connection:keep- alive,Keep-alive:15两行,意思就是让客户端浏览器明白,这次socket连接我这边还没关闭呢,你可以在15内继续使用这个连接,并 发送http请求,于是乎浏览器就知道应该怎么做了。

PHP怎么做

那么,PHP的MySQL连接资源是怎么被hold住的呢,这需要查看PHP的mysql_pconnect的函数代码,我看了下,大概的做法就 是mysql_pconnect根据当前Apache进程号,生成hash key,找hash表内有无对应的连接资源,没有则推入hash表,有则直接使用。有些代码片段可以说明(具体可查看PHP5.3.8源码 ext/mysql/PHP_mysql.c文件690行PHP_mysql_do_connect函数)

	#1.生成hash key
user=php_get_current_user();//获取当前PHP执行者(Apache)的进程唯一标识号
//hashed_details就是hash key
hashed_details_length = spprintf(&hashed_details, 0, "MySQL__%s_", user);
#2.如果未找到已有资源,就推入hash表,名字叫persistent_list,如果找到就直接使用
/* try to find if we already have this link in our persistent list */
if (zend_hash_find(&EG(persistent_list), hashed_details, hashed_details_length+1, (void **) &le)==FAILURE) {
/* we don't */
...
...
/* hash it up(推入hash表) */
Z_TYPE(new_le) = le_plink;
new_le.ptr = mysql;
if (zend_hash_update(&EG(persistent_list), hashed_details, hashed_details_length+1, (void *) &new_le, sizeof(zend_rsrc_list_entry), NULL)==FAILURE) {
...
...
}
}
else
{/* The link is in our list of persistent connections(连接已在hash表里)*/
...
...
mysql = (PHP_mysql_conn *) le->ptr;//直接使用对应的sql连接资源
...
...
}

zend_hash_find比较容易看明白,原型是zend_hash_find(hash表,key名,key长,value);如果找到,value就有值了。

MySQL的wait_timeout和interactive_timeout

说完Keep-Alive,该到MySQL家串串门了,说的是mysql_pconnect,怎么能绕开MySQL的设置。影响 mysql_pconnect最重要的两个参数就是wait_timeout和interactive_timeout,它们是什么东西?先撇一边,首先 让我们把上面的代码改动一下PHP代码

<?php
$conn = mysql_pconnect("localhost","root","123456") or die("Can not connect to MySQL");
echo "MySQL线程号:". MySQL_thread_id($conn). "<br />";
echo "Apache进程号". getmypid();
?>

以上的代码没啥好解释的,让我们用浏览器浏览这个页面,看到什么?看到两个显眼的数字。一个是MySQL线程号,一个是Apache进程号,好 了,15秒后再刷新这个页面,发现这两个id都变了,因为已经是新的Apache进程了,进程id是新的,hash key就变了,PHP只好重新连接MySQL,连接资源推入persistent list。如果15内刷新呢?Apache进程肯定不变,MySQL线程号会变吗?答案得问MySQL了。首先这个MySQL_thread_id是什么 东西?shell方式登录MySQL后执行命令'show processlist;',看到了什么?

mysql> show processlist;
+-----+------+-----------+------+--------+-----+------+-----------------+
| Id | User | Host | db | Command| Time| State| Info |
+-----+------+-----------+------+--------+-----+------+-----------------+
| 348 | root | localhost | NULL | Query | 0| NULL | show processlist|
| 349 | root | localhost | NULL | Sleep | 2| | NULL |
+-----+------+-----------+------+--------+-----+------+-----------------+

发现了很重要的信息,这个processlist列表就是记录了正在跑的线程,忽略Info列为show processlist那行,那行是你当前shell登录MySQL的线程。PHP连接MySQL的线程就是Id为349那行,如果读者自己做测试,应该 知道这个Id=349在你的测试环境里是另外一个值,我们把这个值和网页里输出的MySQL_thread_id($conn)做做比较,对!他们是一样 的。接下来最重要的是观察Command列和Time列,Command = Sleep,表明什么?表明我们mysql_pconnect连接后就一直在sleep,Time字段就告诉我们,这个线程Sleep了多久,那么 Sleep了多久这个线程才能作废呢?那就是wait_timeout或者interactive_timeout要做的工作了,他们默认的值都是8小 时,天啊,太久了,所以如果说web服务器关掉KeepAlive支持,那个这个processlist很容易就被撑爆,就爆出那个Too many connections的错误了,max_connectiosns配置得再多也没用。为了观察这两个参数,我们可以在MySQL配置文件my.cnf里 设置这两个值,找到[MySQLd]节点,在里面设置多两行

interactive_timeout = 60
wait_timeout = 30

配置完后,重启MySQL,shell登录MySQL,这时候show processlist可以发现只有当前线程(本人修改后重启, 在上面测试没有关闭浏览器的情况下刷新, 出现MySQL server has gone away, 刷新后无报错, 说明原来所用的那个mysql的长连接已经过期!)。然后运行那个带有mysql_pconnect的PHP页面,再回来MySQL端show processlist可发现,多了一个Commond为Sleep的线程,不停的show processlist(方向键上+enter键)观察Time列的变化2,5,10...14!,突然那个Sleep线程程被kill掉了,咋回事,还 没到30秒呢,噢!忘了修改一下Apache keepalive的参数了,把KeepAliveTimeOut从15改成120(只为观察,才这么改),重启Apache。刷新那个页面,好,开始不 停的show processlist,2..5..10..14,15,..20...26....28,29!线程被kill,这次是因为wait_timeout 起了作用,浏览器那边停了30秒,30内如果浏览器刷新,那这个Time又会从0开始计时。这种连接不属于interactive connection(MySQL shell登录那种连接就属于interactive connection),所以采用了wait_timeout的值。如果mysql_pconnect的第4个参数改改呢

<?php
$conn = mysql_pconnect('localhost','root','123456',MySQL_CLIENT_INTERACTIVE);
echo "MySQL线程号:".MySQL_thread_id($conn)."<br />";
echo "Apache进程号:".getmypid();
?>

刷新下页面,MySQL那边开始刷show processlist,这回Time > 30也不会被kill,>60才被kill了,说明设置了MySQL_CLIENT_INTERACTIVE,就会被MySQL视为 interactive connection,那么这次PHP的MySQL连接在120秒内未刷新的情况下,何时作废将取决于MySQL的 interactive_timeout的配置值。

总结

每个已建立的联机只为来自同一部 web server、使用同一组账号,且存取同一数据库的使用者服务

PHP的mysql_pconnect要达到功效,首先必须保证Apache是支持keep alive的,其次KeepAliveTimeOut应该设置多久呢,要根据自身站点的访问情况做调整,时间太短,keep alive没啥意义,时间太长,就很可能为一个闲客户端连接牺牲很多服务器资源,毕竟hold住socket监听进程是要消耗cpu内存的。最后 Apache的KeepAliveTimeOut配置得和MySQL的time out配置要有个平衡点,联系以上的观察,假设mysql_pconnect未带上第4个参数,如果Apache的KeepAliveTimeOut设置 的秒数比wait_timeout小,那真正对mysql_pconnect起作用的是Apache而不是MySQL的配置。这时如果MySQL的 wait_timeout偏大,并发量大的情况下,很可能就一堆废弃的connection了,MySQL这边如果不及时回收,那就很可能Too many connections了。可是如果KeepAliveTimeOut太大呢,又回到之前的问题,所以貌似Apache。KeepAliveTimeOu 不要太大,但比MySQL。wait_timeout 稍大,或者相等是比较好的方案,这样可以保证keep alive过期后,废弃的MySQL连接可以及时被回收。

自测:PHP-FPM 与 MySQL 长连接(使用 PDO)

  PDO 如果想使用持久连接,必须在传递给 PDO 构造函数的驱动选项数组中设置 PDO::ATTR_PERSISTENT 。如果是在对象初始化之后用 PDO::setAttribute() 设置此属性,则驱动程序将不会使用持久连接。

  经过本人测试,得出如下(利用调整 php-fpm 个数、tcpdump、MySQL 命令行(show full processlist; kill 线程ID;) 测试,请谨慎采纳!)

    Nginx + PHP-FPM 支持 PDO 长连接,但是每个 PHP-FPM 都拥有自己的连接池(各个 PHP-FPM 的连接池不共用)!

    PHP-FPM 与 MySQL 服务器的长连接,在 PHP-FPM 重启或 MySQL 执行 kill 时会自动向对方发送 FIN。

PHP 中的 MySQL 长连接由于 PHP 的运行方式有多种,因而长连接实现也有多种

  与 Apache 配合时需要 Apache 服务器支持才可以实现长连接,因为 PHP 是没有进程池跟连接池这种概念的,绝大多数情况下 PHP 应用本身不是一个应用服务器(后起之秀 swoole, 是一个优秀的 PHP 应用服务器,不过是在c层面做的)。此时长连接其实是搭载 Apache 这样的带有 mpm 模块的 web 服务器,Linux 下 Apache 会维护一个进程池,开启了 Apache mpm 功能之后,Apache 会默认维持一个进程池,MySQL 长连接之后的连接,并没有作为 socet 连接关闭,而是作为一个不释放的东西,放进了进程池/线程池里面去。等需要连接的时,Apache 从它维护的进程池/线程池里面取出 MySQL  socket connnection, 然后就可以复用此连接了。如果 Apche + mod_php 不开启 mpm 模块的话,无论 mysql_pconnect、pdo_mysql,页面访问完毕,MySQL 连接即释放,即长连接无效。

  与 Nginx 配合,PHP-FPM 支持长连接。

  作为 Web 服务器,比如使用 swoole 开发的应用服务器,支持自实现连接池。

Apache 或 PHP-FPM 长连接缺点及连接池的优点(摘自:基于swoole扩展实现真正的PHP数据库连接池

  假设有 100 台 PHP 的应用服务器,每个机器需要启动 100 个 Apache 或 PHP-FPM 工作进程,那每个进程都会产生一个长连接到 MySQL。这一共会产生 1 万个 MySQL 连接。大家都知道 MySQL 是每个连接会占用 1 个线程。那 MYSQL 就需要创建 1 万个线程,这样大量的系统资源被浪费在线程间上下文切换上。而你的业务代码中并不是所有地方都在做数据库操作,所以这个就是浪费的。  

  连接池就不同了,100 个 worker 进程,公用 10 个数据库连接即可,当操作完数据库后,立即释放资源给其他 worker 进程。这样就算有 100 台 PHP 的服务器,那也只会创建 1000 个 MySQL 的连接,完全可以接受的。

参考

  http://www.cnblogs.com/wpjamer/articles/7106389.html

  http://www.nowamagic.net/librarys/veda/detail/95

PHP-数据库长连接mysql_pconnect的细节的更多相关文章

  1. 让Django支持数据库长连接(可以提高不少性能哦)

    书接上回 上回我们说到:<在生产系统使用Tornado WebServer来代替FastCGI加速你的Django应用> 那么现在很流行用一些高性能的nonblock的app server ...

  2. 使django与数据库保持长连接

    最近遇到一个很蛋疼的问题,写了一个后台管理系统, 由于是后台管理系统,所以使用频率不是很高,当django程序在闲置一段时间后,再次打开后台系统,就变得很慢,然后又好了.查了很多方面,从模板引擎到请求 ...

  3. http的长连接和短连接(数据库也一样)

    长连接与短连接 所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持. 短连接是指通信双方有数据交互时 ...

  4. 聊聊OkHttp实现WebSocket细节,包括鉴权和长连接保活及其原理!

    一.序 OkHttp 应该算是 Android 中使用最广泛的网络库了,我们通常会利用它来实现 HTTP 请求,但是实际上它还可以支持 WebSocket,并且使用起来还非常的便捷. 那本文就来聊聊, ...

  5. 使用mysql的长连接

    有个资料看得我云里雾里的.现在用自己的言语来总结一下,写文字,能够加深自己的理解.也会在写的过程中帮助自己发现理解方面瑕疵,继续查资料求证. 短链接的缺点:创建一个连接,程序执行完毕后,就会自动断掉与 ...

  6. Comet:基于 HTTP 长连接的“服务器推”技术

    “服务器推”技术的应用 请访问 Ajax 技术资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档.教程.论坛.blog.wiki 和新闻.任何 Ajax 的新信息都能在这里找到. c ...

  7. Comet技术详解:基于HTTP长连接的Web端实时通信技术

    前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...

  8. [转]Hibernate不能自动建表解决办法及Hibernate不同数据库的连接及SQL方言

    最近开始学Hibernate,看的是李刚的那本<轻量级java ee企业应用实战>.头一个hibernate程序,我原原本本的按照书上例子写下来,同时只是改动了些mysql的连接参数,并且 ...

  9. mysql长连接和短连接的问题 转

    什么是长连接? 其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态. 通常的短连接操作步骤是: 连接->数据传输->关闭连接: 而长连接通常就是: 连接-> ...

随机推荐

  1. Testcase的编写

    ‘ID’用于记录某一功能: ‘标题’用于表示某一Case:(一个功能有多个Case) ‘优先级’标记Case的重要等级,运行顺序 ‘测试步骤’记录测试流程 1.Given:条件 2.And:进行某些前 ...

  2. jquery如何判断checkbox(复选框)是否被选中(转)

    谁都知道 在html 如果一个复选框被选中 是 checked="checked". 但是我们如果用jquery alert($("#id").attr(&qu ...

  3. Tasker, Android系统增强神器

    Tasker是一个让系统根据用户定制的”配置文件”(Profiles),在特定的”背景”下(Contexts),执行指定”任务”(Tasks)的软件, 除此之外,它还提供”可供点击”的(Clickab ...

  4. rabbitmq 连接測试

    1.假设写错了host (如:factory.setHost("locathost"); )报错: Exception in thread "main" jav ...

  5. mmap函数使用

    UNIX网络编程第二卷进程间通信对mmap函数进行了说明.该函数主要用途有三个:1.将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能:2. ...

  6. 资源合并fis-postpackager-simple插件的使用

    FIS默认只会进行文件打包,不会对页面中的静态资源引用进行替换,这时可以利用fis-postpackager-simple插件进行资源替换. 安装: npm install -g fis-postpa ...

  7. ssh 远程登陆指定端口

    ssh 到指定端口  ssh -p xx user@ip      xx 为 端口号    user为用户名   ip为要登陆的ip SSH 原理及远程登录 http://www.ruanyifeng ...

  8. 解决用户自生成meta导入kylin后报错问题Can not deserialize instance of java.lang.String[] out of VALUE_STRING token

    报错栈: -- ::, ERROR [http-bio--exec-] cube.CubeManager: : Error during load cube instance, skipping : ...

  9. 探索 Python、机器学习和 NLTK 库 开发一个应用程序,使用 Python、NLTK 和机器学习对 RSS 提要进行分类

    挑战:使用机器学习对 RSS 提要进行分类 最近,我接到一项任务,要求为客户创建一个 RSS 提要分类子系统.目标是读取几十个甚至几百个 RSS 提要,将它们的许多文章自动分类到几十个预定义的主题领域 ...

  10. 14.ThreadLocal

    ThreadLocal     1.线程局部变量,是一种多线程并发访问变量的解决方案,与同步技术 synchronize 加锁的方式不同,threadlocal完全不提供锁,而使用        空间 ...