Так как же удалить миллионы файлов из одной папки?

от автора

Феерическая расстановка точек над i в вопросе удаления файлов из переполненной директории.

Прочитал статью Необычное переполнение жесткого диска или как удалить миллионы файлов из одной папки и очень удивился. Неужели в стандартном инструментарии Linux нет простых средств для работы с переполненными директориями и необходимо прибегать к столь низкоуровневым способам, как вызов getdents() напрямую.

Для тех, кто не в курсе проблемы, краткое описание: если вы случайно создали в одной директории огромное количество файлов без иерархии — т.е. от 5 млн файлов, лежащих в одной единственной плоской директории, то быстро удалить их не получится. Кроме того, не все утилиты в linux могут это сделать в принципе — либо будут сильно нагружать процессор/HDD, либо займут очень много памяти.

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

Подготовка

Так как создавать переполненную директорию на своём HDD рабочего компьютера, потом мучиться с её удалением ну никак не хочется, создадим виртуальную ФС в отдельном файле и примонтируем её через loop-устройство. К счастью, в Linux с этим всё просто.

Создаём пустой файл размером 200Гб

#!python f = open("sparse", "w") f.seek(1024 * 1024 * 1024 * 200) f.write("\0") 

Многие советуют использовать для этого утилиту dd, например dd if=/dev/zero of=disk-image bs=1M count=1M, но это работает несравнимо медленнее, а результат, как я понимаю, одинаковый.

Форматируем файл в ext4 и монтируем его как файловую систему

mkfs -t ext4 -q sparse  # TODO: less FS size, but change -I option sudo mount sparse /mnt mkdir /mnt/test_dir 

К сожалению, я узнал об опции -I команды mkfs.ext4 уже после экспериментов. Она позволяет увеличить лимит на количество inode на FS, не увеличивая размер файла образа. Но, с другой стороны, стандартные настройки — ближе к реальным условиям.

Создаем множество пустых файлов (будет работать несколько часов)

#!python for i in xrange(0, 13107300):     f = open("/mnt/test_dir/{0}_{0}_{0}_{0}".format(i), "w")     f.close()     if i % 10000 == 0:         print i 

Кстати, если в начале файлы создавались достаточно быстро, то последующие добавлялись всё медленнее и медленнее, появлялись рандомные паузы, росло использование памяти ядром. Так что хранение большого числа файлов в плоской директории само по себе плохая идея.

Проверяем, что все айноды на ФС исчерпаны.

$ df -i /dev/loop0      13107200      13107200     38517           100% /mnt

Размер файла директории ~360Мб

$ ls -lh /mnt/ drwxrwxr-x 2 seriy seriy 358M нояб.  1 03:11 test_dir

Теперь попробуем удалить эту директорию со всем её содержимым различными способами.

Тесты

После каждого теста сбрасываем кеш файловой системы
sudo sh -c 'sync && echo 1 > /proc/sys/vm/drop_caches'
для того чтобы не занять быстро всю память и сравнивать скорость удаления в одинаковых условиях.

Удаление через rm -r

$ rm -r /mnt/test_dir/
Под strace несколько раз подряд (!!!) вызывает getdents(), затем очень много вызывает unlinkat() и так в цикле. Занял 30Мб RAM, не растет.
Удаляет содержимое успешно.

iotop  7664 be/4 seriy      72.70 M/s    0.00 B/s  0.00 % 93.15 % rm -r /mnt/test_dir/  5919 be/0 root       80.77 M/s   16.48 M/s  0.00 % 80.68 % [loop0]

Т.е. удалять переполненные директории с помощью rm -r /путь/до/директории вполне нормально.

