Очередной пост-ответ на статью на хабре. Прочитал статью
Необычное переполнение жесткого диска или как удалить миллионы файлов из одной папки и очень удивился. Неужели в стандартном инструментарии Linux нет простых средств для работы с переполненными директориями и необходимо прибегать к столь низкоуровневым способам, как вызов
getdents()
напрямую.
Для тех, кто не в курсе проблемы, краткое описание: если вы случайно создали в одной директории огромное количество файлов без иерархии - т.е. от 5 млн файлов, лежащих в одной единственной директории, то быстро удалить их не получится. Кроме того, не все утилиты в linux могут это сделать в принципе - либо будут сильно нагружать процессор/HDD, либо займут очень много памяти.
Так что я выделил время, организовал тестовый полигон и попробовал различные средства, как предложенные в комментариях, так и найденные в различных статьях и свои собственные.
Своего рода, расстановка точек над i в вопросе удаления файлов из переполненной директории.
Подготовка
Так как создавать переполненную директорию на своём 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 постоянно меняется
Удаляет успешно.
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);
}
}
}
$ 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
. - Не верить всему, что пишут в интернетах, конечно же!
P.S.: К сожалению, не нашел в Python функции для итеративного чтения директории, чему крайне удивлён; os.listdir() и os.walk() читают директорию целиком. Даже в PHP есть readdir.