PHP的MySQL持久化鏈接,美好的目標,卻擁有糟糕的口碑,每每使人敬而遠之。這究竟是爲啥麼。近距離觀察後發現,這傢伙也不容易啊,要看Apache的臉色,還得聽MySQL指揮。php
對於做爲Apache模塊運行的PHP來講,要實現MySQL持久化鏈接,首先得取決於Apache這個web服務器是否支持Keep-Alive。html
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秒)。mysql
好了,重啓Apache,測試一下,趕忙寫行東西:ios
<?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進程。web
瀏覽器是怎麼知道不須要從新進行TCP鏈接就能夠直接發送http請求呢?由於http返回頭裏就會帶上Connection:keep- alive,Keep-alive:15兩行,意思就是讓客戶端瀏覽器明白,此次socket鏈接我這邊還沒關閉呢,你能夠在15內繼續使用這個鏈接,並 發送http請求,因而乎瀏覽器就知道應該怎麼作了。sql
那麼,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函數)shell
#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就有值了。數據庫
說完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