Ускорение чтения большого количества файлов (случайное чтение)

Вопрос или проблема

Я пытаюсь запустить bash-скрипт на всех xml-файлах в папке. После некоторых усилий я пришел к выводу, что узкое место, вероятно, заключается в чтении файлов, учитывая имена файлов. Мой скрипт, вероятно, работает достаточно быстро, чтобы CPU не был узким местом.

Вот команда, которую я хотел бы запустить

find /home/ec2-user/books -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -print0 | while IFS= read -r -d '' file; do myscript.sh "$file"; done

Вот статистика по набору данных:

$ du -sh ~/books
16G     /home/ec2-user/books
$ find /home/ec2-user/books -type f | wc -l
find: ‘/home/ec2-user/books/dir_113fa74f0fcfabeeeee0abc0ab4f35c0/OEBPS’: Доступ запрещен
696755
$ find /home/ec2-user/books -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' | wc -l
544952

$ mkdir ~/justxml; find /home/ec2-user/books/ -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -exec cp {} justxml \;
$ mkdir ~/justxml; find /home/ec2-user/books/ -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -exec cp {} justxml \;
$ du -sh ~/justxml
981M    justxml
$ ls ~/justxml | wc -l
48243

Вот время, затраченное на поиск и доступ к файлам

$ date; find /home/ec2-user/books -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -print0 | while IFS= read -r -d '' file; do touch "$file"; done; date
Ср Окт  9 08:10:58 UTC 2024
Ср Окт  9 08:32:19 UTC 2024
$ date; find /home/ec2-user/books -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -print0 >~/temp.txt; date
Ср Окт  9 08:34:14 UTC 2024
Ср Окт  9 08:34:16 UTC 2024

Моя фактическая команда занимает более 2 часов (по оценке), поэтому я не запускал её до завершения. Я не хочу просто ждать 2 часа, потому что мне нужно повторить эту задачу много раз с другими данными, подобными этим.

Сам find не занимает времени, но доступ к файлам требует времени. 21 минута на доступ к 545k файлам — это около 432 файлов в секунду.

Я использую объем AWS EBS (SSD), прикрепленный к экземпляру AWS EC2 t2.medium. Если я правильно понимаю результаты fio, вот производительность диска.

  • ~4 MB/s случайное чтение с размером блока 4K в одном потоке
  • ~50 MB/s последовательное чтение с размером блока 16K в одном потоке
  • ~100 MB/s последовательное чтение с размером блока 1M в одном потоке

Могу ли я что-то сделать, чтобы ускорить эту задачу?

Я не эксперт в том, как работают жесткие диски или как оптимизировать чтение файлов, поэтому задаю этот вопрос.

Мой предположение заключается в том, что моя скрипт медленный, потому что он производит случайное чтение, но последовательное чтение было бы гораздо быстрее (при 100 MB/s я мог бы прочитать весь 16 ГБ за менее чем 3 минуты теоретически).

  1. Есть ли способ, которым я могу воспользоваться скоростью последовательного чтения здесь?

  2. Сделает ли чтение zip-файлов какую-то разницу?

Папка книжек фактически распределяется в виде tar-архива с большим количеством файлов, я сначала разархивировал tar-архив, затем разархивировал каждый zip-файл, чтобы получить папку книжек. Если есть способ напрямую читать оригинальные zip-файлы, это было бы быстрее, я готов сделать это.

  1. Сделает ли копирование файлов в /tmp или /dev/shm разницу?

Я попытался скопировать всю директорию в /tmp (на машине с достаточным объемом ОЗУ). Это увеличило скорость чтения, но первоначальное копирование заняло время. Есть ли способ разбить папку книжек на части и обрабатывать каждую часть в ОЗУ так, чтобы общее время операции было меньше? Я предполагаю, что это не должно быть быстрее, но я не уверен.

  1. Сделает ли параллелизация / асинхронный код какую-то разницу?

Если я правильно понимаю, диск бездействует, пока myscript.sh работает с файлом, поэтому, если я начну читать следующий файл, пока myscript.sh работает с предыдущим, это может иметь значение. На практике, однако, я пытался использовать GNU parallel вместо цикла while, но это не дало результата.

Я использую только машину с 4 ГБ. Я мог бы арендовать машину с большим количеством потоков, но я предполагаю, что платить вдвое за удвоение потоков означало бы только удвоение производительности, поэтому это не помогает мне. (Фактический показатель, который меня интересует, вероятно, что-то вроде затраты долларов / выход в GB и с обеспечением времени программиста менее 1 месяца)

  1. Есть ли более быстрый способ сделать это на C, чем на bash?

Я предполагаю, что да. Есть ли какие-либо ресурсы по написанию оптимизированного кода на C для доступа к файлам внутри папки? Я понимаю основы, такие как fseek и fscanf, но не много про оптимизацию.


