PHP超時處理全面總結

發表於2012-08-08

作者:@黑夜路人V    原文:http://blog.csdn.net/heiyeshuwu/article/details/7841366

【 概述 】

在PHP開發中工作裡非常多使用到超時處理到超時的場合,我說幾個場景:

1. 非同步獲取資料如果某個後端資料來源獲取不成功則跳過,不影響整個頁面展現

2. 為了保證Web伺服器不會因為當個頁面處理效能差而導致無法訪問其他頁面,則會對某些頁面操作設定

3. 對於某些上傳或者不確定處理時間的場合,則需要對整個流程中所有超時設定為無限,否則任何一個環節設定不當,都會導致莫名執行中斷

4. 多個後端模組(MySQL、Memcached、HTTP介面),為了防止單個介面效能太差,導致整個前面獲取資料太緩慢,影響頁面開啟速度,引起雪崩

5. 很多需要超時的場合

這些地方都需要考慮超時的設定,但是PHP中的超時都是分門別類,各個處理方式和策略都不同,為了系統的描述,我總結了PHP中常用的超時處理的總結。

【Web伺服器超時處理】

[ Apache ]

一般在效能很高的情況下,預設所有超時配置都是30秒,但是在上傳檔案,或者網路速度很慢的情況下,那麼可能觸發超時操作。

目前 apache fastcgi php-fpm 模式 下有三個超時設定:

fastcgi 超時設定:

修改 httpd.conf 的fastcgi連線配置,類似如下:

 

 

預設配置是 30s,如果需要定製自己的配置,需要修改配置,比如修改為100秒:(修改後重啟 apache):

 

 

如果超時會返回500錯誤,斷開跟後端php服務的連線,同時記錄一條apache錯誤日誌:

 

其他 fastcgi 配置引數說明:

IdleTimeout 發呆時限

ProcessLifeTime 一個程式的最長生命週期,過期之後無條件kill

MaxProcessCount 最大程式個數

DefaultMinClassProcessCount 每個程式啟動的最小程式個數

DefaultMaxClassProcessCount 每個程式啟動的最大程式個數

IPCConnectTimeout 程式響應超時時間

IPCCommTimeout 與程式通訊的最長時間,上面的錯誤有可能就是這個值設定過小造成的

MaxRequestsPerProcess 每個程式最多完成處理個數,達成後自殺

[ Lighttpd ]

配置:lighttpd.conf

Lighttpd配置中,關於超時的引數有如下幾個(篇幅考慮,只寫讀超時,寫超時引數同理):

主要涉及選項:

server.max-keep-alive-idle = 5

server.max-read-idle = 60

server.read-timeout = 0

server.max-connection-idle = 360

 

 

說明:

對於一個keep-alive連線上的連續請求,傳送第一個請求內容的最大間隔由引數max-read-idle決定,從第二個請求起,傳送請求內容的最大間隔由引數max-keep-alive-idle決定。請求間的間隔超時也由max-keep-alive-idle決定。傳送請求內容的總時間超時由引數read-timeout決定。Lighttpd與後端互動資料的超時由max-connection-idle決定。

延伸閱讀:

http://www.snooda.com/read/244

[ Nginx ]

配置:nginx.conf

 

 

 

說明:

Nginx 的超時設定倒是非常清晰容易理解,上面超時針對不同工作模式,但是因為超時帶來的問題是非常多的。

延伸閱讀:

http://hi.baidu.com/pibuchou/blog/item/a1e330dd71fb8a5995ee3753.html

http://hi.baidu.com/pibuchou/blog/item/7cbccff0a3b77dc60b46e024.html

http://hi.baidu.com/pibuchou/blog/item/10a549818f7e4c9df703a626.html

http://www.apoyl.com/?p=466

【PHP本身超時處理】

[ PHP-fpm ] 

配置:php-fpm.conf

 

 

說明:

在 php.ini 中,有一個引數 max_execution_time 可以設定 PHP 指令碼的最大執行時間,但是,在 php-cgi(php-fpm) 中,該引數不會起效。真正能夠控制 PHP 指令碼最大執行時:

就是說如果是使用 mod_php5.so 的模式執行 max_execution_time 是會生效的,但是如果是php-fpm模式中執行時不生效的。

延伸閱讀:

http://blog.s135.com/file_get_contents/

[ PHP ]

配置:php.ini

選項:

max_execution_time = 30

或者在程式碼裡設定:

ini_set(“max_execution_time”, 30);

set_time_limit(30);

說明:

對當前會話生效,比如設定0一直不超時,但是如果php的 safe_mode 開啟了,這些設定都會不生效。

效果一樣,但是具體內容需要參考php-fpm部分內容,如果php-fpm中設定了 request_terminate_timeout 的話,那麼 max_execution_time 就不生效。

【後端&介面訪問超時】

【HTTP訪問】

一般我們訪問HTTP方式很多,主要是:curl, socket, file_get_contents() 等方法。

如果碰到對方伺服器一直沒有響應的時候,我們就悲劇了,很容易把整個伺服器搞死,所以在訪問http的時候也需要考慮超時的問題。

[ CURL 訪問HTTP]

CURL 是我們常用的一種比較靠譜的訪問HTTP協議介面的lib庫,效能高,還有一些併發支援的功能等。

CURL:

curl_setopt($ch, opt) 可以設定一些超時的設定,主要包括:

*(重要) CURLOPT_TIMEOUT 設定cURL允許執行的最長秒數。

*(重要) CURLOPT_TIMEOUT_MS 設定cURL允許執行的最長毫秒數。 (在cURL 7.16.2中被加入。從PHP 5.2.3起可使用。 )

CURLOPT_CONNECTTIMEOUT 在發起連線前等待的時間,如果設定為0,則無限等待。

CURLOPT_CONNECTTIMEOUT_MS 嘗試連線等待的時間,以毫秒為單位。如果設定為0,則無限等待。  在cURL 7.16.2中被加入。從PHP 5.2.3開始可用。

CURLOPT_DNS_CACHE_TIMEOUT 設定在記憶體中儲存DNS資訊的時間,預設為120秒。

curl普通秒級超時:

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL,$url);

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

curl_setopt($ch, CURLOPT_TIMEOUT, 60);   //只需要設定一個秒的數量就可以

curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

curl_setopt($ch, CURLOPT_USERAGENT, $defined_vars[‘HTTP_USER_AGENT’]);

curl普通秒級超時使用:

curl_setopt($ch, CURLOPT_TIMEOUT, 60);

curl如果需要進行毫秒超時,需要增加:

curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);

或者是:

curl_setopt ( $ch,  CURLOPT_NOSIGNAL, true); 是可以支援毫秒級別超時設定的

curl一個毫秒級超時的例子:

 

 

其他一些技巧:

1. 按照經驗總結是:cURL 版本 >= libcurl/7.21.0 版本,毫秒級超時是一定生效的,切記。

2. curl_multi的毫秒級超時也有問題。。單次訪問是支援ms級超時的,curl_multi並行調多個會不準

[流處理方式訪問HTTP]

除了curl,我們還經常自己使用fsockopen、或者是file操作函式來進行HTTP協議的處理,所以,我們對這塊的超時處理也是必須的。

一般連線超時可以直接設定,但是流讀取超時需要單獨處理。

自己寫程式碼處理:

 

 

或者使用內建流處理函式 stream_set_timeout() 和 stream_get_meta_data() 處理:

 

 

file_get_contents 超時:

fopen 超時:

【MySQL】

php中的mysql客戶端都沒有設定超時的選項,mysqli和mysql都沒有,但是libmysql是提供超時選項的,只是我們在php中隱藏了而已。

那麼如何在PHP中使用這個操作捏,就需要我們自己定義一些MySQL操作常量,主要涉及的常量有:

 

 

這兩個,定義以後,可以使用 options 設定相應的值。

不過有個注意點,mysql內部實現:

1. 超時設定單位為秒,最少配置1秒

2. 但mysql底層的read會重試兩次,所以實際會是 3 秒

重試兩次 + 自身一次 = 3倍超時時間,那麼就是說最少超時時間是3秒,不會低於這個值,對於大部分應用來說可以接受,但是對於小部分應用需要優化。

檢視一個設定訪問mysql超時的php例項:

 

延伸閱讀:

http://blog.csdn.net/heiyeshuwu/article/details/5869813

 

【Memcached】

[PHP擴充套件]

php_memcache 客戶端:

連線超時:bool Memcache::connect ( string $host [, int $port [, int $timeout ]] )

在get和set的時候,都沒有明確的超時設定引數。

libmemcached 客戶端:在php介面沒有明顯的超時引數。

說明:所以說,在PHP中訪問Memcached是存在很多問題的,需要自己hack部分操作,或者是參考網上補丁。

[C&C++訪問Memcached]

客戶端:libmemcached 客戶端

說明:memcache超時配置可以配置小點,比如5,10個毫秒已經夠用了,超過這個時間還不如從資料庫查詢。

下面是一個連線和讀取set資料的超時的C++示例:

 

//Memcache讀取資料超時 (沒有設定)

libmemcahed 原始碼中介面定義:

 

 

從介面中可以看出在讀取資料的時候,是沒有超時設定的。

延伸閱讀:

http://hi.baidu.com/chinauser/item/b30af90b23335dde73e67608

http://libmemcached.org/libMemcached.html

 

【如何實現超時】

程式中需要有超時這種功能,比如你單獨訪問一個後端Socket模組,Socket模組不屬於我們上面描述的任何一種的時候,它的協議也是私有的,那麼這個時候可能需要自己去實現一些超時處理策略,這個時候就需要一些處理程式碼了。

[PHP中超時實現] 

一、初級:最簡單的超時實現 (秒級超時)

思路很簡單:連結一個後端,然後設定為非阻塞模式,如果沒有連線上就一直迴圈,判斷當前時間和超時時間之間的差異。

php socket 中實現原始的超時:(每次迴圈都當前時間去減,效能會很差,cpu佔用會較高)

 

 

 

二、升級:使用PHP自帶非同步IO去實現(毫秒級超時)

說明:

非同步IO:非同步IO的概念和同步IO相對。當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果。實際處理這個呼叫的部件在完成後,通過狀態、通知和回撥來通知呼叫者。非同步IO將位元分成小組進行傳送,小組可以是8位的1個字元或更長。傳送方可以在任何時刻傳送這些位元組,而接收方從不知道它們會在什麼時候到達。

多路複用:複用模型是對多個IO操作進行檢測,返回可操作集合,這樣就可以對其進行操作了。這樣就避免了阻塞IO不能隨時處理各個IO和非阻塞佔用系統資源的確定。

 

使用 socket_select() 實現超時

socket_select(…, floor($timeout), ceil($timeout*1000000));

select的特點:能夠設定到微秒級別的超時!

使用socket_select() 的超時程式碼(需要了解一些非同步IO程式設計的知識去理解)

[C&C++中超時實現] 

一般在Linux C/C++中,可以使用:alarm() 設定定時器的方式實現秒級超時,或者:select()、poll()、epoll() 之類的非同步複用IO實現毫秒級超時。也可以使用二次封裝的非同步io庫(libevent, libev)也能實現。

一、使用alarm中用訊號實現超時 (秒級超時)

說明:Linux核心connect超時通常為75秒,我們可以設定更小的時間如10秒來提前從connect中返回。這裡用使用訊號處理機制,呼叫alarm,超時後產生SIGALRM訊號 (也可使用select實現)

用 alarym 秒級實現 connect 設定超時程式碼示例:

同樣可以為 recv 設定超時,5秒內收不到任何應答就中斷

signal( … );

alarm(5);

recv( … );

alarm(0);

static void sig_alarm(int signo){return;}

當客戶端阻塞於讀(readline,…)時,如果此時伺服器崩了,客戶TCP試圖從伺服器接收一個ACK,持續重傳 資料分節,大約要等9分鐘才放棄重傳,並返回一個錯誤。因此,在客戶讀阻塞時,呼叫超時。

