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