PHP 内存泄漏分析定位
轉載地址:https://mp.weixin.qq.com/s/98D_VtkFEM5bZsu9cazggg?
目錄
- 場景一 程序操作數據過大
- 場景二 程序操作大數據時產生拷貝
- 場景三 配置不合理系統資源耗盡
- 場景四 無用的數據未及時釋放
- 深入了解
- php內存管理
- php-fpm內存泄露問題
- 常駐進程內存泄露問題
前言
本文開始撰寫時我負責的項目需要用 php 開發一個通過 Socket 與服務端建立長連接后持續實時上報數據的常駐進程程序,在程序業務功能開發聯調完畢后實際運行發送大量數據后發現內存增長非常迅速,在很短的時間內達到了 php 默認可用內存上限 128M ,并報錯:
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
我第一反應是內存泄露了,但是不知道在哪。第二反應是無用的變量應該用完就 unset 掉,修改完畢后問題依舊。經過了幾番周折終于解決了問題。就決定好好把類似情況整理一下,遂有此文,與諸君共勉。
觀察 PHP 程序內存使用情況
php 提提供了兩個方法來獲取當前程序的內存使用情況。
- memory_get_usage(),這個函數的作用是獲取目前PHP腳本所用的內存大小。
- memory_get_peak_usage(),這個函數的作用返回當前腳本到目前位置所占用的內存峰值,這樣就可能獲取到目前的腳本的內存需求情況。
int memory_get_usage ([ bool $real_usage = false ] )
int memory_get_peak_usage ([ bool $real_usage = false ] )
場景一:程序操作數據過大
情景還原:一次性讀取超過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 查詢返回大量數據、一次把大文件讀取進程序等。
解決方法:
能用錢解決的問題都不是問題,如果程序要讀大文件的機會不是很多,且上限可預期,那么通過 ini_set('memory_limit', '1G'); 來設置一個更大的值或者 memory_limit=-1。內存管夠的話讓程序一直跑也可以。
如果程序需要考慮在小內存機器上也能正常使用,那就需要優化程序了。如下,代碼復雜了很多。
<?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 8Call 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.confpm = 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 進程都啟動后是否會把系統內存耗盡。
場景四:無用的數據未及時釋放
情景還原:這種問題從程序邏輯上不是問題,但是無用的數據大量占用內存導致資源不夠用,應該有針對性的做代碼優化。
Laravel開發中用于監聽數據庫操作時有如下代碼:
DB::listen(function ($query) {// $query->sql// $query->bindings// $query->time}); 復制代碼啟用數據庫監聽后,每當有 SQL 執行時會 new 一個 QueryExecuted 對象并傳入匿名函數以便后續操作,對于執行完畢就結束進程釋放資源的 php 程序來說沒有什么問題,而如果是一個常駐進程的程序,程序每執行一條 SQL 內存中就會增加一個 QueryExecuted 對象,程序不結束內存就會始終增長。
問題現象:程序運行期間內存逐漸增長,程序結束后內存正常釋放。
問題分析:
此類問題不易察覺,定位困難,尤其是有些框架封裝好的方法,要明確其適用場景。
解決方法:
本例中要通過DB::listen方法獲取所有執行的 SQL 語句記錄并寫入日志,但此方法存在內存泄露問題,在開發環境下無所謂,在生產環境下則應停用,改用其他途徑獲取執行的 SQL 語句并寫日志。
深入了解
-
內存泄漏(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.2 以前, PHP 使用引用計數(Reference count)來做資源管理, 當一個 zval 的引用計數為 0 的時候, 它就會被釋放.。雖然存在循環引用(Cycle reference), 但這樣的設計對于開發 Web 腳本來說, 沒什么問題, 因為 Web 腳本的特點和它追求的目標就是執行時間短, 不會長期運行。對于循環引用造成的資源泄露, 會在請求結束時釋放掉. 也就是說, 請求結束時釋放資源, 是一種部補救措施( backup ).然而, 隨著 PHP 被越來越多的人使用, 就有很多人在一些后臺腳本使用 PHP , 這些腳本的特點是長期運行, 如果存在循環引用, 導致引用計數無法及時釋放不用的資源, 則這個腳本最終會內存耗盡退出.所以在 PHP 5.3 以后, 我們引入了 GC . 復制代碼—— 摘自鳥哥博客文章《請手動釋放你的資源》
在 PHP 5.3 以后引入了同步周期回收算法(Concurrent Cycle Collection)來處理內存泄露問題,代價是對性能有一定影響,不過一般 web 腳本應用程序影響很小。
PHP 的垃圾回收機制是默認打開的,php.ini 可以設置 zend.enable_gc=0 來關閉。也能通過分別調用 gc_enable() 和 gc_disable() 函數來打開和關閉垃圾回收機制。
雖然垃圾回收讓 php 開發者在內存管理上無需擔心了,但也有極端的反例: php 界著名的包管理工具 composer 曾因加入一行 gc_disable();性能得到極大提升。
引用計數基本知識(http://php.net/manual/zh/features.gc.refcounting-basics.php)
回收周期(Collecting Cycles)(http://docs.php.net/manual/zh/features.gc.collecting-cycles.php)
上面兩個鏈接是php官方手冊中的內存管理、GC相關知識講解,圖文并茂,這里不再贅述。
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 進程被銷毀后,它所占用的所有內存都會被回收。
總結
遇到了內存泄露時先觀察是程序本身內存不足還是外部資源導致,然后搞清楚程序運行中用到了哪些資源:寫入磁盤日志、連接數據庫 SQL 查詢、發送 Curl 請求、 Socket 通信等, I/O 操作必然會用到內存,如果這些地方都沒有發生明顯的內存泄露,檢查哪里處理大量數據沒有及時釋放資源,如果是 php 5.3 以下版本還需考慮循環引用的問題。
多了解一些 Linux 下的分析輔助工具,解決問題時可以事半功倍。
最后宣傳一下穿云團隊今年最新開源的應用透明鏈路追蹤工具 Molten:https://github.com/chuan-yun/Molten。
轉載于:https://juejin.im/post/5aa7c5575188255589497de2
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的PHP 内存泄漏分析定位的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高效使用PC需要记住的快捷键
- 下一篇: Mac下安装Mysql以及修改Mysql