定位分析記憶體漏失的原因和後果

2020-07-16 10:05:44

內部洩漏錯誤程式碼:

Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)

觀察php程式記憶體使用情況

php提提供了兩個方法來獲取當前程式的記憶體使用情況。
memorygetusage(),這個函數的作用是獲取目前PHP指令碼所用的記憶體大小。

memorygetpeak_usage(),這個函數的作用返回當前指令碼到目前位置所佔用的記憶體峰值,這樣就可能獲取到目前的指令碼的記憶體需求情況。

int memory_get_usage ([ bool $real_usage = false ] )  
int memory_get_peak_usage ([ bool $real_usage = false ] )

函數預設得到的是呼叫emalloc()占用的記憶體,如果設定引數為TRUE,則得到的是實際程式向系統申請的記憶體。因為 PHP 有自己的記憶體管理機制,所以有時候儘管內部已經釋放了記憶體但並沒有還給系統。

linux 系統檔案 /proc/{$pid}/status 會記錄某個進程的執行狀態,裡面的 VmRSS 欄位記錄了該進程使用的常駐實體記憶體(Residence),這個就是該進程實際占用的實體記憶體了,用這個資料比較靠譜,在程式裡面提取這個值也很容易 。

場景一:程式運算元據過大

情景還原:一次性讀取超過php可用記憶體上限的資料導致記憶體耗盡

範例:

<?php  ini_set('memory_limit', '128M');  
$string = str_pad('1', 128 * 1024 * 1024);    
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 134217729 bytes) 
in /Users/zouyi/php-oom/bigfile.php on line 3

這是告訴我們程式執行時試圖分配新記憶體時由於達到了PHP允許分配的記憶體上限而丟擲致命錯誤,無法繼續執行了,在 java 開發中一般稱之為 OOM ( Out Of Memory ) 。
PHP 設定記憶體上限是在php.ini中設定memory_limit,PHP 5.2 以前這個預設值是8M,PHP 5.2 的預設值是16M,在這之後的版本預設值都是128M。
問題現象:特定資料處理時可復現,做任何 IO 操作都有可能遇到此類問題,比如:一次 mysql 查詢返回大量資料、一次把大檔案讀取進程式等。

解決方法:

1、能用錢解決的問題都不是問題,如果程式要讀大檔案的機會不是很多,且上限可預期,那麼通過ini_set('memory_limit', '1G');來設定一個更大的值或者memory_limit=-1。記憶體管夠的話讓程式一直跑也可以。

2、如果程式需要考慮在小記憶體機器上也能正常使用,那就需要優化程式了。如下,程式碼複雜了很多。

<?php  
//php7 以下版本通過 composer 引入 paragonie/random_compat ,為了方便來生成一個隨機名稱的臨時檔案  
require "vendor/autoload.php";    
ini_set('memory_limit', '128M');  
//生成臨時檔案存放大字串  
$fileName = 'tmp'.bin2hex(random_bytes(5)).'.txt';  
touch($fileName);  
for ( $i = 0; $i < 128; $i++ ) {      
$string = str_pad('1', 1 * 1024 * 1024);      
file_put_contents($fileName, $string, FILE_APPEND);  
}  
$handle = fopen($fileName, "r");  
for ( $i = 0; $i <= filesize($fileName) / 1 * 1024 * 1024; $i++ )  {     
//do something     
$string = fread($handle, 1 * 1024 * 1024);  
}    
fclose($handle);  
unlink($fileName);

場景二、程式操作巨量資料時產生拷貝

情景還原:執行過程中對大變數進行了複製,導致記憶體不夠用。

<?php  
ini_set("memory_limit",'1M');    
$string = str_pad('1', 1* 750 *1024);  
$string2 = $string;  $string2 .= '1';    
Fatal error: Allowed memory size of 1048576 bytes exhausted (tried to allocate 768001 bytes) 
in /Users/zouyi/php-oom/unset.php on line 8    
Call Stack:      
0.0004     235440   1. {main}() /Users/zouyi/php-oom/unset.php:0    zend_mm_heap corrupted

問題現象:區域性程式碼執行過程中佔用記憶體翻倍。

問題分析:
php 是寫時複製(Copy On Write),也就是說,當新變數被賦值時記憶體不發生變化,直到新變數的內容被操作時才會產生複製。

解決方法:

及早釋放無用變數,或者以參照的形式操作原始資料。

<?php  
ini_set("memory_limit",'1M');    
$string = str_pad('1', 1* 750 *1024);  
$string2 = $string;  unset($string);  
$string2 .= '1';    
<?php  
ini_set("memory_limit",'1M');    
$string = str_pad('1', 1* 750 *1024);  
$string2 = &$string;  
$string2 .= '1';    
unset($string2, $string);

場景三、設定不合理系統資源耗盡

情景還原:因設定不合理導致記憶體不夠用,2G 記憶體機器上設定最大可以啟動 100 個 php-fpm 子進程,但實際啟動了 50 個 php-fpm 子進程後無法再啟動更多進程 。

