請(qǐng)手動(dòng)釋放你的資源
我從來不認(rèn)為這個(gè)問題是個(gè)問題, 直到昨天.
昨天晚上的時(shí)候, 我提交了一個(gè)RFC, 關(guān)于引入finally到PHP, 實(shí)現(xiàn)這個(gè)功能的出發(fā)點(diǎn)很簡(jiǎn)單, 因?yàn)槲铱匆姴簧偃说男枨? 另外還有就是Stas說, 一直只看到討論, 沒看到有人實(shí)現(xiàn). 于是我就給實(shí)現(xiàn)了.
發(fā)到郵件組以后, 一個(gè)開發(fā)組的同學(xué)Nikita Popov(nikic), 表示強(qiáng)烈反對(duì)這個(gè)RFC, 當(dāng)然最初的論點(diǎn)他說了很多, ***我們?cè)诰€討論的時(shí)候, 他表達(dá)了一個(gè)他的觀點(diǎn):
“PHP在請(qǐng)求結(jié)束后會(huì)釋放所有的資源, 所以我們沒有必要調(diào)用fclose,或者mysql_close來釋放資源, PHP會(huì)替我們做”
并且他表示, 他從來都不會(huì)調(diào)用fclose, 認(rèn)為fclose的存在只是為了繼承C函數(shù)族.
我很驚訝, 我也不知道還有多少人是和他一樣的想法, 所以我決定寫這篇文章.
在PHP5.2以前, PHP使用引用計(jì)數(shù)(Reference count)來做資源管理, 當(dāng)一個(gè)zval的引用計(jì)數(shù)為0的時(shí)候, 它就會(huì)被釋放. 雖然存在循環(huán)引用(Cycle reference), 但這樣的設(shè)計(jì)對(duì)于開發(fā)Web腳本來說, 沒什么問題, 因?yàn)閃eb腳本的特點(diǎn)和它追求的目標(biāo)就是執(zhí)行時(shí)間短, 不會(huì)長(zhǎng)期運(yùn)行. 對(duì)于循環(huán)引用造成的資源泄露, 會(huì)在請(qǐng)求結(jié)束時(shí)釋放掉. 也就是說, 請(qǐng)求結(jié)束時(shí)釋放資源, 是一種部補(bǔ)救措施(backup).
然而, 隨著PHP被越來越多的人使用, 就有很多人在一些后臺(tái)腳本使用PHP, 這些腳本的特點(diǎn)是長(zhǎng)期運(yùn)行, 如果存在循環(huán)引用, 導(dǎo)致引用計(jì)數(shù)無法及時(shí)釋放不用的資源, 則這個(gè)腳本最終會(huì)內(nèi)存耗盡退出.
所以在PHP5.3以后, 我們引入了GC, 也就是說, 我們引入GC是為了解決用戶無法解決的問題.
這個(gè)是歷史, 我簡(jiǎn)單介紹下, 現(xiàn)在讓我們回頭來看開頭的問題, 是不是因?yàn)镻HP會(huì)在請(qǐng)求結(jié)束后釋放所有的資源, 于是我們就可以不用手動(dòng)釋放呢?
看一個(gè)例子:
Mysql***連接數(shù)(mysql.max_connections)
- <?php
- $db = mysql_connect() ;
- $resut = mysql_query();
- // process result...
- usleep(500);
- //mysql_close($db); let's say, you didn't call to this
- // other logic, assuming it costs 5s
- sleep(5);
- exit(0); //finish
上面的例子, 我們會(huì)保持一個(gè)和Mysql的連接5秒鐘, 這樣的腳本對(duì)于一般的應(yīng)用來說沒有關(guān)系, 但是對(duì)于一個(gè)請(qǐng)求量很大的腳本來說, 會(huì)導(dǎo)致一個(gè)致命問題:
比如一個(gè)繁忙的應(yīng)用, 每秒要處理來自用戶的1000個(gè)請(qǐng)求, 那么5秒鐘請(qǐng)求多少個(gè)? 5 * 1000 = 5000, 而Mysql有***連接數(shù)限制(mysql.max_connections), 這個(gè)數(shù)字一般不超過2000, 默認(rèn)的會(huì)更低:(mysql.max_connections),
那么, 這樣代碼會(huì)導(dǎo)致你的應(yīng)用, 根本無法正常提供服務(wù). 而如果我們?cè)趯?duì)Mysql的處理完成后就關(guān)閉這個(gè)連接, 那么就不會(huì)觸發(fā)這個(gè)問題.
而我們?cè)趯?shí)踐中, 遇到過一個(gè)更加實(shí)際的問題, 看下面的例子:
- <?PHP
- $mmc = new Memcached();
- $mysql = mysql_connect();
- //process
- mysql_close($mysql);
- $mmc->close();
這是一個(gè)真實(shí)的教訓(xùn), 代碼如上面所示, 突然有一天我們的Mysql出現(xiàn)了問題, 導(dǎo)致連接Mysql的耗時(shí)增大, 然后就導(dǎo)致, 一個(gè)腳本對(duì)Memcached連接占用過長(zhǎng), ***Memcache因?yàn)檫B接數(shù)太多, 就拒絕服務(wù)了..
所以, 我們一定要讓連接代價(jià)***的資源, ***初始化.
系統(tǒng)***句柄 (/proc/sys/fs/file-max)
這個(gè)很簡(jiǎn)單, 如果你持續(xù)打開句柄, 而不釋放, 那么你有可能觸發(fā)系統(tǒng)***句柄限制, 對(duì)于進(jìn)程來說, 自己還有進(jìn)程可打開句柄數(shù)限制(ulimit -n).
系統(tǒng)調(diào)用是昂貴的(System call is expensive)
PHP之所以會(huì)在請(qǐng)求結(jié)束后正確的釋放掉所有的資源, 內(nèi)存, 這是因?yàn)楫?dāng)我們?cè)谀_本中使用新的內(nèi)存的時(shí)候, PHP會(huì)向OS申請(qǐng)一大塊內(nèi)存(ZEND_MM_SEG_SIZE大小), 然后分給你你需要的合適的一塊小內(nèi)存.
當(dāng)你不使用這塊小內(nèi)存的時(shí)候, PHP也不會(huì)返還給OS, 而是保留下來給后續(xù)的處理使用.
我們知道, malloc(3)會(huì)導(dǎo)致系統(tǒng)調(diào)用(brk(2))(當(dāng)然也可能是mmap, 我們此處不考慮這個(gè)細(xì)節(jié), thanks to 華裔), 而系統(tǒng)調(diào)用是昂貴的.
所以, 如果你使用完了資源不及時(shí)釋放, 那么后續(xù)的邏輯如果請(qǐng)求內(nèi)存, PHP發(fā)現(xiàn)之前申請(qǐng)的一大塊內(nèi)存已經(jīng)分光了, 它就只好再次向OS發(fā)起malloc調(diào)用, 得到一塊新的大內(nèi)存. 并且它還需要對(duì)這個(gè)大內(nèi)存做一些標(biāo)記處理..
而如果你使用完資源, 及時(shí)釋放的話, 那么下次腳本申請(qǐng)內(nèi)存的時(shí)候, 你之前歸還的內(nèi)存塊就可以被重復(fù)利用, 那么也許你的整個(gè)腳本只需要和OS申請(qǐng)一次內(nèi)存.
內(nèi)存峰值(Memory peak usage)
這個(gè)和上面的有一定的關(guān)系, 當(dāng)你使用完資源就釋放, 然后后續(xù)又使用這樣的資源. 那么PHP的內(nèi)存占用會(huì)是:
資源+1 -> 資源-1 -> 資源+1 -> 資源-1 (峰值是1)
而如果你是等到PHP請(qǐng)求結(jié)束再釋放:
資源+1 -> 資源 + 1 …. -> 資源 -1 -> 資源 – 1 (峰值是2)
也就說, 一個(gè)良好的編寫的腳本可能要比一個(gè)瞎寫的腳本, 要省很多峰值內(nèi)存..
考慮一個(gè)極端情況, 對(duì)一個(gè)很繁忙的服務(wù)器來說, 比如有10個(gè)PHP進(jìn)程, 每個(gè)PHP進(jìn)程***1G內(nèi)存, 而服務(wù)器只有8G內(nèi)存.
結(jié)論 (conclusion)
結(jié)論很明顯, 我開頭也說過了, 我從來不認(rèn)為這個(gè)是個(gè)問題.
這里說一句, 如果你買了一本PHP的書, 它告訴你: “不用在PHP主動(dòng)釋放資源, 因?yàn)镻HP會(huì)幫你釋放”的話, 我建議你, 燒了它.
原文鏈接:http://www.laruence.com/2012/07/25/2662.html
【編輯推薦】