记一次zend引擎的采坑事件

开放平台有一个通知的需求,需要一个消费进程不断的去读取buffer中的数据,然后消费并删除。于是,我用cron每分钟起一个php进程去读取数据库,并做通知的工作。同时,增加了一个文件锁,保证同一时间只能有一个进程在干活。

某一天,突然发现buffer中的数据有增无减,进程的工作日志也已经没有输出了。ps看了一下进程,还是处于运行态,ltracestrace都是空,看上去像是进入了某个while(true)的逻辑,看了一下CPU,果真,已经飙到98%了。gdb到这个进程,发现进程纠结在php_shutdown_handler这个函数,php的错误日志显示out of memory的错误。至此,事故的原因已经有所眉目了。

1. 事故原因

我们在php进程的退出事件中注册了一个回调函数php_shutdown_handler,在函数中做了上报monitor和报警等操作。我们可以想象一下,如果进程退出的原因是内存不足时,会发生什么情况?

正如想象,zend引擎进入了上图所示的死循环状态,因内存不足导致进程需要退出,在退出前进入了我们注册的php_shutdown_handler函数,在此函数中需要进程留下遗言,然而遗言讲到一半,再一次out of memory,又一次触发了退出事件,并进入php_shutdown_handler函数,从而进入了无限的递归循环当中,再也无法走到正确退出的逻辑。从而导致了我们之前观察到的那些现象。

2. 解决方法

2.1 利用ini_set()增大进程的内存限制

这是我第一个想到的解决办法。由于在cgi模式下,同一台机器上运行的进程有成百上千,zend为了保护系统资源,默认为每一个php进程做了内存限制,每次zend需要申请内存时会检查一下配置项中的memory_limit, 如果超出限制,会直接退出进程。这个配置项可以通过修改php.ini中的参数来改变,也可以通过调用ini_set(‘memory_limit‘)函数来在进程执行的过程中动态的修改。所以,这个想法也就自然而然的产生了,即在php_shutdown_handler中调用ini_set将原有的内存限制(memory_limit)扩展为2倍。

$mem_limit = ini_get(‘memory_limit‘); //64M
$num = intval(substr($mem_limit, 0, strlen($mem_limit)-1)); //64
$last = $mem_limit[strlen($mem_limit)]-1; //M
$mem_limit = $num * 2 . $last;  //128M
ini_set(‘memory_limit‘, $mem_limit);

比较不方便的是,通过ini_get()调用得到的memory_limit是"64M"格式的字符串,想要double的话,需要做一些字符串变换,而这些字符串变换操作依然需要申请内存。所以上述方法是行不通的。

那我们用一个暴力点的办法吧,直接将内存限制改为1G。

ini_set(‘memory_limit‘, ‘1024M‘);

测试了一下,依然没有解决问题。我们知道php的配置项在zend当中存放在一个配置哈希表当中,虽然我们仅仅修改了其中的一个值,也占用了一些内存(实际测下来大约为500B),在极限情况下还是无法解决问题。

2.2 先占个坑,再释放

既然加内存的方法已经失败了,我们再换个方法试试看吧。一个比较直观的方法就是先占个坑,需要内存的时候,释放这个坑,再用这部分内存就行了。

step1:

在注册函数之前,在全局变量中预分配一个1M的内存。

//预先非配内存
$_SERVER[‘php_shutdown_mem_pre_alloc‘] = str_repeat(‘*‘, 1024 * 1024 );
//注册回调函数
register_shutdown_function(‘php_shutdown_handler‘);

step2:

在回调函数中回收内存,将预分配的全局变量回收。

// php异常出错跳出的处理函数
function php_shutdown_handler() {
    unset($_SERVER[‘php_shutdown_mem_pre_alloc‘]);

    $error = error_get_last(); // 如果没错返回null
    PHPShutdownHandler::report($error);
}

好,再去测试一下。结果也在意料之中,又失败了

回想一下,当时确实有些操之过急,你以为php的unset就是delete操作么?那就错了,一般来说php这种神奇的脚本语言都有自己的垃圾回收机制的。如果想立即启用gc,还需要一点点操作。

// php异常出错跳出的处理函数
function php_shutdown_handler() {
    unset($_SERVER[‘php_shutdown_mem_pre_alloc‘]);
    gc_collect_cycles();

    $error = error_get_last(); // 如果没错返回null
    PHPShutdownHandler::report($error);
}

主动调用gc_collect_cycles()立即启用垃圾回收机制,问题就迎刃而解了。

3. 一些优化

当然,这样的代码还不能立即发布,原因是生产环境的代码是在php auto prepend当中注册退出回调函数的,也就意味着所有的php脚本都会执行这段代码。如果预分配和进程结束都加逻辑的话,那么造成的时间成本可能让所有服务器性能优化的努力付诸东流。另外,由于每个进程预分配了1M的内存,假如一台机器上配置了500个php-fpm进程,那么当他们同时执行的时候,预分配的无用内存就占到了500M。这在高峰期是不能容忍的内存消耗。

3.1 预分配内存时间消耗测试

我们预分配函数str_repeat()等消耗的时间做了测试,平均大概在0.7ms左右,这个时间成本相对于一个生产环境的CGI来说是可以忽略不计的。

3.2 上报方法内存消耗测试

内存消耗的测试测试的结果显示,一次上报大概消耗260KB左右的内存。如果一台机器启用300个php-fpm, 每个预分配400KB的内存,则预分配的内存峰值大约是120M。经过讨论,这样的开销在合理范围内,至此问题便解决了。