問題現象:線上業務請求量小的時候不出現問題,請求量一旦很大後部分請求就會執行失敗 。

問題分析:一般為了安全方面考慮, php 限制表單請求的最大可提交的數量及大小等引數,post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level。 假設頻寬足夠,使用者頻繁的提交post_max_size = 8M資料到伺服器端,nginx 轉發給 php-fpm 處理,那麼每個 php-fpm 子進程除了自身占用的記憶體外,即使什麼都不做也有可能多佔用 8M 記憶體。

解決方法:合理設定post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level等引數並調優 php-fpm 相關引數。

php.ini程式碼

$ php -i |grep memory  
memory_limit => 1024M => 1024M //php指令碼執行最大可使用記憶體  
$php -i |grep max  max_execution_time => 0 => 0 //最大執行時間,指令碼預設為0不限制,web請求預設30s  
max_file_uploads => 20 => 20 //一個表單裡最大上傳檔案數量  
max_input_nesting_level => 64 => 64 //一個表單裡資料最大陣列深度層數  
max_input_time => -1 => -1 //php從接收請求開始處理資料後的超時時間  
max_input_vars => 1000 => 1000 //一個表單(包括get、post、cookie的所有資料)最多提交1000個欄位  
post_max_size => 8M => 8M //一次post請求最多提交8M資料  
upload_max_filesize => 2M => 2M //一個可上傳的檔案最大不超過2M

如果上傳設定不合理那麼出現大量記憶體被佔用的情況也不奇怪,比如有些內網場景下需要 post 超大字串post_max_size=200M,那麼當從表單提交了 200M 資料到伺服器端, php 就會分配 200M 記憶體給這條資料,直到請求處理完畢釋放記憶體。

Php-fpm.conf程式碼

pm = dynamic //僅dynamic模式下以下引數生效  
pm.max_children = 10 //最大子進程數  
pm.start_servers = 3 //啟動時啟動子進程數  
pm.min_spare_servers = 2 //最小空閒進程數,不夠了啟動更多進程  
pm.max_spare_servers = 5 //最大空閒進程數,超過了結束一些進程  
pm.max_requests = 500 //最大請求數,注意這個引數是一個php-fpm如果處理了500個請求後會自己重新啟動一下,
可以避免一些三方擴充套件的記憶體洩露問題

一個 php-fpm 進程按 30MB 記憶體算,50 個 php-fpm 進程就需要 1500MB 記憶體,這裡需要簡單估算一下在負載最重的情況下所有 php-fpm 進程都啟動後是否會把系統記憶體耗盡。

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的設定,檔案描述符的設定是比較小的,一般生產環境設定要大得多。

場景四、無用的資料未及時釋放

情景還原:這種問題從程式邏輯上不是問題,但是無用的資料大量佔用記憶體導致資源不夠用,應該有針對性的做程式碼優化。

Laravel開發中用於監聽資料庫操作時有如下程式碼:

程式碼:

DB::listen(function ($query) {      
// $query->sql      
// $query->bindings      
// $query->time  
});

啟用資料庫監聽後,每當有 SQL 執行時會 new 一個 QueryExecuted 物件並傳入匿名函數以便後續操作,對於執行完畢就結束進程釋放資源的php程式來說沒有什麼問題,而如果是一個常駐進程的程式,程式每執行一條 SQL 記憶體中就會增加一個 QueryExecuted 物件,程式不結束記憶體就會始終增長。

問題現象:程式執行期間記憶體逐漸增長,程式結束後記憶體正常釋放。

問題分析:此類問題不易察覺,定位困難,尤其是有些框架封裝好的方法,要明確其適用場景。

解決方法:本例中要通過DB::listen方法獲取所有執行的 SQL 語句記錄並寫入紀錄檔,但此方法存在記憶體洩露問題,在開發環境下無所謂,在生產環境下則應停用,改用其他途徑獲取執行的 SQL 語句並寫紀錄檔。

深入了解

1、名詞解釋

記憶體漏失(Memory Leak):是程式在管理記憶體分配過程中未能正確的釋放不再使用的記憶體導致資源被大量佔用的一種問題。在物件導向程式設計時,造成記憶體洩露的原因常常是物件在記憶體中儲存但是執行中的程式碼卻無法存取他。由於產生類似問題的情況很多,所以只能從原始碼上入手分析定位並解決。

垃圾回收(Garbage Collection,簡稱GC):是一種自動記憶體管理的形式,GC程式檢查並處理程式中那些已經分配出去但卻不再被物件使用的記憶體。最早的GC是1959年前後John McCarthy發明的,用來簡化在Lisp中手動控制記憶體管理。 PHP的核心中已自帶記憶體管理的功能,一般應用場景下,不易出現記憶體洩露。

追蹤法(Tracing):從某個根物件開始追蹤,檢查哪些物件可存取,那麼其他的(不可存取)就是垃圾。

