很久很久以前,我也是因为工作上的bug,研究了php mysql client的连接驱动mysqlnd 与libmysql之间的区别php与mysql通讯那点事,这次又遇到一件跟他们有联系的事情,mysqli与mysql持久链接的区别。写出这篇文章,用了好一个多月,其一是我太懒了,其二是工作也比较忙。最近才能腾出时间,来做这些事情。每次做总结,都要认真阅读源码,理解含义,测试验证,来确认这些细节。而每一个步骤都需要花费很长的时间,而且,还不能被打断。一旦被打断了,都需要很长时间去温习上下文。也故意强迫自己写这篇总结,改改自己的惰性。
在我和我的小伙伴们如火如荼的开发、测试时发生了“mysql server too many connections”的错误,稍微排查了一下,发现是php后台进程建立了大量的链接,而没有关闭。服务器环境大约如下php5.3.x 、mysqli api、mysqlnd 驱动。代码情况是这样:
//后台进程a/*配置信息'mysql'=>array( 'driver'=>'mysqli',// 'driver'=>'pdo',// 'driver'=>'mysql', 'host'=>'192.168.111.111', 'user'=>'root', 'port'=>3306, 'dbname'=>'dbname', 'socket'=>'', 'pass'=>'pass', 'persist'=>true, //下面有提到哦,这是持久链接的配置 ),*/$config=yaf_registry::get('config');$driver = afx_db_factory::dbdriver($config['mysql']['driver']); //mysql mysqli$driver::debug($config['debug']); //注意这里$driver->setconfig($config['mysql']); //注意这里afx_module::instance()->setadapter($driver); //注意这里,哪里不舒服,就注意看哪里。$queue=afx_queue::instance();$combat = new combatengine();$role = new role(1,true);$idle_max=isset($config['idle_max'])?$config['idle_max']:1000;while(true){ $data = $queue->pop(mtypes::ectype_combat_queue, 1); if(!$data){ usleep(50000); //休眠0.05秒 ++$idle_count; if($idle_count>=$idle_max) { $idle_count=0; afx_db_factory::ping(); } continue; } $idle_count=0; $role->setid($data['attacker']['role_id']); $property = $role->getmodule('property'); $mounts = $role->getmodule('mounts'); //............ unset($property, $mounts/*.....*/);}
从这个后台进程代码中,可以看出“$property”变量以及“$mounts”变量频繁被创建,销毁。而role对象的getmodule方法是这样写的
//role对象的getmodule方法class role extends afx_module_abstract{ public function getmodule ($member_class) { $property_name = '__m' . ucfirst($member_class); if (! isset($this->$property_name)) { $this->$property_name = new $member_class($this); } return $this->$property_name; }}//property 类class property extends afx_module_abstract{ public function __construct ($mrole) { $this->__mrole = $mrole; }}
可以看出getmodule方法只是模拟单例,new了一个新对象返回,而他们都继承了afx_module_abstract类。afx_module_abstract类大约代码如下:
abstract class afx_module_abstract{ public function setadapter ($_adapter) { $this->_adapter = $_adapter; }}
类afx_module_abstract中关键代码如上,跟db相关的,就setadapter一个方法,回到“后台进程a”,setadapter方法是将afx_db_factory::dbdriver($config['mysql']['driver'])的返回,作为参数传了进来。继续看下afx_db_factory类的代码
class afx_db_factory{ const db_mysql = 'mysql'; const db_mysqli = 'mysqli'; const db_pdo = 'pdo'; public static function dbdriver ($type = self::db_mysqli) { switch ($type) { case self::db_mysql: $driver = afx_db_mysql_adapter::instance(); break; case self::db_mysqli: $driver = afx_db_mysqli_adapter::instance(); //走到这里了 break; case self::db_pdo: $driver = afx_db_pdo_adapter::instance(); break; default: break; } return $driver; }}
一看就知道是个工厂类,继续看真正的db adapter部分代码
class afx_db_mysqli_adapter implements afx_db_adapter{ public static function instance () { if (! self::$__instance instanceof afx_db_mysqli_adapter) { self::$__instance = new self(); //这里是单例模式,为何新生成了一个mysql的链接呢? } return self::$__instance; } public function setconfig ($config) { $this->__host = $config['host']; //... $this->__user = $config['user']; $this->__persist = $config['persist']; if ($this->__persist == true) { $this->__host = 'p:' . $this->__host; //这里为持久链接做了处理,支持持久链接 } $this->__config = $config; } private function __init () { $this->__link = mysqli_init(); $this->__link->set_opt(mysqli_opt_connect_timeout, $this->__timeout); $this->__link->real_connect($this->__host, $this->__user, $this->__pass, $this->__dbname, $this->__port, $this->__socket); if ($this->__link->errno == 0) { $this->__link->set_charset($this->__charset); } else { throw new afx_db_exception($this->__link->error, $this->__link->errno); } }}
从上面的代码可以看到,我们已经启用长链接了啊,为何频繁建立了这么多链接呢?为了模拟重现这个问题,我在本地开发环境进行测试,无论如何也重现不了,对比了下环境,我的开发环境是windows7、php5.3.x、mysql、libmysql,跟服务器上的不一致,问题很可能出现在mysql跟mysqli的api上,或者是libmysql跟mysqlnd的问题上。为此,我又小心翼翼的翻开php源码(5.3.x最新的),终于功夫不负有心人,找到了这些问题的原因。
//在文件ext\mysql\php_mysql.c的907-916行//mysql_connect、mysql_pconnect都调用它,区别是持久链接标识就是persistent为false还是truestatic void php_mysql_do_connect(internal_function_parameters, int persistent){/* hash it up */z_type(new_le) = le_plink;new_le.ptr = mysql;//注意下面的if里面的代码if (zend_hash_update(&eg(persistent_list), hashed_details, hashed_details_length+1, (void *) &new_le, sizeof(zend_rsrc_list_entry), null)==failure) { free(mysql); efree(hashed_details); mysql_do_connect_return_false();}mysg(num_persistent)++;mysg(num_links)++;}
从mysql_pconnect的代码中,可以看到,当php拓展mysql api与mysql server建立tcp链接后,就立刻将这个链接存入persistent_list中,下次建立链接是,会先从persistent_list里查找是否存在同ip、port、user、pass、client_flags的链接,存在则用它,不存在则新建。
而php的mysqli拓展中,不光用了一个persistent_list来存储链接,还用了一个free_link来存储当前空闲的tcp链接。当查找时,还会判断是否在空闲的free_link链表中存在,存在了才使用这个tcp链接。而在mysqli_closez之后或者rshutdown后,才将这个链接push到free_links中。(mysqli会查找同ip,port、user、pass、dbname、socket来作为同一标识,跟mysql不同的是,没了client,多了dbname跟socket,而且ip还包括长连接标识“p”)
//文件ext\mysqli\mysqli_nonapi.c 172行左右 mysqli_common_connect创建tcp链接(mysqli_connect函数调用时)do { if (zend_ptr_stack_num_elements(&plist->free_links)) { mysql->mysql = zend_ptr_stack_pop(&plist->free_links); //直接pop出来,同一个脚本的下一个mysqli_connect再次调用时,就找不到它了 myg(num_inactive_persistent)--; /* reset variables */ #ifndef mysqli_no_change_user_on_pconnect if (!mysqli_change_user_silent(mysql->mysql, username, passwd, dbname, passwd_len)) { //(让你看时,你再看)注意看这里mysqli_change_user_silent #else if (!mysql_ping(mysql->mysql)) { #endif #ifdef mysqli_use_mysqlnd mysqlnd_restart_psession(mysql->mysql); #endif}//文件ext\mysqli\mysqli_api.c 585-615行/* {{{ php_mysqli_close */void php_mysqli_close(my_mysql * mysql, int close_type, int resource_status tsrmls_dc){ if (resource_status > mysqli_status_initialized) { myg(num_links)--; } if (!mysql->persistent) { mysqli_close(mysql->mysql, close_type); } else { zend_rsrc_list_entry *le; if (zend_hash_find(&eg(persistent_list), mysql->hash_key, strlen(mysql->hash_key) + 1, (void **)&le) == success) { if (z_type_p(le) == php_le_pmysqli()) { mysqli_plist_entry *plist = (mysqli_plist_entry *) le->ptr;#if defined(mysqli_use_mysqlnd) mysqlnd_end_psession(mysql->mysql);#endif zend_ptr_stack_push(&plist->free_links, mysql->mysql); //这里在push回去,下次又可以用了 myg(num_active_persistent)--; myg(num_inactive_persistent)++; } } mysql->persistent = false; } mysql->mysql = null; php_clear_mysql(mysql);}/* }}} */
mysqli为什么要这么做?为什么同一个长连接不能在同一个脚本中复用?
在c函数mysqli_common_connect中看到了有个mysqli_change_user_silent的调用,如上代码,mysqli_change_user_silent对应这libmysql的mysql_change_user或mysqlnd的mysqlnd_change_user_ex,他们都是调用了c api的mysql_change_user来清理当前tcp链接的一些临时的会话变量,未完整写的提交回滚指令,锁表指令,临时表解锁等等(这些指令,都是mysql server自己决定完成,不是php 的mysqli 判断已发送的sql指令然后做响应决定),见手册的说明the mysqli extension and persistent connections。这种设计,是为了这个新特性,而mysql拓展,不支持这个功能。
从这些代码的浅薄里理解上来看,可以理解mysqli跟mysql的持久链接的区别了,这个问题,可能大家理解起来比较吃力,我后来搜了下,也发现了一个因为这个原因带来的疑惑,大家看这个案例,可能理解起来就非常容易了。mysqli persistent connect doesn’t work回答者没具体到mysqli底层实现,实际上也是这个原因。 代码如下:
array() ); public static function dbdriver ($type = self::db_mysqli, $create = false) //新增$create 参数 { $driver = null; switch ($type) { case self::db_mysql: $driver = afx_db_mysql_adapter::instance($create); break; case self::db_mysqli: $driver = afx_db_mysqli_adapter::instance($create); break; case self::db_pdo: $driver = afx_db_pdo_adapter::instance($create); break; default: break; } self::$drivers[$type][] = $driver; return $driver; }}//mysqli adapterclass afx_db_mysqli_adapter implements afx_db_adapter{ public static function instance ($create = false) { if ($create) { return new self(); //新增$create参数的判断 } if (! self::$__instance instanceof afx_db_mysqli_adapter) { self::$__instance = new self(); } return self::$__instance; }}
看来,开发环境跟运行环境一致是多么的重要,否则就不会遇到这些问题了。不过,如果没遇到这么有意思的问题,岂不是太可惜了