Дополнение

Бенчмарки диска AWS EBS 50 ГБ SSD, прикрепленного к экземпляру AWS EC2 t2.medium

$ fio --name TEST --eta-newline=5s --filename=fio-tempfile.dat --rw=randread --size=500m --io_size=10g --blocksize=4k --ioengine=libaio 
--fsync=1 --iodepth=1 --direct=1 --numjobs=1 --runtime=60 --group_reporting                                                                                          
TEST: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=1                                                           
fio-3.32                                                                                                                                                             
Запуск 1 процесса                                                                                                                                                   
Работы: 1 (f=1): [r(1)][11.7%][r=4408KiB/s][r=1102 IOPS][eta 00m:53s]                                                                                                  
Работы: 1 (f=1): [r(1)][21.7%][r=4100KiB/s][r=1025 IOPS][eta 00m:47s] 
Работы: 1 (f=1): [r(1)][31.7%][r=3984KiB/s][r=996 IOPS][eta 00m:41s]  
Работы: 1 (f=1): [r(1)][41.7%][r=4104KiB/s][r=1026 IOPS][eta 00m:35s]
Работы: 1 (f=1): [r(1)][51.7%][r=3819KiB/s][r=954 IOPS][eta 00m:29s] 
Работы: 1 (f=1): [r(1)][61.7%][r=2666KiB/s][r=666 IOPS][eta 00m:23s] 
Работы: 1 (f=1): [r(1)][71.7%][r=3923KiB/s][r=980 IOPS][eta 00m:17s]  
Работы: 1 (f=1): [r(1)][81.7%][r=3864KiB/s][r=966 IOPS][eta 00m:11s] 
Работы: 1 (f=1): [r(1)][91.7%][r=3988KiB/s][r=997 IOPS][eta 00m:05s] 
Работы: 1 (f=1): [r(1)][100.0%][r=3819KiB/s][r=954 IOPS][eta 00m:00s]
TEST: (groupid=0, jobs=1): err= 0: pid=3336320: Ср Окт  9 09:18:22 2024
  чтение: IOPS=1006, BW=4025KiB/s (4121kB/s)(236MiB/60001msec)
    slat (usec): min=10, max=1954, avg=26.74, stdev=28.11
    clat (nsec): min=1973, max=74708k, avg=962785.46, stdev=670441.37
     lat (usec): min=266, max=74736, avg=989.52, stdev=671.14
    clat перцентили (usec):
     |  1.00th=[  351],  5.00th=[  490], 10.00th=[  537], 20.00th=[  668],
     | 30.00th=[  766], 40.00th=[  840], 50.00th=[  922], 60.00th=[ 1004],
     | 70.00th=[ 1090], 80.00th=[ 1205], 90.00th=[ 1369], 95.00th=[ 1532],
     | 99.00th=[ 1909], 99.50th=[ 2089], 99.90th=[ 2900], 99.95th=[ 7635],
     | 99.99th=[27395]
   bw (  KiB/s): min= 1424, max= 5536, per=100.00%, avg=4030.04, stdev=438.60, samples=119
   iops        : min=  356, max= 1384, avg=1007.50, stdev=109.65, samples=119
  lat (usec)   : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
  lat (usec)   : 100=0.01%, 250=0.01%, 500=6.19%, 750=22.12%, 1000=31.05%
  lat (msec)   : 2=39.89%, 4=0.64%, 10=0.03%, 20=0.03%, 50=0.01%
  lat (msec)   : 100=0.01%
  cpu          : usr=0.65%, sys=4.06%, ctx=60390, majf=0, minf=11
  Глубина ввода-вывода    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     выдано чтения-записи: всего=60374,0,0,0 коротко=0,0,0,0 сброшено=0,0,0,0
     задержка   : target=0, window=0, percentile=100.00%, depth=1

Статус выполнения группы 0 (все задания):
   ЧТЕНИЕ: bw=4025KiB/s (4121kB/s), 4025KiB/s-4025KiB/s (4121kB/s-4121kB/s), io=236MiB (247MB), run=60001-60001msec

Статистика диска (чтение/запись):
  xvda: ios=60263/99, merge=0/4, ticks=50400/91, in_queue=50490, util=85.39%
