惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
H
Heimdal Security Blog
Jina AI
Jina AI
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Hacker News
The Hacker News
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threatpost
S
Securelist
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
博客园 - 叶小钗
J
Java Code Geeks
V
V2EX
博客园 - Franky
Spread Privacy
Spread Privacy
K
Kaspersky official blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
C
Cybersecurity and Infrastructure Security Agency CISA
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
NISL@THU
NISL@THU
罗磊的独立博客
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
V
Visual Studio Blog

博客园 - tooli

没有技术要求,只需5步就能完成数据分析和图表制作,DuckDB可将数据分析门槛铲平。 在超大数据集下 DuckDB 与 MySQL 查询速度对比 ChatGPT有多强?真的能替代程序员?一起来看看! 在windwos创建的脚本文件在linux环境中无法执行的问题 PHP + Redis 实现定时任务触发 简析小黑是如何盗取cookie登录用户账号 一个'&'引起md5签名不一致问题 linux学习:curl与netcat用法整理 swoole+websocket+redis实现一对一聊天 使用COOKIE实现登录 VS 使用SESSION实现登录 巧用PHP中__get()魔术方法 用户表分表原理 linux学习:用户管理 linux学习:网络(防火墙)及系统安全相关命令学习 MySQL语句技巧 实习那些事儿 shell 环境下MySQL的基本操作指令总结 linux学习:归档,备份及进程相关命令用法整理 Docker简介以及使用docker搭建lnmp的过程(多PHP版本)
测试PHP几种方法写入文件的效率与安全性
tooli · 2021-02-05 · via 博客园 - tooli

前置条件:

所有测试生成的都写入一个新文件,如果是同一个文件名,那么每次执行脚本前,需要把该日志文件删掉,确保每次执行时日志文件都是重新创建的。

每次执行都是往日志文件中使用多进程写入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;
         }
}
?>      

结果: