ずっと昔、仕事上のバグのため、PHP mysql クライアントの接続ドライバー mysqlnd と libmysql の違いと、PHP と mysql 間の通信について研究しました。今回は、それらに関連する別のこと、mysqli に遭遇しました。 mysql 永続リンクとの違い。第一に、私はとても怠け者であり、第二に、仕事がとても忙しいです。こういったことをする時間を見つけたのはつい最近のことです。まとめを作成するたびに、ソースコードを注意深く読み、意味を理解し、それを確認するためにテストと検証を行う必要があります。各ステップには長い時間がかかり、中断することはできません。一度中断されると、コンテキストを確認するのに長い時間がかかります。私も自分の怠け心を変えるために、わざと自分にこのまとめを書かせました。
友人と私が本格的に開発とテストを行っていたときに、「mysql サーバーの接続が多すぎます」エラーが発生しました。少しトラブルシューティングを行った結果、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 メソッドはシングルトンのみをシミュレートし、新しいオブジェクトを作成して返し、それらはすべて Afx_Module_Abstract クラスを継承していることがわかります。 Afx_Module_Abstract クラスのおおよそのコードは次のとおりです:
abstract class Afx_Module_Abstract { public function setAdapter ($_adapter) { $this->_adapter = $_adapter; } }
Afx_Module_Abstract クラスのキーコードは上記の通りです。 DB に関しては、「バックグラウンドプロセス A」に戻る setAdapter メソッドが 1 つだけあり、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 アダプター部分を見てください。
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 でした。サーバー上の API と矛盾していました。この問題は、mysql と mysqli、または libmysql と mysqlnd で発生する可能性があります。このため、慎重に PHP ソース コード (最新 5.3.x) を開き、ついに苦労の甲斐あって、これらの問題の原因を突き止めました。
//在文件ext\mysql\php_mysql.c的907-916行 //mysql_connect、mysql_pconnect都调用它,区别是持久链接标识就是persistent为false还是true static 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 サーバーとの TCP リンクを確立すると、そのリンクが直ちにpersistent_list に保存され、次回リンクが確立されるときに最初にチェックされることがわかります。 IP、PORT、USER、PASS、CLIENT_FLAGS のリンクが存在する場合はそれを使用し、存在しない場合は新しいリンクを作成します。
PHP の mysqli 拡張では、リンクを保存するためにpersistent_list が使用されるだけでなく、現在アイドル状態の TCP リンクを保存するために free_link も使用されます。検索時には、無料の free_link リンクリストに存在するかどうかも判断され、存在する場合にのみ TCP リンクが使用されます。 mysqli_closez または RSHUTDOWN の後、このリンクは 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 は、C API の mysql_change_user を呼び出して、一時セッション変数をクリーンアップします。現在の TCP リンクは不完全です。コミット ロールバック命令、テーブルのロック命令、一時テーブルのロック解除などを記述します (これらの命令はすべて、送信された SQL 命令を判断して応答を決定する PHP の mysqli ではなく、mysql サーバー自体によって決定されます)。 )、マニュアル『mysqli 拡張機能と永続接続』の手順を参照してください。この設計はこの新機能用であり、mysql 拡張機能はこの機能をサポートしていません。
从这些代码的浅薄里理解上来看,可以理解mysqli跟mysql的持久链接的区别了,这个问题,可能大家理解起来比较吃力,我后来搜了下,也发现了一个因为这个原因带来的疑惑,大家看这个案例,可能理解起来就非常容易了。Mysqli persistent connect doesn’t work回答者没具体到mysqli底层实现,实际上也是这个原因。 代码如下:
<?php $links = array(); for ($i = 0; $i < 15; $i++) { $links[] = mysqli_connect('p:192.168.1.40', 'USER', 'PWD', 'DB', 3306); } sleep(15);
查看进程列表里是这样的结果:
netstat -an grep 192.168.1.40:3306 tcp 0 0 192.168.1.6:52441 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52454 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52445 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52443 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52446 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52449 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52452 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52442 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52450 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52448 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52440 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52447 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52444 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52451 192.168.1.40:3306 ESTABLISHED tcp 0 0 192.168.1.6:52453 192.168.1.40:3306 ESTABLISHED
这样看代码,就清晰多了,验证我的理解对不对也比较简单,这么一改就看出来了
for ($i = 0; $i < 15; $i++) { $links[$i] = mysqli_connect('p:192.168.1.40', 'USER', 'PWD', 'DB', 3306); var_dump(mysqli_thread_id($links[$i])); //如果你担心被close掉了,这是新建的TCP链接,那么你可以打印下thread id,看看是不是同一个ID,就区分开了 mysqli_close($links[$i]) } /* 结果如下: root@cnxct:/home/cfc4n# netstat -antp grep 3306grep -v "php-fpm" tcp 0 0 192.168.61.150:55148 192.168.71.88:3306 ESTABLISHED 5100/php5 root@cnxct:/var/www# /usr/bin/php5 4.php int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) int(224218) */
如果你担心被close掉了,这是新建的TCP链接,那么你可以打印下thread id,看看是不是同一个ID,就清楚了。(虽然我没回复这个帖子,但不能证明我很坏。)以上是CLI模式时的情况。在FPM模式下时,每个页面请求都会由单个fpm子进程处理。这个子进程将负责维护php与mysql server建立的长链接,故当你多次访问此页面,来确认是不是同一个thread id时,可能会分别分发给其他fpm子进程处理,导致看到的结果不一样。但最终,每个fpm子进程都会分别维持这些TCP链接。
总体来说,mysqli拓展跟mysql拓展的区别是下面几条
好了,知道这个原因,那我们文章开头提到的问题就好解决了,大家肯定第一个想到的是在类似Property的类中,__destruct析构函数中增加一个mysqli_close方法,当被销毁时,就调用关闭函数,把持久链接push到free_links里。如果你这么想,我只能恭喜你,答错了,最好的解决方案就是压根不让它创建这么多次。同事dietoad同学给了个解决方案,对DB ADAPTER最真正单例,并且,可选是否新创建链接。如下代码:
// DB FACTORY class Afx_Db_Factory { const DB_MYSQL = 'mysql'; const DB_MYSQLI = 'mysqli'; const DB_PDO = 'pdo'; static $drivers = array( 'mysql'=>array(),'mysqli'=>array(),'pdo'=>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 adapter class 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; } }
看来,开发环境跟运行环境一致是多么的重要,否则就不会遇到这些问题了。不过,如果没遇到这么有意思的问题,岂不是太可惜了