php5.2的curl-bug 服务器被php进程卡死问题排查


Posted in PHP onSeptember 19, 2016

前几天东政同学反馈说Linode服务器快卡死了,今天有时间排查了一下具体原因,最终原因稍微有点悲壮:file_get_contents没有设置超时时间,加上我用的php5.2关于curl的代码有个bug,于是导致PHP进程进入死循环。

今天下午又发现系统负载很高,于是上去看了一下,发现一大坨PHP进程没有退出,占用了很多CPU,如图:

php5.2的curl-bug 服务器被php进程卡死问题排查

问题进程:

php5.2的curl-bug 服务器被php进程卡死问题排查

后面运行的脚本是我的RSS定时更新任务,看来PHP代码什么地方有问题,于是strace -p 14043看了一下:

select(5, [4], [4], [], {15, 0}) = 1 (out [4], left {14, 999996})
poll([{fd=4, events=POLLIN|POLLPRI}], 1, 0) = 0 (Timeout)
clock_gettime(CLOCK_MONOTONIC, {4582888, 760370017}) = 0
clock_gettime(CLOCK_MONOTONIC, {4582888, 760468615}) = 0
clock_gettime(CLOCK_MONOTONIC, {4582888, 760565053}) = 0
select(5, [4], [4], [], {15, 0}) = 1 (out [4], left {14, 999997})

在4号fd上面死循环了,于是看看FD是什么:ll /proc/14043/fd

lrwx—— 1 wuhaiwen wuhaiwen 64  7月 21 11:00 4 -> socket:[53176380]

再看了一下原来是在请求CSDN的一个网页的时候死循环了,但不知道什么地方请求的,想到GDB一下php进程看看,bt显示:

(gdb) bt
#0 0x00007f6721f8f013 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:82
#1 0×0000000000481952 in php_curl_stream_read (stream=0×2280650,
buf=0x22ea5d0 “2Fwww.laruence.com%2Ftag%2F%25e6%25ad%25a3%25e5%2588%2599%27+class%3D%27tag-link-191%27+title%3D%273+topics%27+style%3D%27font-size%3A+9.0243902439pt%3B%27%3E%E6%AD%A3%E5%88%99%3C%2Fa%3E%3C%2Ftags%3E\”"…, count=8192) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/ext/curl/streams.c:169
#2 0x00000000006738f9 in php_stream_fill_read_buffer (stream=0×2280650, size=4283) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/main/streams/streams.c:554
#3 0x0000000000673c39 in _php_stream_read (stream=0×2280650,
buf=0x2301fd5 “f='http://www.laruence.com/tag/json' class='tag-link-79′ title='3 topics' style='font-size: 9.0243902439pt;'>json</a>\n<a href='http://www.laruence.com/tag/module' class='tag-link-43′ title='2 topics' “…, size=4283) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/main/streams/streams.c:600
#4 0x0000000000674c51 in _php_stream_copy_to_mem (src=0×2280650, buf=0x7fff376ed898, maxlen=<optimized out>, persistent=0)
at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/main/streams/streams.c:1267
#5 0x00000000005fdb85 in zif_file_get_contents (ht=<optimized out>, return_value=0x2223da0, return_value_ptr=<optimized out>, this_ptr=<optimized out>, return_value_used=<optimized out>)
at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/ext/standard/file.c:565
#6 0x00000000006c2a59 in zend_do_fcall_common_helper_SPEC (execute_data=0x7fff376edc60) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/Zend/zend_vm_execute.h:200
#7 0x00000000006c239f in execute (op_array=0x1f26730) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/Zend/zend_vm_execute.h:92
·············
#16 0x0000000000730d8e in main (argc=4, argv=0x7fff376f2468) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/sapi/cli/php_cli.c:1133

看一下当前PHP执行的脚步是什么:

(gdb) p *op_array
$4 = {type = 2 '\002', function_name = 0x1e54278 "getContent", scope = 0x1f8e850, fn_flags = 257, prototype = 0x0, num_args = 2, required_num_args = 1, arg_info = 0x1fd5e20,
pass_rest_by_reference = 0 '\000', return_reference = 0 '\000', refcount = 0x1fd3ab8, opcodes = 0x1fddcc8, last = 28, size = 28, vars = 0x1fd3cc0, last_var = 6, size_var = 16, T = 15,
brk_cont_array = 0x0, last_brk_cont = 0, current_brk_cont = 4294967295, try_catch_array = 0x0, last_try_catch = 0, static_variables = 0x0, start_op = 0x0, backpatch_count = 0,
done_pass_two = 1 '\001', uses_this = 0 '\000', filename = 0x1fd3b58 "/home/wuhaiwen/webroot/kulvrss/libs/Myrss/Model/UrlContenter.php", line_start = 9, line_end = 30, doc_comment = 0x0,
doc_comment_len = 0, reserved = {0x0, 0x0, 0x0, 0x0}}