$ sudo fio --directory=/ --name fio_test_file --direct=1 --rw=randread --bs=16k --size=1G --numjobs=16 --time_based --runtime=180 --grou
p_reporting --norandommap                                                                                                                                            
fio_test_file: (g=0): rw=randread, bs=(R) 16.0KiB-16.0KiB, (W) 16.0KiB-16.0KiB, (T) 16.0KiB-16.0KiB, ioengine=psync, iodepth=1                                       
...                                                                                                                                                                  
fio-3.32                                                                                                                                                             
Запуск 16 процессов                                                                                                                                                
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)                                                                                                                 
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)
fio_test_file: Укладывание IO файла (1 файл / 1024MiB)
Работы: 16 (f=16): [r(16)][100.0%][r=46.9MiB/s][r=3001 IOPS][eta 00m:00s]
fio_test_file: (groupid=0, jobs=16): err= 0: pid=3336702: Ср Окт  9 09:31:27 2024
  чтение: IOPS=3015, BW=47.1MiB/s (49.4MB/s)(8482MiB/180005msec)
    clat (usec): min=223, max=120278, avg=5301.13, stdev=803.46
     lat (usec): min=223, max=120278, avg=5301.85, stdev=803.46
    clat перцентили (usec):
     |  1.00th=[ 3425],  5.00th=[ 4555], 10.00th=[ 4752], 20.00th=[ 4948],
     | 30.00th=[ 5080], 40.00th=[ 5211], 50.00th=[ 5276], 60.00th=[ 5407],
     | 70.00th=[ 5538], 80.00th=[ 5669], 90.00th=[ 5932], 95.00th=[ 6128],
     | 99.00th=[ 6652], 99.50th=[ 6849], 99.90th=[ 8029], 99.95th=[12387],
     | 99.99th=[27132]
   bw (  KiB/s): min=43192, max=144017, per=100.00%, avg=48297.40, stdev=319.26, samples=5744
   iops        : min= 2698, max= 9000, avg=3017.45, stdev=19.95, samples=5744
  lat (usec)   : 250=0.01%, 500=0.04%, 750=0.20%, 1000=0.13%
  lat (msec)   : 2=0.32%, 4=0.69%, 10=98.55%, 20=0.04%, 50=0.02%
  lat (msec)   : 100=0.01%, 250=0.01%
  cpu          : usr=0.14%, sys=0.64%, ctx=543370, majf=0, minf=222
  Глубина ввода-вывода    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     выдано чтения-записи: всего=542833,0,0,0 коротко=0,0,0,0 сброшено=0,0,0,0
     задержка   : target=0, window=0, percentile=100.00%, depth=1

Статус выполнения группы 0 (все задания):
   ЧТЕНИЕ: bw=47.1MiB/s (49.4MB/s), 47.1MiB/s-47.1MiB/s (49.4MB/s-49.4MB/s), io=8482MiB (8894MB), run=180005-180005msec

Статистика диска (чтение/запись):
  xvda: ios=542583/252, merge=0/7, ticks=2828968/1381, in_queue=2830349, util=85.25%

Ответ или решение

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

1. Использование последовательного чтения

Очевидно, что ваша текущая ситуация связана с большим числом последовательных операций чтения, что делает ваш процесс затратным по времени. Одно из решений состоит в том, чтобы собрать все целевые файлы и объединить их в один архив — например, с помощью tar или zip. Таким образом, вы сможете последовательно читать содержимое, а не случайно читать с диска:

tar -cvzf books_archive.tar.gz -C /home/ec2-user/books . --wildcards '*.html' '*.htm' '*.xml' '*.xhtml' '*.xhtm'

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

2. Чтение из zip-файлов

Чтение из zip-файлов может быть более эффективным, так как часто zip-файлы лучше сжаты для хранения. Вы можете использовать unzip -l или другие средства для работы с zip-файлами. Тем не менее, прямое чтение из zip может не подходить, если ваша скрипт работает с распакованными файлами.

3. Копирование в /tmp или /dev/shm

Перемещение файлов в /tmp или /dev/shm действительно может увеличить скорость чтения, так как это временные каталоги, использующие оперативную память как хранилище. Вы можете скопировать туда только нужные файлы, а затем обработать их:

mkdir /tmp/xml_files
cp ~/justxml/*.xml /tmp/xml_files/

4. Параллелизация и асинхронное выполнение

Попробуйте использовать GNU Parallel более эффективно, чтобы обрабатывать файлы параллельно, сохраняя качество чтения. Вы можете соединить его с find:

find /home/ec2-user/books -type f -regex '.*\.\(html\|htm\|xml\|xhtml\|xhtm\)$' -print0 | \
    parallel -0 myscript.sh {}

5. Оптимизация с использованием C

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

Пример кода на C для чтения файла:

#include <stdio.h>
#include <stdlib.h>

void process_file(const char *filename) {
    FILE *file = fopen(filename, "rb");
    if (!file) {
        perror("Error opening file");
        return;
    }

    // Обработка содержимого файла
    // ...

    fclose(file);
}

int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++) {
        process_file(argv[i]);
    }
    return 0;
}

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

Оцените материал
Добавить комментарий

Капча загружается...