mysql_pconnect的水挺深,apache下的數據庫長連接

先了解什麼是數據庫的持久化連接:

持久化連接背後的思想是客戶端進程和數據庫之間的連接可以通過一個客戶端進程來保持重用, 而不是多次的創建和銷燬。這降低了每次需要創建一個新連接的開銷,未使用的連接被緩存起來並且準備隨時被重用。

下面是來自網絡上的解釋,個人覺得這個比喻更好理解,就借用過來:

持久化鏈接就像酒店的服務生,服務完一位客人後,它不休息而繼續服務下一位客人,注意是服務完客人後纔會爲下一位自客人提供服務,如果它認爲還沒有服務完是不會爲下一位客人提供服務的。

適用情況: 

使用 1 部 web server 與 1 部 MySQL server(兩者可能同在一部主機上),而 web server 固定只對 MySQL server 上的某一個數據庫進行存取動作。

 因爲每次存取數據庫時,都是由 web 那邊使用同一賬號對 MySQL 上的同一數據庫作業,若我們將 MySQL 與 web server 的「同時聯機數」都調整爲 200,就好像 MySQL 這邊一直有 200 位「服務生」,隨時等着接待來自 web 的 200 位「顧客」似的。而且「顧客」離開之後,「服務生」也不下場休息,時時都站在門口等着接待下一個「顧客」。

 在這種情況下,您只要注意將 MySQL 的「同時聯機數」調得比 web server 的高或相等,就會發現使用 mysql_pconnect( ) 是個不錯的選擇,如果併發量很大,選擇使用持久連接,這樣可以減少關閉和建立連接的次數,提高效率

不適用情況:

 使用 1 部 web server 與 1 部 MySQL server(兩者可能同在一部主機上),而 web server 會對 MySQL server 上的兩個數據庫進行存取動作。

 從 web server 那邊提出數據存取需求時,有時是針對第 1 個數據庫(DB1),有時則是針對第 2 個數據庫(DB2)。若我們也將 MySQL 與 web server 的「同時聯機數」都調整爲 200,這樣一來,就好像 MySQL 這邊有 200 位「服務生」,但同時經營兩個「吧檯」(DB1 與 DB2),而「顧客」可能多達 200 位。

 一開始,DB1 這個「吧檯」比較熱門,MySQL 派了 150 位「服務生」上場接待;同樣地,當「顧客」離開之後,這 150 位「服務生」仍守着 DB1 而不下場休息。後來,DB2 那邊也熱鬧起來了,「顧客」越來越多,MySQL 得加派「服務生」上場,有幾個能派?答案是 50 個!

 爲什麼「服務生」的人力調配會捉襟見肘?那是因爲 web 那邊使用了 mysql_pconnect( ) 來建立聯機。「服務生」一開始被指定到哪個「吧檯」工作,就會持續在那邊停留,絕不「轉檯」。

 請注意,當使用持續性的聯機時,每個已建立的聯機只爲來自同一部 web server、使用同一組賬號,且存取同一數據庫的使用者服務。

 如此一來,假設每部 web server 的「同時聯機數」都是 200,而且同時使用 2 部 web server 會怎麼樣呢?從 web1 來了 50 個「顧客」,先是到 DB1 走一趟,接着再到 DB2 晃一圈,這樣需要多少「服務生」接待他們?100 個(web1->DB1: 50 web1->DB2: 50)!又從 web2 來了 50 個「顧客」,也做了同樣的動作(web2->DB1: 50 web2->DB2: 50)。在此之後,還有「服務生」是閒着的嗎?後續若從 web1 或 web2 同時涌入多於 50 位「顧客」時,誰來應付他們?

 倘若您使用的是像 Apache 這類的 multi-process web server(一個 parent process 協調一組 children processes 運作),某個 children process 建立的「持續聯機」,是不能分享給其它 children process 來使用的(「服務生」只對先前接待過的「顧客」服務)。在這樣的情況下,將會使得 MySQL 上閒置的 process 越積越多(很多「服務生」站在門口等着「老顧客」上門,而不理會「新顧客」)。
 

mysql_pconnect() 和 mysql_connect() 非常相似,但有兩個主要區別。
首先,當連接的時候本函數將先嚐試尋找一個在同一個主機上用同樣的用戶名和密碼已經打開的(持久)連接,如果找到,則返回此連接標識而不打開新連接。

其次,當腳本執行完畢後到 SQL 服務器的連接不會被關閉,此連接將保持打開以備以後使用(mysql_close() 不會關閉由mysql_pconnect() 建立的連接)。


下面是來自另外一位網友的文章,主要幫助理解持久化連接:

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的函數代碼,我看了下,大概的做法就是根據當前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_length = spprintf(&hashed_details, 0, "mysql__%s_", user);//hashed_details就是hash key
#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
$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_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的配置值。

  總結

  #1.php的mysql_pconnect要達到功效,首先必須保證apache是支持keep alive的,然後KeepAliveTimeOut應該設置多久呢,要根據自身站點的訪問情況做調整,時間太短,keep alive沒啥意義,時間太長,就很可能爲一個閒客戶端連接犧牲很多服務器資源,畢竟hold住socket監聽進程是要消耗cpu內存的.

  #2.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.KeepAliveTimeOut不要太大,但比Mysql.wait_timeout 稍大,或者相等是比較好的方案,這樣可以保證keep alive過期後,廢棄的mysql連接可以及時被回收. 

  後記

  Pdo數據庫的長連接機制是否和mysql_pconnect一樣?經過試驗觀察和源碼探究,發現也是一樣的處理方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章