十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
本篇文章主要探討內(nèi)存泄漏的原因和后果。通過這篇文章,希望你能收獲更多。下面是探討內(nèi)存泄漏的原因和后果的詳細內(nèi)容。
內(nèi)部泄漏錯誤代碼:
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
觀察php程序內(nèi)存使用情況
php提提供了兩個方法來獲取當(dāng)前程序的內(nèi)存使用情況。
memorygetusage(),這個函數(shù)的作用是獲取目前PHP腳本所用的內(nèi)存大小。
memorygetpeak_usage(),這個函數(shù)的作用返回當(dāng)前腳本到目前位置所占用的內(nèi)存峰值,這樣就可能獲取到目前的腳本的內(nèi)存需求情況。
int memory_get_usage ([ bool $real_usage = false ] ) int memory_get_peak_usage ([ bool $real_usage = false ] )
函數(shù)默認得到的是調(diào)用emalloc()占用的內(nèi)存,如果設(shè)置參數(shù)為TRUE,則得到的是實際程序向系統(tǒng)申請的內(nèi)存。因為 PHP 有自己的內(nèi)存管理機制,所以有時候盡管內(nèi)部已經(jīng)釋放了內(nèi)存但并沒有還給系統(tǒng)。
linux 系統(tǒng)文件 /proc/{$pid}/status 會記錄某個進程的運行狀態(tài),里面的 VmRSS 字段記錄了該進程使用的常駐物理內(nèi)存(Residence),這個就是該進程實際占用的物理內(nèi)存了,用這個數(shù)據(jù)比較靠譜,在程序里面提取這個值也很容易 。
場景一:程序操作數(shù)據(jù)過大
情景還原:一次性讀取超過php可用內(nèi)存上限的數(shù)據(jù)導(dǎo)致內(nèi)存耗盡
實例:
這是告訴我們程序運行時試圖分配新內(nèi)存時由于達到了PHP允許分配的內(nèi)存上限而拋出致命錯誤,無法繼續(xù)執(zhí)行了,在 java 開發(fā)中一般稱之為 OOM ( Out Of Memory ) 。
PHP 配置內(nèi)存上限是在php.ini中設(shè)置memory_limit,PHP 5.2 以前這個默認值是8M,PHP 5.2 的默認值是16M,在這之后的版本默認值都是128M。
問題現(xiàn)象:特定數(shù)據(jù)處理時可復(fù)現(xiàn),做任何 IO 操作都有可能遇到此類問題,比如:一次 mysql 查詢返回大量數(shù)據(jù)、一次把大文件讀取進程序等。解決方法:
1、能用錢解決的問題都不是問題,如果程序要讀大文件的機會不是很多,且上限可預(yù)期,那么通過ini_set('memory_limit', '1G');來設(shè)置一個更大的值或者memory_limit=-1。內(nèi)存管夠的話讓程序一直跑也可以。
2、如果程序需要考慮在小內(nèi)存機器上也能正常使用,那就需要優(yōu)化程序了。如下,代碼復(fù)雜了很多。
場景二、程序操作大數(shù)據(jù)時產(chǎn)生拷貝
情景還原:執(zhí)行過程中對大變量進行了復(fù)制,導(dǎo)致內(nèi)存不夠用。
問題現(xiàn)象:局部代碼執(zhí)行過程中占用內(nèi)存翻倍。
問題分析:
php 是寫時復(fù)制(Copy On Write),也就是說,當(dāng)新變量被賦值時內(nèi)存不發(fā)生變化,直到新變量的內(nèi)容被操作時才會產(chǎn)生復(fù)制。解決方法:
及早釋放無用變量,或者以引用的形式操作原始數(shù)據(jù)。
場景三、配置不合理系統(tǒng)資源耗盡
情景還原:因配置不合理導(dǎo)致內(nèi)存不夠用,2G 內(nèi)存機器上設(shè)置大可以啟動 100 個 php-fpm 子進程,但實際啟動了 50 個 php-fpm 子進程后無法再啟動更多進程 。
問題現(xiàn)象:線上業(yè)務(wù)請求量小的時候不出現(xiàn)問題,請求量一旦很大后部分請求就會執(zhí)行失敗 。
問題分析:一般為了安全方面考慮, php 限制表單請求的大可提交的數(shù)量及大小等參數(shù),post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level。 假設(shè)帶寬足夠,用戶頻繁的提交post_max_size = 8M數(shù)據(jù)到服務(wù)端,nginx 轉(zhuǎn)發(fā)給 php-fpm 處理,那么每個 php-fpm 子進程除了自身占用的內(nèi)存外,即使什么都不做也有可能多占用 8M 內(nèi)存。
解決方法:合理設(shè)置post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level等參數(shù)并調(diào)優(yōu) php-fpm 相關(guān)參數(shù)。
php.ini代碼:
$ php -i |grep memory memory_limit => 1024M => 1024M //php腳本執(zhí)行大可使用內(nèi)存 $php -i |grep max max_execution_time => 0 => 0 //大執(zhí)行時間,腳本默認為0不限制,web請求默認30s max_file_uploads => 20 => 20 //一個表單里大上傳文件數(shù)量 max_input_nesting_level => 64 => 64 //一個表單里數(shù)據(jù)大數(shù)組深度層數(shù) max_input_time => -1 => -1 //php從接收請求開始處理數(shù)據(jù)后的超時時間 max_input_vars => 1000 => 1000 //一個表單(包括get、post、cookie的所有數(shù)據(jù))最多提交1000個字段 post_max_size => 8M => 8M //一次post請求最多提交8M數(shù)據(jù) upload_max_filesize => 2M => 2M //一個可上傳的文件大不超過2M如果上傳設(shè)置不合理那么出現(xiàn)大量內(nèi)存被占用的情況也不奇怪,比如有些內(nèi)網(wǎng)場景下需要 post 超大字符串post_max_size=200M,那么當(dāng)從表單提交了 200M 數(shù)據(jù)到服務(wù)端, php 就會分配 200M 內(nèi)存給這條數(shù)據(jù),直到請求處理完畢釋放內(nèi)存。
Php-fpm.conf代碼 :
pm = dynamic //僅dynamic模式下以下參數(shù)生效 pm.max_children = 10 //大子進程數(shù) pm.start_servers = 3 //啟動時啟動子進程數(shù) pm.min_spare_servers = 2 //最小空閑進程數(shù),不夠了啟動更多進程 pm.max_spare_servers = 5 //大空閑進程數(shù),超過了結(jié)束一些進程 pm.max_requests = 500 //大請求數(shù),注意這個參數(shù)是一個php-fpm如果處理了500個請求后會自己重啟一下, 可以避免一些三方擴展的內(nèi)存泄露問題一個 php-fpm 進程按 30MB 內(nèi)存算,50 個 php-fpm 進程就需要 1500MB 內(nèi)存,這里需要簡單估算一下在負載最重的情況下所有 php-fpm 進程都啟動后是否會把系統(tǒng)內(nèi)存耗盡。
Ulimit代碼:
$ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8192 -c: core file size (blocks) 0 -v: address space (kbytes) unlimited -l: locked-in-memory size (kbytes) unlimited -u: processes 1024 -n: file descriptors 1024這是我本地mac os的配置,文件描述符的設(shè)置是比較小的,一般生產(chǎn)環(huán)境配置要大得多。
場景四、無用的數(shù)據(jù)未及時釋放
情景還原:這種問題從程序邏輯上不是問題,但是無用的數(shù)據(jù)大量占用內(nèi)存導(dǎo)致資源不夠用,應(yīng)該有針對性的做代碼優(yōu)化。
Laravel開發(fā)中用于監(jiān)聽數(shù)據(jù)庫操作時有如下代碼:
代碼:
DB::listen(function ($query) { // $query->sql // $query->bindings // $query->time });啟用數(shù)據(jù)庫監(jiān)聽后,每當(dāng)有 SQL 執(zhí)行時會 new 一個 QueryExecuted 對象并傳入匿名函數(shù)以便后續(xù)操作,對于執(zhí)行完畢就結(jié)束進程釋放資源的php程序來說沒有什么問題,而如果是一個常駐進程的程序,程序每執(zhí)行一條 SQL 內(nèi)存中就會增加一個 QueryExecuted 對象,程序不結(jié)束內(nèi)存就會始終增長。
問題現(xiàn)象:程序運行期間內(nèi)存逐漸增長,程序結(jié)束后內(nèi)存正常釋放。
問題分析:此類問題不易察覺,定位困難,尤其是有些框架封裝好的方法,要明確其適用場景。
解決方法:本例中要通過DB::listen方法獲取所有執(zhí)行的 SQL 語句記錄并寫入日志,但此方法存在內(nèi)存泄露問題,在開發(fā)環(huán)境下無所謂,在生產(chǎn)環(huán)境下則應(yīng)停用,改用其他途徑獲取執(zhí)行的 SQL 語句并寫日志。
深入了解
1、名詞解釋
內(nèi)存泄漏(Memory Leak):是程序在管理內(nèi)存分配過程中未能正確的釋放不再使用的內(nèi)存導(dǎo)致資源被大量占用的一種問題。在面向?qū)ο缶幊虝r,造成內(nèi)存泄露的原因常常是對象在內(nèi)存中存儲但是運行中的代碼卻無法訪問他。由于產(chǎn)生類似問題的情況很多,所以只能從源碼上入手分析定位并解決。
垃圾回收(Garbage Collection,簡稱GC):是一種自動內(nèi)存管理的形式,GC程序檢查并處理程序中那些已經(jīng)分配出去但卻不再被對象使用的內(nèi)存。最早的GC是1959年前后John McCarthy發(fā)明的,用來簡化在Lisp中手動控制內(nèi)存管理。 PHP的內(nèi)核中已自帶內(nèi)存管理的功能,一般應(yīng)用場景下,不易出現(xiàn)內(nèi)存泄露。
追蹤法(Tracing):從某個根對象開始追蹤,檢查哪些對象可訪問,那么其他的(不可訪問)就是垃圾。
引用計數(shù)法(reference count):每個對象都一個數(shù)字用來標(biāo)示被引用的次數(shù)。引用次數(shù)為0的可以回收。當(dāng)對一個對象的引用創(chuàng)建時他的引用計數(shù)就會增加,引用銷毀時計數(shù)減少。引用計數(shù)法可以保證對象一旦不被引用時第一時間銷毀。但是引用計數(shù)有一些缺陷:1.循環(huán)引用,2.引用計數(shù)需要申請更多內(nèi)存,3.對速度有影響,4.需要保證原子性,5.不是實時的。
2、php內(nèi)存管理
在 PHP 5.3 以后引入了同步周期回收算法(Concurrent Cycle Collection)來處理內(nèi)存泄露問題,代價是對性能有一定影響,不過一般 web 腳本應(yīng)用程序影響很小。PHP的垃圾回收機制是默認打開的,php.ini 可以設(shè)置zend.enable_gc=0來關(guān)閉。也能通過分別調(diào)用gcenable() 和 gcdisable()函數(shù)來打開和關(guān)閉垃圾回收機制。
雖然垃圾回收讓php開發(fā)者在內(nèi)存管理上無需擔(dān)心了,但也有極端的反例:php界著名的包管理工具composer曾因加入一行g(shù)c_disable();性能得到極大提升。3、php-fpm內(nèi)存泄漏問題
在一臺常見的 nginx + php-fpm 的服務(wù)器上:
nginx 服務(wù)器 fork 出 n 個子進程(worker), php-fpm 管理器 fork 出 n 個子進程。當(dāng)有用戶請求, nginx 的一個 worker 接收請求,并將請求拋到 socket 中。
php-fpm 空閑的子進程監(jiān)聽到 socket 中有請求,接收并處理請求。
一個 php-fpm 的生命周期大致是這樣的:
模塊初始化(MINIT)-> 請求初始化(RINIT)-> 請求處理 -> 請求結(jié)束(RSHUTDOWN) -> 請求初始化(RINIT)-> 請求處理 -> 請求結(jié)束(RSHUTDOWN)……. 請求初始化(RINIT)-> 請求處理 -> 請求結(jié)束(RSHUTDOWN)-> 模塊關(guān)閉(MSHUTDOWN)。
在請求初始化(RINIT)-> 請求處理 -> 請求結(jié)束(RSHUTDOWN)這個“請求處理”過程是: php 讀取相應(yīng)的 php 文件,對其進行詞法分析,生成 opcode , zend 虛擬機執(zhí)行 opcode 。
php 在每次請求結(jié)束后自動釋放內(nèi)存,有效避免了常見場景下內(nèi)存泄露的問題,然而實際環(huán)境中因某些擴展的內(nèi)存管理沒有做好或者 php 代碼中出現(xiàn)循環(huán)引用導(dǎo)致未能正常釋放不用的資源。
在 php-fpm 配置文件中,將pm.max_requests這個參數(shù)設(shè)置小一點。這個參數(shù)的含義是:一個 php-fpm 子進程最多處理pm.max_requests個用戶請求后,就會被銷毀。當(dāng)一個 php-fpm 進程被銷毀后,它所占用的所有內(nèi)存都會被回收。4、常駐進程內(nèi)存泄漏問題
Valgrind 包括如下一些工具:
Memcheck。這是 valgrind 應(yīng)用最廣泛的工具,一個重量級的內(nèi)存檢查器,能夠發(fā)現(xiàn)開發(fā)中絕大多數(shù)內(nèi)存錯誤使用情況,比如:使用未初始化的內(nèi)存,使用已經(jīng)釋放了的內(nèi)存,內(nèi)存訪問越界等。Callgrind。它主要用來檢查程序中函數(shù)調(diào)用過程中出現(xiàn)的問題。
Cachegrind。它主要用來檢查程序中緩存使用出現(xiàn)的問題。
Helgrind。它主要用來檢查多線程程序中出現(xiàn)的競爭問題。
Massif。它主要用來檢查程序中堆棧使用中出現(xiàn)的問題。
Extension??梢岳胏ore提供的功能,自己編寫特定的內(nèi)存調(diào)試工具。
Memcheck 對調(diào)試 C/C++ 程序的內(nèi)存泄露很有幫助,它的機制是在系統(tǒng) alloc/free 等函數(shù)調(diào)用上加計數(shù)。 php 程序的內(nèi)存泄露,是由于一些循環(huán)引用,或者 gc 的邏輯錯誤, valgrind 無法探測,因此需要在檢測時需要關(guān)閉 php 自帶的內(nèi)存管理。
代碼:
$ export USE_ZEND_ALLOC=0 # 設(shè)置環(huán)境變量關(guān)閉內(nèi)存管理 valgrind --tool=memcheck --num-callers=30 --log-file=php.log /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php引用:
definitely lost
: 肯定內(nèi)存泄露indirectly lost
: 非直接內(nèi)存泄露possibly lost
: 可能發(fā)生內(nèi)存泄露still reachable
: 仍然可訪問的內(nèi)存suppressed
: 外部造成的內(nèi)存泄露Callgrind 配合 php 擴展 xdebug 輸出的 profile 分析日志文件可以分析程序運行期間各個函數(shù)調(diào)用時占用的內(nèi)存、 CPU 占用情況。
總結(jié):遇到了內(nèi)存泄露時先觀察是程序本身內(nèi)存不足還是外部資源導(dǎo)致,然后搞清楚程序運行中用到了哪些資源:寫入磁盤日志、連接數(shù)據(jù)庫 SQL 查詢、發(fā)送 Curl 請求、 Socket 通信等, I/O 操作必然會用到內(nèi)存,如果這些地方都沒有發(fā)生明顯的內(nèi)存泄露,檢查哪里處理大量數(shù)據(jù)沒有及時釋放資源,如果是 php 5.3 以下版本還需考慮循環(huán)引用的問題。多了解一些 Linux 下的分析輔助工具,解決問題時可以事半功倍。
最后宣傳一下穿云團隊今年最新開源的應(yīng)用透明鏈路追蹤工具 Molten:https://github.com/chuan-yun/Molten。安裝好php擴展后就能幫你實時收集程序的 curl,pdo,mysqli,redis,mongodb,memcached 等請求的數(shù)據(jù),可以很方便的與 zipkin 集成。關(guān)于內(nèi)存泄漏的原因和后果就分享到這里了,希望以上內(nèi)容可以對大家有一定的參考價值,可以學(xué)以致用。如果喜歡本篇文章,不妨把它分享出去讓更多的人看到。
新聞標(biāo)題:深入分析內(nèi)存泄漏的原因和后果-創(chuàng)新互聯(lián)
標(biāo)題網(wǎng)址:http://m.jiaotiyi.com/article/gddgi.html