內部洩漏錯誤代碼:
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
觀察php程式記憶體使用量
php提提供了兩個方法來取得目前程式的記憶體使用量。
memorygetusage(),這個函數的功能是取得目前PHP腳本所使用的記憶體大小。
memorygetpeak_usage(),這個函數的作用傳回目前腳本到目前位置所佔用的記憶體峰值,這樣就可能取得到目前的腳本的記憶體需求情況。
int memory_get_usage ([ bool $real_usage = false ] ) int memory_get_peak_usage ([ bool $real_usage = false ] )
函數預設得到的是呼叫emalloc()佔用的內存,如果設定參數為TRUE,則得到的是實際程式向系統申請的記憶體。因為 PHP 有自己的記憶體管理機制,所以有時候儘管內部已經釋放了記憶體但並沒有還給系統。
linux 系統檔案/proc/{$pid}/status 會記錄某個行程的運作狀態,裡面的VmRSS 欄位記錄了該行程使用的常駐實體記憶體(Residence),這就是該行程實際佔用的實體記憶體了,用這個資料比較可靠,在程式裡面提取這個值也很容易。
場景一:程式操作資料過大
情境還原:一次讀取超過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 查詢傳回大量資料、一次把大檔案讀進程式等。
解決方法:
1、能用錢解決的問題都不是問題,如果程式要讀大檔案的機會不是很多,且上限可預期,那麼透過ini_set('memory_limit' , '1G');來設定一個更大的值或memory_limit=-1。內存管夠的話讓程式一直跑也可以。
2、如果程式需要考慮在小型記憶體機器上也能正常使用,那就需要優化程式了。如下,程式碼複雜了很多。
<?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 進程都啟動後是否會把系統記憶體耗盡。
Ulimit程式碼:
$ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8192 -c: core file size (blocks) 0 -v: address space (kbytes) unlimited -l: locked-in-memory size (kbytes) unlimited -u: processes 1024 -n: file descriptors 1024
這是我本機mac os的配置,檔案描述符的設定是比較小的,一般生產環境配置要大得多。
場景四、無用的資料未及時釋放
情景还原:这种问题从程序逻辑上不是问题,但是无用的数据大量占用内存导致资源不够用,应该有针对性的做代码优化。
Laravel开发中用于监听数据库操作时有如下代码:
代码:
DB::listen(function ($query) { // $query->sql // $query->bindings // $query->time });
启用数据库监听后,每当有 SQL 执行时会 new 一个 QueryExecuted 对象并传入匿名函数以便后续操作,对于执行完毕就结束进程释放资源的php程序来说没有什么问题,而如果是一个常驻进程的程序,程序每执行一条 SQL 内存中就会增加一个 QueryExecuted 对象,程序不结束内存就会始终增长。
问题现象:程序运行期间内存逐渐增长,程序结束后内存正常释放。
问题分析:此类问题不易察觉,定位困难,尤其是有些框架封装好的方法,要明确其适用场景。
解决方法:本例中要通过DB::listen方法获取所有执行的 SQL 语句记录并写入日志,但此方法存在内存泄露问题,在开发环境下无所谓,在生产环境下则应停用,改用其他途径获取执行的 SQL 语句并写日志。
深入了解
1、名词解释
内存泄漏(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.3 以后引入了同步周期回收算法(Concurrent Cycle Collection)来处理内存泄露问题,代价是对性能有一定影响,不过一般 web 脚本应用程序影响很小。PHP的垃圾回收机制是默认打开的,php.ini 可以设置zend.enable_gc=0来关闭。也能通过分别调用gcenable() 和 gcdisable()函数来打开和关闭垃圾回收机制。
虽然垃圾回收让php开发者在内存管理上无需担心了,但也有极端的反例:php界著名的包管理工具composer曾因加入一行gc_disable();性能得到极大提升。
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 进程被销毁后,它所占用的所有内存都会被回收。
4、常驻进程内存泄漏问题
Valgrind 包括如下一些工具:
Memcheck。这是 valgrind 应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
Cachegrind。它主要用来检查程序中缓存使用出现的问题。
Helgrind。它主要用来检查多线程程序中出现的竞争问题。
Massif。它主要用来检查程序中堆栈使用中出现的问题。
Extension。可以利用core提供的功能,自己编写特定的内存调试工具。
Memcheck 对调试 C/C++ 程序的内存泄露很有帮助,它的机制是在系统 alloc/free 等函数调用上加计数。 php 程序的内存泄露,是由于一些循环引用,或者 gc 的逻辑错误, valgrind 无法探测,因此需要在检测时需要关闭 php 自带的内存管理。
代码:
$ export USE_ZEND_ALLOC=0 # 设置环境变量关闭内存管理 valgrind --tool=memcheck --num-callers=30 --log-file=php.log /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php
引用:
definitely lost
: 肯定内存泄露 indirectly lost
: 非直接内存泄露 possibly lost
: 可能发生内存泄露 still reachable
: 仍然可访问的内存 suppressed
: 外部造成的内存泄露
Callgrind 配合 php 扩展 xdebug 输出的 profile 分析日志文件可以分析程序运行期间各个函数调用时占用的内存、 CPU 占用情况。
总结:遇到了内存泄露时先观察是程序本身内存不足还是外部资源导致,然后搞清楚程序运行中用到了哪些资源:写入磁盘日志、连接数据库 SQL 查询、发送 Curl 请求、 Socket 通信等, I/O 操作必然会用到内存,如果这些地方都没有发生明显的内存泄露,检查哪里处理大量数据没有及时释放资源,如果是 php 5.3 以下版本还需考虑循环引用的问题。多了解一些 Linux 下的分析辅助工具,解决问题时可以事半功倍。
最后宣传一下穿云团队今年最新开源的应用透明链路追踪工具 Molten:https://github.com/chuan-yun/Molten。安装好php扩展后就能帮你实时收集程序的 curl,pdo,mysqli,redis,mongodb,memcached 等请求的数据,可以很方便的与 zipkin 集成。
以上内容仅供参考!
以上是定位分析記憶體洩漏的原因和後果的詳細內容。更多資訊請關注PHP中文網其他相關文章!