php5.2的curl-bug 服務(wù)器被php進(jìn)程卡死問題排查
前幾天東政同學(xué)反饋說Linode服務(wù)器快卡死了,今天有時(shí)間排查了一下具體原因,最終原因稍微有點(diǎn)悲壯:file_get_contents沒有設(shè)置超時(shí)時(shí)間,加上我用的php5.2關(guān)于curl的代碼有個(gè)bug,于是導(dǎo)致PHP進(jìn)程進(jìn)入死循環(huán)。
今天下午又發(fā)現(xiàn)系統(tǒng)負(fù)載很高,于是上去看了一下,發(fā)現(xiàn)一大坨PHP進(jìn)程沒有退出,占用了很多CPU,如圖:
問題進(jìn)程:
后面運(yùn)行的腳本是我的RSS定時(shí)更新任務(wù),看來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號(hào)fd上面死循環(huán)了,于是看看FD是什么:ll /proc/14043/fd
lrwx—— 1 wuhaiwen wuhaiwen 64 7月 21 11:00 4 -> socket:[53176380]
再看了一下原來是在請(qǐng)求CSDN的一個(gè)網(wǎng)頁的時(shí)候死循環(huán)了,但不知道什么地方請(qǐng)求的,想到GDB一下php進(jìn)程看看,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 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
看一下當(dāng)前PHP執(zhí)行的腳步是什么:
(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}}
找到了問題代碼位置,原來是一個(gè)file_get_contents($url)調(diào)用,沒有設(shè)置超時(shí)時(shí)間,于是PHP卡死在網(wǎng)絡(luò)請(qǐng)求了。于是用stream_context_create 設(shè)置超時(shí)時(shí)間搞定。
到這里 似乎問題解決了,但是,為什么沒有設(shè)置超時(shí)時(shí)間就導(dǎo)致php進(jìn)程占用CPU,系統(tǒng)負(fù)載那么高?按理說應(yīng)該等待I/O才是呀?看上面CPU情況,完全是進(jìn)入了死循環(huán)的節(jié)奏。
根據(jù)上面的bt堆棧,首先看倒數(shù)第二個(gè)函數(shù)的調(diào)用:
#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進(jìn)去發(fā)現(xiàn),代碼一直在里面的do-while里面循環(huán)了!心想curl_multi_fdset怎么不用先FD_ZERO 清空FD呢?一般做法都是會(huì)先清空的。
莫非是PHP的bug, 于是網(wǎng)上找了一下發(fā)現(xiàn)了這個(gè)Pierrick-Charron的commit,確實(shí)是一個(gè)bug, 其實(shí)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驗(yàn)證一下,我在上面的do下面,curl_multi_fdset調(diào)用之前,手動(dòng)將fd清空,看看能否退出循環(huán):
(gdb) print FD_ZERO(&curlstream->readfds)
No symbol “FD_ZERO” in current context.
FD_ZERO竟然沒有,不管了,其本來是個(gè)宏定義,展開就行:#define FD_ZERO(p) bzero((char *)(p), sizeof(*(p)))
直接用call修改curl_muti_fdset的三個(gè)參數(shù)數(shù)組如下:
(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單步執(zhí)行,如期的由于curlstream->pending變?yōu)?,從而退出了循環(huán),回到php_stream_fill_read_buffer的大函數(shù)了
到此基本結(jié)束。有問題的PHP版本應(yīng)該是5.2. 具體沒有細(xì)看,讀者可以參考下上面的這個(gè)提交改動(dòng)或者直接看自己的版本代碼是否有問題。
- PHP使用CURL_MULTI實(shí)現(xiàn)多線程采集的例子
- 解析php中curl_multi的應(yīng)用
- php中的curl_multi系列函數(shù)使用例子
- PHP使用curl_multi實(shí)現(xiàn)并發(fā)請(qǐng)求的方法示例
- php curl post 時(shí)出現(xiàn)的問題解決
- 關(guān)于PHP的curl開啟問題探討
- php使用curl詳細(xì)解析及問題匯總
- PHP CURL 內(nèi)存泄露問題解決方法
- php中curl和soap方式請(qǐng)求服務(wù)超時(shí)問題的解決
- PHP使用curl_multi_select解決curl_multi網(wǎng)頁假死問題的方法
相關(guān)文章
PHP將MySQL的查詢結(jié)果轉(zhuǎn)換為數(shù)組并用where拼接的示例
這篇文章主要介紹了PHP將MySQL的查詢結(jié)果轉(zhuǎn)換為數(shù)組并用where拼接的示例,這樣處理where條件時(shí)便可以在一定程度上優(yōu)化查詢和轉(zhuǎn)化的性能,需要的朋友可以參考下2016-05-05PHP中ini_set和ini_get函數(shù)的用法小結(jié)
本篇文章主要是對(duì)PHP中ini_set和ini_get函數(shù)的用法進(jìn)行了總結(jié)介紹,需要的朋友可以過來參考下,希望對(duì)大家有所幫助2014-02-02PHP+MariaDB數(shù)據(jù)庫操作基本技巧備忘總結(jié)
這篇文章主要介紹了PHP+MariaDB數(shù)據(jù)庫操作基本技巧,結(jié)合實(shí)例形式總結(jié)分析了PHP+MariaDB數(shù)據(jù)庫連接、判斷以及基于PHP+MariaDB的用戶登陸、管理、刪除等相關(guān)操作實(shí)現(xiàn)技巧與注意事項(xiàng),需要的朋友可以參考下2018-05-05PHP中全局變量global和$GLOBALS[]的區(qū)別分析
$GLOBALS['var']是外部的全局變量本身,global $var是外部$var的同名引用或者指針2012-08-08php實(shí)現(xiàn)隨機(jī)顯示圖片方法匯總
本文分享一個(gè)php實(shí)現(xiàn)的隨機(jī)顯示圖片的函數(shù),可以將指定文件夾中存放的圖片隨機(jī)地顯示出來。有興趣的朋友研究下吧。2015-05-05非常實(shí)用的php彈出錯(cuò)誤警告函數(shù)擴(kuò)展性強(qiáng)
利用php彈出警告的函數(shù),整理到自己的代碼庫中去在遇到錯(cuò)誤的地方可用到此函數(shù),擴(kuò)展性強(qiáng),喜歡的朋友可以收藏下2014-01-01