二、使用非同步複用IO使用 (毫秒級超時)

非同步IO執行流程:

1.首先將標誌位設為Non-blocking模式,準備在非阻塞模式下呼叫connect函式

2.呼叫connect,正常情況下,因為TCP三次握手需要一些時間;而非阻塞呼叫只要不能立即完成就會返回錯誤,所以這裡會返回EINPROGRESS,表示在建立連線但還沒有完成。

3.在讀套介面描述符集(fd_set rset)和寫套介面描述符集(fd_set wset)中將當前套介面置位(用FD_ZERO()、FD_SET()巨集),並設定好超時時間(struct timeval *timeout)

4.呼叫select( socket, &rset, &wset, NULL, timeout )

返回0表示connect超時,如果你設定的超時時間大於75秒就沒有必要這樣做了,因為核心中對connect有超時限制就是75秒。

//select 實現毫秒級超時示例:

 

 

說明:在超時實現方面,不論是什麼指令碼語言:PHP、Python、Perl 基本底層都是C&C++的這些實現方式,需要理解這些超時處理,需要一些Linux 程式設計和網路程式設計的知識。

延伸閱讀:

http://blog.sina.com.cn/s/blog_4462f8560100tvgo.html

http://blog.csdn.net/thimin/article/details/1530839

http://hi.baidu.com/xjtdy888/item/93d9daefcc1d31d1ea34c992

http://blog.csdn.net/byxdaz/article/details/5461142

http://blog.163.com/xychenbaihu@yeah/blog/static/13222965520112163171778/

http://hi.baidu.com/suyupin/item/df10004decb620e91f19bcf5

http://stackoverflow.com/questions/7092633/connect-timeout-with-alarm

http://stackoverflow.com/questions/7089128/linux-tcp-connect-with-select-fails-at-testserver?lq=1

http://cppentry.com/bencandy.php?fid=54&id=1129

 

【 總結 】

1. PHP應用層如何設定超時?

PHP在處理超時層次有很多,不同層次,需要前端包容後端超時:

瀏覽器(客戶端) -> 接入層 -> Web伺服器  -> PHP  -> 後端 (MySQL、Memcached)

就是說,接入層(Web伺服器層)的超時時間必須大於PHP(PHP-FPM)中設定的超時時間,不然後面沒處理完,你前面就超時關閉了,這個會很杯具。還有就是PHP的超時時間要大於PHP本身訪問後端(MySQL、HTTP、Memcached)的超時時間,不然結局同前面。

2. 超時設定原則是什麼?

如果是希望永久不超時的程式碼(比如上傳,或者定期跑的程式),我仍然建議設定一個超時時間,比如12個小時這樣的,主要是為了保證不會永久夯住一個php程式或者後端,導致無法給其他頁面提供服務,最終引起所有機器雪崩。

如果是要要求快速響應的程式,建議後端超時設定短一些,比如連線500ms,讀1s,寫1s,這樣的速度,這樣能夠大幅度減少應用雪崩的問題,不會讓伺服器負載太高。

3. 自己開發超時訪問合適嗎?

一般如果不是萬不得已,建議用現有很多網路程式設計框架也好、基礎庫也好,裡面一般都帶有超時的實現,比如一些網路IO的lib庫,儘量使用它們內建的,自己重複造輪子容易有bug,也不方便維護(不過如是是基於學習的目的就當別論了)。

4. 其他建議

超時在所有應用裡都是大問題,在開發應用的時候都要考慮到。我見過一些應用超時設定上百秒的,這種效能就委實差了,我舉個例子:

比如你php-fpm開了128個php-cgi程式,然後你的超時設定的是32s,那麼我們如果後端服務比較差,極端情況下,那麼最多每秒能響應的請求是:

128 / 32 = 4個

你沒看錯,1秒只能處理4個請求,那服務也太差了!雖然我們可以把php-cgi程式開大,但是記憶體佔用,還有程式之間切換成本也會增加,cpu呀,記憶體呀都會增加,服務也會不穩定。所以,儘量設定一個合理的超時值,或者督促後端提高效能。

 

 

 

相關文章