【问题标题】:PHP: Writing a lot of small files the fastest or/and most efficient wayPHP:以最快或/和最有效的方式编写大量小文件
【发布时间】:2017-01-19 02:29:40
【问题描述】:

假设一个活动将有 10,000 到 30,000 个大约 4kb 的文件写入磁盘。

而且,将同时运行几个广告系列。 10 顶。

目前,我使用通常的方式:file_put_contents

它完成了工作,但速度很慢,它的 php 进程一直占用 100% 的 cpu 使用率。

fopen, fwrite, fclose,嗯,结果和file_put_contents差不多。

我尝试了一些异步 io 的东西,例如 php eioswoole

它更快,但一段时间后会产生“打开的文件太多”。

php -r 'echo exec("ulimit -n");' 结果是 800000。

任何帮助将不胜感激!


嗯,这有点尴尬...你们是对的,瓶颈是它如何生成文件内容...

【问题讨论】:

  • 我会尝试动态地将文件放入 gzip 存档而不是磁盘,然后将它们解压缩。老实说,我不知道它是否更快或更有效(或根本没有效率),但这肯定是我会选择的(从不喜欢对硬盘进行太多 IO 操作)。好吧,这只是另一个愚蠢的想法;)
  • 是否可以选择写入 RAM 磁盘/tmpfs?那应该比任何撞击真实磁盘的东西都要快。最大的权衡是 RAM 更昂贵的“实际磁盘空间”(但您需要最大 400mb,所以应该没问题)并且它是不稳定的,这意味着关闭/重新启动意味着数据已经消失。跨度>
  • 硬盘驱动器可能非常繁忙,但没有太多 CPU 活动。在这篇文章中,听起来需要做很多工作才能将没有多少原始数据放入磁盘。所以在任何文件存在之前就已经发生了一些事情,我相信这是可以提高速度的地方

标签: php io


【解决方案1】:

我假设您无法遵循 SomeDude 关于使用数据库的非常好的建议,并且您已经执行了可以执行的硬件调整(例如增加缓存、增加 RAM 以避免交换抖动、购买 SSD 驱动器)。

我会尝试将文件生成卸载到不同的进程。

你可以例如安装 Redis 并将文件内容存储到 keystore 中,速度非常快。然后,一个不同的并行进程可以从密钥库中提取数据,将其删除,然后写入磁盘文件。

这会从 PHP 主进程中移除所有磁盘 I/O,并让您监控积压(有多少密钥对仍未刷新:理想情况下为零)并专注于内容生成的瓶颈。您可能需要一些额外的 RAM。

另一方面,这与写入 RAM 磁盘并没有太大区别。您还可以将数据输出到 RAM 磁盘,它可能会更快:

# As root
mkdir /mnt/ramdisk
mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk
mkdir /mnt/ramdisk/temp 
mkdir /mnt/ramdisk/ready
# Change ownership and permissions as appropriate

在 PHP 中:

$fp = fopen("/mnt/ramdisk/temp/{$file}", "w");
fwrite($fp, $data);
fclose($fp);
rename("/mnt/ramdisk/temp/{$file}", "/mnt/ramdisk/ready/{$file}");

然后有一个不同的进程(crontab?或持续运行的守护进程?)将文件从 RAM 磁盘的“就绪”目录移动到磁盘,然后删除 RAM 就绪文件。

文件系统

创建文件所需的时间取决于目录中文件的数量,各种依赖函数本身都依赖于文件系统。 ext4、ext3、zfs、btrfs 等将表现出不同的行为。具体来说,如果文件数量超过某个数量,您可能会遇到明显的减速。

因此,您可能想尝试在一个目录中创建大量示例文件的时间,看看这个时间如何随着数量的增长而增长。请记住,访问不同目录会降低性能,因此再次不建议直接使用大量子目录。

<?php
    $payload    = str_repeat("Squeamish ossifrage. \n", 253);
    $time       = microtime(true);
    for ($i = 0; $i < 10000; $i++) {
        $fp = fopen("file-{$i}.txt", "w");
        fwrite($fp, $payload);
        fclose($fp);
    }
    $time = microtime(true) - $time;
    for ($i = 0; $i < 10000; $i++) {
        unlink("file-{$i}.txt");
    }
    print "Elapsed time: {$time} s\n";

在我的系统上创建 10000 个文件需要 0.42 秒,但创建 100000 个文件 (10x) 需要 5.9 秒,而不是 4.2 秒。另一方面,在 8 个单独的目录中创建其中八分之一的文件(我发现的最佳折衷方案)需要 6.1 秒,因此不值得。

但是假设创建 300000 个文件需要 25 秒而不是 17.7 秒;将这些文件分成十个目录可能需要 22 秒,并且使目录拆分值得。

并行处理:r 策略

TL;DR 这在我的系统上运行不佳,尽管您的里程可能会有所不同。如果要完成的操作冗长(这里它们不是)并且与主进程的绑定不同,那么将它们分别卸载到不同的线程可能是有利的,前提是您不产生太多很多线程。

您需要安装pcntl functions

$payload    = str_repeat("Squeamish ossifrage. \n", 253);

$time       = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    $pid = pcntl_fork();
    switch ($pid) {
        case 0:
            // Parallel execution.
            $fp = fopen("file-{$i}.txt", "w");
            fwrite($fp, $payload);
            fclose($fp);
            exit();
        case -1:
            echo 'Could not fork Process.';
            exit();
        default:
            break;
    }
}
$time = microtime(true) - $time;
print "Elapsed time: {$time} s\n";

(花哨的名字r strategy取自生物学)。

在这个例子中,如果与每个孩子需要做的相比,产卵时间是灾难性的。因此,整体处理时间猛增。有了更复杂的孩子,事情会变得更好,但你必须小心不要把脚本变成叉子炸弹。

如果可能的话,一种可能性是将要创建的文件划分为例如每个 10% 的块。然后,每个孩子将使用 chdir() 更改其工作目录,并在不同的目录中创建其文件。这将抵消在不同子目录中写入文件的惩罚(每个子目录写入 当前目录),同时受益于写入更少的文件。在这种情况下,在子节点中使用非常轻量级和 I/O 绑定的操作,该策略再次不值得(我得到双倍的执行时间)。

并行处理:K策略

TL;DR 这更复杂但运行良好......在我的系统上。您的里程可能会有所不同。 虽然 r 策略涉及 许多 即发即弃的线程,但 K 策略需要一个经过精心培养的有限(可能是一个)孩子。在这里,我们将所有文件的创建卸载到一个并行线程,并通过套接字与其通信。

$payload    = str_repeat("Squeamish ossifrage. \n", 253);

$sockets = array();
$domain = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' ? AF_INET : AF_UNIX);
if (socket_create_pair($domain, SOCK_STREAM, 0, $sockets) === false) {
   echo "socket_create_pair failed. Reason: ".socket_strerror(socket_last_error());
}
$pid = pcntl_fork();
if ($pid == -1) {
    echo 'Could not fork Process.';
} elseif ($pid) {
    /*parent*/
    socket_close($sockets[0]);
} else {
    /*child*/
    socket_close($sockets[1]);
    for (;;) {
        $cmd = trim(socket_read($sockets[0], 5, PHP_BINARY_READ));
        if (false === $cmd) {
            die("ERROR\n");
        }
        if ('QUIT' === $cmd) {
            socket_write($sockets[0], "OK", 2);
            socket_close($sockets[0]);
            exit(0);
        }
        if ('FILE' === $cmd) {
            $file   = trim(socket_read($sockets[0], 20, PHP_BINARY_READ));
            $len    = trim(socket_read($sockets[0], 8, PHP_BINARY_READ));
            $data   = socket_read($sockets[0], $len, PHP_BINARY_READ);
            $fp     = fopen($file, "w");
            fwrite($fp, $data);
            fclose($fp);
            continue;
        }
        die("UNKNOWN COMMAND: {$cmd}");
    }
}

