>  기사  >  백엔드 개발  >  메모리 누수의 원인과 결과를 찾아 분석합니다.

메모리 누수의 원인과 결과를 찾아 분석합니다.

王林
王林원래의
2019-09-03 17:20:573568검색

메모리 누수의 원인과 결과를 찾아 분석합니다.

내부 누수 오류 코드:

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)를 기록합니다. 네, 이 데이터를 사용하는 것이 더 안정적이고 프로그램에서 이 값을 추출하는 것이 쉽습니다.

시나리오 1: 프로그램 연산 데이터가 너무 큽니다

시나리오 복원: PHP의 사용 가능한 메모리 한도를 한 번에 초과하는 데이터를 읽으면 메모리가 소진됩니다

예:

<?php  ini_set(&#39;memory_limit&#39;, &#39;128M&#39;);  
$string = str_pad(&#39;1&#39;, 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이었습니다.
문제 현상: 특정 데이터를 처리할 때 재현될 수 있습니다. 하나의 mysql 쿼리에서 많은 양의 데이터를 반환하거나 대용량 파일을 한 번에 프로그램으로 읽는 등의 IO 작업을 수행할 때 이러한 문제가 발생할 수 있습니다.

해결책:

1. 프로그램이 대용량 파일을 읽을 기회가 많지 않고 상한이 예측 가능한 경우에는 ini_set('memory_limit', '1G'); 더 큰 값 또는 memory_limit=-1. 메모리가 충분하면 프로그램을 계속 실행할 수 있습니다.

2. 프로그램을 작은 메모리 머신에서 정상적으로 사용할 수 있어야 한다면 프로그램을 최적화해야 합니다. 아래에 표시된 것처럼 코드가 훨씬 더 복잡해졌습니다.

<?php  
//php7 以下版本通过 composer 引入 paragonie/random_compat ,为了方便来生成一个随机名称的临时文件  
require "vendor/autoload.php";    
ini_set(&#39;memory_limit&#39;, &#39;128M&#39;);  
//生成临时文件存放大字符串  
$fileName = &#39;tmp&#39;.bin2hex(random_bytes(5)).&#39;.txt&#39;;  
touch($fileName);  
for ( $i = 0; $i < 128; $i++ ) {      
$string = str_pad(&#39;1&#39;, 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);

시나리오 2: 프로그램이 빅데이터로 작동할 때 복사본이 생성됩니다.

시나리오 복원: 실행 중에 큰 변수가 복사되어 메모리가 부족합니다.

<?php  
ini_set("memory_limit",&#39;1M&#39;);    
$string = str_pad(&#39;1&#39;, 1* 750 *1024);  
$string2 = $string;  $string2 .= &#39;1&#39;;    
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",&#39;1M&#39;);    
$string = str_pad(&#39;1&#39;, 1* 750 *1024);  
$string2 = $string;  unset($string);  
$string2 .= &#39;1&#39;;    
<?php  
ini_set("memory_limit",&#39;1M&#39;);    
$string = str_pad(&#39;1&#39;, 1* 750 *1024);  
$string2 = &$string;  
$string2 .= &#39;1&#39;;    
unset($string2, $string);

시나리오 3: 불합리한 구성으로 인해 시스템 리소스가 고갈되었습니다

시나리오 복원: 불합리한 구성, 메모리 부족으로 인해 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 code:

$ 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_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 code:

$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의 구성입니다. 파일 설명자 설정은 비교적 작고 일반 프로덕션 환경 구성은 훨씬 큽니다.

시나리오 4: 쓸모없는 데이터가 제때 공개되지 않음

情景还原:这种问题从程序逻辑上不是问题,但是无用的数据大量占用内存导致资源不够用,应该有针对性的做代码优化。 

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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.