首先,傳統的跑在 FPM 下的 PHP 代碼是沒有「內存泄漏」
一說的,所謂的內存泄漏就是忘記釋放內存,致使進程佔用的物理內存(附1)
持續增加,得益於 PHP 的短生命週期,PHP 內核有一個關鍵函數叫作php_request_shutdown
此函數會在請求結束後,把請求期間申請的全部內存都釋放掉,這從根本上杜絕了內存泄漏,極大的提升了 PHPer 的開發效率,同時也會致使性能的降低,例如單例對象,不必每次請求都從新申請釋放這個單例對象的內存。(這也是Swoole
等cli
方案的優點之一,由於 cli 請求結束不會清理內存)。php
相信 PHPer 都碰見過這個報錯Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes)
,是因爲向 PHP 申請的內存達到了上限致使的,在 FPM 下必定是由於此次 web 請求有大內存塊申請,例如 Sql 查詢返回一個超大結果集,但在 Cli 下報這個錯大機率是由於你的 PHP 代碼出現了內存泄漏。web
常見的泄漏姿式有:api
//不停的調用foo() 內存就會一直漲 function foo(){ ClassA::$pro[] = "the big string"; }
//不停的調用foo() 內存就會一直漲 function foo(){ $GLOBAL['arr'][] = "the big string"; }
//不停的調用foo() 內存就會一直漲 function foo(){ static $arr = []; $arr[] = "the big string"; }
有的同窗可能會說很簡單嘛,把追加的變量在請求結束後unset()
掉就能夠了。但真實場景遠沒有你想的那麼簡單:緩存
function foo() { $obj = new ClassA(); //foo函數結束後將自動釋放 $obj對象 $obj->pro[] = str_repeat("big string", 1024); } while (1) { foo(); sleep(1); }
上述代碼 Cli 運行起來會泄漏嗎?肉眼來看確定不會泄漏,由於foo()
函數結束後$obj
是棧上的對象自動釋放掉了,但答案是可能泄漏也可能沒泄漏,這取決於ClassA
的定義:
class classA { public $pro; public function __construct() { $this->pro = &$GLOBALS['arr']; //pro是其餘變量的引用 } }
若是
ClassA
的定義是上面的樣子,那麼這個例子就是泄漏的!!
class Test { public $pro = null; function run() { $var = "Im global var now";//此處 $var 是長生命週期。 $http = new \Swoole\Http\Server("0.0.0.0", 9501, SWOOLE_BASE); $http->on("request", function($req, $resp) { //此處沒有給類的靜態屬性賦值,沒有給全局變量賦值, //也沒有給函數的靜態變量賦值,可是這裏是泄漏的,由於 $this 變成長生命週期了。 $this->pro[] = str_repeat("big string", 1024); $resp->end("hello world"); }); $http->start(); echo "run done\n"; //輸出不了 //這個函數永遠不會結束,局部變量也變成了"全局變量" } } (new Test())->run();
new Test()
的本意雖然是建立一個臨時的對象,可是run()
方法觸發了server->start()
方法,代碼將不向下執行,run()
函數結束不了,run()
函數的局部變量$var
和臨時對象自己均可以視爲全局變量了,給其追加數據都是泄漏的!!
因爲php_request_shutdown
的存在,不少 PHP 擴展實際上是有內存泄漏的(emalloc 後沒有 efree),可是在 FPM 下是能夠正常運行的,而這些擴展放到 Cli 下就會有內存泄漏問題,若是沒有工具,Cli 下遇到擴展的泄漏問題,那也只能 gg 了-.-!還有就是當咱們調用第三方的類庫的函數,要傳一個參數,這個參數是全局變量,我不知道這個第三方庫會不會給這個參數追加數據,一旦追加數據就會產生泄漏,同理別人給個人函數傳的參數我也不敢賦值,第三方函數的返回值有沒有全局變量我也不知道。swoole
綜上咱們須要一個檢測工具,相對於其餘語言 PHP 在這個領域是空白的,能夠說沒有這個工具整個 Cli 生態就沒法真正的發展起來,由於複雜的項目都會遇到泄漏問題。併發
Swoole Tracker能夠檢測泄漏問題,但它是一款商業產品,如今咱們決定重構這個工具,把內存泄漏檢測的功能(下文簡稱Leak工具
)徹底免費給 PHP 社區使用,完善 PHP 生態,回饋社區,下面我將概述它的具體用法和工做原理。ide
Leak工具
的實現原理是直接攔截系統底層的 emalloc,erealloc,以及 efree 調用,記錄一個巨大的指針表,emalloc/erealloc 的時候添加,efree 的時候刪除表中的記錄,若是請求結束,指針表中仍然有值就證實產生了內存泄漏,不只能發現 PHP 代碼的泄漏,擴展層甚至 PHP 語言層面的泄漏都能發現,從根本上杜絕泄漏問題。函數
使用方式很簡單:工具
extension=swoole_tracker.so ;總開關 apm.enable=1 ;Leak檢測開關 apm.enable_malloc_hook=1
Swoole
的OnReceive函數,workerman 的OnMessage函數,以及上文例一中的foo()
函數, 在循環體主函數(下文簡稱主函數
)最開始加上trackerHookMalloc()
調用便可:function foo() { trackerHookMalloc(); //標記主函數,開始hook malloc $obj = new ClassA(); $obj->pro[] = str_repeat("big string", 1024); } while (1) { foo(); sleep(1); }
每次調用主函數
結束後(第一次調用不會被記錄),都會生成一個泄漏的信息到/tmp/trackerleak
日誌裏面。oop
在 Cli 命令行調用trackerAnalyzeLeak()
函數便可分析泄漏日誌,生成泄漏報告,能夠直接php -r "trackerAnalyzeLeak();"
便可。
下面是泄漏報告的格式:
[16916 (Loop 5)] ✅ Nice!! No Leak Were Detected In This Loop
其中16916
表示進程 id,Loop 5
表示第 5 次調用主函數
生成的泄漏信息
[24265 (Loop 8)] /Users/guoxinhua/tests/mem_leak/http_server.php:125 => <span style="color:red">[12928]</span>
[24265 (Loop 8)] /Users/guoxinhua/tests/mem_leak/http_server.php:129 => <span style="color:red">[12928]</span>
[24265 (Loop 8)] ❌ This Loop TotalLeak: <span style="color:red">[25216]</span>
表示第 8 次調用
http_server.php
的 125 行和 129 行,分別泄漏了 12928 字節內存,總共泄漏了 25216 字節內存。
經過調用trackerCleanLeak()
能夠清除泄漏日誌,從新開始。
想象一個場景,第一次請求運行主函數
的時候申請 10 字節內存,而後請求結束前釋放掉,而後第二次請求申請了 100 字節,請求結束再釋放掉,雖然每次都能正確的釋放內存可是每次又都申請更多的內存,最終致使內存爆掉,Leak工具
支持這種檢測,若是某一行代碼有N次
(默認 5 次)這種行爲就會報"可疑的內存泄漏"
,格式以下:
<span style="color:#b0b05f">The Possible Leak As Malloc Size Keep Growth:</span>
/Users/guoxinhua/tests/mem_leak/hook_malloc_incri.php:39 => <span style="color:red"> Growth Times : [8]; Growth Size : [2304]</span>
表示 39 行有 8 次 malloc size 的增加,總共增加了 2304 字節。
//Swoole Http Server的OnRequest回調 $http->on("request", function($request, $response) { trackerHookMalloc(); if(isset(classA::$leak['tmp'])){ unset(classA::$leak['tmp']);//每一次loop都釋放上一次loop申請的內存 } classA::$leak['tmp'] = str_repeat("big string", 1024);//申請內存 並在本次loop結束後不釋放 $response->end("hello world"); });
按照正常的檢測泄漏的理論,上述代碼每次都會檢測出泄漏,由於每次都給classA::$leak['tmp']
賦值並在 Loop 結束也沒有釋放,但實際業務代碼常常這樣寫,而且此代碼也是不會產生泄漏的,由於本次 Loop 的泄漏會在下次釋放掉,Leak工具
會跨相鄰 2 個Loop 進行分析,自動對衝上面這種狀況的泄漏信息,若是是跨多個 Loop 的釋放,會以以下格式輸出:
[28316 (Loop 2)] /Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:37 => <span style="color:red">[-12288]</span>
<span style="color:#5f92b0">Free Pre (Loop 0) : /Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42 => [12288]</span>
[28316 (Loop 2)] /Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42 => <span style="color:red">[12288]</span>
[28316 (Loop 2)] ✅ Nice!! No Leak Were Detected In This Loop
上述信息表示 Loop 2 釋放了 Loop 0 的 12288 字節內存,而後 Loop 2 又申請了 12288 字節內存,整體來講本次 Loop 跑下來沒有內存泄漏。
首先簡單的介紹一下循環引用問題:
function foo() { $o = new classA(); $o->pro[] = $o; //foo結束後 $o沒法釋放,由於本身引用了本身,即循環引用 } while (1) { foo(); sleep(1); }
由於循環引用,上面的代碼每次運行foo()
內存都會增加,可是這個代碼確實沒有內存泄漏的,由於增加到必定程度 PHP 會開啓同步垃圾回收,把這種循環引用的內存都釋放掉。
可是這給Leak工具
帶來了麻煩,由於$o
的變量是延遲釋放的,foo()
結束後會報泄漏,而這種寫法又確實不是泄漏。
Swoole Tracker
的Leak工具
會自動識別上面的狀況,會立刻釋放循環引用的內存,不會形成誤報。
若是你發現你的進程內存一直漲,開啓了 Tracker 的泄漏檢測,經過
memory_get_usage(false);
打印發現內存不漲了,那麼證實你的應用存在循環引用,而且原本就沒有內存泄漏問題。
function loop() { trackerHookMalloc(); classA::$leak[] = str_repeat("big string", 1024);//申請內存 go(function() { echo co::getcid() . "child\n"; go(function() { echo co::getcid()."child2\n"; classA::$leak = [];//釋放內存 }); }); } Co\run(function(){ while (1) { loop(); sleep(1); } });
上述代碼申請的內存會在第二個子協程裏面釋放,Leak工具
會自動識別協程環境,會在全部子協程都結束後才統計彙總,因此上述代碼不會有誤報狀況。
$http->on("request", function($request, $response) { trackerHookMalloc(); $context = Co::getContext(); $context['data'] = str_repeat("big string", 1024);//context會在協程結束自動釋放 classA::$leak[] = str_repeat("big string1", 1024); defer(function() { classA::$leak = [];//註冊defer釋放內存 }); $response->end("hello world"); });
Leak工具
會自動識別協程環境,若是存在 defer 和 context,會在 defer 執行結束和 context 釋放以後再統計彙總,因此上述代碼不會有誤報狀況,固然若是上面沒有註冊 defer 也會正確的報告泄漏信息。
例如一個進程由主函數
響應請求(OnRequest 等),而後還有個定時器在運行(旁路函數),咱們但願檢測的是主循環函數的泄漏狀況,而當主循環函數執行到一半的時候定時器函數執行了,並申請了內存,而後又切回到主循環函數,此時會誤報,Leak工具
會支持識別出旁路函數而後不收集旁路函數的 malloc 數據。
除了上述這些,Leak工具
還支持internd string
抓取等等,在此再也不展開。
apm.enable_malloc_hook = 1
壓測。trackerHookMalloc()
函數。Swoole4.5.3
因爲底層 api 有問題,Leak工具
沒法正常工做,請升級到最新版Swoole
或者降級Swoole
版本。附件: