测试PHP几种方法写入文件的效率与安全性
前置条件:
所有测试生成的都写入一个新文件,如果是同一个文件名,那么每次执行脚本前,需要把该日志文件删掉,确保每次执行时日志文件都是重新创建的。
每次执行都是往日志文件中使用多进程写入90000行日志。每种方式分成四种对照组测试:
30*3000 加锁(即30个进程每个进程写入3000行,总共90000行,写入时需对日志文件上独占锁)。
30*3000 不加锁(即30个进程每个进程写入3000行,总共90000行,写入时日志文件不上锁)。
90*1000 加锁(即90个进程每个进程写入1000行,总共90000行,写入时需对日志文件上独占锁)。
90*1000 不加锁(即90个进程每个进程写入1000行,总共90000行,写入时日志文件不上锁)。
方式一:
使用file_put_contents() 函数写入文件。为了避免内容覆盖,须使用FILE_APPEND模式写入。
加锁:(n=3000 | n=1000)
for($i=0;$i<n,$i++){
$msg = "test text";
file_put_contents($log, $msg, FILE_APPEND|LOCK_EX);
}
不加锁:(n=3000 | n=1000)
for($i=0;$i<n,$i++){
$msg = "test text";
file_put_contents($log, $msg, FILE_APPEND);
}
执行情况如下表:
序号 |
进程数 |
每个进程写入行数 |
是否加锁 |
第一次执行平均耗时(s) |
第二次执行平均耗时(s) |
第三次执行平均耗时(s) |
1-1 |
30 |
3000 |
Y |
2.831 |
2.815 |
2.861 |
1-2 |
30 |
3000 |
N |
2.826 |
2.855 |
2.751 |
1-3 |
90 |
1000 |
Y |
2.407 |
2.396 |
2.278 |
1-4 |
90 |
1000 |
N |
1.779 |
2.052 |
2.01 |
方式二:
加锁:(n=3000 | n=1000)
$handle = fopen($log,’a’);
flock($handle,LOCK_EX);
for($i=0;$i<n,$i++){
$msg = "test text";
fwrite($handle,$msg);
}
flock($handle,LOCK_UN);
fclose($handle);
不加锁:(n=3000 | n=1000)
$handle = fopen($log,’a’);
for($i=0;$i<n,$i++){
$msg = "test text";
fwrite($handle,$msg);
}
fclose($handle);
执行情况如下表:
序号 |
进程数 |
写入行数/每个进程 |
是否加锁 |
第一次执行平均耗时(s) |
第二次执行平均耗时(s) |
第三次执行平均耗时(s) |
2-1 |
30 |
3000 |
Y |
0.66 |
0.659 |
0.658 |
2-2 |
30 |
3000 |
N |
1.272 |
1.17 |
1.161 |
2-3 |
90 |
1000 |
Y |
0.83 |
0.855 |
0.836 |
2-4 |
90 |
1000 |
N |
0.952 |
1.097 |
0.947 |
以方式一跟方式二的表格为参照,同一种方式,上不上锁,性能相差不是很大,从效率上讲,方式二要比方式一高效。
最根本的原因是file_put_contents()函数每次执行相当于执行了 fopen(),fwrite(),fclose()三个函数,所以单次执行耗时会比较长。
如果把方式二做个调整,比如把fopen()和fclose都放进for循环里,那么方式二跟方式一基本没太大差别。比如下面代码:
for($i=0;$i<n,$i++){
$handle = fopen($log,’a’);
//flock($handle,'LOCK_EX');
$msg = "test text";
fwrite($handle,$msg);
//flock($handle,'LOCK_UN');
fclose($handle);
}
当然,如果用这种写法本身就不合理,还不如直接使用file_put_contents()来的简单。
不上锁的情况,日志写进去时无序的,各个进程之间穿插着写入一行日志。
上锁的情况,日志相对有序,基本是一个进程写完n行后释放了独占锁才轮到另一个进程。但是进程之间也是无序的。比如第一个子进程写完,被第5个子进程抢到独占锁,那么就是第5个子进程先写,第二个只能继续等。所以,上锁的情况同一个进程写的日志才是有序的。
<?php set_time_limit(30); $log = '/data/tmp/a.log'; for($i = 0;$i<30;$i++){ pcntl_signal(SIGCHLD, SIG_IGN); $fid = pcntl_fork(); if($fid === 0){ try { $start = microtime(true); $handle = fopen($log,'a'); flock($handle,LOCK_EX); for($j=0;$j<3000;$j++){ $start_time = microtime(true); //TODO 其他业务逻辑 //打点记录并行任务执行状况 $fid = posix_getpid(); $ffid = posix_getppid(); $date = date('YmdHis'); $end_time = microtime(true); $usetime = round($end_time-$start_time,2); $msg = PHP_EOL."序号:{$i}:{$j}; 时间:{$date}; 当前进程ID:{$fid}; 父进程ID:{$ffid}; 任务开始:{$start_time}; 任务结束:{$end_time}; 耗时:{$usetime}"; //file_put_contents($log,$msg,FILE_APPEND|LOCK_EX); fwrite($handle,$msg); } flock($handle,LOCK_UN); fclose($handle); unset($handle); $end = microtime(true); $s = round($end-$start,3);echo "进程:{$i},开始:{$start},结束:{$end},耗时:{$s}".PHP_EOL; }finally{ if(function_exists("posix_kill")){ posix_kill(getmypid(),SIGTERM); }else{ system('kill -9 '.getmypid()); } } } } echo 'over'.PHP_EOL;
我们如果是使用 fopen($file,'a') 这种模式打开文件,或者file_put_contents($file,$log,FILE_APPEND) 打开文件去写入,那么写操作就不从文件描述符的当前位置开始,而是在文件末尾追加写入,每一行的写入都是一个独立的操作,所以基本没有上锁的必要。
系统层面上对每个写入请求之前的位置更新操作应该具有原子性,且对每个写操作也是具有完整性保证的。不会导致两个写操作交叉执行的情况。
那么在上锁的情况下,如果某个子进程在解除文件锁之前就挂掉了,会不会导致文件被锁死而导致其他进程一直等待呢?
这里做个测试:开5个子进程,每个进程写入5行日志,日志编号序号(子进程编号:日志编号)总共25行日志。
如果在第三个子进程上了独占锁,然后写入第三行日志前,让该子进程退出。具体过程如下:
<?php set_time_limit(30); $log = '/data/tmp/a.log'; for($i = 1;$i<=5;$i++){ pcntl_signal(SIGCHLD, SIG_IGN); $fid = pcntl_fork(); if($fid === 0){ try { $start = microtime(true); $handle = fopen($log,'a'); flock($handle,LOCK_EX); for($j=1;$j<=5;$j++){ if($i==3 && $j==3){ break;//第三个子进程在写入第三行日志时退出该子进程 } $start_time = microtime(true); //TODO 其他业务逻辑 //打点记录并行任务执行状况 $fid = posix_getpid(); $ffid = posix_getppid(); $date = date('YmdHis'); $end_time = microtime(true); $usetime = round($end_time-$start_time,2); $msg = "序号:{$i}:{$j}; 时间:{$date}; 当前进程ID:{$fid}; 父进程ID:{$ffid}; 任务开始:{$start_time}; 任务结束:{$end_time}; 耗时:{$usetime}".PHP_EOL; fwrite($handle,$msg); } flock($handle,LOCK_UN); fclose($handle); unset($handle); $end = microtime(true); $s = round($end-$start,3); echo PHP_EOL.$s.','; //echo "进程:{$i},开始:{$start},结束:{$end},耗时:{$s}".PHP_EOL; }finally{ if(function_exists("posix_kill")){ posix_kill(getmypid(),SIGTERM); }else{ system('kill -9 '.getmypid()); } } } } echo 'over'.PHP_EOL;
最终得到得日志总数时22行,因为第3个子进程只写了2行就退出了,执行结果如下图:
由图可见,就算第三个子进程中途退出了,没有释放日志文件的独占锁,但是其他进程仍然正常按照独占的方式写入日志。
原因是当子进程挂掉的时候,该子进程对日志文件的独占锁也会被自动解除。所以就算某个子进程上完独占锁,没来得及解除就退出了,也不用担心会影响到其他进程对该日志文件得使用。
另外,使用 pcntl_fork() 创建进程时需要注意的一些点
pcntl_fork()函数执行的时候,会创建一个子进程。该子进程会复制当前进程,也就是父进程的所有的变量数据,代码,还有状态。也就是说,在一个子进程创建之前,定义的变量,常量,函数等,在子进程内都可以使用。
如果创建成功,并且该子进程在父进程内,则返回0,在子进程内返回自身的进程号,失败则返回-1。
(1)当我们在 for 循环 或者 foreach 的循环里创建子进程,那么在子进程执行的结尾记得将子进程杀死,不然子进程也会进入 for 循环和 foreach 循环,从而形成递归创建子进程的情况。
例如:
$arr = array(1,2....n);
foreach($arr as $k=>$v){
pcntl_signal(SIGCHLD, SIG_IGN);
$fid = pcntl_fork();
}
或者:
for($i=1;$i<=n;$i++){
pcntl_signal(SIGCHLD, SIG_IGN);
$fid = pcntl_fork();
}
这两种情况最终产生的进程数有 2^n (2的n次方) ,这里面包含一个父进程,出去父进程,就有 2^n -1 个子进程。
如果我们只是要 n 个子进程去处理,那么,就需要在每个子进程的最后将该子进程杀死。
例如上面有部分例子的代码中在 try{} finally {} 中将子进程杀死,不让其进入递归。
(2)不论时使用for循环还是foreach循环,都不会按照顺序去执行。
比如第(1)部分的两个例子中,可能最后一个子进程先执行,最终先进入循环递归,结果第n个子进程执行了2n次。
而第一个子进程进程如果最后执行到,就只能执行1次。当然这是在每个子进程执行完没有杀死的情况。比如:
<?php $pid = $fid = posix_getpid(); $arr = array('num1','num2','num3','num4'); foreach($arr as $k=>$v){ pcntl_signal(SIGCHLD, SIG_IGN); $fid = pcntl_fork(); if($fid === 0){ $fid = posix_getpid(); $ffid = posix_getppid(); $msg = "循环次数{$v};主进程ID:{$pid}; 父进程ID:{$ffid}; 当前进程ID:{$fid};".PHP_EOL; echo $msg; } } ?>
结果: