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