找到了问题代码位置,原来是一个file_get_contents($url)调用,没有设置超时时间,于是PHP卡死在网络请求了。于是用stream_context_create 设置超时时间搞定。

到这里 似乎问题解决了,但是,为什么没有设置超时时间就导致php进程占用CPU,系统负载那么高?按理说应该等待I/O才是呀?看上面CPU情况,完全是进入了死循环的节奏。

根据上面的bt堆栈,首先看倒数第二个函数的调用:

#1 0×0000000000481952 in php_curl_stream_read (stream=0×2280650,
buf=0x22ea5d0 “2Fwww.laruence.com%2Ftag%2F%25e6%25ad%25a3%25e5%2588%2599%27+class%3D%27tag-link-191%27+title%3D%273+topics%27+style%3D%27font-size%3A+9.0243902439pt%3B%27%3E%E6%AD%A3%E5%88%99%3C%2Fa%3E%3C%2Ftags%3E\”"…, count=8192) at /home/wuhaiwen/install/php-env/src/php/php-5.2.8/ext/curl/streams.c:169

看一下代码,我用的事5.2.8版本的PHP,比较老。代码如下:

static size_t php_curl_stream_read(php_stream *stream, char *buf, size_t count TSRMLS_DC)
{
    php_curl_stream *curlstream = (php_curl_stream *) stream->abstract;
    size_t didread = 0;
    if (curlstream->readbuffer.readpos >= curlstream->readbuffer.writepos && curlstream->pending) {
//········
        do {
            /* get the descriptors from curl */
            curl_multi_fdset(curlstream->multi, &curlstream->readfds, &curlstream->writefds, &curlstream->excfds, &curlstream->maxfd);
            /* if we are in blocking mode, set a timeout */
            tv.tv_usec = 0;
            tv.tv_sec = 15; /* TODO: allow this to be configured from the script */
            /* wait for data */
            switch (select(curlstream->maxfd + 1, &curlstream->readfds, &curlstream->writefds, &curlstream->excfds, &tv)) {
                case -1:
                    /* error */
                    return 0;
                case 0:
                    /* no data yet: timed-out */
                    return 0;
                default:
                    /* fetch the data */
                    do {
                        curlstream->mcode = curl_multi_perform(curlstream->multi, &curlstream->pending);
                    } while (curlstream->mcode == CURLM_CALL_MULTI_PERFORM);
            }
        } while (curlstream->readbuffer.readpos >= curlstream->readbuffer.writepos && curlstream->pending > 0);
    }
//··········
    return didread;
}

GDB进去发现,代码一直在里面的do-while里面循环了!心想curl_multi_fdset怎么不用先FD_ZERO 清空FD呢?一般做法都是会先清空的。

莫非是PHP的bug, 于是网上找了一下发现了这个Pierrick-Charron的commit,确实是一个bug, 其实curl_multi_fdset 的文档开头写了的:

This function extracts file descriptor information from a given multi_handle. libcurl returns its fd_set sets. The application can use these to select() on, but be sure to FD_ZERO them before calling this function as curl_multi_fdset(3) only adds its own descriptors,

好吧,最后用GDB验证一下,我在上面的do下面,curl_multi_fdset调用之前,手动将fd清空,看看能否退出循环:

(gdb) print FD_ZERO(&curlstream->readfds)
No symbol “FD_ZERO” in current context.

FD_ZERO竟然没有,不管了,其本来是个宏定义,展开就行:#define FD_ZERO(p) bzero((char *)(p), sizeof(*(p)))

直接用call修改curl_muti_fdset的三个参数数组如下:

(gdb) call bzero((char *)(&curlstream->readfds), sizeof(*(&curlstream->readfds)))

$5 = 17055392

(gdb) call bzero((char *)(&curlstream->writefds),sizeof(*(&curlstream->writefds)))

$6 = 17055520

(gdb) call bzero((char *)(&curlstream->excfds), sizeof(*(&curlstream->excfds)))

$7 = 17055648

然后GDB单步执行,如期的由于curlstream->pending变为0,从而退出了循环,回到php_stream_fill_read_buffer的大函数了

