场景二 程序操作大数据时产生拷贝

场景三 配置不合理系统资源耗尽

场景四 无用的数据未及时开释

php内存泄露PHP 内存泄露剖析定位 Node.js

深入理解

php内存管理

php-fpm内存透露问题

常驻进程内存透露问题

序言

本文开始撰写时我卖力的项目须要用 php 开拓一个通过 Socket 与做事端建立长连接后持续实时上报数据的常驻进程程序,在程序业务功能开拓联调完毕后实际运行发送大量数据后创造内存增长非常迅速,在很短的韶光内达到了 php 默认可用内存上限 128M ,并报错:

Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)

我第一反应是内存透露了,但是不知道在哪。
第二反应是无用的变量该当用完就 unset 掉,修正完毕后问题依旧。
经由了几番周折终于办理了问题。
就决定好好把类似情形整理一下,遂有此文,与诸君共勉。

不雅观察 PHP 程序内存利用情形

php 提供应了两个方法来获取当出路序的内存利用情形。

memory_get_usage(),这个函数的浸染是获取目前PHP脚本所用的内存大小。

memory_get_peak_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可用内存上限的数据导致内存耗尽

<?phpini_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);场景二:程序操作大数据时产生拷贝

情景还原:实行过程中对大变量进行了复制,导致内存不足用。

<?phpini_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 8Call Stack: 0.0004 235440 1. {main}() /Users/zouyi/php-oom/unset.php:0zend_mm_heap corrupted

问题征象:局部代码实行过程中占用内存翻倍。

问题剖析:

php 是写时复制(Copy On Write),也便是说,当新变量被赋值时内存不发生变革,直到新变量的内容被操作时才会产生复制。

办理方法:

及早开释无用变量,或者以引用的形式操作原始数据。

<?phpini_set(\"大众memory_limit\"大众,'1M');$string = str_pad('1', 1 750 1024);$string2 = $string;unset($string);$string2 .= '1';

<?phpini_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 memorymemory_limit => 1024M => 1024M //php脚本实行最大可利用内存$php -i |grep maxmax_execution_time => 0 => 0 //最大实行韶光,脚本默认为0不限定,web要求默认30smax_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.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 进程被销毁后,它所占用的所有内存都会被回收。

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

通过命令行实行 valgrind 剖析可能有内存透露的文件

==12075== Memcheck, a memory error detector==12075== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.==12075== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info==12075== Command: /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php==12075== Parent PID: 42043==12075====12075== Syscall param msg->desc.port.name points to uninitialised byte(s)==12075== at 0x10121F34A: mach_msg_trap (in /usr/lib/system/libsystem_kernel.dylib)==12075== by 0x10121E796: mach_msg (in /usr/lib/system/libsystem_kernel.dylib)==12075== by 0x101218485: task_set_special_port (in /usr/lib/system/libsystem_kernel.dylib)==12075== by 0x1013B410E: _os_trace_create_debug_control_port (in /usr/lib/system/libsystem_trace.dylib)==12075== by 0x1013B4458: _libtrace_init (in /usr/lib/system/libsystem_trace.dylib)==12075== by 0x100DF09DF: libSystem_initializer (in /usr/lib/libSystem.B.dylib)==12075== by 0x100C37A1A: ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) (in /usr/lib/dyld)==12075== by 0x100C37C1D: ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) (in /usr/lib/dyld)==12075== by 0x100C334A9: ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)==12075== by 0x100C33440: ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)==12075== by 0x100C32523: ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)==12075== by 0x100C325B8: ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) (in /usr/lib/dyld)==12075== by 0x100C24433: dyld::initializeMainExecutable() (in /usr/lib/dyld)==12075== by 0x100C288C5: dyld::_main(macho_header const, unsigned long, int, char const, char const, char const, unsigned long) (in /usr/lib/dyld)==12075== by 0x100C23248: dyldbootstrap::start(macho_header const, int, char const, long, macho_header const, unsigned long) (in /usr/lib/dyld)==12075== by 0x100C23035: _dyld_start (in /usr/lib/dyld)==12075== by 0x1: ???==12075== by 0x1054AC862: ???==12075== by 0x1054AC891: ???==12075== Address 0x1054aa98c is on thread 1's stack==12075== in frame #2, created by task_set_special_port (???:)==12075==--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option (repeated 2 times)--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option (repeated 4 times)==12075====12075== HEAP SUMMARY:==12075== in use at exit: 125,805 bytes in 185 blocks==12075== total heap usage: 14,686 allocs, 14,501 frees, 3,261,322 bytes allocated==12075====12075== LEAK SUMMARY:==12075== definitely lost: 3 bytes in 1 blocks==12075== indirectly lost: 0 bytes in 0 blocks==12075== possibly lost: 72 bytes in 3 blocks==12075== still reachable: 107,582 bytes in 23 blocks==12075== suppressed: 18,148 bytes in 158 blocks==12075== Rerun with --leak-check=full to see details of leaked memory==12075====12075== For counts of detected and suppressed errors, rerun with: -v==12075== Use --track-origins=yes to see where uninitialised values come from==12075== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)

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 集成。

参考资料

http://php.net/manual/zh/features.gc.php

http://www.php-internals.com/book/?p=chapt06/06-07-memory-leaks

http://www.programering.com/a/MDN5UjMwATk.html

https://stackoverflow.com/questions/20458136/using-valgrind-to-debug-a-php-cli-segmentation-fault

http://www.laruence.com/2013/08/14/2899.html

https://mengkang.net/873.html

本文来自作者 邹毅 在 GitChat 上分享「 PHP 内存泄露剖析定位」,「阅读原文」理解更多知识。