PHP: Написание большого количества небольших файлов самый быстрый и/или эффективный способ


Представьте, что в кампании будет от 10 000 до 30 000 файлов размером около 4 Кб каждый, которые должны быть записаны на диск.

И в то же время будет проведено несколько кампаний. 10 вершин.

В настоящее время я иду обычным путем: file_put_contents.

Он выполняет свою работу, но медленно, и его процесс php полностью использует 100%-ную загрузку процессора.

fopen, fwrite, fclose, что ж, результат похож на file_put_contents.

Я пробовал некоторые асинхронные операции ввода-вывода, такие как php eio и swoole.

Это быстрее, но через некоторое время это приведет к "слишком большому количеству открытых файлов".

php -r 'echo exec("ulimit -n");' результат - 800000.

Любая помощь будет признательна!


Ну, это как-то неловко... вы, ребята, правы, узким местом является то, как он генерирует содержимое файла...

 3
Author: Jesse, 2016-09-11

3 answers

Прочитав ваше описание, я понимаю, что вы пишете много файлов, каждый из которых довольно мал. Как обычно работает PHP (по крайней мере, на сервере Apache), для каждого доступа к файловой системе существуют накладные расходы: для каждого файла открывается и поддерживается указатель на файл и буфер. Поскольку здесь нет примеров кода для просмотра, трудно понять, где кроются недостатки.

Однако использование file_put_contents() для 300 000+ файлов представляется несколько менее эффективным, чем использование fopen() и fwrite() или fflush() напрямую, затем fclose(), когда вы закончите. Я говорю это, основываясь на тесте, проведенном коллегой в комментариях к документации PHP для file_put_contents() по адресу http://php.net/manual/en/function.file-put-contents.php#105421 Далее, когда имеешь дело с такими небольшими размерами файлов, кажется, что есть отличная возможность использовать базу данных вместо плоских файлов (я уверен, что у вас это было раньше). База данных, будь то MySQL или PostgreSQL, высоко оптимизирована для одновременный доступ ко многим записям и может внутренне сбалансировать нагрузку на процессор таким образом, чтобы доступ к файловой системе никогда не был возможен (и двоичные данные в записях тоже возможны). Если вам не нужен доступ к реальным файлам непосредственно с жестких дисков вашего сервера, база данных может имитировать множество файлов, позволяя PHP возвращать отдельные записи в виде данных файла через Интернет (т. Е. с помощью функции заголовка()). Опять же, я предполагаю, что этот PHP работает как веб-интерфейс на сервере.

В целом, то, что я читаю предполагает, что может быть неэффективность где-то еще, помимо доступа к файловой системе. Как создается содержимое файла? Как операционная система обрабатывает доступ к файлам? Задействовано ли сжатие или шифрование? Это изображения или текстовые данные? Записывается ли ОС на один жесткий диск, программный RAID-массив или какой-либо другой макет? Вот некоторые из вопросов, которые я могу придумать, просто взглянув на вашу проблему. Надеюсь, мой ответ помог. Ваше здоровье.

 6
Author: SomeDude, 2016-09-16 07:07:27

Я предполагаю, что вы не можете следовать очень хорошим советам SomeDude по использованию баз данных вместо этого, и вы уже выполнили то, что можно было бы выполнить для настройки оборудования (например, увеличение кэша, увеличение оперативной памяти, чтобы избежать перестановки, покупка SSD-накопителей).

Я бы попытался перенести генерацию файлов в другой процесс.

Вы можете, например, установить Redis и сохранить содержимое файла в хранилище ключей, что очень быстро. Затем другой, параллельный процесс мог бы извлечь данные из хранилища ключей, удалите их и запишите в файл на диске.

Это удаляет все операции ввода-вывода с диска из основного процесса PHP и позволяет вам отслеживать отставание (сколько пар ключей все еще не очищено: в идеале ноль) и сосредоточиться на узком месте в создании контента. Возможно, вам понадобится дополнительная оперативная память.

С другой стороны, это не слишком отличается от записи на оперативный диск. Вы также можете выводить данные на оперативный диск, и это, вероятно, будет еще быстрее:

# 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? Или постоянно работающий демон?) переместите файлы из каталога "ready" на диске оперативной памяти на диск, удалив затем готовый файл оперативной памяти.

Файловая система

Время, необходимое для создания файла, зависит от количества файлов в каталоге с различными функциями зависимостей, которые сами зависят от файловой системы. 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 .

$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-стратегия взято из биологии).

В этом примере время нереста составляет катастрофический по сравнению с тем, что должен делать каждый ребенок. Таким образом, общее время обработки резко возрастает. С более сложными детьми дела пошли бы лучше, но вы должны быть осторожны, чтобы не превратить сценарий в бомбу-вилку.

Одним из возможных вариантов, если это возможно, может быть разделение создаваемых файлов, скажем, на куски по 10% каждый. Затем каждый дочерний элемент изменит свой рабочий каталог с помощью chdir() и создаст свои файлы в другом каталоге. Это свело бы на нет наказание за запись файлов в разные подкаталоги (каждый ребенок записывает в свой текущий каталог), при этом выигрывает от записи меньшего количества файлов. В этом случае, с очень легкими и связанными с вводом-выводом операциями в дочернем устройстве, опять же, стратегия не стоит (я получаю удвоенное время выполнения).

Параллельная обработка: стратегия 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]);

ЭТО СИЛЬНО ЗАВИСИТ ОТ КОНФИГУРАЦИИ СИСТЕМЫ. Например, на монопроцессорном, одноядерном, непоточном процессоре это безумие - вы, по крайней мере, удвоите общее время выполнения, но, скорее всего, оно будет идти от трех до десяти раз медленнее.

Итак, это определенно не способ прокачать что-то, работающее в старой системе.

На современном многопоточном процессоре и при условии, что основной цикл создания контента привязан к процессору, вы можете столкнуться с обратным - сценарий может работать в десять раз быстрее.

В моей системе решение "разветвления" выше работает немного меньше, чем в три раза быстрее. Я ожидал большего, но вот ты где.

Конечно, стоит ли производительность дополнительной сложности и обслуживания, остается подлежащий оценке.

Плохие новости

Экспериментируя выше, я пришел к выводу, что создание файлов на разумно настроенной и производительной машине в Linux чертовски быстро, поэтому не только трудно выжать больше производительности, но если вы испытываете замедление, очень вероятно, что это не связано с файлами. Попробуйте подробнее рассказать о том, как вы создаете этот контент.

 5
Author: LSerni, 2016-09-20 07:11:11

Основная идея состоит в том, чтобы иметь меньше файлов. Пример: 1000 файлов могут быть добавлены в 100 файлов, каждый из которых содержит 10 файлов - и проанализированы с помощью explode, и вы получите в 5 раз быстрее при записи и в 14 раз быстрее при чтении+анализе
с оптимизированными функциями file_put_contents и fwrite вы не получите скорость более 1.x. Это решение может быть полезно для чтения/записи. Другим решением может быть mysql или другая база данных.

На моем компьютере для создания 30 тысяч файлов с небольшой строкой требуется 96,38 секунды, а для добавления 30 тысяч раз одна и та же строка в одном файле занимает 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;
    }   
 1
Author: CatalinB, 2016-09-20 00:31:44