测试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;
         }
}
?>      

结果:

 

posted @ 2021-02-05 20:03  喜欢哲学的猴子  阅读(188)  评论(0编辑  收藏  举报