Необычное переполнение жесткого диска или как удалить миллионы файлов из одной папки

в 12:43, , рубрики: bash, ispmanager, mysql, php, системное администрирование, метки: , , , ,

Предисловие

Скорей всего, матерым системным администраторам статья будет не очень интересна. В первую очередь она ориентирована на новичков, а также на людей, которые столкнулись с подобной проблемой — необходимостью удалить огромное количество файлов из одной папки в ОС 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 %dn", deleted);

    exit(EXIT_SUCCESS);
}

Код компиллируется обычным gcc

gcc listdir.c -o listdir

И просто запускается из командной строки:

./listdir mod-tmp2

Получившийся файл я поставил в крон и теперь у меня удаляется по 547 файлов в минуту, при этом нагрузка на сервер в пределах нормы — и я надеюсь, в течение недели-другой все файлы все-таки удалятся.

Выводы

  1. Если df -h показывает, что на жестком диске еще есть место — его может и не быть. Надо смотреть также df -i
  2. Не стоит надеяться на механизмы авто-очистки таких вещей, как файлы сессий — в какой-то момент они могут не сработать, и вы окажетесь у целой горы файлов, удалить которые — задача нетривиальная
  3. Стандартные команды линукс, такие как ls, rm, find и т.д. могут пасовать перед нестандартными ситуациями вроде миллионов файлов в одной папке. В таком случае надо использовать низкоуровневые системные вызовы

Автор: rednaxi

Поделиться

* - обязательные к заполнению поля