$time       = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    socket_write($sockets[1], sprintf("FILE %20.20s%08.08s", "file-{$i}.txt", strlen($payload)));
    socket_write($sockets[1], $payload, strlen($payload));
    //$fp = fopen("file-{$i}.txt", "w");
    //fwrite($fp, $payload);
    //fclose($fp);
}
$time = microtime(true) - $time;
print "Elapsed time: {$time} s\n";

socket_write($sockets[1], "QUIT\n", 5);
$ok = socket_read($sockets[1], 2, PHP_BINARY_READ);
socket_close($sockets[1]);

这在很大程度上取决于系统配置。例如,在单处理器、单核、非线程 CPU 上,这太疯狂了 - 您至少会使总运行时间增加一倍,但更有可能会慢三到十倍.

所以这绝对是不是在旧系统上运行某些东西的方法。

在现代多线程 CPU 上并假设主要内容创建循环受 CPU 限制,您可能会遇到相反的情况 - 脚本可能会快十倍。

在我的系统上,上述“分叉”解决方案的运行速度略低于 三倍。我期待更多,但你来了。

当然,性能是否值得增加复杂性和维护,还有待评估。

坏消息

在进行上述实验时,我得出的结论是,在 Linux 中配置合理且性能良好的机器上创建文件快得要命,因此不仅很难获得更多性能,而且如果你'再次遇到缓慢,它很可能与 文件相关。尝试详细说明您是如何创建该内容的。

【讨论】:

  • 感谢您的冗长回答。他们对我来说是新的,我学到了很多东西。你说得对,我发现瓶颈在于内容创建。
【解决方案2】:

主要思想是减少文件。 例如:可以在 100 个文件中添加 1,000 个文件,每个文件包含 10 个文件 - 并使用explode 进行解析,您将获得 5 倍的写入速度和 14 倍的读取+解析速度
使用 file_put_contents 和 fwrite 优化,您不会获得超过 1.x 的速度。此解决方案可用于读/写。其他解决方案可能是mysql或其他db。

在我的电脑上用一个小字符串创建 30k 个文件需要 96.38 秒,在一个文件中附加 30k 次相同的字符串需要 0.075 秒

我可以为您提供一个不寻常的解决方案,当您可以使用较少次数的 file_put_contents 函数时。下面我向您展示一个简单的代码来了解它是如何工作的。

$start = microtime(true);

    $str = "Aaaaaaaaaaaaaaaaaaaaaaaaa";

    if( !file_exists("test/") ) mkdir("test/");

    foreach( range(1,1000) as $i ) {
        file_put_contents("test/".$i.".txt",$str);
    }

    $end = microtime(true); 
    echo "elapsed_file_put_contents_1: ".substr(($end - $start),0,5)." sec\n";

    $start = microtime(true);


    $out = '';
    foreach( range(1,1000) as $i ) {
        $out .= $str;
    }
    file_put_contents("out.txt",$out);

    $end = microtime(true); 
    echo "elapsed_file_put_contents_2: ".substr(($end - $start),0,5)." sec\n";

这是一个包含 1000 个文件和经过时间的完整示例