总结来说,在PHP 5.3的版本当中,zend没有保证_onExit注册的函数最多只能执行一次(后续版本是否修复此bug还没有验证),而PHP作为一种脚本语言又不太容易直接操作内存,使得问题的解决方法不是特别直观。

时间: 2024-08-09 11:12:37

记一次zend引擎的采坑事件的相关文章

easy-ui采坑事件

新用户首次登陆修改密码 imput标签中使用easyui自带的class="easyui-passwordbox"可以是密码隐藏变成黑点但是无法禁用输入法,然后果断的加了一个type=password提交了上去,没一会儿回测的时候告诉我input中竟然有示据就像是密码回显上来了一样,打开控制台看了半天发现是prompt被class="easyui-passwordbox"隐藏变成了黑点,然后我把prompt改成了placeholder,竟然无效,然后我上百度查了一下

websocket采坑记

项目中想用做个实时统计,像是110警情大屏那种,所以用到了websocket,结果踩了不少坑,再次记录下. 环境:spring,springMVC(4.2.4.RELEASE),tomcat7 问题1:session对象是不一样的 http的时候,是javax.servlet.http.HttpSession 而websocket的时候javax.websocket.Session http的session一般用于保存用户信息,根据用户,http请求的时候携带Cookie:JSESSIONID,

github 采坑记 —— 项目提交到github后部分文件缺失

在使用git push到GitHub上后,发现部分文件缺失,如下图所示: 可以看到dist文件夹为 运行 npm run build 之后打包生成的文件,node_modules 文件也是缺失的 导致文件没有提交的原因是在项目根目录下有个文件: 打开文件: 可以看再提交时有些文件被忽略了,可以将相应代码删除,然后重新push到GitHub上 想要的文件就提交上去了! 多多采坑,多多总结,多多练习~~~ 原文地址:https://www.cnblogs.com/amy2017/p/10087455

记一次虚拟机共享文件夹的采坑之旅

1.官网下载VMware,安装~~ 2.找一个linux镜像,创建虚拟机~~ 3.进入系统,安装gcc 报错 could not retrieve mirrorlist...... 解决:sudo vim /etc/sysconfig/network-scripts/ifcfg-ens33 将ONBOOT改为yes,wq!保存退出     重新启动网络  service network restart 4.虚拟机设置->选项->共享文件夹->开启并添加一个共享文件夹 5.貌似一切都很顺利

采坑 - 字符串的 "" 与 pd.isnull()

主要是记录一个采坑的过程. 当字符串 的 " " 和 pandas 中的 " " 不是一个概念 . 需求 一个小伙伴要用 pandas 来处理一个, 表格填充的的问题, 脱敏数据大致是这样的. 区域名称 门店 店组 龙华新城大区 壹城中心店 壹城中心一组 益田大区 皇岗口岸店 皇岗口岸一组 双龙大区 龙城中央旗舰店 AAA店 深西大区 德佑麒麟A组 A宏畅房地产经纪有限责任公司 A地产经纪有限责任公司门店 宏畅房地产经纪有限责任公司 B地产经纪有限责任公司门店 东部

phoenegap3.5 采坑

上周5晚上在家看Node.js视频的时候,老大来一条短信让研究下 phoengap打包一个web网站. 遂 陷入了phonegap的深渊中. phoengap很早开始使用 cli模式安装开发环境 ,借助node npm来管理 环境要求: JAVA Android SDK Ant Node.js 以上几个的下载地址就不提供. 主要是这中间一些坑,装好上面的环境 需要自己手动配置环境变量, 痛苦由此开始啊!!!! 多次操作修改环境变量 都没能成功.多次卸载,遇到各种BUG. 在修改环境变量的时候 往

记一次 jquery mobile被AJAX坑了。

简单情况是 MVC 重定向,URL不变 试了N种方式,跳来跳去,无解,服务端跳,写JS跳,生成跳转中间页跳.失败 后来一看,明明已经跳到新页了,样式什么还是原页的,有点火大了. 出去溜一圈,喝杯水,和同事东拉西扯一通. 回头一看,突然反应过来,这不是AJAX的效果么,坑我半个多小时. 为加验证,是手动调用的submit.没往AJAX上想. $("form").submit(); 但jquery mobile 内部会把submit也通过AJAX访问. 解决办法是禁用. 注意 绑定mobi

6.PHP内核探索:Zend引擎

相信很多人都听说过 Zend Engine 这个名词,也有很多人知道 Zend Engine 就是 PHP 语言的核心,但若要问一句:Zend Engine 到底存在于何处?或者说,Zend Engine 究竟是在什么时候怎么发挥作用让 PHP 源码输出我们想要的东西的? Zend引擎是PHP实现的核心,提供了语言实现上的基础设施.例如:PHP的语法实现,脚本的编译运行环境, 扩展机制以及内存管理等,当然这里的PHP指的是官方的PHP实现(除了官方的实现, 目前比较知名的有facebook的hi

第一节 生命周期和Zend引擎

一切的开始: SAPI接口 SAPI(Server Application Programming Interface)指的是PHP具体应用的编程接口, 就像PC一样,无论安装哪些操作系统,只要满足了PC的接口规范都可以在PC上正常运行, PHP脚本要执行有很多种方式,通过Web服务器,或者直接在命令行下,也可以嵌入在其他程序中. 通常,我们使用Apache或者Nginx这类Web服务器来测试PHP脚本,或者在命令行下通过PHP解释器程序来执行. 脚本执行完后,Web服务器应答,浏览器显示应答信