PHP:以最快或最有效的方式编写大量小文件

Jes*_*sse 5 php io

想象一下,一个活动将有10,000到30,000个文件,每个文件大约4kb,应写入磁盘.

而且,会有几个广告系列同时运行.10个上衣.

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

它完成了工作,但速度很慢,而且它的php进程一直占用100%的CPU.

fopen, fwrite, fclose好吧,结果类似于file_put_contents.

我尝试了一些异步的东西,比如php eioswoole.

它会更快,但在一段时间后它会产生"太多的打开文件".

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

任何帮助,将不胜感激!


好吧,这有点令人尴尬......你们是正确的,瓶颈是它如何生成文件内容......

LSe*_*rni 10

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

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

您可以安装Redis并将文件内容存储到密钥库中,这非常快.然后,一个不同的并行进程可以从密钥库中提取数据,删除它,并写入磁盘文件.

这将从主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
Run Code Online (Sandbox Code Playgroud)

在PHP中:

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

然后有一个不同的进程(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";
Run Code Online (Sandbox Code Playgroud)

在我的系统上创建10000个文件需要0.42秒,但创建100000个文件(10x)需要5.9秒,而不是4.2.另一方面,在8个单独的目录中创建八分之一的文件(我找到的最好的折衷方案)需要6.1秒,所以这不值得.

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

并行处理:r策略

TL; DR这在我的系统上不能很好地工作,尽管你的里程可能会有所不同.如果要完成的操作很(这里它们不是)并且与主进程有不同的约束,那么将它们分别卸载到不同的线程可能是有利的,前提是您不会产生太多的线程.

您将需要安装pcntl功能.

$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";
Run Code Online (Sandbox Code Playgroud)

(花式名称r策略取自生物学).

在这个例子中,与每个孩子需要做的事情相比,产卵时间是灾难性的.因此,整体处理时间猛增.对于更复杂的孩子来说,事情会变得更好,但是你必须小心不要将剧本变成一个二战炸弹.

如果可能的话,一种可能性是将要创建的文件分成例如每个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]);
Run Code Online (Sandbox Code Playgroud)

这很依赖于系统配置.例如,在单处理器,单核,非线程CPU上,这很疯狂 - 你至少会使总运行时间翻倍,但更有可能是慢三到十倍.

所以这绝不是在旧系统上运行的东西.

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

在我的系统上,上面的"分叉"解决方案运行速度不到三倍.我期待更多,但你有.

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

坏消息

在上面进行实验的时候,我得出的结论是,在Linux上合理配置和高性能的机器上创建文件很快就会如此,所以不仅难以挤出更多的性能,而且如果你遇到的速度很慢,很可能是没有文件相关.尝试详细介绍如何创建该内容.

  • 感谢您的冗长回答。他们对我来说是新的,我学到了很多东西。你是对的,我发现瓶颈在于内容创建。 (2认同)

Som*_*ude 6

阅读完您的描述后,我理解您正在编写许多文件,每个文件都很小.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文档的评论中所做的基准来做的. 接下来,当处理这么小的文件大小时,听起来很有机会使用数据库而不是平面文件(我相信你之前已经有了).无论是mySQL还是PostgreSQL,数据库都经过高度优化,可以同时访问许多记录,并且可以通过文件系统访问永远无法实现的内部平衡CPU工作负载(并且记录中的二进制数据也可以).除非您需要直接从服务器硬盘驱动器访问真实文件,否则数据库可以通过允许PHP将单个记录作为文件数据通过Web返回(即使用header()函数)来模拟许多文件.同样,我假设这个PHP作为服务器上的Web界面运行.

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