到此基本结束。有问题的PHP版本应该是5.2. 具体没有细看,读者可以参考下上面的这个提交改动或者直接看自己的版本代码是否有问题。

PHP 相关文章推荐
php生成WAP页面
Oct 09 PHP
PHP 编程安全性小结
Jan 08 PHP
php顺序查找和二分查找示例
Mar 27 PHP
php使用imagick模块实现图片缩放、裁剪、压缩示例
Apr 17 PHP
php实现统计网站在线人数的方法
May 12 PHP
php插入含有特殊符号数据的处理方法
Nov 24 PHP
PHP获取中国时间(上海时区时间)及美国时间的方法
Feb 23 PHP
删除PHP数组中头部、尾部、任意元素的实现代码
Apr 10 PHP
万能的php分页类
Jul 06 PHP
PHP注释语法规范与命名规范详解篇
Jan 21 PHP
PHP实现的无限分类类库定义与用法示例【基于thinkPHP】
Aug 06 PHP
PHP7导出Excel报ERR_EMPTY_RESPONSE解决方法
Apr 16 PHP
php支付宝在线支付接口开发教程
Sep 19 #PHP
iOS10推送通知开发教程
Sep 19 #PHP
PHP 中 DOMDocument保存xml时中文出现乱码问题的解决方案
Sep 19 #PHP
手把手编写PHP框架 深入了解MVC运行流程
Sep 19 #PHP
PHP 接入支付宝即时到账功能
Sep 18 #PHP
PHP 等比例缩放图片详解及实例代码
Sep 18 #PHP
php mysql 封装类实例代码
Sep 18 #PHP
You might like
PHP 获取MySQL数据库里所有表的实现代码
2011/07/13 PHP
thinkphp实现like模糊查询实例
2014/10/29 PHP
CentOS 7.2 下编译安装PHP7.0.10+MySQL5.7.14+Nginx1.10.1的方法详解(mini版本)
2016/09/01 PHP
使用 laravel sms 构建短信验证码发送校验功能
2017/11/06 PHP
PHP通过GD库实现验证码功能示例
2019/02/23 PHP
两个listbox实现选项的添加删除和搜索
2013/03/01 Javascript
JavaScript获取网页、浏览器、屏幕高度和宽度汇总
2014/12/18 Javascript
js父页面中使用子页面的方法
2016/01/09 Javascript
JavaScript实现跑马灯抽奖活动实例代码解析与优化(一)
2016/02/16 Javascript
js实现按钮控制带有停顿效果的图片滚动
2016/08/30 Javascript
jQuery 判断元素整理汇总
2017/02/28 Javascript
Vue的移动端多图上传插件vue-easy-uploader的示例代码
2017/11/27 Javascript
js断点调试心得分享(必看篇)
2017/12/08 Javascript
详解vue项目接入微信JSSDK的坑
2018/12/14 Javascript
ES6中Set和Map数据结构,Map与其它数据结构互相转换操作实例详解
2019/02/28 Javascript
es6函数中的作用域实例分析
2020/04/18 Javascript
基于canvas实现手写签名(vue)
2020/05/21 Javascript
[01:03:00]DOTA2上海特级锦标赛A组败者赛 EHOME VS CDEC第一局
2016/02/25 DOTA
python实现ftp客户端示例分享
2014/02/17 Python
Python实现破解猜数游戏算法示例
2017/09/25 Python
详解Python的循环结构知识点
2019/05/20 Python
python解析xml文件方式(解析、更新、写入)
2020/03/05 Python
基于Django signals 信号作用及用法详解
2020/03/28 Python
基于Python的一个自动录入表格的小程序
2020/08/05 Python
一款纯css3实现的tab选项卡的实列教程
2014/12/11 HTML / CSS
浅析canvas元素的html尺寸和css尺寸对元素视觉的影响
2019/07/22 HTML / CSS
int *p=NULL和*p= NULL有什么区别
2014/10/23 面试题
医学院学生的自我评价分享
2013/11/19 职场文书
驾驶员岗位职责
2014/01/29 职场文书
两只小狮子教学反思
2014/02/05 职场文书
亲戚结婚的请假条
2014/02/11 职场文书
贯彻学习两会心得体会范文
2014/03/17 职场文书
小学生作文评语集锦
2014/12/25 职场文书
2015年体育教学工作总结
2015/05/20 职场文书
丧事酒宴答谢词
2015/09/30 职场文书
基于python的matplotlib制作双Y轴图
2021/04/20 Python