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

3440

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

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

Для тех, кто не в курсе проблемы, краткое описание: если вы случайно создали в одной директории огромное количество файлов без иерархии — т.е. от 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 -N option
sudo mount sparse /mnt
mkdir /mnt/test_dir
К сожалению, я узнал об опции -N команды 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.

источник