PHP與MySQL通訊那點事
在我們的一款WebGame的生產環境中,一次無意的strace抓包時,發現了php與mysql大量通訊的數據。這種情況,在游戲服務器剛啟動時,是正常的,但如果是運行一段時間之后,出現大量SELECT的SQL查詢,絕對是有問題的,而且,所操作的數據庫并不是配置庫,那意味著,我們程序員的程序出現了違規的操作。具體結果大約如下:
如上圖所示,php持續接收讀取進程內描述符為3的響應包數據,描述符為3的為php與mysql建立的TCP通訊鏈接,這點也可以從313行的SELECT語句來確認。(原始數據丟失了,我模仿了一條。所以是配置庫的SQL語句)
這是什么程序,想實現什么邏輯?為何要取這么多數據?
跟著這里的SELECT的sql語句,我定位到了相應的程序段:
- /*
- ** 業務邏輯的代碼
- */
- public function SItem($roleId,$baseId) {
- //...
- // ############寫出下面這種代碼的人都得死.##################
- $this->dbrRole->select('*');
- $this->dbrRole->from('role_items');
- $this->dbrRole->where('role_id',$roleId);
- $this->dbrRole->where('baseId',$baseId);
- $result = $this->dbrRole->get()->row(); //看上去,這里好像正常,我們都以為框架會給我們只取一條。
- //...
- }
我們從代碼上來看,好像明白程序員想根據對應的role_id到role_items表里取一條想符合的數據,所以,他調用了row方法,來取一條。看上去,這里好像正常,我們都以為框架會給我們只取一條。但實際上,框架是如何處理的呢?
我們來看下框架的對應row方法的實現過程。對了,我們是CodeIgniter框架的一個較老的版本。
- /*
- ** 框架中,DB drive中,row相關方法的代碼
- **
- */
- public function row($n = 0,$type = 'array'){
- if(!is_numeric($n)){
- if(! is_array($this->_rowData)){
- $this->_rowData = $this->rowArray(0);
- }
- if(isset($this->_rowData[$n])){
- return $this->_rowData[$n];
- }
- $n = 0;
- }
- return ($type == 'object') ? $this->rowObject($n) : $this->rowArray($n);
- }
- //繼續跟進rowArray方法
- public function rowArray($n = 0){
- $result = $this->resultArray();
- if(count($result) == 0){
- return $result;
- }
- if($n != $this->_current && isset($result[$n])){
- $this->_current = $n;
- }
- return $result[$this->_current];
- }
- //繼續跟進resultArray方法 ###這個方法是重點###
- public function resultArray(){
- if(count($this->resultArray) > 0){
- return $this->resultArray;
- }
- if(false === $this->resulter || 0 == $this->recordCount()){
- return array();
- }
- $this->_dataSeek(0);
- while($row = $this->_fetchAssoc()){
- $this->resultArray[] = $row; //###########這個數組每次都增加_fetchAssoc()結果的內存大小數量#########################
- }
- return $this->resultArray;
- }
- //繼續跟進_fetchAssoc方法
- /*
- ** 對應driver的_fetchAssoc方法的代碼
- */
- protected function _fetchAssoc(){
- return mysql_fetch_assoc($this->resulter);
- }
我們可以看到CodeIgniter框架的resultArray方法使用mysql(我們的php調用mysql的api用的是mysql函數,有點繞,后面解釋)的mysql_fetch_assoc函數對緩沖區的數據進行遍歷轉換。將所有緩沖區的數據全部復制給$this->resultArray屬性,再判斷row方法中所需要的key的結果是否存在,再與返回的。
也就是說,框架層并沒有只從mysql server(潛意識上的mysql server)那邊取一條給我們調用者,而是取了所有結果,再返回一條。(先別噴,后面解釋) 當然,CI這種做法,也不是錯。但我覺得有更好的改進方法。
這個問題,我們組的dietoad (征婚) 發現了這個問題,并給了修復方案。有些同學認為,這是程序員的錯,程序員的SELECT語句沒有加limit來限制條數。這我絕對贊同,而且,覺得寫出這種代碼的人都得死。
- 業務層:為這種業務需求的SQL語句加上limit限制
- 框架層:框架對于這種需求,自動控制,發現這種情況,直接返回1條
對于解決方案1,我寫了一個正則,匹配select()方法被調用之后,row()方法被調用之前,中間沒有使用limit()方法的所有代碼,結果,發現量并不小。后來,我們決定兩種方案同時實施,防止第二種出現漏掉的情況。
#p#
dietoad給出如下改進:
- /*
- ** //改進為當_rowData不存在時,從_rowData的數量開始取,取小于$n條記錄,避免 上面 resultArray方法中從緩沖區取所有數據,復制雙倍數據,占用內存的情況
- */
- public function row ($n = 0, $type = 'array')
- {
- if(isset($this->_rowData[$n]))
- {
- return $this->_rowData[$n];
- }
- if (! is_numeric($n))
- {
- return $this->rowObject($n);
- }
- $ln=count($this->_rowData);
- //繼續上次位置
- while($ln++<=$n&&$r=$this->_fetchAssoc())
- {
- $this->_rowData[]=$r;
- }
- //需要幾條就讀幾條
- //防止記錄集為空報warning
- return isset($this->_rowData[$n])?$this->_rowData[$n]:array();
- }
在今年的4月末,鄙人寫過另一篇關于CodeIgniter框架的設計缺陷問題,給我們游戲項目帶來較大的影響,后來提交到github issues,并沒得到回復,想了想,雖然官方的2.1.3版本中,也存在這個小問題。不過我覺得,這就不提交了,或許,我們的做法也符合他們的設計初衷。不過,我們還是在我們的項目中改進了。
如此改進之后,我們使用php的memory_get_usage()函數觀察前后兩個row()方法的結果時,果然發現內存使用情況有較大改善(改善幅度取決于SELECT的返回數據量)。
似乎,到這里就應該結束了,問題就這么被發現,被解決了。
但,我總覺得少了些什么呢?當我再次strace抓包時,發現仍然存在大量的數據通訊,就像文章開頭的那副截圖一模一樣。然而,這又是什么原因呢?
我順手寫了個內存占用的測試代碼如下:
- $db = mysql_connect('192.168.xx.xx','xxxx','xxxx');
- $sql = 'SELECT * from items';
- mysql_select_db('jv01',$db);
- echo 'SELECT_DB: ',convert(memory_get_usage()),"\n"; //619.26 kb
- $r = mysql_query($sql,$db);
- echo 'QUERY_SQL: ',convert(memory_get_usage()),"\n"; //619.98 kb ###什么?查詢完之后,內存大小居然只增加了不到1k?我那個表可是幾十M的數據啊
- //sleep(50); // hold住進程,別銷毀,留著看當前進程的內存分配1
- $arr = array();
- while ($rs = mysql_fetch_assoc($r))
- {
- $arr[]=$rs;
- }
- echo 'FETCH_RS: ',convert(memory_get_usage()),"\n"; //27.11 mb ###什么?剛剛不是只增加了1k嗎?這里的遍歷的結果集怎么突增幾十M啊?尼瑪這到底是什么情況?
- unset($arr);
- echo 'UNSET: ',convert(memory_get_usage()),"\n"; //620.12 kb #### $arr z占了 幾十M
- mysql_free_result($r);
- echo 'FREE_R: ',convert(memory_get_usage()),"\n"; //620 kb ### 結果集居然只有0.12 k?這不扯淡么? 莫非。。。莫非緩沖區的數據php統計不到?莫非不是調用zend 內存申請函數來申請內存的?
- //sleep(50); // hold住進程,別銷毀,留著看當前進程的內存分配2
- function convert($size)
- {
- $unit=array('b','kb','mb','gb','tb','pb');
- return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
- }
- /*
- //返回結果如下:
- SELECT_DB: 619.26 kb
- QUERY_SQL: 619.98 kb
- FETCH_RS: 27.11 mb
- UNSET: 620.12 kb
- FREE_R: 620 kb
- */
看到結果時,我不禁XX一緊,什么?這你媽什么情況?查詢完之后,內存大小居然只增加了不到1k?我那個表可是幾十M的數據啊?遍歷結果集之后,怎么突增幾十M啊?尼瑪這到底是什么情況?strace返回的大量數據到底存在哪的?算不算php進程申請的?
后來,我再次執行如上程序,再定時用free、/proc/PID/maps 之類系統工具,查看系統的內存使用情況,確認了當前進程的內存占用確實存在。那么可能的情況就是memory_get_usage()函數并沒有獲取到 mysql_query之后的內存占用情況。由于比較懷疑,末學跟進了memory_get_usage()函數的源碼,該函數直接交給 zend_memory_usage函數處理。
- //這個是php的memory_get_usage()函數的 相關代碼,見Zend_alloc.c line:2640
- ZEND_API size_t zend_memory_usage(int real_usage TSRMLS_DC)
- {
- if (real_usage) {
- return AG(mm_heap)->real_size;
- } else {
- size_t usage = AG(mm_heap)->size;
- #if ZEND_MM_CACHE
- usage -= AG(mm_heap)->cached;
- #endif
- return usage;
- }
- }
- //這個是Zend內存分配函數的代碼
- //Zend_alloc.c line:2418
- ZEND_API void *_emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
- {
- TSRMLS_FETCH();
- if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
- return AG(mm_heap)->_malloc(size);
- }
- return _zend_mm_alloc_int(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
- }
php的內存管理 (中文地址:php-zend的內存管理中文版)這塊,對于末學來說,太復雜了,只是稍微看懂直接 返回了mm_heap結構體的real_size/size的值。(兩篇都是鳥哥寫的,中文的地址也就是鳥哥博客最近一直打不開,抽風得厲害)
那mysql_query的結果集,存在哪的呢?如何申請內存的,莫非不是調用zend的_emalloc內存分配函數的?這得先明確mysql客戶端類庫問題,也就是我們使用哪個類庫?libmysql還是mysqlnd,通過查看編譯參數,發現(我的虛擬機)是libmysql,編譯參數是這樣的
- ./configure' '--prefix=/services/php_5.3.19' '--with-config-file-path=/services/php_5.3.19/etc' '--with-pdo-mysql=/usr/bin/mysql_config' '--with-mysql=/usr/bin/mysql_config' '--with-mysqli=/usr/bin/mysql_config' '--enable-bcmath' '--enable-fpm
- //生產服務器如下:
- ./configure' '--prefix=/services/php' '--with-config-file-path=/services/php/etc' '--with-pdo-mysql=mysqlnd' '--with-mysql=mysqlnd' '--with-mysqli=mysqlnd' '--enable-bcmath' '--enable-fpm
有點亂:
mysql、mysqli、pdo-mysql、libmysql、mysqlnd 好多名詞,有點亂,沒關系,一張圖讓你清晰起來:
mysql、mysqli、pdo-mysql、libmysql、mysqlnd之間關系
mysqlnd跟libmysql一樣,都是直接與mysql server通訊的驅動類庫。 而php程序員使用的mysql、mysqli、pdo-mysql是面向程序員調用的API接口。。
#p#
繼續:
libmysql類庫是MYSQL官方提供的類庫,每次PHP編譯都是指定參數來確定mysql\mysqli\pdo-mysql所使用的連接驅動是哪個。并且,前提你的得先裝好mysql的客戶端(libmysql類庫),以確保有libmysqlclient.so ,
末學抱著試試看的心態,心情沉重的打開了libmysql的源碼,終于在Safemalloc.c的line:120附近找到類似libmysqlclient申請內存的代碼
- //libmysql客戶端庫Safemalloc.c line:120
- /* Allocate some memory. */
- void *_mymalloc(size_t size, const char *filename, uint lineno, myf MyFlags)
- {
- ...
- /*
- Test for memory limit overrun.
- If compiled with DBUG, test for error injection. Described in my_sys.h.
- */
- if ((size + sf_malloc_cur_memory > sf_malloc_mem_limit)
- IF_DBUG(|| my_malloc_error_inject))
- {
- IF_DBUG(if (my_malloc_error_inject)
- errno= ENOMEM;
- my_malloc_error_inject= 0);
- irem= 0;
- }
- else
- {
- /* Allocate the physical memory */
- irem= (struct st_irem *) malloc (ALIGN_SIZE(sizeof(struct st_irem)) +
- sf_malloc_prehunc +
- size + /* size requested */
- 4 + /* overrun mark */
- sf_malloc_endhunc); //系統的內存分配函數 malloc
- }
- ...
- }
- //下面是mysqlnd驅動的代碼,為了省的再弄一個代碼高亮的區塊,特意放一起了.
- // Mysqlnd客戶端庫Mysqlnd_alloc.c line:77
- /* {{{ _mysqlnd_emalloc */
- void * _mysqlnd_emalloc(size_t size MYSQLND_MEM_D)
- {
- ...
- ret = _emalloc(REAL_SIZE(size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_ORIG_RELAY_CC); //調用zend的內存分配函數 _emalloc
- ...
- if (ret && collect_memory_statistics) {
- *(size_t *) ret = size;
- MYSQLND_INC_GLOBAL_STATISTIC_W_VALUE2(STAT_MEM_EMALLOC_COUNT, 1, STAT_MEM_EMALLOC_AMOUNT, size);
- }
- TRACE_ALLOC_RETURN(FAKE_PTR(ret));
- }
- /* }}} */
也就是說,libmysql沒有調用zend的內分分配函數_emalloc,就沒法將內存的使用情況記錄到mm_heap結構體中,也就是PHP的memory_get_usage()函數統計不到的原因。好了,雖然末學不是很能讀懂源碼,但似乎符合問題發生的現象了。
好像,末學又想到一個問題,如果libmysql保存的結果集所占用的內存的話,那么php的配置文件中的memory_limit也就無法限制他的內存使用情況了?也就是說,如果我們很理想的根據系統剩余內存分配了若干個php-fpm進程來啟動運行的話,如果發生這情況,將會出現內存不夠用的情況,libmysql占用的內存沒有被統計到。。。結果是顯然的,果然限制不了它。
libmysql與mysqlnd跟memory_limit之間的關系
那mysqlnd可以嗎?mysqlnd的內存分配是使用zend的_emalloc函數嗎?是的,沒錯mysqlnd 是我們的大救星。Mysqlnd_alloc.c line:77里代碼中,明確看到了。各位SA在編譯php時,一定要使用mysqlnd作為php連接mysql server的類庫驅動哦。
Mysqlnd的好處可不止這么一點點啊。
內存還是內存:
末學苦于薄弱的英語,冒死翻過GFW,終于在“萬惡的資本主義”國家的網站上找到了這些資料,mysqlnd將比libmysql節省將近40%的內存占用哦。如圖:
mysqlnd比libmysql節省40%的內存占用
,而且,memory_limit參數可以管的了它哦…
#p#
速度,速度:
國外友人給了一份測試結果,比較的API是mysql\mysqli,比較的驅動是libmysql\mysqlnd
- 使用mysqlnd驅動的ext\mysqli接口速度最快
- 使用libmysql驅動的ext\mysqli接口慢了6%
- 使用libmysql驅動的ext\mysql接口慢了3%
并且給出了mysqli在兩個驅動下的執行時間:
mysqli_select_varchar_buffered
還有,還有哦…mysqlnd還支持各種debug調試哦,各種strace跟蹤哦…還支持….算了,你自己下載mysqlnd相比libmysql的優點看吧。末學可是搜了很久才搜到這個ppt。
推薦:
1,再推薦一片關于mysqlnd持久鏈接的文章:PHP 5.3: Persistent Connections with ext/mysqli
2,你的應用的cache的存儲是程序員自己根據DB數據結果,查詢條件,hash取值,存到memcache中的嗎?想不想嘗試下自動實現的?mysqlnd的插件可以嘗試下:PHP: Client side caching for all MySQL extensions ,支持memcached,apc,sqlit哦。
回到開始:
有人說,當php調用mysql_query時,mysql server會返回本次查詢的結果到php所在服務器的緩沖區中。當程序調用mysql_fetch_assoc/mysql_fetch_row /mysql_fetch_array/mysql_fetch_object之類函數時,都是調用php_mysql_fetch_hash函數去緩沖區讀取數據的。我要是用mysql_unbuffered_query()函數呢?讓結果集不直接在查詢之后返回,當調用mysql_fetch_x函數時,再拉回來呢? 這…你讓mysql server的緩沖區來存儲這些數據么?你以為客戶端就你自己么?其他的客戶端也要連的啊,尤其是php,如果用 mysql_unbuffered_query()函數,他們都會將結果集放到mysql server的緩沖區的,mysql server的內存占用豈不是成本增長…你想讓DBA砍死你?
手冊上也說了,mysql_unbuffered_query返回的結果集之上不能使用 mysql_num_rows() 和 mysql_data_seek()。我幾乎沒用過這個函數,這算非主流的函數么?
有人說我們方案1節省了從結果集取出,遍歷賦值給新數組的內存占用,并沒有減少網絡數據的傳輸。沒錯,你說的對,一點都沒錯。也就是說,我們的解決方案2只能稍微緩解這種問題的負面效果,徹底解決的話,還得程序層上去正確的調用,取回該要的數據。(其實,如果使用mysqlnd驅動的話,我們的改動基本沒有優勢,節省不了內存。mysqlnd時,結果集的讀取只是引用緩沖區的數據。libmysql的話,有明顯效果。)我更加鑒定的贊同的那句話“寫出這種代碼的人都得死”。不使用mysqlnd作為php連接驅動的SA都是耍流氓。
結論:
api推薦mysqli,驅動推薦mysqlnd.
溫故而知新?
在回家之后,末學刷了幾局《保衛蘿卜》,除了幾個需要養成才解鎖的關卡之外,均可恥的”全清”+”金蘿卜”,玩著玩著,突然想起一件事情,就是末學在去年寫過一篇博客php5.3.8中編譯pdo_mysql的艱難歷程中, 之前運維的編譯參數中,mysqli使用的是mysqlnd,而mysql使用的是libmysql,后來再裝的pdo-mysql也使用了 libmysql了….3個api,指定兩個連接驅動,莫非上次的錯誤是因為這個?而末學的編譯參數雖然巧合的解決了問題,當初并沒有理解真正的原因?下周驗證一下… [2012/12/15 23:31更新]
知恥而后勇?
今天剛寫完這篇學習筆記后,回家玩游戲時,想起鳥哥曾提到過mysqlnd,再次回去看看,看鳥哥如何講解mysqlnd的,我理解的是否有誤,才發現鳥哥這里已經有了個Ulf Wendel博客的鏈接,末學卻在網絡搜索N久才找到那篇文章,同時,發現其blog上有大量mysqlnd的文章,還暗自偷笑,以為自己發現了大金礦,現在才發現….哎,慚愧慚愧…[2012/12/15 23:58更新]
末學對于本次學習經歷中遇到的知識點,有大量的盲區,將會在以后的時間里,慢慢摸索熟悉,也歡迎各位前輩的點撥。
好像…好像…末學的問題太多了…
原文鏈接:http://www.cnxct.com/libmysql-mysqlnd-which-is-best-and-what-about-mysqli-pdomysql-mysql/