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 相关文章推荐
phpmyadmin导入(import)文件限制的解决办法
Dec 11 PHP
php数据结构与算法(PHP描述) 快速排序 quick sort
Jun 21 PHP
关于PHP的相似度计算函数:levenshtein的使用介绍
Apr 15 PHP
PHP循环结构实例讲解
Feb 10 PHP
跟我学Laravel之请求(Request)的生命周期
Oct 15 PHP
PHP动态编译出现Cannot find autoconf的解决方法
Nov 05 PHP
php有道翻译api调用方法实例
Dec 22 PHP
ThinkPHP控制器详解
Jul 27 PHP
PHP递归实现文件夹的复制、删除、查看大小操作示例
Aug 11 PHP
ThinkPHP框架整合微信支付之JSAPI模式图文详解
Apr 09 PHP
php实现微信小程序授权登录功能(实现流程)
Nov 13 PHP
YII2框架使用控制台命令的方法分析
Mar 18 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
Codeigniter购物车类不能添加中文的解决方法
2014/11/29 PHP
php 使用html5实现多文件上传实例
2016/10/24 PHP
PHP实现的回溯算法示例
2017/08/15 PHP
PHP7扩展开发之基于函数方式使用lib库的方法详解
2018/01/15 PHP
PHP根据key删除数组中指定的元素
2019/02/28 PHP
javaScript同意等待代码实现心得
2011/01/01 Javascript
解决3.01版的jquery.form.js中文乱码问题的解决方法
2012/03/08 Javascript
使用百度地图api实现根据地址查询经纬度
2014/12/11 Javascript
jquery if条件语句的写法
2016/05/19 Javascript
JS重载实现方法分析
2016/12/16 Javascript
BootStrap Fileinput的使用教程
2016/12/30 Javascript
微信小程序实战之自定义toast(6)
2017/04/18 Javascript
JavaScript之map reduce_动力节点Java学院整理
2017/06/29 Javascript
详解Nuxt.js Vue服务端渲染摸索
2018/02/08 Javascript
webpack v4 从dev到prd的方法
2018/04/02 Javascript
Javascript中弹窗confirm与prompt的区别
2018/10/26 Javascript
JS大坑之19位数的Number型精度丢失问题详解
2019/04/22 Javascript
vuex vue简单使用知识点总结
2019/08/29 Javascript
Node.js API详解之 dns模块用法实例分析
2020/05/15 Javascript
windows 下python+numpy安装实用教程
2017/12/23 Python
python3.6中@property装饰器的使用方法示例
2019/08/17 Python
简单了解python中的与或非运算
2019/09/18 Python
Python 脚本拉取 Docker 镜像问题
2019/11/10 Python
CSS3 仿微信聊天小气泡实例代码
2017/04/05 HTML / CSS
梅西百货澳大利亚:Macy’s Australia
2017/07/26 全球购物
Rowdy Gentleman服装和配饰:美好时光
2019/09/24 全球购物
以下的初始化有什么区别
2013/12/16 面试题
毕业生个人求职信范例分享
2013/12/17 职场文书
小学生毕业评语
2014/12/26 职场文书
行为习惯主题班会
2015/08/14 职场文书
《山中访友》教学反思
2016/02/24 职场文书
python批量更改目录名/文件名的方法
2021/04/18 Python
Unity连接MySQL并读取表格数据的实现代码
2021/06/20 MySQL
vue判断按钮是否可以点击
2022/04/09 Vue.js
Python读取和写入Excel数据
2022/04/20 Python
JavaScript架构localStorage特殊场景下二次封装操作
2022/06/21 Javascript