with 1000 files writing file_put_contens: elapsed: 194.4 sec writing file_put_contens APPNED :elapsed: 37.83 sec ( 5x faster ) ............ reading file_put_contens elapsed: 2.401 sec reading append elapsed: 0.170 sec ( 14x faster )

    $start = microtime(true);

    $allow_argvs = array("gen_all","gen_few","read_all","read_few");

    $arg = isset($argv[1]) ? $argv[1] : die("php ".$argv[0]." gen_all ( ".implode(", ",$allow_argvs).")");

    if( !in_array($arg,$allow_argvs) ) {
        die("php ".$argv[0]." gen_all ( ".implode(", ",$allow_argvs).")");
    }


    if( $arg=='gen_all' ) {

        $dir_campain_all_files = "campain_all_files/";
        if( !file_exists($dir_campain_all_files) ) die("\nFolder ".$dir_campain_all_files." not exist!\n");

        $exists_campaings = false;
        foreach( range(1,10) as $i ) { if( file_exists($dir_campain_all_files.$i) ) { $exists_campaings = true; } }
        if( $exists_campaings ) {
            die("\nDelete manualy all subfolders from ".$dir_campain_all_files." !\n");
        }   
        build_campain_dirs($dir_campain_all_files);

        // foreach in campaigns
        foreach( range(1,10) as $i ) {
            $campain_dir = $dir_campain_all_files.$i."/";
            $nr_of_files = 1000;  
            foreach( range(1,$nr_of_files) as $f ) {
                $file_name = $f.".txt";
                $data_file = generateRandomString(4*1024);
                $dir_file_name = $campain_dir.$file_name;
                file_put_contents($dir_file_name,$data_file);
            }
            echo "campaing #".$i." done! ( ".$nr_of_files." files writen ).\n";
        }   
    }


    if( $arg=='gen_few' ) { 
        $delim_file = "###FILE###";
        $delim_contents = "@@@FILE@@@";

        $dir_campain = "campain_few_files/";
        if( !file_exists($dir_campain) ) die("\nFolder ".$dir_campain_all_files." not exist!\n");   

        $exists_campaings = false;
        foreach( range(1,10) as $i ) { if( file_exists($dir_campain.$i) ) { $exists_campaings = true; } }
        if( $exists_campaings ) {
            die("\nDelete manualy all files from ".$dir_campain." !\n");
        }           

        $amount = 100; // nr_of_files_to_append

        $out = ''; // here will be appended

        build_campain_dirs($dir_campain);

        // foreach in campaigns
        foreach( range(1,10) as $i ) {
            $campain_dir = $dir_campain.$i."/";

            $nr_of_files = 1000; 
            $cnt_few=1;
            foreach( range(1,$nr_of_files) as $f ) {

                $file_name = $f.".txt";
                $data_file = generateRandomString(4*1024);

                $my_file_and_data = $file_name.$delim_file.$data_file;
                $out .= $my_file_and_data.$delim_contents;

                // append in a new file
                if( $f%$amount==0 ) {
                    $dir_file_name = $campain_dir.$cnt_few.".txt";
                    file_put_contents($dir_file_name,$out,FILE_APPEND);
                    $out = '';
                    $cnt_few++;
                }

            }
            // append remaning files 
            if( !empty($out) ) {
                $dir_file_name = $campain_dir.$cnt_few.".txt";
                file_put_contents($dir_file_name,$out,FILE_APPEND);
                $out = '';

            }
            echo "campaing #".$i." done! ( ".$nr_of_files." files writen ).\n";
        }
    }


    if( $arg=='read_all' ) {    
        $dir_campain = "campain_all_files/";

        $exists_campaings = false;
        foreach( range(1,10) as $i ) {
            if( file_exists($dir_campain.$i) ) {
                $exists_campaings = true;
            }
        }

        foreach( range(1,10) as $i ) {
            $campain_dir = $dir_campain.$i."/";
            $files = getFiles($campain_dir); 
            foreach( $files as $file ) {
                $data = file_get_contents($file);
                $substr = substr($data, 100, 5); // read 5 chars after char100       
            }
            echo "campaing #".$i." done! ( ".count($files)." files readed ).\n";

        }   
    }



    if( $arg=='read_few' ) {
        $dir_campain = "campain_few_files/";

        $exists_campaings = false;
        foreach( range(1,10) as $i ) {
            if( file_exists($dir_campain.$i) ) {
                $exists_campaings = true;
            }
        }

        foreach( range(1,10) as $i ) {
            $campain_dir = $dir_campain.$i."/";
            $files = getFiles($campain_dir); 
            foreach( $files as $file ) {
                $data_temp = file_get_contents($file);
                $explode = explode("@@@FILE@@@",$data_temp);
                //@mkdir("test/".$i);
                foreach( $explode as $exp ) {
                    $temp_exp = explode("###FILE###",$exp);
                    if( count($temp_exp)==2 ) {
                        $file_name = $temp_exp[0];
                        $file_data = $temp_exp[1];
                        $substr = substr($file_data, 100, 5); // read 5 chars after char100     
                        //file_put_contents("test/".$i."/".$file_name,$file_data); // test if files are recreated correctly
                    }
                }
                //echo $file." has ".strlen($data_temp)." chars!\n";
            }
            echo "campaing #".$i." done! ( ".count($files)." files readed ).\n";

        }   
    }

    $end = microtime(true); 
    echo "elapsed: ".substr(($end - $start),0,5)." sec\n";


    echo "\n\nALL DONE!\n\n";






    /*************** FUNCTIONS ******************/


    function generateRandomString($length = 10) {
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $randomString = '';
        for ($i = 0; $i < $length; $i++) {
            $randomString .= $characters[rand(0, $charactersLength - 1)];
        }
        return $randomString;
    }

    function build_campain_dirs($dir_campain) {
        foreach( range(1,10) as $i ) {
            $dir = $dir_campain.$i;
            if( !file_exists($dir) ) {
                mkdir($dir);
            }
        }
    }

    function getFiles($dir) {
        $arr = array();
        if ($handle = opendir($dir)) {
            while (false !== ($file = readdir($handle))) {
                if ($file != "." && $file != "..") {
                    $arr[] = $dir.$file;
                }
            }
            closedir($handle);
        }
        return $arr;
    }   

