PHP超时处理应用场合及解决方案全面总结
【 概述 】
在PHP开发中工作里非常多使用到超时处理到超时的场合,我说几个场景:
1. 异步获取数据如果某个后端数据源获取不成功则跳过,不影响整个页面展现
2. 为了保证Web服务器不会因为当个页面处理性能差而导致无法访问其他页面,则会对某些页面操作设置
3. 对于某些上传或者不确定处理时间的场合,则需要对整个流程中所有超时设置为无限,否则任何一个环节设置不当,都会导致莫名执行中断
4. 多个后端模块(MySQL、Memcached、HTTP接口),为了防止单个接口性能太差,导致整个前面获取数据太缓慢,影响页面打开速度,引起雪崩
5. 。。。很多需要超时的场合
这些地方都需要考虑超时的设定,但是PHP中的超时都是分门别类,各个处理方式和策略都不同,为了系统的描述,我总结了PHP中常用的超时处理的总结。
【Web服务器超时处理】
[ Apache ]
一般在性能很高的情况下,缺省所有超时配置都是30秒,但是在上传文件,或者网络速度很慢的情况下,那么可能触发超时操作。
目前apachefastcgiphp-fpm模式下有三个超时设置:
fastcgi超时设置:
修改httpd.conf的fastcgi连接配置,类似如下:
代码如下 | 复制代码 |
<IfModulemod_fastcgi.c> 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/" AddHandlerphp-fastcgi.php Actionphp-fastcgi/fcgi-bin/php-cgi AddTypeapplication/x-httpd-php.php </IfModule> |
缺省配置是30s,如果需要定制自己的配置,需要修改配置,比如修改为100秒:(修改后重启apache):
代码如下 | 复制代码 |
<IfModulemod_fastcgi.c> FastCgiExternalServer/home/forum/apache/apache_php/cgi-bin/php-cgi-socket/home/forum/php5/etc/php-fpm.sock-idle-timeout100 ScriptAlias/fcgi-bin/"/home/forum/apache/apache_php/cgi-bin/" AddHandlerphp-fastcgi.php Actionphp-fastcgi/fcgi-bin/php-cgi AddTypeapplication/x-httpd-php.php </IfModule> |
如果超时会返回500错误,断开跟后端php服务的连接,同时记录一条apache错误日志:
代码如下 | 复制代码 |
[ThuJan2718:30:152011][error][client10.81.41.110]FastCGI:commwithserver"/home/forum/apache/apache_php/cgi-bin/php-cgi"aborted:idletimeout(30sec) [ThuJan2718:30:152011][error][client10.81.41.110]FastCGI:incompleteheaders(0bytes)receivedfromserver"/home/forum/apache/apache_php/cgi-bin/php-cgi" |
其他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的最大请求数,默认值是16 server.max-keep-alive-requests=100 #keep-alive的最长等待时间,单位是秒,默认值是5 server.max-keep-alive-idle=1200 #lighttpd的work子进程数,默认值是0,单进程运行 server.max-worker=2 #限制用户在发送请求的过程中,最大的中间停顿时间(单位是秒), #如果用户在发送请求的过程中(没发完请求),中间停顿的时间太长,lighttpd会主动断开连接 #默认值是60(秒) server.max-read-idle=1200 #限制用户在接收应答的过程中,最大的中间停顿时间(单位是秒), #如果用户在接收应答的过程中(没接完),中间停顿的时间太长,lighttpd会主动断开连接 #默认值是360(秒) server.max-write-idle=12000 #读客户端请求的超时限制,单位是秒,配为0表示不作限制 #设置小于max-read-idle时,read-timeout生效 server.read-timeout=0 #写应答页面给客户端的超时限制,单位是秒,配为0表示不作限制 #设置小于max-write-idle时,write-timeout生效 server.write-timeout=0 #请求的处理时间上限,如果用了mod_proxy_core,那就是和后端的交互时间限制,单位是秒 server.max-connection-idle=1200 |
--------------------------------------------------
说明:
对于一个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
代码如下 | 复制代码 |
http{ #Fastcgi:(针对后端的fastcgi生效,fastcgi不属于proxy模式) fastcgi_connect_timeout5;#连接超时 fastcgi_send_timeout10; #写超时 fastcgi_read_timeout10;#读取超时 #Proxy:(针对proxy/upstreams的生效) proxy_connect_timeout15s;#连接超时 proxy_read_timeout24s;#读超时 proxy_send_timeout10s; #写超时 } |
说明:
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
代码如下 | 复制代码 |
<?xmlversion="1.0"?> <configuration> //... Setsthelimitonthenumberofsimultaneousrequeststhatwillbeserved. EquivalenttoApacheMaxClientsdirective. EquivalenttoPHP_FCGI_CHILDRENenvironmentinoriginalphp.fcgi Usedwithanypm_style. |
#php-cgi的进程数量
代码如下 | 复制代码 |
<valuename="max_children">128</value> Thetimeout(inseconds)forservingasinglerequestafterwhichtheworkerprocesswillbeterminated Shouldbeusedwhen'max_execution_time'inioptiondoesnotstopscriptexecutionforsomereason '0s'means'off' |
#php-fpm 请求执行超时时间,0s为永不超时,否则设置一个 Ns 为超时的秒数
代码如下 | 复制代码 |
<valuename="request_terminate_timeout">0s</value> Thetimeout(inseconds)forservingofsinglerequestafterwhichaphpbacktracewillbedumpedtoslow.logfile '0s'means'off' <valuename="request_slowlog_timeout">0s</value> </configuration> |
说明:
在php.ini中,有一个参数max_execution_time可以设置PHP脚本的最大执行时间,但是,在php-cgi(php-fpm)中,该参数不会起效。真正能够控制PHP脚本最大执行时:
代码如下 | 复制代码 |
<valuename="request_terminate_timeout">0s</value> |
就是说如果是使用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允许执行的最长毫秒数。(在cURL7.16.2中被加入。从PHP5.2.3起可使用。)
CURLOPT_CONNECTTIMEOUT在发起连接前等待的时间,如果设置为0,则无限等待。
CURLOPT_CONNECTTIMEOUT_MS尝试连接等待的时间,以毫秒为单位。如果设置为0,则无限等待。在cURL7.16.2中被加入。从PHP5.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一个毫秒级超时的例子:
代码如下 | 复制代码 |
<?php if(!isset($_GET['foo'])){ //Client $ch=curl_init('http://example.com/'); curl_setopt($ch,CURLOPT_RETURNTRANSFER,true); curl_setopt($ch,CURLOPT_NOSIGNAL,1);//注意,毫秒超时一定要设置这个 curl_setopt($ch,CURLOPT_TIMEOUT_MS,200);//超时毫秒,cURL7.16.2中被加入。从PHP5.2.3起可使用 $data=curl_exec($ch); $curl_errno=curl_errno($ch); $curl_error=curl_error($ch); curl_close($ch); if($curl_errno>0){ echo"cURLError($curl_errno):$curl_errorn"; }else{ echo"Datareceived:$datan"; } }else{ //Server sleep(10); echo"Done."; } ?> |
其他一些技巧:
1. 按照经验总结是:cURL版本>=libcurl/7.21.0版本,毫秒级超时是一定生效的,切记。
2. curl_multi的毫秒级超时也有问题。。单次访问是支持ms级超时的,curl_multi并行调多个会不准
[流处理方式访问HTTP]
除了curl,我们还经常自己使用fsockopen、或者是file操作函数来进行HTTP协议的处理,所以,我们对这块的超时处理也是必须的。
一般连接超时可以直接设置,但是流读取超时需要单独处理。
自己写代码处理:
代码如下 | 复制代码 |
$tmCurrent=gettimeofday(); $intUSGone=($tmCurrent['sec']-$tmStart['sec'])*1000000 +($tmCurrent['usec']-$tmStart['usec']); if($intUSGone>$this->_intReadTimeoutUS){ returnfalse; } |
或者使用内置流处理函数stream_set_timeout()和stream_get_meta_data()处理:
代码如下 | 复制代码 |
<?php //Timeoutinseconds $timeout=5; $fp=fsockopen("example.com",80,$errno,$errstr,$timeout); if($fp){ fwrite($fp,"GET/HTTP/1.0rn"); fwrite($fp,"Host:example.comrn"); fwrite($fp,"Connection:Closernrn"); stream_set_blocking($fp,true);//重要,设置为非阻塞模式 stream_set_timeout($fp,$timeout);//设置超时 $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"ConnectionTimedOut!"; }else{ echo$data; } } file_get_contents超时: <?php $timeout=array( 'http'=>array( 'timeout'=>5//设置一个超时时间,单位为秒 ) ); $ctx=stream_context_create($timeout); $text=file_get_contents("http://example.com/",0,$ctx); ?> |
fopen超时:
代码如下 | 复制代码 |
<?php $timeout=array( 'http'=>array( 'timeout'=>5//设置一个超时时间,单位为秒 ) ); $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客户端都没有设置超时的选项,mysqli和mysql都没有,但是libmysql是提供超时选项的,只是我们在php中隐藏了而已。
那么如何在PHP中使用这个操作捏,就需要我们自己定义一些MySQL操作常量,主要涉及的常量有:
代码如下 | 复制代码 |
MYSQL_OPT_READ_TIMEOUT=11; MYSQL_OPT_WRITE_TIMEOUT=12; |
这两个,定义以后,可以使用options设置相应的值。
不过有个注意点,mysql内部实现:
1.超时设置单位为秒,最少配置1秒
2.但mysql底层的read会重试两次,所以实际会是3秒
重试两次+ 自身一次=3倍超时时间,那么就是说最少超时时间是3秒,不会低于这个值,对于大部分应用来说可以接受,但是对于小部分应用需要优化。
查看一个设置访问mysql超时的php实例:
代码如下 | 复制代码 |
<?php //自己定义读写超时常量 if(!defined('MYSQL_OPT_READ_TIMEOUT')){ define('MYSQL_OPT_READ_TIMEOUT',11); } if(!defined('MYSQL_OPT_WRITE_TIMEOUT')){ define('MYSQL_OPT_WRITE_TIMEOUT',12); } //设置超时 $mysqli=mysqli_init(); $mysqli->options(MYSQL_OPT_READ_TIMEOUT,3); $mysqli->options(MYSQL_OPT_WRITE_TIMEOUT,1); //连接数据库 $mysqli->real_connect("localhost","root","root","test"); if(mysqli_connect_errno()){ printf("Connectfailed:%s/n",mysqli_connect_error()); exit(); } //执行查询sleep1秒不超时 printf("Hostinformation:%s/n",$mysqli->host_info); if(!($res=$mysqli->query('selectsleep(1)'))){ echo"query1error:".$mysqli->error."/n"; }else{ echo"Query1:querysuccess/n"; } //执行查询sleep9秒会超时 if(!($res=$mysqli->query('selectsleep(9)'))){ echo"query2error:".$mysqli->error."/n"; }else{ echo"Query2:querysuccess/n"; } $mysqli->close(); echo"closemysqlconnection/n"; ?> |
延伸阅读:
http://blog.csdn.net/heiyeshuwu/article/details/5869813
【Memcached】
[PHP扩展]
php_memcache客户端:
连接超时:boolMemcache::connect(string$host[,int$port[,int$timeout]])
在get和set的时候,都没有明确的超时设置参数。
libmemcached客户端:在php接口没有明显的超时参数。
说明:所以说,在PHP中访问Memcached是存在很多问题的,需要自己hack部分操作,或者是参考网上补丁。
[C&C++访问Memcached]
客户端:libmemcached客户端
说明:memcache超时配置可以配置小点,比如5,10个毫秒已经够用了,超过这个时间还不如从数据库查询。
下面是一个连接和读取set数据的超时的C++示例:
/
代码如下 | 复制代码 |
/创建连接超时(连接到Memcached) //参数MEMCACHED_BEHAVIOR_NO_BLOCK为1使超时配置生效,不设置超时会不生效,关键时候会悲剧的,容易引起雪崩 |
//Memcache读取数据超时(没有设置)
libmemcahed源码中接口定义:
代码如下 | 复制代码 |
LIBMEMCACHED_APIchar*memcached_get(memcached_st*ptr,constchar*key,size_tkey_length,size_t*value_length,uint32_t*flags,memcached_return_t*error); LIBMEMCACHED_APImemcached_return_tmemcached_mget(memcached_st*ptr,constchar*const*keys,constsize_t*key_length,size_tnumber_of_keys); |
从接口中可以看出在读取数据的时候,是没有超时设置的。
延伸阅读:
http://hi.baidu.com/chinauser/item/b30af90b23335dde73e67608
http://libmemcached.org/libMemcached.html
【如何实现超时】
程序中需要有超时这种功能,比如你单独访问一个后端Socket模块,Socket模块不属于我们上面描述的任何一种的时候,它的协议也是私有的,那么这个时候可能需要自己去实现一些超时处理策略,这个时候就需要一些处理代码了。
[PHP中超时实现]
一、初级:最简单的超时实现 (秒级超时)
思路很简单:链接一个后端,然后设置为非阻塞模式,如果没有连接上就一直循环,判断当前时间和超时时间之间的差异。
phpsocket中实现原始的超时:(每次循环都当前时间去减,性能会很差,cpu占用会较高)
代码如下 | 复制代码 |
<? $host="127.0.0.1"; $port="80"; $timeout=15;//timeoutinseconds $socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP) ordie("Unabletocreatesocketn"); socket_set_nonblock($socket) //务必设置为阻塞模式 ordie("Unabletosetnonblockonsocketn"); $time=time(); //循环的时候每次都减去相应值 while(!@socket_connect($socket,$host,$port))//如果没有连接上就一直死循环 { $err=socket_last_error($socket); if($err==115||$err==114) { if((time()-$time)>=$timeout)//每次都需要去判断一下是否超时了 { socket_close($socket); die("Connectiontimedout.n"); } sleep(1); continue; } die(socket_strerror($err)."n"); } socket_set_block($this->socket)//还原阻塞模式 ordie("Unabletosetblockonsocketn"); ?> |
二、升级:使用PHP自带异步IO去实现(毫秒级超时)
说明:
异步IO:异步IO的概念和同步IO相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。异步IO将比特分成小组进行传送,小组可以是8位的1个字符或更长。发送方可以在任何时刻发送这些比特组,而接收方从不知道它们会在什么时候到达。
多路复用:复用模型是对多个IO操作进行检测,返回可操作集合,这样就可以对其进行操作了。这样就避免了阻塞IO不能随时处理各个IO和非阻塞占用系统资源的确定。
使用socket_select()实现超时
代码如下 | 复制代码 |
socket_select(...,floor($timeout),ceil($timeout*1000000)); |
select的特点:能够设置到微秒级别的超时!
使用socket_select()的超时代码(需要了解一些异步IO编程的知识去理解)
编程 调用类 编程#
代码如下 | 复制代码 |
<?php $server=newServer; $client=newClient; for(;;){ foreach($select->can_read(0)as$socket){ if($socket==$client->socket){ //NewClientSocket $select->add(socket_accept($client->socket)); } else{ //there'ssomethingtoreadon$socket } } } ?> |
编程 异步多路复用IO & 超时连接处理类 编程
代码如下 | 复制代码 |
<?php classselect{ var$sockets; functionselect($sockets){ $this->sockets=array(); foreach($socketsas$socket){ $this->add($socket); } } functionadd($add_socket){ array_push($this->sockets,$add_socket); } functionremove($remove_socket){ $sockets=array(); foreach($this->socketsas$socket){ if($remove_socket!=$socket) $sockets[]=$socket; } $this->sockets=$sockets; } functioncan_read($timeout){ $read=$this->sockets; socket_select($read,$write=NULL,$except=NULL,$timeout); return$read; } functioncan_write($timeout){ $write=$this->sockets; socket_select($read=NULL,$write,$except=NULL,$timeout); return$write; } } ?> |
[C&C++中超时实现]
一般在LinuxC/C++中,可以使用:alarm()设置定时器的方式实现秒级超时,或者:select()、poll()、epoll()之类的异步复用IO实现毫秒级超时。也可以使用二次封装的异步io库(libevent,libev)也能实现。
一、使用alarm中用信号实现超时 (秒级超时)
说明:Linux内核connect超时通常为75秒,我们可以设置更小的时间如10秒来提前从connect中返回。这里用使用信号处理机制,调用alarm,超时后产生SIGALRM信号(也可使用select实现)
用alarym秒级实现 connect设置超时代码示例:
代码如下 | 复制代码 |
//信号处理函数 staticvoidconnect_alarm(intsigno) { debug_printf("SignalHandler"); return; } //alarm超时连接实现 staticvoidconn_alarm() { Sigfunc*sigfunc;//现有信号处理函数 sigfunc=signal(SIGALRM,connect_alarm);//建立信号处理函数connect_alarm,(如果有)保存现有的信号处理函数 inttimeout=5; //设置闹钟 if(alarm(timeout)!=0){ //...闹钟已经设置处理 } //进行连接操作 if(connect(m_Socket,(structsockaddr*)&addr,sizeof(addr))<0){ if(errno==EINTR){//如果错误号设置为EINTR,说明超时中断了 debug_printf("Timeout"); |
以前我们讲过php单态设计模式之单例模式的理解及单例模式(Singleton)的常见应用场景,现在我们在原来的基础上总结一下。
相关文章:析php单态设计模式之单例模式的理解
相关文章:php设计模式——单例模式(Singleton)的常见应用场景<
单例模式,就是保持一个对象只存在一个实例。并且为该唯一实例提供一个全局访问点(一般是一个静态的getInstance方法)。单例模式应用场景非常广泛,例如:
数据库操作对象
日志写入对象
全局配置解析对象等
这些场景的共同特征是从业务逻辑上来看运行期间改对象却是只需要一个实例、不断new多个实例会增加不必要的资源消耗、全局调用便利。下面分别说明这三个方面:
1. 业务上只需要一个实例
以数据库连接对象为例,加入有一个购物网站,有一个MySQL数据库 127.0.0.1:3306, 那么在一个进程中无论我们需要进行多少次针对改数据库的操作,都只需要连接数据库一次,使用相同的数据库连接句柄(MySQL Connection Resource),从业务需求上来看就只需要一个实例。
相反,同样以购物网站为例,存在许多商品,这些商品都不一样(id,name,price..),这个时候需要显示一个商品列表,加入我们建立一个 Product 作为数据映射对象,那么从业务需求上来说,一个实例就无法满足业务需求,因为每个商品都不一样。
2. 不断new操作增加不必要的资源消耗
我们一般会在类的构造方法(new操作肯定会调用)中进行一些业务操作,例如数据库连接对象可能会在构造方法中尝试读取数据库配置并进行数据库连接(如mysqli::__construct())、日志写入对象会判断日志写入目录是否存在并写入(不存在可能尝试创建改目录)、全局配置解析对象可能需要定位配置文件的保存目录并进行文件扫描等。
这些业务都会消耗相当的资源,如果在一个进程中我们值需要做一次,将会非常有利于我们提高应用的运行效率。
3. 全局调用便利
因为单例模式的一大特点就是通过静态方法获取对象实例,那么就意味着访问对象的方法时不需要先new一个对象的实例,如果改对象需要很多地方使用,则提高了调用的便利性。
通过一个日志操作类来举例:
代码如下 | 复制代码 |
class Logger{ //首先,需要一个私有的静态变量来存储产生的对象实例 private static $instance; //业务变量,保存日志写入路径 private $logDir; //构造方法,注意必须也是私有的,不允许被外部实例化(即在外部被new) private function __construct(){ //调试输出,测试对象被new的次数 echo "new Logger instance rn"; $logDir = sys_get_temp_dir(). DIRECTORY_SEPARATOR . "logs"; if(!is_dir($logDir) || !file_exists($logDir)){ @mkdir($logDir); } $this->logDir = $logDir; } //类唯一实例的全局访问点,用于判断并返回对象实例,供外部调用 public static function getInstance(){ if(is_null(self::$instance)){ $class = __CLASS__; //获取本对象的类型,也可以用new self()方式 self::$instance = new $class(); } return self::$instance; } //重载__clone()方法,不允许对象对克隆 public function __clone(){ throw new Exception("Singleton Class Can Not Be Cloned"); } //具体的业务方法,实际可以有很多方法 public function logError($message){ $logFile = $this->logDir . DIRECTORY_SEPARATOR . "error.log"; error_log($message, 3, $logFile); } } //日志调用 $logger = Logger::getInstance(); $logger->logError("An error occured"); $logger->logError("Another error occured"); //或者这样调用 Logger::getInstance()->logError("Still have error"); Logger::getInstance()->logError("I should fix it"); |
在单例模式中可能遇到一种比较特殊的情况,比如数据库连接对象,对于大型应用来说,很可能需要连接多台数据库,那么不同的数据库公用一个对象可能会产生问题,比如连接的分配、获取insert_id,last_error等。这个问题也比较好解决,就是把我们的$instance变量变成一个关联数组,通过给getInstance方法传入不同的参数获取不同的"单例对象"(引号的含义是:严格来说类可能被new多次,但是这个new也是在我们的控制之内的,而不是在类外部):
代码如下 | 复制代码 |
class MysqlServer{ //注意,变成复数了哦^_^ 当然只是为了标识而已 private static $instances = array(); //业务变量,保持当前实例的mysqli对象 private $conn; //显著特征:私有的构造方法,避免在类外部被实例化 private function __construct($host, $username, $password, $dbname, $port){ $this->conn = new mysqli($host, $username, $password, $dbname, $port); } //类唯一实例的全局访问点 public static function getInstance($host='localhost', $username='root', $password='123456', $dbname='mydb', $port='3306'){ $key = "{$host}:{$port}:{$username}:{$dbname}"; if (empty(self::$instances[$key])){ //这里也可以用 new self(); 的方式 $class = __CLASS__; self::$instances[$key] = new $class($host, $username, $password, $dbname, $port); } return self::$instances[$key]; } //重载__clone方法,不允许对象实例被克隆 public function __clone(){ throw new Exception("Singleton Class Can Not Be Cloned"); } //查询业务方法,后面省略其它业务方法 public function query($sql){ return $this->conn->query($sql); } //尽早释放资源 public function __destruct(){ $this->conn->close(); } } |
问题1:单例类能否拥有子类,因为单例类的构造方法是私有的,因此无法被继承,如果要继承则需要将构造方法改为protected或public,这就违背了单例模式的本意。因此,如果你想给单例类加子类,那就需要回头想想是否错用了模式,或者结构设计上有问题。
问题2:单例滥用,单例模式相对来说比较好理解和实现,因此一旦认识到单例模式的好处,很可能什么类都想写成单例,因此在使用次模式之前一定要考虑上述3种情况,看是否真的有必要使用。
我们可能会很少去考虑用“最佳做法”去开发一个php程序,也许是没有意识到,也许是不知道何为最佳做法。下面是本站整理国外php大师对php开发时的原则。
1. 在合适的时候使用PHP – Rasmus Lerdorf
没有谁比PHP的创建者Rasmus Lerdorf明白PHP用在什么地方是更合理的,他于1995年发布了PHP这门语言,从那时起,PHP就像燎原之火,烧遍了整个开发阵营,改变了互联网的世界。可是,Rasmus并不是因此而创建PHP的。PHP是为了解决WEB开发者的实际问题而诞生的。
和许多开源项目一样,PHP变得流行,流行的动机并不能用正常的哲学来进行解释,甚至流行得有些孤芳自赏。它完全可以作为一个案例,一个解决各种Web问题的工具需求所引起的案例,因此当PHP刚出现的时候,这种工具需求全部聚焦到PHP的身上。
但是,你不能奢望PHP可以解决所有问题。Lerdorf是第一个承认PHP只是一种工具的人,并且PHP也有很多力所不能及的情况。
根据工作的不同来选择合适的工具。我跑了很多家公司,为了说服他们部署和使用PHP,但是这并不意味着PHP对所有问题都适用。它只是可以一个解决大部分问题的front-end脚步语言。
作为一个web开发者,尝试用PHP解决所有问题是不科学的,同时也会浪费你的时间。当PHP玩不转的时候,不要犹豫,试用一下其他的语言吧。
2. 使用多表存储提高规模伸缩性 – Matt Mullenweg
没有人愿意质疑Matt Mullenweg在PHP方面的权威性,他开发了这个星球上最流行的blog系统,(依靠一个强大的社区力量支持): WordPress. 创建Wordpress以后,Matt和他的团队启动了WordPress.com平台,一个基于WordPress MU的免费blog站点。现在,Wordpress.com已经拥有大约400万用户, 这些用户每天提供超过 140,000篇的日志。
如果有人知道如何让网站的规模伸缩自如,这个人一定是Matt Mullenweg。2006年的时候 Matt对Wordpress的数据结构进行了前瞻性的改进,并且解释了为什么Wordpress MU对每个blog使用独立的MYSQL表格, 而不是把所有的blog数据都塞进一个巨大的表格。
我们测试过这个方法,但是发现如果要扩展它的伸缩性,代价太高。如果用一个整体的数据结构,在大流量面前,你将会面临服务器硬件的问题。在MU里面。用户们都被分布到独立的表格当中,并且可以轻易地组织起来。举个例子,WordPress.com把用户的数据分散存储到4096个数据库中,这些数据库可以分散大规模的数据访问,实现流量和压力分流。
数据表的可迁移性让代码(blog)可以运行得更快,并且让系统具备更强的伸缩性。依靠强大的缓存策略和灵活的数据库运用策略, Matt向人们展示了时下最流行的Facebook和Wordpress.com都可以在PHP下稳定运行,并且处理惊人的访问量。
3. 千万不要相信用户 – Dave Child
Dave Child是Added Bytes (previously ilovejackdaniels.com) 网站的核心人物,这个网站以他出色的《cheat sheets for many programming languages》而闻名。 Dave为很多英国的公司服务,并且已经在编程世界里树立起相当的权威。
Dave为PHP开发者提供了很多深谋远虑的建议,并总结成了《writing secure code in PHP》:千万不要相信你的用户,他们甚至可能会伤害你。
有一条web开发的基本原则,我重复多少遍都觉得不够,那就是:千万不要相信你的用户,同时要假设你网站中的每个数据单元都是从用户那里收集来的恶意代码。很多时候,你必须用JAVAscript在客户端检验表单提交过来的内容, 如果你习惯了如此,那么,这是一个好习惯。如果安全性对你来说很重要,这就是最重要最需要学习的原则。
Dave目前正致力于为它的《Writing Secure PHP》系列书籍整理实例,书的最后他说:
最后,变得偏执一点吧。除非你认为你的站点永远不会受到攻击,否则就正视所有的问题,当问题真正发生的时候,你的情况会变得很糟。你需要把每个用户都看成会带来一场攻防站的黑客,想尽一切办法来保护站点的安全,同时想好相应问题的解决方案。
4. 多使用PHP缓存 – Ben Balbo
Ben Balbo开发了Site Point,一个为developers和designers提供指导的网站。他是墨尔本PHP开发和开源俱乐部的成员, 因此他对PHP有一定的了解,同时对PHP caching有一定的想法和经验。
如果你拥有一个访问量很大,但更新并不频繁的站点(比如blog,基于某种CMS),或许它需要进行一些改造,这些改造不会花费太多的时间,但是对性能有突出的贡献。 如果要为一个复杂/更新频率很快的站点建立缓存机制,过程可能会很曲折,但是好处也是显而易见的。
PHP缓存技术有很多种,Ben为我们推荐了如下一些:
缓存函数的运行结果
设置过期时间
缓存IE下载的文件
模板缓存技术
Cache_Lite
由于PHP作为动态语言的特性,缓存机制对于更新频率并不快的站点来说非常重要。
5. 使用IDE, Templates和Snippets加速PHP开发 – Chad Kieffer
当Chad Kieffer从UI设计和数据库优化的工作中抽身出来的时候,他会在他的博客2 tablespoons上分享很多技术经验。由于Chad多方面的全面发展,他经常可以发现其他程序员不能发现的问题,并形成相关经验,尤其是他开发网站的方法。他参与了网站开发的各个环节,因此他的建议对于提高网站开发的大局观非常有用。
Chad认为使用Eclipse PDT(Eclipse’s PHP development package) 这样的IDE,同时使用一些模板技术和开源项目可以有效地提高PHP的开发速度。
紧凑的计划,长长的to do lists以及deadlines让开发人员非常苦闷。不过有些功能,比如Eclipse Templates,可以有效减少编码的时间和出错的几率。
通常来说,任何项目都可以自动化,自动化程度越高, 你完成项目的时间就越短。花时间来开发使用频率很高的框架和模板,将会节省你以后更多时间。同时,使用像Eclipse and the PDT package这样的IDE,你会发现效率得到明显提高,IDE可以自动闭合,补全分号并且可以在本地debug。
6. 利用好PHP的过滤函数 – Joey Sochacki
或许Joey Sochacki并不像Matt Mullenweg那样有名 ,但他也是一个经验丰富的开发者,并且通过他的博客Devolio分享了很多技术经验
Joey发现在编写php代码的过程中有很多地方需要进行过滤,但却并没有太多的coder关注php的内置过滤函数。
过滤数据是我们经常需要做的事情,但是很多功能丰富的PHP内置过滤函数却不为人知。使用类似filter_* 的PHP内置函数,我们几乎可以处理所有的过滤任务,包括数据类型验证/URL/email和IP地址验证/特殊字符处理等等。
过滤是一件复杂的事情,但是我相信joey的发现会给你很多启发,让你认识到PHP强大的过滤功能。
7. 使用PHP框架 – Josh Sharp
对于是否应该使用Zend, CakePHP, Code Igniter, 或者 其他PHP框架,一直存在着很多争议,但是在web开发者的心中,他们有自己衡量的标准。
Josh Sharp自己创建了一家提供面包和黄油服务的网站,因此他对于使用PHP框架来开发网站有一定的经验。他认为使用一个PHP框架来进行项目开发(use a PHP framework ),可以有效地节省时间,并且减少出错的几率。为什么?因为他觉得PHP实在是太好上手了。
PHP的易于使用有时候也有缺陷,因为并不严格的语法,经常会导致很多错误代码的诞生。但如果使用一个PHP框架,出错的几率就会大大减少。
PHP框架可以让你的代码结构更加规范,并且节省大量时间。
8. 不要使用PHP框架 – Rasmus Lerdorf
与Josh的观点恰恰相反,PHP的鼻祖Rasmus Lerdorf却认为最好不要使用PHP框架,为什么?因为不基于框架的PHP性能更好。Rasmus在Drupalcon 2008的演讲上,用“Hello World”的例子来对比了一些框架PHP和简单PHP之间的性能,结果显示框架PHP的性能要远远落后。
9. 使用批处理 – Jack D. Herrington
Jack Herrington对PHP世界并不陌生, 并且为大名鼎鼎的IBM developerWorks贡献过超过30篇的专搞, 同时出版过《PHP Hacks》的书,因此他是一个真正的专家。
Herrington推荐使用批处理和Cron来代替那些可以运行在后台的程序脚步,Web用户并不愿意在线 等待你的处理过程,所以有些事情更适合放到后台来处理。
诚然,在某些情况下,这有点大材小用了,但是你可以清楚地看到,使用Cron, MySQL, PHP面向对象的方法以及Pear::DB这些便捷的工具来创建一个批处理工具并不是一件复杂的事情。
Jack认为使用cron, PHP和MySQL在后台处理一些任务,比起多进程的业务逻辑要划算得多。
两种方法我都尝试过,我认为Cron非常符合”Keep It Simple, Stupid” (KISS) 的原则,它让后台处理变得简单。与多进程的业务逻辑相比,它没有内存溢出的风险。你可以创建一个简单的批处理脚本,并且在cron中运行,这个脚本会定时检查是否有任务需要处理,处理完之后就会自动退出,因此你不用担心是否有进程卡壳,或者陷入死循环。
10. 及时启用错误报告 – David Cummings
David Cummings有一个专门提供CMS软件服务的公司 ,并且获得过几次奖 ,他有非常丰富的PHP开发经验。David曾经写过《two PHP tips he wished he’d learned in the beginning》,其中一点就是:及时启用错误报告,这会节省大量的时间。
我告诉人们,最重要的事情就是最大程度地开启PHP的错误报告,为什么?因为PHP可能会隐藏很多小问题:
变量没有预定义
在代码片段中引用了不可用的变量
使用了未定义的常量这些因素看起来并不是什么大事,除非你在使用面向对象的方法编写一些类库。通常,关闭错误报告将可能使你付出更大的成本来维护你的代码。
错误报告可以帮你轻易地找到代码的问题所在,如果错误报告的等级够高,细微的错误都能被立即发现,帮助你节省整体debug的时间。
源码分析
memcached本身是一个集中式的内存缓存系统,对于分布式的支持服务端并没有实现,只有通过客户端实现;再者,memcached是基于TCP/UDP进行通信,只要客户端语言支持TCP/UDP即可实现客户端,并且可以根据需要进行功能扩展。memchaced-client-forjava 既是使用java语言实现的客户端,并且实现了自己的功能扩展,下面这张类图描述了其主要类之间的关系。
几个重要类的说明:
MemcachedCacheManager: 管理类,负责缓存服务端,客户端,以及相关资源池的初始化工作,获取客户端等等
MemcachedCache:memcached缓存实体类,实现了所有的缓存API,实际上也会调用MemcachedClient进行操作
MemcachedClient:memcached缓存客户端,一个逻辑概念,负责与服务端实例的实际交互,通过调用sockiopool中的socket
SockIOPool:socket连接资源池,负责与memcached服务端进行交互
ClusterProcessor:集群内数据异步操作工具类
客户端可配置化
MemcachedCacheManager是入口,其start方法读取配置文件memcached.xml,初始化各个组建,包括memcached客户端,socket连接池以及集群节点。
memcached客户端是个逻辑概念,并不是和memcached服务端实例一一对应的,可以认为其是一个逻辑环上的某个节点(后面会讲到hash一致性算法时涉及),该配置文件中,可配置一个或多个客户端,每个客户端可配置一个socketPool连接池,如下:
代码如下 | 复制代码 |
<client name="mclient0" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool0”> <errorHandler>com.alisoft.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler> </client> |
扩容
socketpool连接池配置的才是真正连接的memcached服务实例,当然,你可以连接多个memcached服务实例,多个实例可以分布在一台或者多台物理机器上。这样,随着实际业务数据量的增加,可以对现有缓存容量进行扩容,只需在servers中增加memcached实例即可,或者增加多个socketpool配置项,配置如下:
代码如下 | 复制代码 |
<socketpool name="pool0" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="5000" nagle="false" socketTO="3000" aliveCheck="true"> <servers>192.168.1.66:11211,192.168.1.68:11211</servers> </socketpool> |
初始化过程
上文提及的MemcachedCacheManager,该类功能包括有初始化各种资源池,获取所有客户端,重新加载配置文件以及集群复制等。我们重点分析方法start,该方法首先加载配置文件,然后初始化资源池,即方法initMemCacheClientPool,该方法中定义了三个资源池,即socket连接资源池socketpool,memcachedcache资源池cachepool,以及由客户端组成的集群资源池clusterpool,这些资源池的数据结构都是线程安全的ConcurrentHashMap,保证了并发效率。将配置信息分别实例化后,再分别放入对应的资源池容器中,socket连接放入socketpool中,memcached客户端放入cachepool中,定义的集群节点放入clusterpool中。
注意,在实例化socket连接池资源socketpool时,会调用每个pool的初始化方法pool.initialize(),来映射memcached实例到HASH环上,以及初始化socket连接。
单点问题
memcached的分布式,解决了容量水平扩容的问题,但是当某个节点失效时,还是会丢失一部分数据,单点故障依然存在,分布式只是解决了数据整体失效问题,而在实际项目中,特别是GAP平台适应的企业级项目中,是不允许数据不一致的,所以对每一份保存的数据都需要进行容灾处理,那么对于定义的每个memcached客户端,都至少增加一个新客户端与其组成一个cluster集群,当更新或者查找数据时,会先定位到该集群中某个节点,如果该节点失效,就去另外一个节点进行操作。在实际项目中,通过合理规划配置cluster和client(memcached客户端),可以最大限度的避免单点故障(当所有client都失效时还会丢失数据)。在配置文件中,集群配置如下:
代码如下 | 复制代码 |
<cluster name="cluster1" mode="active"> <memCachedClients>mclient1,mclient2</memCachedClients> </cluster> |
下图展现了扩容和单点故障解决方案:
HASH一致性算法
在memcached支持分布式部署场景下,如何获取一个memcached实例?如何平均分配memcached实例的存储?这些需要一个算法来实现,我们选择的是HASH一致性算法,具体就体现在客户端如何获取一个连接memcached服务端的socket上,也就是如何定位memcached实例的问题?算法要求能够根据每次提供的同一个key获得同一个实例。
HASH闭环的初始化
本质上,hash一致性算法是需要实现一个逻辑环,如图所示,环上所有的节点即为一个memcached实例,如何实现?其实是根据每个memcached实例所在的ip地址,将所有的实例映射到hash数值空间中,构成一个闭合的圆环。
HASH环映射的初始化的代码位于SocketIOPool.populateConsistentBuckets方法中,主要代码如下:
代码如下 | 复制代码 |
private void populateConsistentBuckets() { ……... for (int i = 0; i < servers.length; i++) { int thisWeight = 1; if (this.weights != null && this.weights[i] != null) thisWeight = this.weights[i]; double factor = Math .floor(((double) (40 * this.servers.length * thisWeight)) / (double ) this.totalWeight); for (long j = 0; j < factor; j++) { byte[] d = md5.digest((servers[i] + "-" + j).getBytes()); for (int h = 0; h < 4; h++) { // k 的值使用MD5hash算法计算获得 Long k = ((long) (d[3 + h * 4] & 0xFF) << 24) | ((long) (d[2 + h * 4] & 0xFF) << 16) | ((long) (d[1 + h * 4] & 0xFF) << 8) | ((long) (d[0 + h * 4] & 0xFF)); // 用treemap来存储memcached实例所在的ip地址, // 也就是将每个缓存实例所在的ip地址映射到由k组成的hash环上 consistentBuckets.put(k, servers[i]); if (log.isDebugEnabled()) log.debug("++++ added " + servers[i] + " to server bucket"); } } ……... } } |
获取socket连接
在实际获取memcahced实例所在服务器的soket时,只要使用基于同一个存储对象的key的MD5Hash算法,就可以获得相同的memcached实例所在的ip地址,也就是可以准确定位到hash环上相同的节点,代码位于SocketIOPool.getSock方法中,主要代码如下:
代码如下 | 复制代码 |
public SockIO getSock(String key, Integer hashCode){ …………. // from here on, we are working w/ multiple servers // keep trying different servers until we find one // making sure we only try each server one time Set<String> tryServers = new HashSet<String>(Arrays.asList(servers)); // get initial bucket // 通过key值计算hash值,使用的是基于MD5的算法 long bucket = getBucket(key, hashCode); String server = (this.hashingAlg == CONSISTENT_HASH) ? consistentBuckets .get(bucket) : buckets.g et((int) bucket); …………... } private long getBucket(String key, Integer hashCode) { / / 通过key值计算hash值,使用的是基于MD5的算法 long hc = getHash(key, hashCode); if (this.hashingAlg == CONSISTENT_HASH) { return findPointFor(hc); } else { long bucket = hc % buckets.size(); if (bucket < 0) bucket *= -1; return bucket; } } /** * Gets the first available key equal or above the given one, if none found, * returns the first k in the bucket * * @param k * key * @return */ private Long findPointFor(Long hv) { // this works in java 6, but still want to release support for java5 // Long k = this.consistentBuckets.ceilingKey( hv ); // return ( k == null ) ? this.consistentBuckets.firstKey() : k; // 该consistentBuckets中存储的是HASH结构初始化时,存入的所有memcahced实例节点,也就是整个hash环 // tailMap方法是取出大于等于hv的所有节点,并且是递增有序的 SortedMap<Long, String> tmap = this.consistentBuckets.tailMap(hv); // 如果tmap为空,就默认返回hash环上的第一个值,否则就返回最接近hv值的那个节点 return (tmap.isEmpty()) ? this.consistentBuckets.firstKey() : tmap .firstKey(); } /** * Returns a bucket to check for a given key. * * @param key * String key cache is stored under * @return int bucket */ private long getHash(String key, Integer hashCode) { if (hashCode != null) { if (hashingAlg == CONSISTENT_HASH) return hashCode.longValue() & 0xffffffffL; else return hashCode.longValue(); } else { switch (hashingAlg) { case NATIVE_HASH: return (long) key.hashCode(); case OLD_COMPAT_HASH: return origCompatHashingAlg(key); case NEW_COMPAT_HASH: return newCompatHashingAlg(key); case CONSISTENT_HASH: return md5HashingAlg(key); default: // use the native hash as a default hashingAlg = NATIVE_HASH; return (long) key.hashCode(); } } } /** * Internal private hashing method. * * MD5 based hash algorithm for use in the consistent hashing approach. * * @param key * @return */ private static long md5HashingAlg(String key) { / /通过key值计算hash值,使用的是基于MD5的算法 MessageDigest md5 = MD5.get(); md5.reset(); md5.update(key.getBytes()); byte[] bKey = md5.digest(); long res = ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF); return res; } |
通过以上代码的分析,整个memcahced服务端实例HASH环的初始化,以及数据更新和查找使用的算法都是基于同一种算法,这就保证了通过同一个key获得的memcahced实例为同一个。
socket连接池
这部分单独介绍,请猛烈地戳这里。
容灾、故障转移以及性能
衡量系统的稳定性,很大程度上是对各种异常情况的处理,充分考虑异常情况,以及合理处理异常是对系统设计人员的要求,下面看看在故障处理和容灾方面系统都做了那些工作。
定位memcached实例时,当第一次定位失败,会对所有其他的属于同一个socketpool中的memcahced实例进行定位,找到一个可用的,代码如下:
代码如下 | 复制代码 |
// log that we tried // 先删除定位失败的实例 tryServers.remove(server); if (tryServers.isEmpty()) break; // if we failed to get a socket from this server // then we try again by adding an incrementer to the // current key and then rehashing int rehashTries = 0; while (!tryServers.contains(server)) { // 重新计算key值 String newKey = new StringBuilder().append(rehashTries).append(key).toString(); // String.format( "%s%s", rehashTries, key ); if (log.isDebugEnabled()) log.debug("rehashing with: " + newKey); // 去HASH环上定位实例节点 bucket = getBucket(newKey, null); server=(this.hashingAlg == CONSISTENT_HASH) ? consistentBuckets.get(bucket) : buckets.get((int) bucket); rehashTries++; } |
查找数据时,当前节点获取不到,会尝试到所在集群中其他的节点查找,成功后,会将数据复制到原先失效的节点中,代码如下:
代码如下 | 复制代码 |
public Object get(String key) { Object result = null; boolean isError = false; ……....... if (result == null && helper.hasCluster()) if (isError || helper.getClusterMode().equals(MemcachedClientClusterConfig.CLUSTER_MODE_ACTIVE)) { List<MemCachedClient> caches = helper.getClusterCache(); for(MemCachedClient cache : caches) { if (getCacheClient(key).equals(cache)) continue; try{ try { result = cache.get(key); } catch(MemcachedException ex) { Logger.error(new StringBuilder(helper.getCacheName()) .append(" cluster get error"),ex); continue; } //仅仅判断另一台备份机器,不多次判断,防止效率低下 if (helper.getClusterMode().equals(MemcachedClientClusterConfig.CLUSTER_MODE_ACTIVE ) && result != null) { Object[] commands = new Object[]{CacheCommand.RECOVER,key,result}; // 加入队列,异步执行复制数据 addCommandToQueue(commands); } break; } catch(Exception e) { Logger.error(new StringBuilder(helper.getCacheName()) .append(" cluster get error"),e); } } } return result; } |
更新数据时,异步更新到集群内其他节点,示例代码如下:
代码如下 | 复制代码 |
public boolean add(String key, Object value) { boolean result = getCacheClient(key).add(key,value); if (helper.hasCluster()) { Object[] commands = new Object[]{CacheCommand.ADD,key,value}; // 加入队列,异步执行 addCommandToQueue(commands); } return result; } |
删除数据时,需要同步执行,如果异步的话,会产生脏数据,代码如下:
代码如下 | 复制代码 |
public Object remove(String key) { Object result = getCacheClient(key).delete(key); //异步删除由于集群会导致无法被删除,因此需要一次性全部清除 if (helper.hasCluster()) { List<MemCachedClient> caches = helper.getClusterCache(); for(MemCachedClient cache : caches) { if (getCacheClient(key).equals(cache)) continue; try { cache.delete(key); } catch(Exception ex) { Logger.error(new StringBuilder(helper.getCacheName()) .append(" cluster remove error"),ex); } } } return result; } |
异步执行集群内数据同步,因为不可能每次数据都要同步执行到集群内每个节点,这样会降低系统性能;所以在构造MemcachedCache对象时,会建立一个队列,线程安全的linked阻塞队列LinkedBlockingQueue,将所有需要异步执行的命令放入队列中,异步执行,具体异步执行由ClusterProcessor类负责。
代码如下 | 复制代码 |
public MemcachedCache(MemCachedClientHelper helper,int statisticsInterval) { this.helper = helper; dataQueue = new LinkedBlockingQueue<Object[]>(); ……… processor = new ClusterProcessor(dataQueue,helper); processor.setDaemon(true); processor.start(); } |
本地缓存的使用是为了降低连接服务端的IO开销,当有些数据变化频率很低时,完全可以放在应用服务器本地,同时可以设置有效时间,直接获取。DefaultCacheImpl类为本地缓存的实现类,在构造MemcachedCache对象时,即初始化。
每次查找数据时,会先查找本地缓存,如果没有再去查缓存,结束后将数据让如本地缓存中,代码如下:
代码如下 | 复制代码 |
public Object get(String key, int localTTL) { Object result = null; // 本地缓存中查找 result = localCache.get(key); if (result == null) { result = get(key); if (result != null) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, localTTL); // 放入本地缓存 localCache.put(key, result,calendar.getTime()); } } return result; } |
增加缓存数据时,会删除本地缓存中对应的数据,代码如下:
代码如下 | 复制代码 |
public Object put(String key, Object value, Date expiry) { boolean result = getCacheClient(key).set(key,value,expiry); //移除本地缓存的内容 if (result) localCache.remove(key); …….. return value; } |
改造部分
据以上分析,我们通过封装,做到了客户端的可配置化,memcached实例的水平扩展,通过集群解决了单点故障问题,并且保证了应用程序只要每次使用相同的数据对象的key值即可获取相同的memcached实例进行操作。但是,为了使缓存的使用对于应用程序来说完全透明,我们对cluster部分进行了再次封装,即把cluster看做一个node,根据cluster名称属性,进行HASH数值空间计算(同样基于MD5算法),映射到一个HASH环上,如下图,。
这部分逻辑放在初始化资源池clusterpool时进行(即放在MemcahedCacheManager.initMemCacheClientPool方法中),与上文中所描述的memcached实例HASH环映射的逻辑一致,部分代码如下:
代码如下 | 复制代码 |
//populate cluster node to hash consistent Buckets MessageDigest md5 = MD5.get(); // 使用cluster的名称计算HASH数值空间 byte[] d = md5.digest((node.getName()).getBytes()); for (int h = 0; h < 4; h++) { Long k = ((long) (d[3 + h * 4] & 0xFF) << 24) | ((long) (d[2 + h * 4] & 0xFF) << 16) | ((long) (d[1 + h * 4] & 0xFF) << 8) | ((long) (d[0 + h * 4] & 0xFF)); consistentClusterBuckets.put(k, node.getName()); if (log.isDebugEnabled()) log.debug("++++ added " + node.getName() + " to cluster bucket"); } |
在进行缓存操作时,仍然使用数据对象的key值获取到某个cluster节点,然后再使用取余算法(这种算法也是经常用到的分布式定位算法,但是有局限性,即随着节点数的增减,定位越来越不准确),拿到cluster中的某个节点,在进行缓存的操作;定位hash环上cluster节点的逻辑也与上文一样,这里不在赘述。部分定位cluster中节点的取余算法代码如下:
public IMemcachedCache getCacheClient(String key){
………….
String clusterNode = getClusterNode(key);
MemcachedClientCluster mcc = clusterpool.get(clusterNode);
List<IMemcachedCache> memcachedCachesClients = mcc.getCaches();
//根据取余算法获取集群中的某一个缓存节点
if (!memcachedCachesClients.isEmpty())
{
long keyhash = key.hashCode();
int index = (int)keyhash % memcachedCachesClients.size();
if (index < 0 )
index *= -1;
return memcachedCachesClients.get(index);
}
return null;
}
这样,对于应用来说,配置好资源池以后,无需关心那个集群或者客户端节点,直接通过MemcachedCacheManager获取到某个memcachedcache,然后进行缓存操作即可。
最后,使用GAP平台分布式缓存组件,需要提前做好容量规划,集群和客户端事先配置好;另外,缓存组件没有提供数据持久化功能。 Varnish是一款高性能的开源HTTP加速器,使用相对复杂,尤其跟drupal配合使用。现在我们来记录关于Drupal7配合Varnish使用的详细设置教程
准备环境
首先安装一个全新的Drupal,推荐使用drush安装,方便迅速。
先要建一个数据库,记住用户和密码。
###进入mysql
mysql -uroot -p
CREATE DATABASE `mydb` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL ON `mydb`.* TO `username`@localhost IDENTIFIED BY 'password';
####退出mysql
Drush安装Drupal7:
drush dl drupal-7.x; #--select 用来选择一个版本
drush site-install standard --account-name=admin --account-pass=admin --db-url=mysql://YourMySQLUser:RandomPassword@localhost/YourMySQLDatabase
默认会安装最新版本的drupal7,如果要选择一个版本,请加参数:–select。
安装成功后,再下载一个varnish模块。
#进入drupal目录
drush dl varnish
安装Varnish
我们默认一个CentOS为例,因为CentOS仓库中的Varnish版本较低,要导入一个新的repo,然后,在升级一下yum软件库。
具体参考这个链接:https://www.varnish-cache.org/installation/redhat
####Varnish 4.0:
rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.0.el6.rpm
yum install varnish
####Varnish 3.0:
###RHEL 5 or a compatible distribution, use:
rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-3.0.el5.rpm
yum install varnish
###RHEL 6 and compatible distributions, use:
rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-3.0.el6.rpm
yum install varnish
#如果安装的版本不对,请update一下。
#yum update
输入如下命令监测是否安装成功:
$ usr/sbin/varnishd -V
varnishd (varnish-3.0.5 revision 1a89b1f)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2011 Varnish Software AS
设置Varnish的VCL
复制如下代码到/etc/varnish/default.vcl里面:
代码如下 | 复制代码 |
backend default { .host = "127.0.0.1"; .port = "80"; } sub vcl_recv { if (req.restarts == 0) { if (req.http.x-forwarded-for) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } # Do not cache these paths. if (req.url ~ "^/status.php$" || req.url ~ "^/update.php$" || req.url ~ "^/ooyala/ping$" || req.url ~ "^/admin/build/features" || req.url ~ "^/info/.$" || req.url ~ "^/flag/.$" || req.url ~ "^./ajax/.$" || req.url ~ "^./ahah/.$") { return (pass); } # Pipe these paths directly to Apache for streaming. if (req.url ~ "^/admin/content/backup_migrate/export") { return (pipe); } # Allow the backend to serve up stale content if it is responding slowly. set req.grace = 6h; # Use anonymous, cached pages if all backends are down. if (!req.backend.healthy) { unset req.http.Cookie; } # Always cache the following file types for all users. if (req.url ~ "(?i).(png|gif|jpeg|jpg|ico|swf|css|js|html|htm)(?[wd=.-]+)?$") { unset req.http.Cookie; } # Remove all cookies that Drupal doesn't need to know about. ANY remaining # cookie will cause the request to pass-through to Apache. For the most part # we always set the NO_CACHE cookie after any POST request, disabling the # Varnish cache temporarily. The session cookie allows all authenticated users # to pass through as long as they're logged in. if (req.http.Cookie) { set req.http.Cookie = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|NO_CACHE)=", "; 1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); # Remove the "has_js" cookie set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", ""); # Remove the "Drupal.toolbar.collapsed" cookie set req.http.Cookie = regsuball(req.http.Cookie, "Drupal.toolbar.collapsed=[^;]+(; )?", ""); # Remove AdminToolbar cookie for drupal6 set req.http.Cookie = regsuball(req.http.Cookie, "DrupalAdminToolbar=[^;]+(; )?", ""); # Remove any Google Analytics based cookies set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", ""); # Remove the Quant Capital cookies (added by some plugin, all __qca) set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", ""); if (req.http.Cookie == "") { # If there are no remaining cookies, remove the cookie header. If there # aren't any cookie headers, Varnish's default behavior will be to cache # the page. unset req.http.Cookie; set req.http.X-Varnish-NoCookie = "TRUE"; } else { # If there is any cookies left (a session or NO_CACHE cookie), do not # cache the page. Pass it on to Apache directly. set req.http.X-Varnish-NoCookie = "FALSE"; set req.http.X-Varnish-CookieData = req.http.Cookie; return (pass); } } # Handle compression correctly. Different browsers send different # "Accept-Encoding" headers, even though they mostly all support the same # compression mechanisms. By consolidating these compression headers into # a consistent format, we can reduce the size of the cache and get more hits. # @see: http:// varnish.projects.linpro.no/wiki/FAQ/Compression if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "gzip") { # If the browser supports it, we'll use gzip. set req.http.Accept-Encoding = "gzip"; } else if (req.http.Accept-Encoding ~ "deflate") { # Next, try deflate if it is supported. set req.http.Accept-Encoding = "deflate"; } else { # Unknown algorithm. Remove it and send unencoded. unset req.http.Accept-Encoding; } } if (req.request != "GET" && req.request != "HEAD" && req.request != "PUT" && req.request != "POST" && req.request != "TRACE" && req.request != "OPTIONS" && req.request != "DELETE") { /* Non-RFC2616 or CONNECT which is weird. */ return (pipe); } if (req.request != "GET" && req.request != "HEAD") { /* We only deal with GET and HEAD by default */ return (pass); } ## Unset Authorization header if it has the correct details... #if (req.http.Authorization == "Basic <hash>") { # unset req.http.Authorization; #} if (req.http.Authorization || req.http.Cookie) { /* Not cacheable by default */ return (pass); } return (lookup); } sub vcl_pipe { # Note that only the first request to the backend will have # X-Forwarded-For set. If you use X-Forwarded-For and want to # have it set for all requests, make sure to have: set bereq.http.connection = "close"; # here. It is not set by default as it might break some broken web # applications, like IIS with NTLM authentication. } # Routine used to determine the cache key if storing/retrieving a cached page. sub vcl_hash { if (req.http.X-Forwarded-Proto == "https") { hash_data(req.http.X-Forwarded-Proto); } } sub vcl_hit { if (req.request == "PURGE") { purge; error 200 "Purged."; } } sub vcl_miss { if (req.request == "PURGE") { purge; error 200 "Purged."; } } # Code determining what to do when serving items from the Apache servers. sub vcl_fetch { set beresp.http.X-Varnish-CookieData = beresp.http.set-cookie; # Don't allow static files to set cookies. if (req.url ~ "(?i).(png|gif|jpeg|jpg|ico|swf|css|js)(?[a-z0-9]+)?$") { # beresp == Back-end response from the web server. unset beresp.http.set-cookie; } else if (beresp.http.Cache-Control) { unset beresp.http.Expires; } if (beresp.status == 301) { set beresp.ttl = 1h; return(deliver); } ## Doesn't seem to work as expected #if (beresp.status == 500) { # set beresp.saintmode = 10s; # return(restart); #} # Allow items to be stale if needed. set beresp.grace = 1h; } # Set a header to track a cache HIT/MISS. sub vcl_deliver { if (obj.hits > 0) { set resp.http.X-Varnish-Cache = "HIT"; } else { set resp.http.X-Varnish-Cache = "MISS"; } } # In the event of an error, show friendlier messages. sub vcl_error { set obj.http.Content-Type = "text/html; charset=utf-8"; set obj.http.Retry-After = "5"; synthetic {" <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>"} + obj.status + " " + obj.response + {"</title> </head> <body> <h1>Error "} + obj.status + " " + obj.response + {"</h1> <p>"} + obj.response + {"</p> <h3>Guru Meditation:</h3> <p>XID: "} + req.xid + {"</p> <hr> <p>Varnish cache server</p> </body> </html> "}; return (deliver); } |
配置完成之后,启动Varnish。
测试效果
打开浏览器,输入druapl7的地址,先看Drupal7是否正常,然后加上端口号。
比如我们的测试地址是:http://drupal7.111cn.net
那么再用浏览器打开varnish的地址,如下:http://drupal7.111cn.net:6081/
测试结果,两边都正常就表示drupal和varnish都工作正常。
让Varnish缓存Drupal页面
用Firebug查看varnish的请求,如果看到http头里面有X-Varnish的标记表示varnish已经起作用,这时候我们要判断varnish是否缓存了页面。
如何判断:X-Varnish后面有一个数字,表示不是缓存,X-Varnish后面有两个数字,表示缓存成功。
到这里,我们会发现所有的varnish都没有缓存命中,那么问题来了。。。(挖掘机不会出现)
如何让varnish缓存起作用:
// Tell Drupal it's behind a proxy.
$conf['reverse_proxy'] = TRUE;
// Tell Drupal what addresses the proxy server(s) use.
$conf['reverse_proxy_addresses'] = array('127.0.0.1');
// Bypass Drupal bootstrap for anonymous users so that Drupal sets max-age < 0.
$conf['page_cache_invoke_hooks'] = FALSE;
// Make sure that page cache is enabled.
$conf['cache'] = 1;
$conf['cache_lifetime'] = 0;
$conf['page_cache_maximum_age'] = 21600;
给Drupal的settings.php添加如下内容,然后刷新浏览器,即可看到X-Varnish的数字变成了两个(多刷几次)。
至此,Varnish已经完全可以缓存Drupal的页面了。如下图所示:
那么,Drupal的Varnish模块是做什么用的?
简单来说,Varnish就是通过Drupal的缓存接口,清除varnish的缓存,比如页面过期。
此外,通过Expire模块,可以精确的控制那些页面,过期时间都可以控制,比较方便。
配置Drupal.org的Varnish模块
启用Varnish模块,阅读一下varnish模块的官方说明: https://www.drupal.org/project/varnish
主要是给Drupal的settings.php添加如下两行:
// Add Varnish as the page cache handler.
$conf['cache_backends'] = array('sites/all/modules/varnish/varnish.cache.inc');
$conf['cache_class_cache_page'] = 'VarnishCache';
然后到Drupal设置页面,路径如下: admin/config/development/varnish
例如:
Varnish Control Terminal: 127.0.0.1:6082
Varnish Control Key: 86b2d660-9768-4a13-ab90-4b0736d6a4d1
点击保存,status就会变为绿色。
(注意:Control Key的值在/etc/varnish/secret 文件里面,复制内容即可)
设置完成,那么清空一下Drupal的缓存,就会发现Varnish里面的缓存值就会刷新,实现了即时清空缓存的目的。
+++++到此完成++++++++(更多问题到drupal大学:http://drupal001.net提问哦!)+++++
哦哦,还有一种情况,就是Varnish安装再另外一台服务器上,因为Varnish控制后台默认监听的是本机,因此,如果要刷新另一台反向代理服务器的缓存,就必须修改配置。可选的方法有两个,
其一,让Varnish的管理后台地址使用内网的IP(比如192.168.1.x),一般这种架构都是内网集群,因此监听内网也是比较合理的。
其二,本机使用autossh,把本机的端口6082映射到内网机器上面。
$ autossh -fN -L 6082:localhost:6082
完成
至此,Drupal7 + Varnish的配置已经完成,Drupal和varnish也完全集成。Varnish的缓存总得来说速度大于其他缓存,所以可以代替Boost缓存。
如果要使用Varnish缓存动态内容,还有更多的内容要做,本文就不再增加大家的阅读量了(^_^)。
+++++++++++++++++++++++
最后,顺便多嘴,说一下Apache的RPAF Module。
Varnish默认使用 X-Forwarded-For作为远程IP的地址信息,但是这个不是一个标准的协议,有时候我们还是用PHP里面的 $_SERVER['REMOTE_ADDR']来获得IP。
Apache有一个模块,RPAF的配置文件 rpaf.conf如下,即可获设置正确的IP地址。(具体安装就不多说)
RPAFenable On
RPAFsethostname On
RPAFproxy_ips 127.0.0.1 192.168. 10.0.0.
RPAFheader X-Forwarded-For
相关文章
- 这篇文章主要介绍了Windows批量搜索并复制/剪切文件的批处理程序实例,需要的朋友可以参考下...2020-06-30
BAT批处理判断服务是否正常运行的方法(批处理命令综合应用)
批处理就是对某对象进行批量的处理,通常被认为是一种简化的脚本语言,它应用于DOS和Windows系统中。这篇文章主要介绍了BAT批处理判断服务是否正常运行(批处理命令综合应用),需要的朋友可以参考下...2020-06-30- file_get_contents的超时处理话说,从PHP5开始,file_get_content已经支持context了(手册上写着:5.0.0 Added the context support. ),也就是说,从5.0开始,file_get_contents其实也可以POST数据。今天说的这篇是讲超时的,确实在...2013-10-04
- 这篇文章主要介绍了C#多线程中的异常处理操作,涉及C#多线程及异常的捕获、处理等相关操作技巧,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了postgresql 中的时间处理小技巧(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-03-29
- 这篇文章主要介绍了Python同时处理多个异常的方法,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-07-29
C#异常处理中try和catch语句及finally语句的用法示例
这篇文章主要介绍了C#异常处理中try和catch语句及finally语句的用法示例,finally语句的使用涉及到了C#的垃圾回收特性,需要的朋友可以参考下...2020-06-25- 这篇文章主要介绍了python如何用moviepy对视频进行简单的处理,帮助大家更好的利用python处理视频,感兴趣的朋友可以了解下...2021-03-11
- 这篇文章主要介绍了JS跨浏览器解析XML应用过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-10-16
- 这篇文章介绍了C#异常处理,有需要的朋友可以参考一下...2020-06-25
vivo X9如何查出后台偷跑流量应用?vivo X9查出后台偷跑流量应用的方法
vivo X9如何查看后台流量偷跑的情况?小编教你轻松查到!还不了解的小伙伴快来看看吧! 1)打开手机自带的【i管家】应用,打开后点击【流量监控】选项。(如下图) 2)接着选...2016-12-31- Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。本文我们来讲解Redis的应用场景实例。 C...2016-11-25
- 这篇文章主要给大家介绍了关于sql server日志处理不当造成的隐患的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用sql server具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-07-11
- Spring MVC是Spring系列框架中使用频率最高的部分。不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分。因此程序员一定要熟练掌握MVC部分。本篇博客简要分析Spring MVC处理一个请求的流程。...2021-02-06
- 这篇文章主要介绍了go语言中的Carbon库时间处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-02-05
- C++ 提供了异常机制,让我们能够捕获运行时错误,本文就详细的介绍了C++异常处理入门,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-08-09
PHP explode()函数的几个应用和implode()函数有什么区别
explode()函数介绍explode() 函数可以把字符串分割为数组。语法:explode(separator,string,limit)。 参数 描述 separator 必需。规定在哪里分割字符串。 string...2015-11-08C#事务处理(Execute Transaction)实例解析
这篇文章主要介绍了C#事务处理(Execute Transaction)实例解析,对于理解和学习事务处理有一定的帮助,需要的朋友可以参考下...2020-06-25- Libevent 是一个用C语言编写的、轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,下文我们就一起来看PHP Libevent扩展安装...2016-11-25
- <? $a="变量的值将被带入"; echo <<< help <pre> php中echo <<< 的应用 虽然echo "...";可以断行,但若其中如出现",则仍需做转义 处理。需写做: echo " ...2016-11-25