Рм в каталоге с миллионами файлов
Предыстория: физический сервер, около двух лет, диски SATA 7200 об / мин, подключенные к RAID-карте 3Ware, noatime с установленной файловой системой ext3 FS и данные = заказано, без сумасшедшей нагрузки, ядро 2.6.18-92.1.22.el5, время безотказной работы 545 дней, Каталог не содержит никаких подкаталогов, только миллионы маленьких (~100 байт) файлов, с некоторыми большими (несколько КБ).
У нас есть сервер, который за последние несколько месяцев немного кукушка, но мы заметили это только на днях, когда он не смог выполнить запись в каталог из-за слишком большого количества файлов. В частности, он начал выдавать эту ошибку в /var/log/messages:
ext3_dx_add_entry: Directory index full!
На рассматриваемом диске осталось много инодов:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda3 60719104 3465660 57253444 6% /
Так что я предполагаю, что это означает, что мы достигли предела того, сколько записей может быть в самом файле каталога. Не знаю, сколько будет файлов, но, как видите, не может быть больше, чем три миллиона или около того. Не то чтобы это хорошо, заметьте! Но это первая часть моего вопроса: что это за верхний предел? Это настраивается? Прежде чем орать на меня - я хочу смягчить это; этот огромный каталог вызвал всевозможные проблемы.
В любом случае, мы отследили проблему в коде, который генерировал все эти файлы, и исправили его. Теперь я застрял с удалением каталога.
Несколько вариантов здесь:
rm -rf (dir)
Я попробовал это первым. Я сдался и убил его после того, как он проработал полтора дня без какого-либо заметного воздействия.
- unlink(2) для каталога: безусловно, стоит подумать, но вопрос в том, будет ли быстрее удалять файлы внутри каталога через fsck, чем удалять через unlink(2). То есть, так или иначе, я должен пометить эти inode как неиспользованные. Это предполагает, конечно, что я могу сказать fsck не сбрасывать записи в файлы в /lost+found; в противном случае я просто перенес свою проблему. В дополнение ко всем другим проблемам, после прочтения об этом, оказывается, что мне, вероятно, придется вызывать некоторые внутренние функции FS, поскольку ни один из найденных мной вариантов unlink(2) не позволил бы мне просто беспечно удалить каталог с записями в нем. Пух.
while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
Это на самом деле сокращенная версия; Реальный, который я запускаю, который просто добавляет некоторые отчеты о прогрессе и чистую остановку, когда у нас заканчиваются файлы для удаления, это:
экспорт я =0; time ( while [ true ]; do ls -Uf | head -n 3 | grep -qF '.png' || break; ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; экспорт i=$(($i+10000)); эхо "$i..."; готово)
Кажется, это работает довольно хорошо. Когда я пишу это, он удалил 260 000 файлов за последние тридцать минут или около того.
- Как упомянуто выше, настраивается ли ограничение на количество записей в каталоге?
- Почему потребовалось "настоящие 7m9.561s / пользователь 0m0.001s / sys 0m0.001s", чтобы удалить один файл, который был первым в списке, возвращенном
ls -U
И, возможно, потребовалось десять минут, чтобы удалить первые 10000 записей с помощью команды в #3, но теперь она тянется довольно счастливо? В этом отношении он удалил 260 000 примерно за тридцать минут, но теперь требуется еще пятнадцать минут, чтобы удалить еще 60 000. Почему огромные колебания в скорости? - Есть ли лучший способ сделать это? Не хранить миллионы файлов в каталоге; Я знаю, что это глупо, и это не случилось бы на моих часах. Поиск в гугле и просмотр SF и SO предлагает множество вариантов
find
это не будет значительно быстрее, чем мой подход по нескольким очевидным причинам. Но есть ли у идеи delete-via-fsck ноги? Или что-то еще целиком? Мне не терпится услышать нестандартное (или не очень известное) мышление.
Окончательный вывод сценария!:
2970000...
2980000...
2990000...
3000000...
3010000...
real 253m59.331s
user 0m6.061s
sys 5m4.019s
Таким образом, три миллиона файлов были удалены за чуть более четырех часов.
23 ответа
data=writeback
Параметр mount заслуживает того, чтобы его попытались предотвратить ведение журнала файловой системы. Это должно быть сделано только во время удаления, однако существует риск, если сервер отключается или перезагружается во время операции удаления.
Согласно этой странице,
Некоторые приложения показывают очень значительное улучшение скорости при использовании. Например, улучшения скорости можно увидеть (...), когда приложения создают и удаляют большие объемы небольших файлов.
Опция устанавливается либо в fstab
или во время операции монтирования, заменяя data=ordered
с data=writeback
, Файловая система, содержащая файлы для удаления, должна быть перемонтирована.
Хотя основной причиной этой проблемы является производительность ext3 с миллионами файлов, действительная основная причина этой проблемы другая.
Когда каталог должен быть в списке, вызывается readdir() для каталога, который выдает список файлов. readdir - это вызов posix, но используемый здесь системный вызов Linux называется "getdents". Получатели перечисляют записи каталога, заполняя буфер записями.
Проблема в основном связана с тем, что readdir() использует фиксированный размер буфера 32 Кб для извлечения файлов. По мере того как каталог становится все больше и больше (размер увеличивается по мере добавления файлов), ext3 становится все медленнее и медленнее для извлечения записей, а дополнительный размер буфера в readdir 32 КБ достаточен только для включения части записей в каталоге. Это заставляет readdir зацикливаться снова и снова и вызывать дорогой системный вызов снова и снова.
Например, в тестовом каталоге, который я создал с более чем 2,6 миллионами файлов внутри, выполнение "ls -1|wc-l" показывает большой вывод результатов многих системных вызовов getdent.
$ strace ls -1 | wc -l
brk(0x4949000) = 0x4949000
getdents(3, /* 1025 entries */, 32768) = 32752
getdents(3, /* 1024 entries */, 32768) = 32752
getdents(3, /* 1025 entries */, 32768) = 32760
getdents(3, /* 1025 entries */, 32768) = 32768
brk(0) = 0x4949000
brk(0x496a000) = 0x496a000
getdents(3, /* 1024 entries */, 32768) = 32752
getdents(3, /* 1026 entries */, 32768) = 32760
...
Кроме того, время, проведенное в этом каталоге, было значительным.
$ time ls -1 | wc -l
2616044
real 0m20.609s
user 0m16.241s
sys 0m3.639s
Чтобы сделать этот процесс более эффективным, нужно вызывать getdents вручную с гораздо большим буфером. Это значительно повышает производительность.
Теперь вы не должны сами вызывать getdents вручную, поэтому для нормального использования интерфейса не существует (проверьте страницу man, чтобы увидеть getdents!), Однако вы можете вызвать его вручную и сделать свой вызов системным вызовом более эффективным.
Это значительно сокращает время загрузки этих файлов. Я написал программу, которая делает это.
/* I can be compiled with the command "gcc -o dentls dentls.c" */
#define _GNU_SOURCE
#include <dirent.h> /* Defines DT_* constants */
#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
struct linux_dirent {
long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[256];
char d_type;
};
static int delete = 0;
char *path = NULL;
static void parse_config(
int argc,
char **argv)
{
int option_idx = 0;
static struct option loptions[] = {
{ "delete", no_argument, &delete, 1 },
{ "help", no_argument, NULL, 'h' },
{ 0, 0, 0, 0 }
};
while (1) {
int c = getopt_long(argc, argv, "h", loptions, &option_idx);
if (c < 0)
break;
switch(c) {
case 0: {
break;
}
case 'h': {
printf("Usage: %s [--delete] DIRECTORY\n"
"List/Delete files in DIRECTORY.\n"
"Example %s --delete /var/spool/postfix/deferred\n",
argv[0], argv[0]);
exit(0);
break;
}
default:
break;
}
}
if (optind >= argc)
errx(EXIT_FAILURE, "Must supply a valid directory\n");
path = argv[optind];
}
int main(
int argc,
char** argv)
{
parse_config(argc, argv);
int totalfiles = 0;
int dirfd = -1;
int offset = 0;
int bufcount = 0;
void *buffer = NULL;
char *d_type;
struct linux_dirent *dent = NULL;
struct stat dstat;
/* Standard sanity checking stuff */
if (access(path, R_OK) < 0)
err(EXIT_FAILURE, "Could not access directory");
if (lstat(path, &dstat) < 0)
err(EXIT_FAILURE, "Unable to lstat path");
if (!S_ISDIR(dstat.st_mode))
errx(EXIT_FAILURE, "The path %s is not a directory.\n", path);
/* Allocate a buffer of equal size to the directory to store dents */
if ((buffer = calloc(dstat.st_size*3, 1)) == NULL)
err(EXIT_FAILURE, "Buffer allocation failure");
/* Open the directory */
if ((dirfd = open(path, O_RDONLY)) < 0)
err(EXIT_FAILURE, "Open error");
/* Switch directories */
fchdir(dirfd);
if (delete) {
printf("Deleting files in ");
for (int i=5; i > 0; i--) {
printf("%u. . . ", i);
fflush(stdout);
sleep(1);
}
printf("\n");
}
while (bufcount = syscall(SYS_getdents, dirfd, buffer, dstat.st_size*3)) {
offset = 0;
dent = buffer;
while (offset < bufcount) {
/* Don't print thisdir and parent dir */
if (!((strcmp(".",dent->d_name) == 0) || (strcmp("..",dent->d_name) == 0))) {
d_type = (char *)dent + dent->d_reclen-1;
/* Only print files */
if (*d_type == DT_REG) {
printf ("%s\n", dent->d_name);
if (delete) {
if (unlink(dent->d_name) < 0)
warn("Cannot delete file \"%s\"", dent->d_name);
}
totalfiles++;
}
}
offset += dent->d_reclen;
dent = buffer + offset;
}
}
fprintf(stderr, "Total files: %d\n", totalfiles);
close(dirfd);
free(buffer);
exit(0);
}
Хотя это не решает основную фундаментальную проблему (много файлов в файловой системе, которая плохо работает с ней). Вероятно, это будет намного, намного быстрее, чем многие из опубликованных альтернатив.
В качестве предварительного решения следует удалить поврежденный каталог и переделать его после. Каталоги только увеличиваются в размере и могут работать плохо даже с несколькими файлами внутри из-за размера каталога.
Редактировать: я убрал это совсем немного. Добавлена опция, позволяющая вам удалять из командной строки во время выполнения и удаляющая кучу вещей из Treewalk, которые, честно говоря, в лучшем случае сомнительны. Также было показано, чтобы произвести повреждение памяти.
Теперь вы можете сделать dentls --delete /my/path
Новые результаты. Исходя из каталога с 1,82 миллиона файлов.
## Ideal ls Uncached
$ time ls -u1 data >/dev/null
real 0m44.948s
user 0m1.737s
sys 0m22.000s
## Ideal ls Cached
$ time ls -u1 data >/dev/null
real 0m46.012s
user 0m1.746s
sys 0m21.805s
### dentls uncached
$ time ./dentls data >/dev/null
Total files: 1819292
real 0m1.608s
user 0m0.059s
sys 0m0.791s
## dentls cached
$ time ./dentls data >/dev/null
Total files: 1819292
real 0m0.771s
user 0m0.057s
sys 0m0.711s
Был немного удивлен, что все еще так хорошо работает!
Можно ли было бы сделать резервную копию всех других файлов из этой файловой системы во временное хранилище, переформатировать раздел, а затем восстановить файлы?
В ext3 не существует предельного значения для каждого каталога, только ограничение inode для файловой системы (хотя я думаю, что существует ограничение на количество подкаталогов).
У вас могут остаться проблемы после удаления файлов.
Когда в каталоге содержатся миллионы файлов, сама запись в каталоге становится очень большой. Запись каталога должна проверяться для каждой операции удаления, и для каждого файла требуется различное количество времени, в зависимости от того, где находится его запись. К сожалению, даже после того, как все файлы были удалены, запись каталога сохраняет свой размер. Таким образом, дальнейшие операции, требующие сканирования записи каталога, все равно будут занимать много времени, даже если каталог теперь пуст. Единственный способ решить эту проблему - переименовать каталог, создать новый со старым именем и перенести все оставшиеся файлы в новый. Затем удалите переименованный.
Я не проверял это, но этот парень сделал:
rsync -a --delete ./emptyDirectoty/ ./hugeDirectory/
TL; DR: используйте
rsync -a --delete emptyfolder/ x
.
У этого вопроса 50 тысяч просмотров и довольно много ответов, но, похоже, никто не сравнивал все разные ответы. Есть одна ссылка на внешний тест, но ему больше 7 лет, и он не смотрел на программу, представленную в этом ответе: /questions/467909/rm-v-kataloge-s-millionami-fajlov/467951#467951
Частично сложность заключается в том, что время, необходимое для удаления файла, сильно зависит от используемых дисков и файловой системы. В моем случае я тестировал оба с потребительским SSD под управлением BTRFS на Arch Linux (обновлено с 2020-03), но я получил такой же порядок результатов в другом дистрибутиве (Ubuntu 18.04), файловой системе (ZFS) и диске. тип (HDD в конфигурации RAID10).
Настройка теста была идентична для каждого запуска:
# setup
mkdir test && cd test && mkdir empty
# create 800000 files in a folder called x
mkdir x && cd x
seq 800000 | xargs touch
cd ..
Результаты теста:
rm -rf x
: 30,43 с
find x/ -type f -delete
: 29,79
perl -e 'for(<*>){((stat)[9]<(unlink))}'
: 37.97 с
rsync -a --delete empty/ x
: 25,11 с
(Ниже приводится программа из этого ответа, но измененная, чтобы ничего не печатать или ждать, прежде чем она удалит файлы.)
./dentls --delete x
: 29,74
В
rsync
версия оказывалась победителем каждый раз, когда я повторял тест, хотя и с довольно низким отрывом. В
perl
команда была медленнее, чем любой другой вариант в моих системах.
Несколько шокирует то, что программа из верхнего ответа на этот вопрос оказалась на моих системах не быстрее, чем простой
rm -rf
. Давайте разберемся, почему это так.
Прежде всего, ответ утверждает, что проблема в том, что
rm
использует
readdir
с фиксированным размером буфера 32 КБ с
getdents
. Это оказалось не так в моей системе Ubuntu 18.04, которая использовала буфер в четыре раза больше. В системе Arch Linux он использовал
getdents64
.
Кроме того, ответ вводит в заблуждение статистику, дающую скорость при перечислении файлов в большом каталоге, но не при их удалении (о чем и был вопрос). Он сравнивает
dentls
к
ls -u1
, но простой
strace
показывает, что
getdents
это не причина, почему
ls -u1
работает медленно, по крайней мере, не в моей системе (Ubuntu 18.04 с 1000000 файлами в каталоге):
strace -c ls -u1 x >/dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
94.00 7.177356 7 1000000 lstat
5.96 0.454913 1857 245 getdents
[snip]
Этот
ls
команда делает миллион звонков
lstat
, что замедляет работу программы. В
getdents
звонки составляют всего 0,455 секунды. Как долго
getdents
звонки принимают
dentls
в той же папке?
strace -c ./dentls x >/dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
99.91 0.489895 40825 12 getdents
[snip]
Это правильно! Даже если
dentls
делает только 12 вызовов вместо 245, на самом деле системе требуется больше времени для выполнения этих вызовов. Таким образом, объяснение, данное в этом ответе, на самом деле неверно - по крайней мере, для двух систем, на которых я смог это проверить.
То же самое касается
rm
и
dentls --delete
. В то время как
rm
занимает 0,42 секунды на вызов
getdents
,
dentls
занимает 0,53 с. В любом случае подавляющее большинство времени тратится на звонки
unlink
!
Короче говоря, не ожидайте увидеть массовое ускорение.
dentls
, если ваша система не похожа на авторскую и не требует больших затрат на отдельные
getdents
. Возможно, разработчики glibc значительно ускорили его за годы, прошедшие с момента написания ответа, и теперь для ответа на разные размеры буфера требуется линейная величина времени. Или, может быть, время отклика
getdents
зависит от архитектуры системы каким-то образом, что не очевидно.
find просто не работает для меня, даже после изменения параметров ext3 fs, как предложено пользователями выше. Потребляется слишком много памяти. Этот скрипт PHP добился цели - быстрое, незначительное использование процессора, незначительное использование памяти:
<?php
$dir = '/directory/in/question';
$dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
unlink($dir . '/' . $file);
}
closedir($dh);
?>
Я опубликовал отчет об ошибке, связанной с этой проблемой, с find: http://savannah.gnu.org/bugs/?31961
Убедитесь, что вы делаете:
mount -o remount,rw,noatime,nodiratime /mountpoint
что должно немного ускорить процесс.
Недавно я столкнулся с подобной проблемой и не смог получить ring0's data=writeback
предложение по работе (возможно, из-за того, что файлы находятся на моем основном разделе). Исследуя обходные пути, я наткнулся на это:
tune2fs -O ^has_journal <device>
Это отключит ведение журнала полностью, независимо от data
вариант дать mount
, Я сочетал это с noatime
и объем имел dir_index
установить, и это, казалось, работало довольно хорошо. Удаление фактически завершилось, и мне не пришлось его убивать, моя система оставалась отзывчивой, и теперь она снова запущена (с журналированием) без проблем.
Очевидно, что от яблок к яблокам здесь нет, но я настроил небольшой тест и сделал следующее:
Создано 100 000 512-байтовых файлов в каталоге (dd
а также /dev/urandom
в цикле); забыл примерить время, но создание этих файлов заняло примерно 15 минут.
Выполнить следующее, чтобы удалить указанные файлы:
ls -1 | wc -l && time find . -type f -delete
100000
real 0m4.208s
user 0m0.270s
sys 0m3.930s
Это Pentium 4 с частотой 2,8 ГГц (пара сотен гигабайт IDE 7200 об / мин, я думаю; EXT3). Ядро 2.6.27.
Очень медленная команда. Пытаться:
find /dir_to_delete ! -iname "*.png" -type f -delete
Пару лет назад я нашел каталог с 16 миллионами файлов XML в /
файловая система. Из-за критики сервера мы использовали следующую команду, которая заняла около 30 часов:
perl -e 'for(<*>){((stat)[9]<(unlink))}'
Это был старый жесткий диск со скоростью 7200 об / мин, и, несмотря на узкое место ввода-вывода и пики ЦП, старый веб-сервер продолжал свою работу.
Является dir_index
установить для файловой системы? (tune2fs -l | grep dir_index
) Если нет, включите его. Обычно для нового RHEL.
Иногда Perl может творить чудеса в подобных случаях. Вы уже пробовали, если такой маленький сценарий может превзойти bash и основные команды оболочки?
#!/usr/bin/perl
open(ANNOYINGDIR,"/path/to/your/directory");
@files = grep("/*\.png/", readdir(ANNOYINGDIR));
close(ANNOYINGDIR);
for (@files) {
printf "Deleting %s\n",$_;
unlink $_;
}
Или другой, возможно, даже более быстрый подход Perl:
#!/usr/bin/perl
unlink(glob("/path/to/your/directory/*.png")) or die("Could not delete files, this happened: $!");
РЕДАКТИРОВАТЬ: я только что попробовал мои сценарии Perl. Более многословный делает что-то правильно. В моем случае я попробовал это с виртуальным сервером с 256 МБ ОЗУ и полмиллиона файлов.
time find /test/directory | xargs rm
Результаты:
real 2m27.631s
user 0m1.088s
sys 0m13.229s
по сравнению с
time perl -e 'opendir(FOO,"./"); @files = readdir(FOO); closedir(FOO); for (@files) { unlink $_; }'
real 0m59.042s
user 0m0.888s
sys 0m18.737s
Мой предпочтительный вариант - уже предложенный подход newfs. Основная проблема заключается в том, что, как уже отмечалось, линейное сканирование для удаления проблематично.
rm -rf
должно быть почти оптимальным для локальной файловой системы (NFS будет другой). Но при миллионах файлов, 36 байтов на имя файла и 4 на индекс (предположение, не проверяя значение для ext3), это 40 * миллионов, которые должны храниться в оперативной памяти только для каталога.
По-видимому, вы перебиваете кэш-память метаданных файловой системы в Linux, так что блоки для одной страницы файла каталога удаляются, пока вы еще используете другую часть, только для того, чтобы снова попасть на эту страницу кэша, когда следующая файл удален Настройка производительности Linux не является моей областью, но /proc/sys/{vm,fs}/, вероятно, содержит что-то важное.
Если вы можете позволить себе простои, вы можете включить функцию dir_index. Он переключает индекс каталога с линейного на нечто гораздо более оптимальное для удаления в больших каталогах (хэшированные b-деревья). tune2fs -O dir_index ...
с последующим e2fsck -D
должно сработать. Тем не менее, хотя я уверен, что это поможет, прежде чем возникнут проблемы, я не знаю, как преобразование (e2fsck с -D
) выполняет при работе с существующим каталогом v.large. Резервное копирование + сосать-и-посмотреть.
Из того, что я помню, удаление inode в файловых системах ext - O(n^2), поэтому чем больше файлов вы удаляете, тем быстрее будут работать остальные.
Был один раз, когда я столкнулся с подобной проблемой (хотя мои оценки оценивали время удаления ~7 часов), в конце концов, в первом комментарии был предложен маршрут, предложенный jftuga.
Хорошо, это было рассмотрено различными способами в остальной части потока, но я думал, что я добавлю свои два цента. Виновником производительности в вашем случае, вероятно, является readdir. Вы получаете список файлов, которые не обязательно каким-либо образом последовательны на диске, что приводит к доступу к диску повсюду, когда вы отсоединяетесь. Файлы настолько малы, что операция unlink, вероятно, не слишком быстро обнуляет пространство. Если вы прочитаете dir, а затем отсортируете по возрастанию inode, вы, вероятно, получите лучшую производительность. Так что читайте в RAM (сортировка по inode) -> unlink -> profit.
Я думаю, что Inode - это грубое приближение... но, исходя из вашего варианта использования, он может быть довольно точным...
Вот как я удаляю миллионы файлов трассировки, которые иногда могут собираться на большом сервере базы данных Oracle:
for i in /u*/app/*/diag/*/*/*/trace/*.tr? ; do rm $i; echo -n . ; done
Я считаю, что это приводит к довольно медленному удалению, которое слабо влияет на производительность сервера, обычно это примерно час на миллион файлов при "типичной" настройке 10000 IOPS.
Для сканирования каталогов, создания исходного списка файлов и удаления первого файла часто требуется несколько минут. Оттуда и далее, а. отображается для каждого удаленного файла.
Задержка, вызванная отражением в терминале, оказалась достаточной задержкой, чтобы предотвратить любую значительную нагрузку во время удаления.
Ну, это не настоящий ответ, но...
Можно ли будет конвертировать файловую систему в ext4 и посмотреть, что изменится?
Я бы, вероятно, вытащил компилятор C и сделал бы моральный эквивалент вашего скрипта. То есть использовать opendir(3)
чтобы получить дескриптор каталога, затем используйте readdir(3)
чтобы получить имя файла, затем подсчитать файлы, когда я их отсоединяю, и время от времени печатать "%d файлов удалено" (и, возможно, истекшее время или текущее время).
Я не ожидаю, что он будет заметно быстрее, чем версия сценария оболочки, просто я привык время от времени вырывать компилятор, потому что нет чистого способа сделать то, что я хочу из оболочки, или потому что в то время как выполнимо в оболочке, это непродуктивно медленно.
Скорее всего, у вас возникли проблемы с перезаписью каталога. Попробуйте сначала удалить самые новые файлы. Посмотрите на параметры монтирования, которые будут откладывать обратную запись на диск.
Для индикатора прогресса попробуйте запустить что-то вроде rm -rv /mystuff 2>&1 | pv -brtl > /dev/null
Вы можете использовать функции распараллеливания 'xargs':
ls -1|xargs -P nb_concurrent_jobs -n nb_files_by_job rm -rf
На самом деле, это немного лучше, если используемая оболочка выполняет расширение командной строки:
ls|cut -c -4|sort|uniq|awk '{ print "echo " $1 ";rm -rf " $1 "*"}' |sh