【讨论】:

  • 感谢您的回答!这是一个有趣的解决方案!
【解决方案3】:

阅读了您的描述后,我了解到您正在编写许多文件,每个文件都相当小。 PHP 通常的工作方式(至少在 Apache 服务器中),每个文件系统访问都有开销:为每个文件打开和维护一个文件指针和缓冲区。由于此处没有可查看的代码示例,因此很难看出哪里效率低下。

但是,对超过 300,000 个文件使用 file_put_contents() 似乎比直接使用 fopen() 和 fwrite() 或 fflush(),然后在完成后使用 fclose() 效率稍低。我的意思是,基于 http://php.net/manual/en/function.file-put-contents.php#105421 的 file_put_contents() 的 PHP 文档的 cmets 中的一个人所做的基准测试 接下来,在处理如此小的文件大小时,听起来像是使用数据库而不是平面文件的绝佳机会(我相信您以前已经掌握了)。数据库,无论是 mySQL 还是 PostgreSQL,都针对同时访问许多记录进行了高度优化,并且可以在内部平衡 CPU 工作负载,这是文件系统访问永远无法做到的(记录中的二进制数据也是可能的)。除非您需要直接从服务器硬盘访问真实文件,否则数据库可以通过允许 PHP 在 Web 上将单个记录作为文件数据返回(即使用 header() 函数)来模拟许多文件。同样,我假设这个 PHP 在服务器上作为 Web 界面运行。

总的来说,我正在阅读的内容表明,除了文件系统访问之外,其他地方可能存在效率低下。文件内容是如何生成的?操作系统如何处理文件访问?是否涉及压缩或加密?这些是图像还是文本数据?操作系统是在写入一个硬盘驱动器、软件 RAID 阵列还是其他布局?这些是我能想到的一些问题,只是浏览你的问题。希望我的回答有所帮助。干杯。

【讨论】:

  • 感谢您的回答。我会收集更多信息。但是是的,生成文件内容的过程有点复杂。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-30
  • 2019-08-24
相关资源
最近更新 更多