轉載地址:https://mp.weixin.qq.com/s/98D_VtkFEM5bZsu9cazggg?php
目錄java
前言mysql
本文開始撰寫時我負責的項目須要用 php 開發一個經過 Socket 與服務端創建長鏈接後持續實時上報數據的常駐進程程序,在程序業務功能開發聯調完畢後實際運行發送大量數據後發現內存增加很是迅速,在很短的時間內達到了 php 默承認用內存上限 128M ,並報錯:nginx
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
git
我第一反應是內存泄露了,可是不知道在哪。第二反應是無用的變量應該用完就 unset 掉,修改完畢後問題依舊。通過了幾番周折終於解決了問題。就決定好好把相似狀況整理一下,遂有此文,與諸君共勉。
觀察 PHP 程序內存使用狀況github
php 提提供了兩個方法來獲取當前程序的內存使用狀況。web
int memory_get_usage ([ bool $real_usage = false ] )
int memory_get_peak_usage ([ bool $real_usage = false ] )
算法
場景一:程序操做數據過大sql
情景還原:一次性讀取超過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 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 進程都啓動後是否會把系統內存耗盡。
場景四:無用的數據未及時釋放
情景還原:這種問題從程序邏輯上不是問題,可是無用的數據大量佔用內存致使資源不夠用,應該有針對性的作代碼優化。
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
。