PHP與MySQL通訊那點事

cfc4n發表於2013-04-26

伯樂線上注:本文來自文章作者  @CFC4N 的投稿(原文連結)。PS:文中圖片均用了縮圖,點選可以檢視原圖。

在我們的一款WebGame的生產環境中,一次無意的strace抓包時,發現了php與mysql大量通訊的資料。這種情況,在遊戲伺服器剛啟動時,是正常的,但如果是執行一段時間之後,出現大量SELECT的SQL查詢,絕對是有問題的,而且,所操作的資料庫並不是配置庫,那意味著,我們程式設計師的程式出現了違規的操作。具體結果大約如下:

strace跟蹤php程式與mysql通訊的日誌

strace跟蹤php程式與mysql通訊的日誌(點選可檢視大圖)

 

如上圖所示,php持續接收讀取程式內描述符為3的響應包資料,描述符為3的為php與mysql建立的TCP通訊連結,這點也可以從313行的SELECT語句來確認。(原始資料丟失了,我模仿了一條。所以是配置庫的SQL語句)

這是什麼程式,想實現什麼邏輯?為何要取這麼多資料?

跟著這裡的SELECT的sql語句,我定位到了相應的程式段:

我們從程式碼上來看,好像明白程式設計師想根據對應的role_id到role_items表裡取一條想符合的資料,所以,他呼叫了row方法,來取一條。看上去,這裡好像正常,我們都以為框架會給我們只取一條。但實際上,框架是如何處理的呢?

我們來看下框架的對應row方法的實現過程。對了,我們是CodeIgniter框架的一個較老的版本。

我們可以看到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()方法的所有程式碼,結果,發現量並不小。後來,我們決定兩種方案同時實施,防止第二種出現漏掉的情況。

dietoad給出如下改進:

在今年的4月末,鄙人寫過另一篇關於CodeIgniter框架的設計缺陷問題,給我們遊戲專案帶來較大的影響,後來提交到github issues,並沒得到回覆,想了想,雖然官方的2.1.3版本中,也存在這個小問題。不過我覺得,這就不提交了,或許,我們的做法也符合他們的設計初衷。不過,我們還是在我們的專案中改進了。

如此改進之後,我們使用php的memory_get_usage()函式觀察前後兩個row()方法的結果時,果然發現記憶體使用情況有較大改善(改善幅度取決於SELECT的返回資料量)。

似乎,到這裡就應該結束了,問題就這麼被發現,被解決了。

但,我總覺得少了些什麼呢?當我再次strace抓包時,發現仍然存在大量的資料通訊,就像文章開頭的那副截圖一模一樣。然而,這又是什麼原因呢?

我順手寫了個記憶體佔用的測試程式碼如下:

看到結果時,我不禁XX一緊,什麼?這你媽什麼情況?查詢完之後,記憶體大小居然只增加了不到1k?我那個表可是幾十M的資料啊?遍歷結果集之後,怎麼突增幾十M啊?尼瑪這到底是什麼情況?strace返回的大量資料到底存在哪的?算不算php程式申請的?

後來,我再次執行如上程式,再定時用free、/proc/PID/maps 之類系統工具,檢視系統的記憶體使用情況,確認了當前程式的記憶體佔用確實存在。那麼可能的情況就是memory_get_usage()函式並沒有獲取到mysql_query之後的記憶體佔用情況。由於比較懷疑,末學跟進了memory_get_usage()函式的原始碼,該函式直接交給zend_memory_usage函式處理。

php的記憶體管理 (中文地址:php-zend的記憶體管理中文版)這塊,對於末學來說,太複雜了,只是稍微看懂直接 返回了mm_heap結構體的real_size/size的值。(兩篇都是鳥哥寫的,中文的地址也就是鳥哥部落格最近一直打不開,抽風得厲害)

那mysql_query的結果集,存在哪的呢?如何申請記憶體的,莫非不是呼叫zend的_emalloc記憶體分配函式的?這得先明確mysql客戶端類庫問題,也就是我們使用哪個類庫?libmysql還是mysqlnd,通過檢視編譯引數,發現(我的虛擬機器)是libmysql,編譯引數是這樣的:

有點亂:
mysql、mysqli、pdo-mysql、libmysql、mysqlnd 好多名詞,有點亂,沒關係,一張圖讓你清晰起來:

mysql、mysqli、pdo-mysql、libmysql、mysqlnd之間關係

mysql、mysqli、pdo-mysql、libmysql、mysqlnd之間關係

mysqlnd跟libmysql一樣,都是直接與mysql server通訊的驅動類庫。 而php程式設計師使用的mysql、mysqli、pdo-mysql是面向程式設計師呼叫的API介面。。

 

繼續:

libmysql類庫是MYSQL官方提供的類庫,每次PHP編譯都是指定引數來確定mysqlmysqlipdo-mysql所使用的連線驅動是哪個。並且,前提你的得先裝好mysql的客戶端(libmysql類庫),以確保有libmysqlclient.so ,

末學抱著試試看的心態,心情沉重的開啟了libmysql的原始碼,終於在Safemalloc.c的line:120附近找到類似libmysqlclient申請記憶體的程式碼:

也就是說,libmysql沒有呼叫zend的內分分配函式_emalloc,就沒法將記憶體的使用情況記錄到mm_heap結構體中,也就是PHP的memory_get_usage()函式統計不到的原因。好了,雖然末學不是很能讀懂原始碼,但似乎符合問題發生的現象了。

好像,末學又想到一個問題,如果libmysql儲存的結果集所佔用的記憶體的話,那麼php的配置檔案中的memory_limit也就無法限制他的記憶體使用情況了?也就是說,如果我們很理想的根據系統剩餘記憶體分配了若干個php-fpm程式來啟動執行的話,如果發生這情況,將會出現記憶體不夠用的情況,libmysql佔用的記憶體沒有被統計到。。。結果是顯然的,果然限制不了它。

libmysql與mysqlnd跟memory_limit之間的關係

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%的記憶體佔用

mysqlnd比libmysql節省40%的記憶體佔用

,而且,memory_limit引數可以管的了它哦…

 

速度,速度:
國外友人給了一份測試結果,比較的API是mysqlmysqli,比較的驅動是libmysqlmysqlnd

  • 使用mysqlnd驅動的extmysqli介面速度最快
  • 使用libmysql驅動的extmysqli介面慢了6%
  • 使用libmysql驅動的extmysql介面慢了3%

並且給出了mysqli在兩個驅動下的執行時間:

mysqli_select_varchar_buffered

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更新]

末學對於本次學習經歷中遇到的知識點,有大量的盲區,將會在以後的時間裡,慢慢摸索熟悉,也歡迎各位前輩的點撥。

 

相關文章