在我們的一款WebGame的生產環境中,一次無意的strace抓包時,發現了php與mysql大量通訊的數據。這種情況,在游戲服務器剛啟動時,是正常的,但如果是運行一段時間之后,出現大量SELECT的SQL查詢,絕對是有問題的,而且,所操作的數據庫并不是配置庫,那意味著,我們程序員的程序出現了違規的操作。具體結果大約如下:
如上圖所示,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抓包時,發現仍然存在大量的數據通訊,就像文章開頭的那副截圖一模一樣。然而,這又是什么原因呢?
我順手寫了個內存占用的測試代碼如下:

看到結果時,什么情況?查詢完之后,內存大小居然只增加了不到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 好多名詞,有點亂,沒關系,一張圖讓你清晰起來:

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占用的內存沒有被統計到。。。結果是顯然的,果然限制不了它。

那mysqlnd可以嗎?mysqlnd的內存分配是使用zend的_emalloc函數嗎?是的,沒錯mysqlnd 是我們的大救星。Mysqlnd_alloc.c line:77里代碼中,明確看到了。各位SA在編譯php時,一定要使用mysqlnd作為php連接mysql server的類庫驅動哦。
Mysqlnd的好處可不止這么一點點啊。
內存還是內存:
末學苦于薄弱的英語,冒死翻過GFW,終于在“萬惡的資本主義”國家的網站上找到了這些資料,mysqlnd將比libmysql節省將近40%的內存占用哦。如圖:
而且,memory_limit參數可以管的了它哦…
速度,速度:
國外友人給了一份測試結果,比較的API是mysqlmysqli,比較的驅動是libmysqlmysqlnd
使用mysqlnd驅動的extmysqli接口速度最快
使用libmysql驅動的extmysqli接口慢了6%
使用libmysql驅動的extmysql接口慢了3%
并且給出了mysqli在兩個驅動下的執行時間:

還有,還有哦…
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技術文章
- PHP中session和cookie的區別
- php設計模式(一):簡介及創建型模式
- php設計模式結構型模式
- Php設計模式(三):行為型模式
- 十款最出色的 PHP 安全開發庫中文詳細介紹
- 12個提問頻率最高的PHP面試題
- PHP 語言需要避免的 10 大誤區
- PHP 死鎖問題分析
- 致PHP路上的“年輕人”
- PHP網站常見安全漏洞,及相應防范措施總結
- 各開源框架使用與設計總結(一)
- 數據庫的本質、概念及其應用實踐(二)
- PHP導出MySQL數據到Excel文件(fputcsv)
- PHP中14種排序算法評測
- 深入理解PHP原理之--echo的實現
- PHP性能分析相關的函數
- PHP 性能分析10則
- 10 位頂級 PHP 大師的開發原則
- 30條爆笑的程序員梗 PHP是最好的語言
- PHP底層的運行機制與原理
- PHP 性能分析與實驗——性能的宏觀分析
- PHP7 性能翻倍關鍵大揭露
- 鳥哥:寫在PHP7發布之際一些話
- PHP與MySQL通訊那點事
- Php session內部執行流程的再次剖析
- 關于 PHP 中的 Class 的幾點個人看法
- PHP Socket 編程過程詳解
- PHP過往及現在及變革
- PHP吉祥物大象的由來
- PHP生成靜態頁面的方法
- 吊炸天的 PHP 7 ,你值得擁有!
- PHP開發中文件操作疑難問答
- MongoDB PHP Driver的連接處理解析
- PHP 雜談《重構-改善既有代碼的設計》之二 對象
- 在php中判斷一個請求是ajax請求還是普通請求的方法
- 使用HAProxy、PHP、Redis和MySQL支撐10億請求每周架構細節
- HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、Web Services 是什么?
- 重構-改善既有代碼的設計
- PHP場景中getshell防御思路分享
- 移動互聯時代,你看看除了PHP你還會些什么
- 安卓系統上搭建本地php服務器環境
- PHP中常見的緩存技術!
- PHP里10個鮮為人知但卻非常有用的函數
- 成為一名PHP專家其實并不難
- PHP 命令行?是的,您可以!
- PHP開發提高效率技巧
- PHP八大安全函數解析
- PHP實現四種基本排序算法
- PHP開發中的中文編碼問題
- php.get.post
- php發送get、post請求的6種方法簡明總結
- 中高級PHP開發者應該掌握哪些技術?
- 前端開發
- web前端知識體系大全
- 前端工程與性能優化(下)
- 前端工程與性能優化(上)
- 2016 年技術發展方向
- Web應用檢查清單
- 如何成為一名優秀的web前端工程師
- 前端組件化開發實踐
- 移動端H5頁面高清多屏適配方案
- 2015前端框架何去何從
- 從前端看“百度遷徙”的技術實現(一)
- 從前端看“百度遷徙”的技術實現(二)
- 前端路上的旅行
- 大公司里怎樣開發和部署前端代碼?
- 5個經典的前端面試問題
- 前端工程師新手必讀
- 手機淘寶前端的圖片相關工作流程梳理
- 一個自動化的前端項目實現(附源碼)
- 前端代碼異常日志收集與監控
- 15年雙11手淘前端技術總結 - H5性能最佳實踐
- 深入理解javascript原型和閉包系列
- 一切都是對象
- 函數和對象的關系
- prototype原型
- 隱式原型
- instanceof
- 繼承
- 原型的靈活性
- 簡述【執行上下文】上
- 簡述【執行上下文】下
- this
- 執行上下文棧
- 簡介【作用域】
- 【作用域】和【上下文環境】
- 從【自由變量】到【作用域鏈】
- 閉包
- 完結
- 補充:上下文環境和作用域的關系
- Linux私房菜