Удаление через rm ./*

$ rm /mnt/test_dir/*
Запускает дочерний процесс шелла, который дорос до 600Мб, прибил по ^C. Ничего не удалил.
Очевидно, что glob по звёздочке обрабатывается самим шеллом, накапливается в памяти и передается команде rm после того как считается директория целиком.

Удаление через find -exec

$ find /mnt/test_dir/ -type f -exec rm -v {} \;
Под strace вызывает только getdents(). процесс find вырос до 600Мб, прибил по ^C. Ничего не удалил.
find действует так же, как и * в шелле — сперва строит полный список в памяти.

Удаление через find -delete

$ find /mnt/test_dir/ -type f -delete
Вырос до 600Мб, прибил по ^C. Ничего не удалил.
Аналогично предыдущей команде. И это крайне удивительно! На эту команду я возлагал надежду изначально.

Удаление через ls -f и xargs

$ cd /mnt/test_dir/ ; ls -f . | xargs -n 100 rm
параметр -f говорит, что не нужно сортировать список файлов.
Создает такую иерархию процессов:

  | - ls 212Кб  | - xargs 108Кб     | - rm 130Кб # pid у rm постоянно меняется

Удаляет успешно.

iotop  # сильно скачет  5919 be/0 root        5.87 M/s    6.28 M/s  0.00 % 89.15 % [loop0]

ls -f в данной ситуации ведет себя адекватнее, чем find и не накапливает список файлов в памяти без необходимости. ls без параметров (как и find) — считывает список файлов в память целиком. Очевидно, для сортировки. Но этот способ плох тем, что постоянно вызывает rm, чем создается дополнительный оверхед.
Из этого вытекает ещё один способ — можно вывод ls -f перенаправить в файл и затем удалить содержимое директории по этому списку.

Удаление через perl readdir

$ perl -e 'chdir "/mnt/test_dir/" or die; opendir D, "."; while ($n = readdir D) { unlink $n }' (взял здесь)
Под strace один раз вызывает getdents(), потом много раз unlink() и так в цикле. Занял 380Кб памяти, не растет.
Удаляет успешно.

iotop  7591 be/4 seriy      13.74 M/s    0.00 B/s  0.00 % 98.95 % perl -e chdi...  5919 be/0 root       11.18 M/s 1438.88 K/s  0.00 % 93.85 % [loop0]

Получается, что использование readdir вполне возможно?

Удаление через программу на C readdir + unlink

//file: cleandir.c #include <dirent.h> #include <sys/types.h> #include <unistd.h>  int main(int argc, char *argv[]) {     struct dirent *entry;     DIR *dp;     chdir("/mnt/test_dir");     dp = opendir(".");     while( (entry = readdir(dp)) != NULL ) {         if ( strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..") ){             unlink(entry->d_name);  // maybe unlinkat ?         }     } } 

$ gcc -o cleandir cleandir.c
$ ./cleandir
Под strace один раз вызывает getdents(), потом много раз unlink() и так в цикле. Занял 128Кб памяти, не растет.
Удаляет успешно.

iotop:  7565 be/4 seriy      11.70 M/s    0.00 B/s  0.00 % 98.88 % ./cleandir  5919 be/0 root       12.97 M/s 1079.23 K/s  0.00 % 92.42 % [loop0]

Опять — же, убеждаемся, что использовать readdir — вполне нормально, если не накапливать результаты в памяти, а удалять файлы сразу.

Выводы

  • Использовать комбинацию функций readdir() + unlink() для удаления директорий, содержащих миллионы файлов, можно.
  • На практике лучше использовать rm -r /my/dir/, т.к. он поступает более умно — сперва строит относительно небольшой список файлов в памяти, вызывая несколько раз readdir(), а затем удаляет файлы по этому списку. Это позволяет более плавно чередовать нагрузку на чтение и запись, чем повышает скорость удаления.
  • Для снижения нагрузки на систему использовать в комбинации с nice или ionice. Либо использовать скриптовые языки и вставлять небольшие sleep() в циклах. Либо генерировать список файлов через ls -l и пропускать его через замедляющий пайп.
  • Не верить всему, что пишут в интернетах, конечно же! В различных блогах часто обсуждают эту проблему, и регулярно подсказывают неработающие решения.

P.S.: К сожалению, не нашел в Python функции для итеративного чтения директории, чему крайне удивлён; os.listdir() и os.walk() читают директорию целиком. Даже в PHP есть readdir.

ссылка на оригинал статьи http://habrahabr.ru/post/157613/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *