測試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; } } ?>
結果:


浙公網安備 33010602011771號