Deprecated
: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in
/www/wwwroot/testblog.58heshihu.com/var/Widget/Archive.php
on line
1057
首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
182 阅读
2
php接口优化 使用curl_multi_init批量请求
144 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
786
篇文章
累计收到
28
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
2
篇与
的结果
2023-08-11
PHP常驻进程,内存泄漏排查指南
PHP常驻进程,内存泄漏排查指南废话不多说,总共分三步1、初步定为泄漏:迫于 996ICU 的压力,广大的 PHPer 一般不会关注泄漏问题,都是在看到报错Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes)Bash才发现泄漏问题,此时我们一般会通过查看进程的RSS占用来确定内存占用,例如这样cat /proc/28806/status |grep RSSBash但一定要注意的是,此处查看的RSS是包含共享内存的(共享的内存会重复计算多次),并不是进程真正占用的内存,USS才是我们 PHP 代码申请的内存,我们更应该关注的是USS指标。(感兴趣的小伙伴可以看这个视频)。怎么看USS,这里推荐smem这个命令,用法和结果如下:RSS 占用特别多,USS 特别少,证明大部分 RSS 都是共享内存占用,此时大概率是你的Swoole Table或者Apcu用的有问题,因为这两个底层是基于共享内存的。2、定位泄漏的代码:可以用Swoole Tracker提供的工具来定位,具体参考这篇文章3、清理内存碎片:如果 Tracker 发现不了泄漏,内存还一直涨,八成是遇到了 PHP 的内存碎片问题,内存碎片问题也是我想写这篇文章的原因,社区里面有个小伙伴用了Swoole Tracker没有发现泄漏,但是通过 smem 命令查看内存确实在涨,即使unset了所有变量,内存仍然无法降下去,代码如下:function main(){for ($i = 1; $i < 2000000; $i++) { $GLOBALS[$i] = str_repeat("str_repeat这个函数会申请内存,但我马上就unset掉", 10); } for ($i = 1; $i < 2000000; $i++) { unset($GLOBALS[$i]); }}main();PHP咋回事呢?根本原因是产生了内存碎片,和 PHP 的内存分配算法有关,这里不展开讲,大概原理是:小于 3072 字节的内存申请 PHP 会认为是小内存,PHP 会把所有申请的小内存块缓存起来,即使释放了也不归还给操作系统,以保证内存管理的效率在 FPM 下请求结束后会释放所有内存,大部分归还给系统,只保留一小部分,但是在 Cli 下没有这样的机制,我们该怎么办?我研究了一天,貌似只能给 php-src 提 pr 了,写了一天发现”咦?”怎么有段代码怎么和我的思路这么相似,仔细一看”日哦”,在 php 高版本提供了一个gc_mem_caches()函数(网上没有任何文章介绍这个函数),可以手动清理这种小内存的碎片问题 - -!我们可以设置一个 Swoole 的定时器,定期调用gc_mem_caches()即可。注意gc_mem_caches()能极大的缓解PHP下面的内存碎片导致的内存增长问题,但是由于zend mm的设计机制问题,长时间运行还是会缓慢增长,这里我们也可以通过替换 PHP 的内存管理模块(比如采用 jemalloc)来彻底避免这种问题,具体方法是自己编译一个jemalloc的so然后启动PHP的时候这样export USE_ZEND_ALLOC=0 && LD_PRELOAD='/usr/local/lib/libjemalloc.so' php /home/guoxinhua/swoole_server.php即可。总结第一步正确的发现泄漏,第二步使用 tracker 定位泄漏代码并 fix 它,第三步如果还是泄漏,尝试调用gc_mem_caches()清理碎片。
2023年08月11日
11 阅读
0 评论
0 点赞
2023-08-11
一文详解PHP的内存泄露
一文详解PHP的内存泄露FPM 的黑魔法首先,传统的跑在 FPM 下的 PHP 代码是没有“内存泄漏”一说的,所谓的内存泄漏就是忘记释放内存,导致进程占用的物理内存(附1)持续增长,得益于 PHP 的短生命周期,PHP 内核有一个关键函数叫做php_request_shutdown此函数会在请求结束后,把请求期间申请的所有内存都释放掉,这从根本上杜绝了内存泄漏,极大的提高了 PHPer 的开发效率,同时也会导致性能的下降,例如单例对象,没必要每次请求都重新申请释放这个单例对象的内存。(这也是Swoole等cli方案的优势之一,因为 cli 请求结束不会清理内存)。Cli 下的内存泄漏相信 PHPer 都遇见过这个报错Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes),是由于向 PHP 申请的内存达到了上限导致的,在 FPM 下一定是因为这次 web 请求有大内存块申请,例如 Sql 查询返回一个超大结果集,但在 Cli 下报这个错大概率是因为你的 PHP 代码出现了内存泄漏。常见的泄漏姿势有:向类的静态属性中追加数据,例如://不停的调用foo() 内存就会一直涨function foo(){ ClassA::$pro[] = "the big string";}向 $GLOBAL 全局变量中追加数据,例如://不停的调用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 了-.-!还有就是当我们调用第三方的类库的函数,要传一个参数,这个参数是全局变量,我不知道这个第三方库会不会给这个参数追加数据,一旦追加数据就会产生泄漏,同理别人给我的函数传的参数我也不敢赋值,第三方函数的返回值有没有全局变量我也不知道。综上我们需要一个检测工具,相对于其他语言 PHP 在这个领域是空白的,可以说没有这个工具整个 Cli 生态就无法真正的发展起来,因为复杂的项目都会遇到泄漏问题。Swoole Tracker可以检测泄漏问题,但它是一款商业产品,现在我们决定重构这个工具,把内存泄漏检测的功能(下文简称Leak工具)完全免费给 PHP 社区使用,完善 PHP 生态,回馈社区,下面我将概述它的具体用法和工作原理。Swoole Tracker 用法Leak工具的实现原理是直接拦截系统底层的 emalloc,erealloc,以及 efree 调用,记录一个巨大的指针表,emalloc/erealloc 的时候添加,efree 的时候删除表中的记录,如果请求结束,指针表中仍然有值就证明产生了内存泄漏,不仅能发现 PHP 代码的泄漏,扩展层甚至 PHP 语言层面的泄漏都能发现,从根本上杜绝泄漏问题。使用方式很简单:前往官网下载最新的 tracker(3.0+) 扩展。php.ini 加入以下配置:extension=swoole_tracker.so;总开关apm.enable=1;Leak检测开关apm.enable_malloc_hook=1在 Cli 模式下主业务逻辑一定是可以抽象成循环体函数的,例如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日志里面。查看泄漏结果在 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 => [12928][24265 (Loop 8)] /Users/guoxinhua/tests/mem_leak/http_server.php:129 => [12928][24265 (Loop 8)] ❌ This Loop TotalLeak: [25216]表示第 8 次调用http_server.php的 125 行和 129 行,分别泄漏了 12928 字节内存,总共泄漏了 25216 字节内存。通过调用trackerCleanLeak()可以清除泄漏日志,重新开始。技术特性(技术难点)支持持续增长检测:想象一个场景,第一次请求运行主函数的时候申请 10 字节内存,然后请求结束前释放掉,然后第二次请求申请了 100 字节,请求结束再释放掉,虽然每次都能正确的释放内存但是每次又都申请更多的内存,最终导致内存爆掉,Leak工具支持这种检测,如果某一行代码有N次(默认 5 次)这种行为就会报"可疑的内存泄漏",格式如下:The Possible Leak As Malloc Size Keep Growth:/Users/guoxinhua/tests/mem_leak/hook_malloc_incri.php:39 => Growth Times : [8]; Growth Size : [2304]表示 39 行有 8 次 malloc size 的增长,总共增长了 2304 字节。支持跨 loop 分析://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 => [-12288]Free Pre (Loop 0) : /Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42 => [12288][28316 (Loop 2)] /Users/guoxinhua/tests/mem_leak/hook_efree_pre_loop.php:42 => [12288][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工具会自动识别协程环境,会在所有子协程都结束后才统计汇总,所以上述代码不会有误报情况。支持 defer,context:$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抓取等等,在此不再展开。注意前几次 Loop 的泄漏信息不用管,因为大部分项目都有一些初始化的缓存是不释放的。检测期间尽量不要有并发。由于开启泄漏检测后性能会非常差,不要在 php.ini 中开启apm.enable_malloc_hook = 1压测。和 Swoole Tracker2.x 的检查泄漏原理不一样,不能一起用。一个进程只能有一个地方调用trackerHookMalloc()函数。Swoole4.5.3由于底层 api 有问题,Leak工具无法正常工作,请升级到最新版Swoole或者降级Swoole版本。附件:免费公开课--如何正确查看进程内存占用
2023年08月11日
13 阅读
0 评论
0 点赞