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

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

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

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

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 файла в секунду.

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

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

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

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

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

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

  2. Изменит ли чтение zip-файлов ситуацию?

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

  1. Изменит ли копирование файлов в /tmp или /dev/shm ситуацию?

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

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

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

Тем не менее, я использую всего лишь 4 ГБ памяти. Я мог бы арендовать машину с большим количеством потоков, но я предполагаю, что платить вдвое за удвоение потоков будет означать лишь удвоение производительности, что для меня не приносит пользы. (Фактическая метрика, которая важна для меня, вероятно, что-то вроде потраченных долларов / выходных ГБ и обеспечение того, чтобы время программирования не превышало 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 00м:17с]  
Работы: 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
  IO глубины    : 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%
     выдано rwts: всего=60374,0,0,0 короткое=0,0,0,0 отброшенное=0,0,0,0
     задержка   : целевая=0, окно=0, процентиль=100.00%, глубина=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
  IO глубины    : 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%
     выдано rwts: всего=542833,0,0,0 короткое=0,0,0,0 отброшенное=0,0,0,0
     задержка   : целевая=0, окно=0, процентиль=100.00%, глубина=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. Использование последовательного чтения

Вы правы в том, что случайное чтение может быть узким местом. Для улучшения производительности вы можете использовать инструменты, которые позволят считывать файлы более эффективно. Например, вы можете объединить несколько файлов в один (с помощью cat или tar), чтобы обработка происходила последовательно. Также попробуйте:

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

Это даст возможность вашей скрипту обрабатывать данные за один проход.

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

Если ваши файлы упакованы в zip-архивы, вы можете получать доступ к содержимому zip-архивов непосредственно, не распаковывая их. Bash поддерживает работу с zip-файлами, что может ускорить чтение данных. Например:

unzip -p archive.zip '*.xml' | myscript.sh

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

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

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

Вы можете выполнить такое копирование, а затем применить свой скрипт к файлам в временной директории:

cp -r /home/ec2-user/books/* /tmp/books

4. Параллелизация

Параллельная обработка может помочь улучшить производительность, но потребует тщательного управлению ресурсами, особенно на ограниченных по ОЗУ машинах, таких как ваша. Попробуйте использовать GNU parallel с ограничением по количеству одновременно работающих процессов:

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

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

Да, написание программы на C может дать больше контроля над тем, как чтение файлов обрабатывается. Работа с файлами на низком уровне может помочь ускорить процесс. Вы можете использовать функции стандартной библиотеки C, такие как fopen, fread, и fclose для эффективного чтения данных. Для обучения оптимизации работы с файлами на C рассмотрите следующие ресурсы:

  • Книги по программированию на C (например, "The C Programming Language" Кернигана и Ричи).
  • Примеры работы с файловой системой на GitHub.
  • Документация по системным вызовам в Unix/Linux.

Заключение

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

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

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