FPM的黑魔法首先,在FPM下运行的传统PHP代码没有“内存泄漏”。由于PHP的生命周期短,PHP内核有一个关键函数叫做php_request_shutdown。该函数会在请求结束后释放请求过程中申请的所有内存,从根本上杜绝了内存泄漏,大大提高了PHPer的开发效率,同时也会导致性能下降,比如单例对象,没必要每次请求时重新申请释放这个单例对象的内存。(这也是Swoole等cli方案的优势之一,因为cli请求结束不会清理内存)。Cli下的内存泄漏相信PHPer遇到过这个错误Fatalerror:Allowedmemorysizeof134217728bytesexhausted(triedtoallocate12288bytes),这是由于PHP申请的内存达到上限造成的。在FPM下,肯定是因为这个web请求需要大内存块的申请。比如一个Sql查询返回一个很大的结果集,但是在Cli下却报这个错误。大概率是你的PHP代码有内存泄漏。常见的泄漏手势有:向类的静态属性中添加数据,例如://不断调用foo()内存会不断增加functionfoo(){ClassA::$pro[]="thebigstring";}向$GLOBAL全局变量添加数据,例如://不断调用foo()内存会不断增加functionfoo(){$GLOBAL['arr'][]="thebigstring";}向函数的静态变量,例如://不断调用foo()内存会不断增加functionfoo(){static$arr=[];$arr[]="thebigstring";}Weneed检测工具的同学可能会说很简单,在请求结束后unset()添加的变量即可。但真实的场景并不像你想象的那么简单:例子一:functionfoo(){$obj=newClassA();//foo函数结束后$obj对象会自动释放$obj->pro[]=str_repeat("bigstring",1024);}while(1){foo();sleep(1);}上面的代码Cli运行起来会不会泄露?肉眼肯定不会泄漏,因为foo()函数结束后,$obj是栈上的一个对象,自动释放,但答案是可能泄漏也可能不泄漏,这取决于ClassA的定义:classclassA{public$pro;公共函数__construct(){$this->pro=&$GLOBALS['arr'];//pro是对其他变量的引用}}如果ClassA的定义如上,那么这个例子就是leak!!示例2:类测试{public$pro=null;functionrun(){$var="Imglobalvarnow";//这里$var是长生命周期。$http=new\Swoole\Http\Server("0.0.0.0",9501,SWOOLE_BASE);$http->on("request",function($req,$resp){//这里没有给类的static属性赋值,没有给全局变量赋值,//没有给函数的静态变量赋值,但是这里有漏洞,因为$this已经变成了一个很长的生命周期。$this->pro[]=str_repeat("大字符串",1024);$resp->end("你好世界");});$http->开始();echo"运行完成\n";//输出不可用//这个函数永远不会结束,局部变量变成“全局变量”}}(newTest())->run();虽然newTest()的初衷是创建一个临时对象,但是run()方法触发了server->start()方法,代码不会向下执行,run()函数也无法结束。局部变量$var和run()函数的临时对象都可以看成是全局变量。向其添加数据是泄漏!!例3:由于php_request_shutdown的存在,很多PHP扩展其实存在内存泄漏(emalloc后没有efree),但是在FPM下可以正常运行,而这些扩展放在Cli下就会内存泄漏。工具,如果在Cli下遇到扩展泄露问题,只能用gg-.-!另外,我们在调用第三方类库的函数时,需要传递一个参数。这个参数是一个全局变量。不知道这个第三方库会不会给这个参数加数据。一旦添加了数据,它就会泄漏。同时我也不敢给别人传给我函数的参数赋值,也不知道第三方函数的返回值里有没有全局变量。综上所述,我们需要一个检测工具。与其他语言相比,PHP在这方面是一片空白。可以说,没有这个工具,整个Cli生态是无法真正发展起来的,因为复杂的项目会遇到泄漏问题。SwooleTracker可以检测泄漏,但它是一个商业产品。现在我们决定对这个工具进行重构,将内存泄漏检测功能(以下简称Leak工具)完全免费提供给PHP社区使用,完善PHP生态,回馈社区。下面我将准确概述它的使用方法和工作原理。SwooleTrackerusageLeak工具的实现原理是直接拦截系统底层的emalloc、errealloc、efree调用,记录一个巨大的指针表,emalloc/errealloc时添加,efree时删除表中的记录,而如果请求结束,指针表中还有值的话,证明存在内存泄漏。不仅可以找到PHP代码的漏洞,还可以找到扩展层乃至PHP语言层面的漏洞,从根本上杜绝漏洞问题。使用方法很简单:去官网下载最新的tracker(3.0+)扩展。在php.ini中添加如下配置:extension=swoole_tracker.so;主开关apm.enable=0;泄漏检测开关apm.enable_malloc_hook=1在Cli模式下,主要的业务逻辑必须抽象成一个循环体函数,比如Swoole的OnReceive函数,workerman的OnMessage函数,以及上面例子1中的foo()函数,只需添加trackerHookMalloc()在循环体的main函数(以下简称main函数)开头调用:functionfoo(){trackerHookMalloc();/标记主函数,开始hookmalloc$obj=newClassA();$obj->pro[]=str_repeat("大字符串",1024);}while(1){foo();sleep(1);}每次调用main函数后(第一次调用不会被记录),/tmp/trackerleak日志中会产生一条泄漏消息。查看泄漏结果,在Cli命令行调用trackerAnalyzeLeak()函数分析泄漏日志,生成泄漏报告。可以直接使用php-r"trackerAnalyzeLeak();"。以下是泄漏报告的格式:没有内存泄漏:[16916(Loop5)]?Nice!!NoLeakWereDetectedInThisLoopwhere16916representedtheprocessid,Loop5代表第五次调用main函数产生的泄漏信息确认有内存泄漏:[24265(Loop8)]/Users/guoxinhua/tests/mem_leak/http_server.php:125=>[12928][24265(Loop8)]/Users/guoxinhua/tests/mem_leak/http_server.php:129=>[12928][24265(Loop8)]?这个LoopTotalLeak:[25216]表示第八个调用http_server.php的第125行和第129行分别泄漏了12928字节的内存,一共泄漏了25216字节的内存。可以通过调用trackerCleanLeak()重新开始来清除泄漏日志。技术特性(技术难点)支持持续增长检测:想象这样一个场景,第一次运行main函数时申请了10字节的内存,然后在请求结束前释放,之后又申请了100字节的内存第二个请求,请求结束然后释放。虽然每次都能正确释放内存,但每次都会申请更多的内存,最终导致内存爆炸。Leak工具支持这种检测。如果一行代码有N次(默认5次)这种行为会报“可疑内存泄漏”,格式如下:ThePossibleLeakAsMallocSi??zeKeep增长:/Users/guoxinhua/tests/mem_leak/hook_malloc_incri。php:39=>增长时间:[8];GrowthSize:[2304]表示第39行增加了8个mallocsize,一共增加了2304字节。支持跨环分析://SwooleHttpServer的OnRequest回调$http->on("request",function($request,$response){trackerHookMalloc();if(isset(classA::$leak['tmp'])){unset(classA::$leak['tmp']);//每次循环释放上一次循环申请的内存}classA::$leak['tmp']=str_repeat("bigstring",1024);//申请内存,本次循环结束后不释放$response->end("helloworld");});按照正常的leakdetection理论,上面的代码每次都会检测到leak,因为每次都是给classA::$leak['tmp']赋值,并没有在Loop结束时release,但是实际业务代码往往是这样写的,这段代码不会产生泄漏,因为下次会释放这个Loop的泄漏,Leak工具会跨2个相邻的Loop进行分析,自动对冲上述情况下的泄漏信息.如果跨多个Loop发布,会输出如下格式:[28316(Loop2)]/Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:37=>[-12288]FreePre(Loop0):/Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42=>[12288][28316(循环2)]/Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42=>[12288][28316(循环2)]?好的!!NoLeakWereDetectedInThisLoop以上信息表明Loop2在Loop0释放了12288字节的内存,然后Loop2申请了12288字节的内存。一般来说,这个Loop是没有内存泄漏的,支持循环引用。情况:首先简单介绍下循环引用问题:functionfoo(){$o=newclassA();$o->pro[]=$o;//$o不能在foo结束后被释放,因为它引用了自己,即循环引用}while(1){foo();sleep(1);}因为循环引用,上面代码的内存会在每次运行foo()时增加,但是这段代码并没有内存泄漏,因为增长到一定程度,PHP会转在同步垃圾回收上释放所有被这个循环引用的内存。但是这给Leak工具带来了麻烦,因为$o的变量是延迟释放的,foo()结束后会报leak,而这种写法确实不是leak。SwooleTracker的Leak工具会自动识别出上述情况,并会立即释放循环引用的内存,不会造成误报。如果你发现你的进程内存一直在增加,打开Tracker的泄漏检测,打印memory_get_usage(false);并且发现内存没有增加,那么就证明你的应用存在循环引用,一开始就不存在内存泄漏问题。支持子协程统计:functionloop(){trackerHookMalloc();classA::$leak[]=str_repeat("bigstring",1024);//申请内存go(function(){echoco::getcid()."child\n";go(function(){echoco::getcid()."child2\n";classA::$leak=[];//释放内存});});}Co\run(function(){while(1){loop();睡眠(1);}});上面代码申请的内存会在第二个子协程中释放,Leak工具会自动识别协程环境,会在所有子协程执行完毕后才会进行统计和汇总,所以上面的代码不会有误报。支持延迟,上下文:$http->on("request",function($request,$response){trackerHookMalloc();$context=Co::getContext();$context['data']=str_repeat("bigstring",1024);//上下文会在协程结束时自动释放classA::$leak[]=str_repeat("bigstring1",1024);defer(function(){classA::$leak=[];//注册延迟释放内存});$response->end("helloworld");});Leak工具会自动识别协程环境。如果存在defer和context,会在defer执行结束,释放context后统计,所以上面的代码不会有误报。当然,如果没有上面注册的defer,泄漏信息也会正确报错。支持bypass函数干扰消除:比如一个进程响应了main函数的请求(OnRequest等),然后有定时器在运行(bypass函数),我们希望检测到mainloop函数的泄漏,以及当主循环函数执行到一半时,执行定时器函数,申请内存,然后切换回主循环函数。这时候就会误报。Leak工具会支持对bypass函数的识别,然后不收集bypass函数的malloc数据。除了以上,Leak工具还支持internd字符串抓取等,这里不再展开。注意忽略前面Loops的泄漏信息,因为大部分项目都有一些未释放的初始化缓存。检测时尽量不要并发。不要在php.ini中开启apm.enable_malloc_hook=1压力测试,因为开启泄漏检测后性能会很差。与SwooleTracker2.x的检漏原理不同,不能同时使用。一个进程只能有一个地方调用trackerHookMalloc()函数。Swoole4.5.3由于底层api问题导致Leak工具无法正常使用。请升级到最新版本的Swoole或降级Swoole版本。附:免费公开课--如何正确查看进程的内存使用情况