【 概述 】
在PHP開(kāi)發(fā)中工作里非常多使用到超時(shí)處理到超時(shí)的場(chǎng)合,我說(shuō)幾個(gè)場(chǎng)景:
1. 異步獲取數(shù)據(jù)如果某個(gè)后端數(shù)據(jù)源獲取不成功則跳過(guò),不影響整個(gè)頁(yè)面展現(xiàn)
2. 為了保證Web服務(wù)器不會(huì)因?yàn)楫?dāng)個(gè)頁(yè)面處理性能差而導(dǎo)致無(wú)法訪問(wèn)其他頁(yè)面,則會(huì)對(duì)某些頁(yè)面操作設(shè)置
3. 對(duì)于某些上傳或者不確定處理時(shí)間的場(chǎng)合,則需要對(duì)整個(gè)流程中所有超時(shí)設(shè)置為無(wú)限,否則任何一個(gè)環(huán)節(jié)設(shè)置不當(dāng),都會(huì)導(dǎo)致莫名執(zhí)行中斷
4. 多個(gè)后端模塊(MySQL、Memcached、HTTP接口),為了防止單個(gè)接口性能太差,導(dǎo)致整個(gè)前面獲取數(shù)據(jù)太緩慢,影響頁(yè)面打開(kāi)速度,引起雪崩
5. 。。。很多需要超時(shí)的場(chǎng)合
這些地方都需要考慮超時(shí)的設(shè)定,但是PHP中的超時(shí)都是分門(mén)別類(lèi),各個(gè)處理方式和策略都不同,為了系統(tǒng)的描述,我總結(jié)了PHP中常用的超時(shí)處理的總結(jié)。
【W(wǎng)eb服務(wù)器超時(shí)處理】
[ Apache ]
一般在性能很高的情況下,缺省所有超時(shí)配置都是30秒,但是在上傳文件,或者網(wǎng)絡(luò)速度很慢的情況下,那么可能觸發(fā)超時(shí)操作。
目前 apache fastcgi php-fpm 模式 下有三個(gè)超時(shí)設(shè)置:
fastcgi 超時(shí)設(shè)置:
修改 httpd.conf 的fastcgi連接配置,類(lèi)似如下:
FastCgiExternalServer /home/forum/apache/apache_php/cgi-bin/php-cgi -socket /home/forum/php5/etc/php-fpm.sock
ScriptAlias /fcgi-bin/ "/home/forum/apache/apache_php/cgi-bin/"
AddHandler php-fastcgi .php
Action php-fastcgi /fcgi-bin/php-cgi
AddType application/x-httpd-php .php
</IfModule>
缺省配置是 30s,如果需要定制自己的配置,需要修改配置,比如修改為100秒:(修改后重啟 apache):
<IfModule mod_fastcgi.c>
FastCgiExternalServer /home/forum/apache/apache_php/cgi-bin/php-cgi -socket /home/forum/php5/etc/php-fpm.sock -idle-timeout 100
ScriptAlias /fcgi-bin/ "/home/forum/apache/apache_php/cgi-bin/"
AddHandler php-fastcgi .php
Action php-fastcgi /fcgi-bin/php-cgi
AddType application/x-httpd-php .php
</IfModule>
如果超時(shí)會(huì)返回500錯(cuò)誤,斷開(kāi)跟后端php服務(wù)的連接,同時(shí)記錄一條apache錯(cuò)誤日志:
[Thu Jan 27 18:30:15 2011] [error] [client 10.81.41.110] FastCGI: comm with server "/home/forum/apache/apache_php/cgi-bin/php-cgi" aborted: idle timeout (30 sec)
[Thu Jan 27 18:30:15 2011] [error] [client 10.81.41.110] FastCGI: incomplete headers (0 bytes) received from server "/home/forum/apache/apache_php/cgi-bin/php-cgi"
其他 fastcgi 配置參數(shù)說(shuō)明:
IdleTimeout 發(fā)呆時(shí)限 ProcessLifeTime 一個(gè)進(jìn)程的最長(zhǎng)生命周期,過(guò)期之后無(wú)條件kill MaxProcessCount 最大進(jìn)程個(gè)數(shù) DefaultMinClassProcessCount 每個(gè)程序啟動(dòng)的最小進(jìn)程個(gè)數(shù) DefaultMaxClassProcessCount 每個(gè)程序啟動(dòng)的最大進(jìn)程個(gè)數(shù) IPCConnectTimeout 程序響應(yīng)超時(shí)時(shí)間 IPCCommTimeout 與程序通訊的最長(zhǎng)時(shí)間,上面的錯(cuò)誤有可能就是這個(gè)值設(shè)置過(guò)小造成的 MaxRequestsPerProcess 每個(gè)進(jìn)程最多完成處理個(gè)數(shù),達(dá)成后自殺 |
[ Lighttpd ]
配置:lighttpd.conf
Lighttpd配置中,關(guān)于超時(shí)的參數(shù)有如下幾個(gè)(篇幅考慮,只寫(xiě)讀超時(shí),寫(xiě)超時(shí)參數(shù)同理):
主要涉及選項(xiàng):
server.max-keep-alive-idle = 5
server.max-read-idle = 60
server.read-timeout = 0
server.max-connection-idle = 360
-------------------------------------------------- # 每次keep-alive 的最大請(qǐng)求數(shù), 默認(rèn)值是16 server.max-keep-alive-requests = 100
# keep-alive的最長(zhǎng)等待時(shí)間, 單位是秒,默認(rèn)值是5 server.max-keep-alive-idle = 1200
# lighttpd的work子進(jìn)程數(shù),默認(rèn)值是0,單進(jìn)程運(yùn)行 server.max-worker = 2
# 限制用戶(hù)在發(fā)送請(qǐng)求的過(guò)程中,最大的中間停頓時(shí)間(單位是秒), # 如果用戶(hù)在發(fā)送請(qǐng)求的過(guò)程中(沒(méi)發(fā)完請(qǐng)求),中間停頓的時(shí)間太長(zhǎng),lighttpd會(huì)主動(dòng)斷開(kāi)連接 # 默認(rèn)值是60(秒) server.max-read-idle = 1200
# 限制用戶(hù)在接收應(yīng)答的過(guò)程中,最大的中間停頓時(shí)間(單位是秒), # 如果用戶(hù)在接收應(yīng)答的過(guò)程中(沒(méi)接完),中間停頓的時(shí)間太長(zhǎng),lighttpd會(huì)主動(dòng)斷開(kāi)連接 # 默認(rèn)值是360(秒) server.max-write-idle = 12000
# 讀客戶(hù)端請(qǐng)求的超時(shí)限制,單位是秒, 配為0表示不作限制 # 設(shè)置小于max-read-idle時(shí),read-timeout生效 server.read-timeout = 0
# 寫(xiě)應(yīng)答頁(yè)面給客戶(hù)端的超時(shí)限制,單位是秒,配為0表示不作限制 # 設(shè)置小于max-write-idle時(shí),write-timeout生效 server.write-timeout = 0
# 請(qǐng)求的處理時(shí)間上限,如果用了mod_proxy_core,那就是和后端的交互時(shí)間限制, 單位是秒 server.max-connection-idle = 1200 -------------------------------------------------- |
說(shuō)明:
對(duì)于一個(gè)keep-alive連接上的連續(xù)請(qǐng)求,發(fā)送第一個(gè)請(qǐng)求內(nèi)容的最大間隔由參數(shù)max-read-idle決定,從第二個(gè)請(qǐng)求起,發(fā)送請(qǐng)求內(nèi)容的最大間隔由參數(shù)max-keep-alive-idle決定。請(qǐng)求間的間隔超時(shí)也由max-keep-alive-idle決定。發(fā)送請(qǐng)求內(nèi)容的總時(shí)間超時(shí)由參數(shù)read-timeout決定。Lighttpd與后端交互數(shù)據(jù)的超時(shí)由max-connection-idle決定。
延伸閱讀:
http://www.snooda.com/read/244
[ Nginx ]
配置:nginx.conf
http { #Fastcgi: (針對(duì)后端的fastcgi 生效, fastcgi 不屬于proxy模式) fastcgi_connect_timeout 5; #連接超時(shí) fastcgi_send_timeout 10; #寫(xiě)超時(shí) fastcgi_read_timeout 10; #讀取超時(shí)
#Proxy: (針對(duì)proxy/upstreams的生效) proxy_connect_timeout 15s; #連接超時(shí) proxy_read_timeout 24s; #讀超時(shí) proxy_send_timeout 10s; #寫(xiě)超時(shí) } |
說(shuō)明:
Nginx 的超時(shí)設(shè)置倒是非常清晰容易理解,上面超時(shí)針對(duì)不同工作模式,但是因?yàn)槌瑫r(shí)帶來(lái)的問(wèn)題是非常多的。
延伸閱讀:
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本身超時(shí)處理】
[ PHP-fpm ]
配置:php-fpm.conf
<?xml version="1.0" ?> <configuration> //... Sets the limit on the number of simultaneous requests that will be served. Equivalent to Apache MaxClients directive. Equivalent to PHP_FCGI_CHILDREN environment in original php.fcgi Used with any pm_style. #php-cgi的進(jìn)程數(shù)量 <value name="max_children">128</value>
The timeout (in seconds) for serving a single request after which the worker process will be terminated Should be used when 'max_execution_time' ini option does not stop script execution for some reason '0s' means 'off' #php-fpm 請(qǐng)求執(zhí)行超時(shí)時(shí)間,0s為永不超時(shí),否則設(shè)置一個(gè) Ns 為超時(shí)的秒數(shù) <value name="request_terminate_timeout">0s</value>
The timeout (in seconds) for serving of single request after which a php backtrace will be dumped to slow.log file '0s' means 'off' <value name="request_slowlog_timeout">0s</value>
</configuration>
|
說(shuō)明:
在 php.ini 中,有一個(gè)參數(shù) max_execution_time 可以設(shè)置 PHP 腳本的最大執(zhí)行時(shí)間,但是,在 php-cgi(php-fpm) 中,該參數(shù)不會(huì)起效。真正能夠控制 PHP 腳本最大執(zhí)行時(shí):
<value name="request_terminate_timeout">0s</value>
就是說(shuō)如果是使用 mod_php5.so 的模式運(yùn)行 max_execution_time 是會(huì)生效的,但是如果是php-fpm模式中運(yùn)行時(shí)不生效的。
延伸閱讀:
http://blog.s135.com/file_get_contents/
[ PHP ]
配置:php.ini
選項(xiàng):
max_execution_time = 30
或者在代碼里設(shè)置:
ini_set("max_execution_time", 30);
set_time_limit(30);
說(shuō)明:
對(duì)當(dāng)前會(huì)話生效,比如設(shè)置0一直不超時(shí),但是如果php的 safe_mode 打開(kāi)了,這些設(shè)置都會(huì)不生效。
效果一樣,但是具體內(nèi)容需要參考php-fpm部分內(nèi)容,如果php-fpm中設(shè)置了 request_terminate_timeout 的話,那么 max_execution_time 就不生效。
【后端&接口訪問(wèn)超時(shí)】
【HTTP訪問(wèn)】
一般我們?cè)L問(wèn)HTTP方式很多,主要是:curl, socket, file_get_contents() 等方法。
如果碰到對(duì)方服務(wù)器一直沒(méi)有響應(yīng)的時(shí)候,我們就悲劇了,很容易把整個(gè)服務(wù)器搞死,所以在訪問(wèn)http的時(shí)候也需要考慮超時(shí)的問(wèn)題。
[ CURL 訪問(wèn)HTTP]
CURL 是我們常用的一種比較靠譜的訪問(wèn)HTTP協(xié)議接口的lib庫(kù),性能高,還有一些并發(fā)支持的功能等。
CURL:
curl_setopt($ch, opt) 可以設(shè)置一些超時(shí)的設(shè)置,主要包括:
*(重要) CURLOPT_TIMEOUT 設(shè)置cURL允許執(zhí)行的最長(zhǎng)秒數(shù)。
*(重要) CURLOPT_TIMEOUT_MS 設(shè)置cURL允許執(zhí)行的最長(zhǎng)毫秒數(shù)。 (在cURL 7.16.2中被加入。從PHP 5.2.3起可使用。 )
CURLOPT_CONNECTTIMEOUT 在發(fā)起連接前等待的時(shí)間,如果設(shè)置為0,則無(wú)限等待。
CURLOPT_CONNECTTIMEOUT_MS 嘗試連接等待的時(shí)間,以毫秒為單位。如果設(shè)置為0,則無(wú)限等待。 在cURL 7.16.2中被加入。從PHP 5.2.3開(kāi)始可用。
CURLOPT_DNS_CACHE_TIMEOUT 設(shè)置在內(nèi)存中保存DNS信息的時(shí)間,默認(rèn)為120秒。
curl普通秒級(jí)超時(shí):
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); //只需要設(shè)置一個(gè)秒的數(shù)量就可以
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, $defined_vars['HTTP_USER_AGENT']);
curl普通秒級(jí)超時(shí)使用:
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl如果需要進(jìn)行毫秒超時(shí),需要增加:
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
或者是:
curl_setopt ( $ch, CURLOPT_NOSIGNAL, true); 是可以支持毫秒級(jí)別超時(shí)設(shè)置的
curl一個(gè)毫秒級(jí)超時(shí)的例子:
<?php if (!isset($_GET['foo'])) { // Client $ch = curl_init('http://example.com/'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_NOSIGNAL, 1); //注意,毫秒超時(shí)一定要設(shè)置這個(gè) curl_setopt($ch, CURLOPT_TIMEOUT_MS, 200); //超時(shí)毫秒,cURL 7.16.2中被加入。從PHP 5.2.3起可使用 $data = curl_exec($ch); $curl_errno = curl_errno($ch); $curl_error = curl_error($ch); curl_close($ch);
if ($curl_errno > 0) { echo "cURL Error ($curl_errno): $curl_error/n"; } else { echo "Data received: $data/n"; } } else { // Server sleep(10); echo "Done."; } ?> |
其他一些技巧:
1. 按照經(jīng)驗(yàn)總結(jié)是:cURL 版本 >= libcurl/7.21.0 版本,毫秒級(jí)超時(shí)是一定生效的,切記。
2. curl_multi的毫秒級(jí)超時(shí)也有問(wèn)題。。單次訪問(wèn)是支持ms級(jí)超時(shí)的,curl_multi并行調(diào)多個(gè)會(huì)不準(zhǔn)
[流處理方式訪問(wèn)HTTP]
除了curl,我們還經(jīng)常自己使用fsockopen、或者是file操作函數(shù)來(lái)進(jìn)行HTTP協(xié)議的處理,所以,我們對(duì)這塊的超時(shí)處理也是必須的。
一般連接超時(shí)可以直接設(shè)置,但是流讀取超時(shí)需要單獨(dú)處理。
自己寫(xiě)代碼處理:
$tmCurrent = gettimeofday();
$intUSGone = ($tmCurrent['sec'] - $tmStart['sec']) * 1000000
+ ($tmCurrent['usec'] - $tmStart['usec']);
if ($intUSGone > $this->_intReadTimeoutUS) {
return false;
}
或者使用內(nèi)置流處理函數(shù) stream_set_timeout() 和 stream_get_meta_data() 處理:
<?php // Timeout in seconds $timeout = 5; $fp = fsockopen("example.com", 80, $errno, $errstr, $timeout); if ($fp) { fwrite($fp, "GET / HTTP/1.0/r/n"); fwrite($fp, "Host: example.com/r/n"); fwrite($fp, "Connection: Close/r/n/r/n"); stream_set_blocking($fp, true); //重要,設(shè)置為非阻塞模式 stream_set_timeout($fp,$timeout); //設(shè)置超時(shí) $info = stream_get_meta_data($fp); while ((!feof($fp)) && (!$info['timed_out'])) { $data .= fgets($fp, 4096); $info = stream_get_meta_data($fp); ob_flush; flush(); } if ($info['timed_out']) { echo "Connection Timed Out!"; } else { echo $data; } } |
file_get_contents 超時(shí):
<?php $timeout = array( 'http' => array( 'timeout' => 5 //設(shè)置一個(gè)超時(shí)時(shí)間,單位為秒 ) ); $ctx = stream_context_create($timeout); $text = file_get_contents("http://example.com/", 0, $ctx); ?> |
fopen 超時(shí):
<?php $timeout = array( 'http' => array( 'timeout' => 5 //設(shè)置一個(gè)超時(shí)時(shí)間,單位為秒 ) ); $ctx = stream_context_create($timeout); if ($fp = fopen("http://example.com/", "r", false, $ctx)) { while( $c = fread($fp, 8192)) { echo $c; } fclose($fp); } ?> |
【MySQL】
php中的mysql客戶(hù)端都沒(méi)有設(shè)置超時(shí)的選項(xiàng),mysqli和mysql都沒(méi)有,但是libmysql是提供超時(shí)選項(xiàng)的,只是我們?cè)趐hp中隱藏了而已。
那么如何在PHP中使用這個(gè)操作捏,就需要我們自己定義一些MySQL操作常量,主要涉及的常量有:
MYSQL_OPT_READ_TIMEOUT=11;
MYSQL_OPT_WRITE_TIMEOUT=12;
這兩個(gè),定義以后,可以使用 options 設(shè)置相應(yīng)的值。
不過(guò)有個(gè)注意點(diǎn),mysql內(nèi)部實(shí)現(xiàn):
1. 超時(shí)設(shè)置單位為秒,最少配置1秒
2. 但mysql底層的read會(huì)重試兩次,所以實(shí)際會(huì)是 3 秒
重試兩次 + 自身一次 = 3倍超時(shí)時(shí)間,那么就是說(shuō)最少超時(shí)時(shí)間是3秒,不會(huì)低于這個(gè)值,對(duì)于大部分應(yīng)用來(lái)說(shuō)可以接受,但是對(duì)于小部分應(yīng)用需要優(yōu)化。
查看一個(gè)設(shè)置訪問(wèn)mysql超時(shí)的php實(shí)例:
<?php //自己定義讀寫(xiě)超時(shí)常量 if (!defined('MYSQL_OPT_READ_TIMEOUT')) { define('MYSQL_OPT_READ_TIMEOUT', 11); } if (!defined('MYSQL_OPT_WRITE_TIMEOUT')) { define('MYSQL_OPT_WRITE_TIMEOUT', 12); } //設(shè)置超時(shí) $mysqli = mysqli_init(); $mysqli->options(MYSQL_OPT_READ_TIMEOUT, 3); $mysqli->options(MYSQL_OPT_WRITE_TIMEOUT, 1);
//連接數(shù)據(jù)庫(kù) $mysqli->real_connect("localhost", "root", "root", "test"); if (mysqli_connect_errno()) { printf("Connect failed: %s/n", mysqli_connect_error()); exit(); } //執(zhí)行查詢(xún) sleep 1秒不超時(shí) printf("Host information: %s/n", $mysqli->host_info); if (!($res=$mysqli->query('select sleep(1)'))) { echo "query1 error: ". $mysqli->error ."/n"; } else { echo "Query1: query success/n"; } //執(zhí)行查詢(xún) sleep 9秒會(huì)超時(shí) if (!($res=$mysqli->query('select sleep(9)'))) { echo "query2 error: ". $mysqli->error ."/n"; } else { echo "Query2: query success/n"; } $mysqli->close(); echo "close mysql connection/n"; ?> |
延伸閱讀:
http://blog.csdn.net/heiyeshuwu/article/details/5869813
【Memcached】
[PHP擴(kuò)展]
php_memcache 客戶(hù)端:
連接超時(shí):bool Memcache::connect ( string $host [, int $port [, int $timeout ]] )
在get和set的時(shí)候,都沒(méi)有明確的超時(shí)設(shè)置參數(shù)。
libmemcached 客戶(hù)端:在php接口沒(méi)有明顯的超時(shí)參數(shù)。
說(shuō)明:所以說(shuō),在PHP中訪問(wèn)Memcached是存在很多問(wèn)題的,需要自己hack部分操作,或者是參考網(wǎng)上補(bǔ)丁。
[C&C++訪問(wèn)Memcached]
客戶(hù)端:libmemcached 客戶(hù)端
說(shuō)明:memcache超時(shí)配置可以配置小點(diǎn),比如5,10個(gè)毫秒已經(jīng)夠用了,超過(guò)這個(gè)時(shí)間還不如從數(shù)據(jù)庫(kù)查詢(xún)。
下面是一個(gè)連接和讀取set數(shù)據(jù)的超時(shí)的C++示例:
//創(chuàng)建連接超時(shí)(連接到Memcached) memcached_st* MemCacheProxy::_create_handle() { memcached_st * mmc = NULL; memcached_return_t prc; if (_mpool != NULL) { // get from pool mmc = memcached_pool_pop(_mpool, false, &prc); if (mmc == NULL) { __LOG_WARNING__("MemCacheProxy", "get handle from pool error [%d]", (int)prc); } return mmc; }
memcached_st* handle = memcached_create(NULL); if (handle == NULL){ __LOG_WARNING__("MemCacheProxy", "create_handle error"); return NULL; }
// 設(shè)置連接/讀取超時(shí) memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_HASH, MEMCACHED_HASH_DEFAULT); memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_NO_BLOCK, _noblock); //參數(shù)MEMCACHED_BEHAVIOR_NO_BLOCK為1使超時(shí)配置生效,不設(shè)置超時(shí)會(huì)不生效,關(guān)鍵時(shí)候會(huì)悲劇的,容易引起雪崩 memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, _connect_timeout); //連接超時(shí) memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_RCV_TIMEOUT, _read_timeout); //讀超時(shí) memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_SND_TIMEOUT, _send_timeout); //寫(xiě)超時(shí) memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_POLL_TIMEOUT, _poll_timeout);
// 設(shè)置一致hash // memcached_behavior_set_distribution(handle, MEMCACHED_DISTRIBUTION_CONSISTENT); memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_DISTRIBUTION, MEMCACHED_DISTRIBUTION_CONSISTENT);
memcached_return rc; for (uint i = 0; i < _server_count; i++){ rc = memcached_server_add(handle, _ips[i], _ports[i]); if (MEMCACHED_SUCCESS != rc) { __LOG_WARNING__("MemCacheProxy", "add server [%s:%d] failed.", _ips[i], _ports[i]); } }
_mpool = memcached_pool_create(handle, _min_connect, _max_connect); if (_mpool == NULL){ __LOG_WARNING__("MemCacheProxy", "create_pool error"); return NULL; }
mmc = memcached_pool_pop(_mpool, false, &prc); if (mmc == NULL) { __LOG_WARNING__("MyMemCacheProxy", "get handle from pool error [%d]", (int)prc); } //__LOG_DEBUG__("MemCacheProxy", "get handle [%p]", handle); return mmc; }
//設(shè)置一個(gè)key超時(shí)(set一個(gè)數(shù)據(jù)到memcached) bool MemCacheProxy::_add(memcached_st* handle, unsigned int* key, const char* value, int len, unsigned int timeout) { memcached_return rc;
char tmp[1024]; snprintf(tmp, sizeof (tmp), "%u#%u", key[0], key[1]); //有個(gè)timeout值 rc = memcached_set(handle, tmp, strlen(tmp), (char*)value, len, timeout, 0); if (MEMCACHED_SUCCESS != rc){ return false; } return true; } |
//Memcache讀取數(shù)據(jù)超時(shí) (沒(méi)有設(shè)置)
libmemcahed 源碼中接口定義:
LIBMEMCACHED_API char *memcached_get(memcached_st *ptr,const char *key, size_t key_length,size_t *value_length,uint32_t *flags,memcached_return_t *error);
LIBMEMCACHED_API memcached_return_t memcached_mget(memcached_st *ptr,const char * const *keys,const size_t *key_length,size_t number_of_keys);
從接口中可以看出在讀取數(shù)據(jù)的時(shí)候,是沒(méi)有超時(shí)設(shè)置的。
延伸閱讀:
http://hi.baidu.com/chinauser/item/b30af90b23335dde73e67608
http://libmemcached.org/libMemcached.html
【如何實(shí)現(xiàn)超時(shí)】
程序中需要有超時(shí)這種功能,比如你單獨(dú)訪問(wèn)一個(gè)后端Socket模塊,Socket模塊不屬于我們上面描述的任何一種的時(shí)候,它的協(xié)議也是私有的,那么這個(gè)時(shí)候可能需要自己去實(shí)現(xiàn)一些超時(shí)處理策略,這個(gè)時(shí)候就需要一些處理代碼了。
[PHP中超時(shí)實(shí)現(xiàn)]
一、初級(jí):最簡(jiǎn)單的超時(shí)實(shí)現(xiàn) (秒級(jí)超時(shí))
思路很簡(jiǎn)單:鏈接一個(gè)后端,然后設(shè)置為非阻塞模式,如果沒(méi)有連接上就一直循環(huán),判斷當(dāng)前時(shí)間和超時(shí)時(shí)間之間的差異。
php socket 中實(shí)現(xiàn)原始的超時(shí):(每次循環(huán)都當(dāng)前時(shí)間去減,性能會(huì)很差,cpu占用會(huì)較高)
<? $host = "127.0.0.1"; $port = "80"; $timeout = 15; //timeout in seconds
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Unable to create socket/n");
socket_set_nonblock($socket) //務(wù)必設(shè)置為阻塞模式 or die("Unable to set nonblock on socket/n");
$time = time(); //循環(huán)的時(shí)候每次都減去相應(yīng)值 while (!@socket_connect($socket, $host, $port)) //如果沒(méi)有連接上就一直死循環(huán) { $err = socket_last_error($socket); if ($err == 115 || $err == 114) { if ((time() - $time) >= $timeout) //每次都需要去判斷一下是否超時(shí)了 { socket_close($socket); die("Connection timed out./n"); } sleep(1); continue; } die(socket_strerror($err) . "/n"); } socket_set_block($this->socket) //還原阻塞模式 or die("Unable to set block on socket/n"); ?> |
二、升級(jí):使用PHP自帶異步IO去實(shí)現(xiàn)(毫秒級(jí)超時(shí))
說(shuō)明:
異步IO:異步IO的概念和同步IO相對(duì)。當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果。實(shí)際處理這個(gè)調(diào)用的部件在完成后,通過(guò)狀態(tài)、通知和回調(diào)來(lái)通知調(diào)用者。異步IO將比特分成小組進(jìn)行傳送,小組可以是8位的1個(gè)字符或更長(zhǎng)。發(fā)送方可以在任何時(shí)刻發(fā)送這些比特組,而接收方從不知道它們會(huì)在什么時(shí)候到達(dá)。
多路復(fù)用:復(fù)用模型是對(duì)多個(gè)IO操作進(jìn)行檢測(cè),返回可操作集合,這樣就可以對(duì)其進(jìn)行操作了。這樣就避免了阻塞IO不能隨時(shí)處理各個(gè)IO和非阻塞占用系統(tǒng)資源的確定。
使用 socket_select() 實(shí)現(xiàn)超時(shí)
socket_select(..., floor($timeout), ceil($timeout*1000000));
select的特點(diǎn):能夠設(shè)置到微秒級(jí)別的超時(shí)!
使用socket_select() 的超時(shí)代碼(需要了解一些異步IO編程的知識(shí)去理解)
### 調(diào)用類(lèi) #### <?php $server = new Server; $client = new Client;
for (;;) { foreach ($select->can_read(0) as $socket) {
if ($socket == $client->socket) { // New Client Socket $select->add(socket_accept($client->socket)); } else { //there's something to read on $socket } } } ?>
### 異步多路復(fù)用IO & 超時(shí)連接處理類(lèi) ### <?php class select { var $sockets;
function select($sockets) {
$this->sockets = array();
foreach ($sockets as $socket) { $this->add($socket); } }
function add($add_socket) { array_push($this->sockets,$add_socket); }
function remove($remove_socket) { $sockets = array();
foreach ($this->sockets as $socket) { if($remove_socket != $socket) $sockets[] = $socket; }
$this->sockets = $sockets; }
function can_read($timeout) { $read = $this->sockets; socket_select($read,$write = NULL,$except = NULL,$timeout); return $read; }
function can_write($timeout) { $write = $this->sockets; socket_select($read = NULL,$write,$except = NULL,$timeout); return $write; } } ?> |
[C&C++中超時(shí)實(shí)現(xiàn)]
一般在Linux C/C++中,可以使用:alarm() 設(shè)置定時(shí)器的方式實(shí)現(xiàn)秒級(jí)超時(shí),或者:select()、poll()、epoll() 之類(lèi)的異步復(fù)用IO實(shí)現(xiàn)毫秒級(jí)超時(shí)。也可以使用二次封裝的異步io庫(kù)(libevent, libev)也能實(shí)現(xiàn)。
一、使用alarm中用信號(hào)實(shí)現(xiàn)超時(shí) (秒級(jí)超時(shí))
說(shuō)明:Linux內(nèi)核connect超時(shí)通常為75秒,我們可以設(shè)置更小的時(shí)間如10秒來(lái)提前從connect中返回。這里用使用信號(hào)處理機(jī)制,調(diào)用alarm,超時(shí)后產(chǎn)生SIGALRM信號(hào) (也可使用select實(shí)現(xiàn))
用 alarym 秒級(jí)實(shí)現(xiàn) connect 設(shè)置超時(shí)代碼示例:
//信號(hào)處理函數(shù) static void connect_alarm(int signo) { debug_printf("SignalHandler"); return; }
//alarm超時(shí)連接實(shí)現(xiàn) static void conn_alarm() { Sigfunc * sigfunc ; //現(xiàn)有信號(hào)處理函數(shù) sigfunc=signal(SIGALRM, connect_alarm); //建立信號(hào)處理函數(shù)connect_alarm,(如果有)保存現(xiàn)有的信號(hào)處理函數(shù) int timeout = 5;
//設(shè)置鬧鐘 if( alarm(timeout)!=0 ){ //... 鬧鐘已經(jīng)設(shè)置處理 }
//進(jìn)行連接操作 if (connect(m_Socket, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) { if ( errno == EINTR ) { //如果錯(cuò)誤號(hào)設(shè)置為EINTR,說(shuō)明超時(shí)中斷了 debug_printf("Timeout"); m_connectionStatus = STATUS_CLOSED; errno = ETIMEDOUT; //防止三次握手繼續(xù)進(jìn)行 return ERR_TIMEOUT; } else { debug_printf("Other Err"); m_connectionStatus = STATUS_CLOSED; return ERR_NET_SOCKET; } } alarm(0);//關(guān)閉時(shí)鐘 signal(SIGALRM, sigfunc); //(如果有)恢復(fù)原來(lái)的信號(hào)處理函數(shù) return; } |
//讀取數(shù)據(jù)的超時(shí)設(shè)置
同樣可以為 recv 設(shè)置超時(shí),5秒內(nèi)收不到任何應(yīng)答就中斷
signal( ... );
alarm(5);
recv( ... );
alarm(0);
static void sig_alarm(int signo){return;}
當(dāng)客戶(hù)端阻塞于讀(readline,...)時(shí),如果此時(shí)服務(wù)器崩了,客戶(hù)TCP試圖從服務(wù)器接收一個(gè)ACK,持續(xù)重傳 數(shù)據(jù)分節(jié),大約要等9分鐘才放棄重傳,并返回一個(gè)錯(cuò)誤。因此,在客戶(hù)讀阻塞時(shí),調(diào)用超時(shí)。
二、使用異步復(fù)用IO使用 (毫秒級(jí)超時(shí))
異步IO執(zhí)行流程:
1.首先將標(biāo)志位設(shè)為Non-blocking模式,準(zhǔn)備在非阻塞模式下調(diào)用connect函數(shù)
2.調(diào)用connect,正常情況下,因?yàn)門(mén)CP三次握手需要一些時(shí)間;而非阻塞調(diào)用只要不能立即完成就會(huì)返回錯(cuò)誤,所以這里會(huì)返回EINPROGRESS,表示在建立連接但還沒(méi)有完成。
3.在讀套接口描述符集(fd_set rset)和寫(xiě)套接口描述符集(fd_set wset)中將當(dāng)前套接口置位(用FD_ZERO()、FD_SET()宏),并設(shè)置好超時(shí)時(shí)間(struct timeval *timeout)
4.調(diào)用select( socket, &rset, &wset, NULL, timeout )
返回0表示connect超時(shí),如果你設(shè)置的超時(shí)時(shí)間大于75秒就沒(méi)有必要這樣做了,因?yàn)閮?nèi)核中對(duì)connect有超時(shí)限制就是75秒。
//select 實(shí)現(xiàn)毫秒級(jí)超時(shí)示例:
static void conn_select() { // Open TCP Socket m_Socket = socket(PF_INET,SOCK_STREAM,0); if( m_Socket < 0 ) { m_connectionStatus = STATUS_CLOSED; return ERR_NET_SOCKET; }
struct sockaddr_in addr; inet_aton(m_Host.c_str(), &addr.sin_addr); addr.sin_port = htons(m_Port); addr.sin_family = PF_INET;
// Set timeout values for socket struct timeval timeouts; timeouts.tv_sec = SOCKET_TIMEOUT_SEC ; // const -> 5 timeouts.tv_usec = SOCKET_TIMEOUT_USEC ; // const -> 0 uint8_t optlen = sizeof(timeouts);
if( setsockopt( m_Socket, SOL_SOCKET, SO_RCVTIMEO,&timeouts,(socklen_t)optlen) < 0 ) { m_connectionStatus = STATUS_CLOSED; return ERR_NET_SOCKET; }
// Set the Socket to TCP Nodelay ( Send immediatly after a send / write command ) int flag_TCP_nodelay = 1; if ( (setsockopt( m_Socket, IPPROTO_TCP, TCP_NODELAY, (char *)&flag_TCP_nodelay, sizeof(flag_TCP_nodelay))) < 0) { m_connectionStatus = STATUS_CLOSED; return ERR_NET_SOCKET; } // Save Socket Flags int opts_blocking = fcntl(m_Socket, F_GETFL); if ( opts_blocking < 0 ) { return ERR_NET_SOCKET; } //設(shè)置為非阻塞模式 int opts_noblocking = (opts_blocking | O_NONBLOCK); // Set Socket to Non-Blocking if (fcntl(m_Socket, F_SETFL, opts_noblocking)<0) { return ERR_NET_SOCKET; } // Connect if ( connect(m_Socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) { // EINPROGRESS always appears on Non Blocking connect if ( errno != EINPROGRESS ) { m_connectionStatus = STATUS_CLOSED; return ERR_NET_SOCKET; } // Create a set of sockets for select fd_set socks; FD_ZERO(&socks); FD_SET(m_Socket,&socks); // Wait for connection or timeout int fdcnt = select(m_Socket+1,NULL,&socks,NULL,&timeouts); if ( fdcnt < 0 ) { return ERR_NET_SOCKET; } else if ( fdcnt == 0 ) { return ERR_TIMEOUT; } } //Set Socket to Blocking again if(fcntl(m_Socket,F_SETFL,opts_blocking)<0) { return ERR_NET_SOCKET; }
m_connectionStatus = STATUS_OPEN; return 0; } |
說(shuō)明:在超時(shí)實(shí)現(xiàn)方面,不論是什么腳本語(yǔ)言:PHP、Python、Perl 基本底層都是C&C++的這些實(shí)現(xiàn)方式,需要理解這些超時(shí)處理,需要一些Linux 編程和網(wǎng)絡(luò)編程的知識(shí)。
延伸閱讀:
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
【 總結(jié) 】
1. PHP應(yīng)用層如何設(shè)置超時(shí)?
PHP在處理超時(shí)層次有很多,不同層次,需要前端包容后端超時(shí):
瀏覽器(客戶(hù)端) -> 接入層 -> Web服務(wù)器 -> PHP -> 后端 (MySQL、Memcached)
就是說(shuō),接入層(Web服務(wù)器層)的超時(shí)時(shí)間必須大于PHP(PHP-FPM)中設(shè)置的超時(shí)時(shí)間,不然后面沒(méi)處理完,你前面就超時(shí)關(guān)閉了,這個(gè)會(huì)很杯具。還有就是PHP的超時(shí)時(shí)間要大于PHP本身訪問(wèn)后端(MySQL、HTTP、Memcached)的超時(shí)時(shí)間,不然結(jié)局同前面。
2. 超時(shí)設(shè)置原則是什么?
如果是希望永久不超時(shí)的代碼(比如上傳,或者定期跑的程序),我仍然建議設(shè)置一個(gè)超時(shí)時(shí)間,比如12個(gè)小時(shí)這樣的,主要是為了保證不會(huì)永久夯住一個(gè)php進(jìn)程或者后端,導(dǎo)致無(wú)法給其他頁(yè)面提供服務(wù),最終引起所有機(jī)器雪崩。
如果是要要求快速響應(yīng)的程序,建議后端超時(shí)設(shè)置短一些,比如連接500ms,讀1s,寫(xiě)1s,這樣的速度,這樣能夠大幅度減少應(yīng)用雪崩的問(wèn)題,不會(huì)讓服務(wù)器負(fù)載太高。
3. 自己開(kāi)發(fā)超時(shí)訪問(wèn)合適嗎?
一般如果不是萬(wàn)不得已,建議用現(xiàn)有很多網(wǎng)絡(luò)編程框架也好、基礎(chǔ)庫(kù)也好,里面一般都帶有超時(shí)的實(shí)現(xiàn),比如一些網(wǎng)絡(luò)IO的lib庫(kù),盡量使用它們內(nèi)置的,自己重復(fù)造輪子容易有bug,也不方便維護(hù)(不過(guò)如是是基于學(xué)習(xí)的目的就當(dāng)別論了)。
4. 其他建議
超時(shí)在所有應(yīng)用里都是大問(wèn)題,在開(kāi)發(fā)應(yīng)用的時(shí)候都要考慮到。我見(jiàn)過(guò)一些應(yīng)用超時(shí)設(shè)置上百秒的,這種性能就委實(shí)差了,我舉個(gè)例子:
比如你php-fpm開(kāi)了128個(gè)php-cgi進(jìn)程,然后你的超時(shí)設(shè)置的是32s,那么我們?nèi)绻蠖朔?wù)比較差,極端情況下,那么最多每秒能響應(yīng)的請(qǐng)求是:
128 / 32 = 4個(gè)
你沒(méi)看錯(cuò),1秒只能處理4個(gè)請(qǐng)求,那服務(wù)也太差了!雖然我們可以把php-cgi進(jìn)程開(kāi)大,但是內(nèi)存占用,還有進(jìn)程之間切換成本也會(huì)增加,cpu呀,內(nèi)存呀都會(huì)增加,服務(wù)也會(huì)不穩(wěn)定。所以,盡量設(shè)置一個(gè)合理的超時(shí)值,或者督促后端提高性能。
本文有部分經(jīng)驗(yàn)值,還有部分參考的內(nèi)容,如果不足之處,還請(qǐng)指正。
作者:heiyeluren
博客:http://blog.csdn.net/heiyeshuwu
時(shí)間:2012/8/8
新聞熱點(diǎn)
疑難解答