用discuzx2.5搭建了一个论坛,清除缓存后经常出现白屏或错误,再次清除缓存后又正常了。
排查了很久发现是模板缓存大小为 零k,或没生成完整。
discuz生成模板缓存的函数部分parse_template。
if(!@$fp = fopen(DISCUZ_ROOT.$cachefile, 'w')) {
$this->error('directory_notfound', dirname(DISCUZ_ROOT.$cachefile));
}
$template = preg_replace("/\"(http)?[\w\.\/:]+\?[^\"]+?&[^\"]+?\"/e", "\$this->transamp('\\0')", $template);
$template = preg_replace("/\<script[^\>]*?src=\"(.+?)\"(.*?)\>\s*\<\/script\>/ies", "\$this->stripscriptamp('\\1', '\\2')", $template);
$template = preg_replace("/[\n\r\t]*\{block\s+([a-zA-Z0-9_\[\]]+)\}(.+?)\{\/block\}/ies", "\$this->stripblock('\\1', '\\2')", $template);
$template = preg_replace("/\<\?(\s{1})/is", "<?php\\1", $template);
$template = preg_replace("/\<\?\=(.+?)\?\>/is", "<?php echo \\1;?>", $template);
flock($fp, 2);
fwrite($fp, $template);
fclose($fp);
生成模板缓存时用了flock锁,高并发时,要排队获取锁。
如同时100个并发,假设没并发每个脚本的执行时间为0.5秒,flock锁没加防堵塞锁flock($fp, LOCK_EX | LOCK_NB),所有获取同一个文件锁时要排队等候,
排第一的执行完成花0.5秒,排第二的要等第一个释放锁后才能获取到,所有执行完成要花1秒,如此类推排100的就要花50秒时间才能完成,从而导致长时间
独占锁,甚至PHP脚本超时。
用flock($fp, LOCK_EX | LOCK_NB),在锁定时不堵塞,就是程序尝试去获取文件锁,没获取到就返回false不堵塞和排队等待。
//start---在锁定时不堵塞,没获得锁返回false。LOCK_NB与线程有关,有时不起作用。 date:2013-05-02
$canWrite = flock($fp, LOCK_EX | LOCK_NB);
if($canWrite){
fwrite($fp, $template);
flock($fp, LOCK_UN | LOCK_NB);
}
fclose($fp);
但测试发现, LOCK_NB有时也要等待,查了一下资料,LOCK_NB与线程有关,用nginx+php-fpm时,LOCK_NB也会堵塞。
http://stackoverflow.com/questions/5524073/lock-nb-ignored
参考了smarty的源码,他们的模板生成的解决办法是生成临时文件,然后rename的方式代替flock的。
每个并发的程序都生成一个临时文件,然后重命名为模板的名字。
/**
* 写临时文件然后rename的方式代替flock().
*
* @param string $cachefile
* @param string $contents
* @return boolean
* @date 2013-05-02
*/
private function tmp_template($cachefile,$contents){
$cachefile = DISCUZ_ROOT.$cachefile;
$_dirname = dirname($cachefile);
// write to tmp file, then rename it to avoid file locking race condition
$_tmp_file = tempnam($_dirname, 'wrt'); //建立一个具有唯一文件名的文件
if (!($fp = @fopen($_tmp_file, 'wb'))) {
$_tmp_file = $_dirname . DIRECTORY_SEPARATOR . uniqid('wrt');
if (!($fp = @fopen($_tmp_file, 'wb'))) {
return false;
}
}
fwrite($fp, $contents);
fclose($fp);
if (DIRECTORY_SEPARATOR == '\\' || !@rename($_tmp_file, $cachefile)) {
// On platforms and filesystems that cannot overwrite with rename()
// delete the file before renaming it -- because windows always suffers
// this, it is short-circuited to avoid the initial rename() attempt
@unlink($cachefile);
@rename($_tmp_file, $cachefile);
}
@chmod($cachefile, 0777);
return true;
}