參照計數法(reference count):每個物件都一個數位用來標示被參照的次數。參照次數為0的可以回收。當對一個物件的參照建立時他的參照計數就會增加,參照銷毀時計數減少。參照計數法可以保證物件一旦不被參照時第一時間銷毀。但是參照計數有一些缺陷:1.迴圈參照,2.參照計數需要申請更多記憶體,3.對速度有影響,4.需要保證原子性,5.不是實時的。

2、php記憶體管理

在 PHP 5.3 以後引入了同步周期回收演算法(Concurrent Cycle Collection)來處理記憶體洩露問題,代價是對效能有一定影響,不過一般 web 指令碼應用程式影響很小。PHP的垃圾回收機制是預設開啟的,php.ini 可以設定zend.enable_gc=0來關閉。也能通過分別呼叫gcenable() 和 gcdisable()函數來開啟和關閉垃圾回收機制。
雖然垃圾回收讓php開發者在記憶體管理上無需擔心了,但也有極端的反例:php界著名的包管理工具composer曾因加入一行gc_disable();效能得到極大提升。

3、php-fpm記憶體漏失問題

在一台常見的 nginx + php-fpm 的伺服器上:
nginx 伺服器 fork 出 n 個子進程(worker), php-fpm 管理器 fork 出 n 個子進程。

當有使用者請求, nginx 的一個 worker 接收請求,並將請求拋到 socket 中。

php-fpm 空閒的子進程監聽到 socket 中有請求,接收並處理請求。

一個 php-fpm 的生命週期大致是這樣的:

模組初始化(MINIT)-> 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN) -> 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)……. 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)-> 模組關閉(MSHUTDOWN)。

在請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)這個「請求處理」過程是: php 讀取相應的 php 檔案,對其進行詞法分析,生成 opcode , zend 虛擬機器執行 opcode 。
php 在每次請求結束後自動釋放記憶體,有效避免了常見場景下記憶體洩露的問題,然而實際環境中因某些擴充套件的記憶體管理沒有做好或者 php 程式碼中出現迴圈參照導致未能正常釋放不用的資源。
在 php-fpm 組態檔中,將pm.max_requests這個引數設定小一點。這個引數的含義是:一個 php-fpm 子進程最多處理pm.max_requests個使用者請求後,就會被銷毀。當一個 php-fpm 進程被銷毀後,它所佔用的所有記憶體都會被回收。

4、常駐進程記憶體漏失問題

Valgrind 包括如下一些工具:
Memcheck。這是 valgrind 應用最廣泛的工具,一個重量級的記憶體檢查器,能夠發現開發中絕大多數記憶體錯誤使用情況,比如:使用未初始化的記憶體,使用已經釋放了的記憶體,記憶體存取越界等。

Callgrind。它主要用來檢查程式中函數呼叫過程中出現的問題。

Cachegrind。它主要用來檢查程式中快取使用出現的問題。

Helgrind。它主要用來檢查多執行緒程式中出現的競爭問題。

Massif。它主要用來檢查程式中堆疊使用中出現的問題。

Extension。可以利用core提供的功能,自己編寫特定的記憶體偵錯工具。

Memcheck 對偵錯 C/C++ 程式的記憶體洩露很有幫助,它的機制是在系統 alloc/free 等函數呼叫上加計數。 php 程式的記憶體洩露,是由於一些迴圈參照,或者 gc 的邏輯錯誤, valgrind 無法探測,因此需要在檢測時需要關閉 php 自帶的記憶體管理。

程式碼:

$ export USE_ZEND_ALLOC=0   
# 設定環境變數關閉記憶體管理  
 valgrind --tool=memcheck --num-callers=30 --log-file=php.log
/Users/zouyi/Downloads/php-5.6.31/sapi/cli/php  leak.php

參照:

definitely lost: 肯定記憶體洩露
indirectly lost: 非直接記憶體洩露
possibly lost: 可能發生記憶體洩露
still reachable: 仍然可存取的記憶體
suppressed: 外部造成的記憶體洩露

Callgrind 配合 php 擴充套件 xdebug 輸出的 profile 分析紀錄檔檔案可以分析程式執行期間各個函數呼叫時占用的記憶體、 CPU 占用情況。

總結:遇到了記憶體洩露時先觀察是程式本身記憶體不足還是外部資源導致,然後搞清楚程式執行中用到了哪些資源:寫入磁碟紀錄檔、連線資料庫 SQL 查詢、傳送 Curl 請求、 Socket 通訊等, I/O 操作必然會用到記憶體,如果這些地方都沒有發生明顯的記憶體洩露,檢查哪裡處理大量資料沒有及時釋放資源,如果是 php 5.3 以下版本還需考慮迴圈參照的問題。多了解一些 Linux 下的分析輔助工具,解決問題時可以事半功倍。
最後宣傳一下穿雲團隊今年最新開源的應用透明鏈路追蹤工具 Molten:https://github.com/chuan-yun/Molten。安裝好php擴充套件後就能幫你實時收集程式的 curl,pdo,mysqli,redis,mongodb,memcached 等請求的資料,可以很方便的與 zipkin 整合。

以上內容僅供參考!

以上就是定位分析記憶體漏失的原因和後果的詳細內容,更多請關注TW511.COM其它相關文章!