Вопрос или проблема
Я пытаюсь запустить 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 минуты в теории).
-
Есть ли способ, которым я могу воспользоваться последовательными скоростями чтения здесь?
-
Изменит ли чтение zip-файлов ситуацию?
Папка с книгами фактически распределена в виде tar-архива большого количества файлов, я сначала распаковал tar-архив, а затем разархивировал каждый zip-файл, чтобы получить папку с книгами. Если есть способ напрямую считывать оригинальные zip-файлы, это было бы быстрее, я готов сделать это.
- Изменит ли копирование файлов в /tmp или /dev/shm ситуацию?
Я пытался скопировать весь каталог в /tmp (на машине с достаточным объемом ОЗУ). Это увеличило скорость чтения, но первоначальное копирование заняло время. Есть ли способ разделить папку с книгами на части и обрабатывать каждую часть в ОЗУ так, чтобы общая операция заняла меньше времени? Я думаю, что это не должно быть быстрее, но я не уверен.
- Изменит ли параллелизация / асинхронный код ситуацию?
Если я правильно понимаю, диск не загружен, пока myscript.sh выполняется на файле, поэтому если я начну читать следующий файл, пока myscript.sh выполняется над предыдущим, это могло бы изменить ситуацию. На практике я попытался использовать GNU parallel вместо цикла, но это не дало эффекта.
Тем не менее, я использую всего лишь 4 ГБ памяти. Я мог бы арендовать машину с большим количеством потоков, но я предполагаю, что платить вдвое за удвоение потоков будет означать лишь удвоение производительности, что для меня не приносит пользы. (Фактическая метрика, которая важна для меня, вероятно, что-то вроде потраченных долларов / выходных ГБ и обеспечение того, чтобы время программирования не превышало 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 должны значительно улучшить скорость чтения ваших файлов.