Предисловие
Скорей всего, матерым системным администраторам статья будет не очень интересна. В первую очередь она ориентирована на новичков, а также на людей, которые столкнулись с подобной проблемой — необходимостью удалить огромное количество файлов из одной папки в ОС Linux (Debian в моем случае), а также с закончившимся местом на диске, когда df -h выдает что почти 30% свободно.
Начало
Ничто не предвещало беды.
Сервер с сайтом работал без никаких проблем уже больше года (uptime почти 500 дней), не было никаких проблем, и я с чистой душой спокойно ушел в отпуск.
В первый же день отпуска мне звонят с жалобой — сайт недоступен. MySQL падает с ошибкой Error 28 «No space left on device».
Казалось бы, проблема банальна — кончилось место на диске. Правда, df -h показывает, что на диске имеется вполне достаточное количество свободного места, ну да я же в отпуске, разбираться лень — посоветовал им поискать на диске ненужные файлы (старые бекапы и т.д.) и их удалить. Удалили, вроде все заработало.
Прошла пара часов и проблема вернулась. Странно — свободное место на жестком диске за это время практически не уменьшилось. После беглого гугления обнаружился топик на serverfault, в котором говорится, что проблема может возникнуть также из-за того, что кончилось не место на диске, а айноды!
Ввожу в консоль
df -i
— и оказывается действительно, айноды у меня закончились.Проблема
Начал искать, где же у меня находится столько файлов на жестком диске, что они сожрали все айноды (а айнодов у меня на 500-гигабайтном жестком диске больше 30 миллионов).
И нашел — оказалось, проблема была в папке с сессиями php.
Видимо, по какой-то причине сломался механизм автоочистки этой папки, что привело к тому, что в ней скопилось огромное количество файлов. Насколько огромное — сказать сложно, потому что никакие стандартные команды линукс, такие, как ls, find, rm и т.д. — с этой папкой не работают. Просто виснут, заодно подвешивая весь сервер. Могу только сказать, что сам файл директории стал весит около гигабайта, а также что файлов там точно более полумиллиона, потому что столько я оттуда уже удалил.
Решение
Решение очевидное — надо удалить все эти файлы сессий. При этом желательно, чтобы сервер продолжал работать в штатном режиме. Для начала я переименовал папку сессий, в которой лежит куча файлов, а вместо нее создал пустую — чтобы спокойно из старой (переименованной) удалять все файлы, и чтобы это не мешало созданию новых файлов сессий.
Также в крон добавил автоматическое удаление файлов сессий старше одного часа, чтобы проблема больше не повторилась.
И перешел к основной проблеме — очистке жесткого диска.
Попробовал решение «в лоб»:
rm -rf ./*
Сервер повис, ничего не удалилось
Попробовал известный способ для удаления большого числа файлов
find . -type f -exec rm -v {} \;
Ничего, сервер виснет, файлы не удаляются.
А теперь что самое интересное — файловый менеджер
mc
достаточно успешно справлялся с задачей удаления этих файлов! То есть, когда запускаешь удаление папки — файлы удаляются, mc не виснет. Удаление идет со скоростью примерно 5 000 файлов в минуту, правда при этом создается огромная нагрузка на жесткий диск, что приводит к неработоспособности сервера.А хотелось бы, чтобы эти файлы постепенно удалялись в фоновом режиме, и не мешали нормальной работе сайта.
Собственно, решение опять нашлось в гугле — Olark делится способом, как он отобразил список из 8 миллионов файлов в 1 папке, используя системный вызов
getdents
Здесь находится документация по функции getdents, а также пример кода, который ее использует.
Правда, этот пример мне не совсем подошел — даже если ставить большой размер буфера, как советует Olark в своем блоге, все равно сервер виснет при попытке прочитать всю папку разом.
Опытным путем подобрал размер буфера в 30 килобайт, который позволяет считать около 550 названий файлов из директории, при этом не подвешивая сервер и не создавая излишней нагрузки на диск. А также немного переписал код примера, чтобы вместо отображения имени файла он его удалял.
В итоге у меня получился такой код:
#define _GNU_SOURCE
#include <dirent.h> /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
struct linux_dirent {
long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[];
};
#define BUF_SIZE 1024*30
int
main(int argc, char *argv[])
{
int fd, nread;
char buf[BUF_SIZE];
struct linux_dirent *d;
int bpos;
int deleted;
char d_type;
char temp[100];
fd = open(argc > 1 ? argv[1] : ".", O_RDONLY | O_DIRECTORY);
if (fd == -1)
handle_error("open");
deleted = 0;
nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
if (nread == -1)
handle_error("getdents");
if (nread != 0) {
for (bpos = 0; bpos < nread;) {
d = (struct linux_dirent *) (buf + bpos);
d_type = *(buf + bpos + d->d_reclen - 1);
if(d->d_ino && d->d_ino != 22332748 && d->d_ino != 22332761) { // тут я прописал inode самой директории и директории верхнего уровня, чтобы он не пытался удалять файлы "." и ".." - принимаю подсказки, как это сделать лучше
sprintf(temp,"%s/%s", argv[1], (char *) d->d_name);
remove(temp);
deleted += 1;
}
bpos += d->d_reclen;
}
}
printf("deleted %d\n", deleted);
exit(EXIT_SUCCESS);
}
Код компиллируется обычным gcc
gcc listdir.c -o listdir
И просто запускается из командной строки:
./listdir mod-tmp2
Получившийся файл я поставил в крон и теперь у меня удаляется по 547 файлов в минуту, при этом нагрузка на сервер в пределах нормы — и я надеюсь, в течение недели-другой все файлы все-таки удалятся.
Выводы
- Если
df -h
показывает, что на жестком диске еще есть место — его может и не быть. Надо смотреть такжеdf -i
- Не стоит надеяться на механизмы авто-очистки таких вещей, как файлы сессий — в какой-то момент они могут не сработать, и вы окажетесь у целой горы файлов, удалить которые — задача нетривиальная
- Стандартные команды линукс, такие как
ls, rm, find
и т.д. могут пасовать перед нестандартными ситуациями вроде миллионов файлов в одной папке. В таком случае надо использовать низкоуровневые системные вызовы