Что делать, если

Закончилось место на диске: почему сайт падает молча и как узнать заранее

Закончилось место на диске Linux — сайт не работает, а в логах пусто. Симптомы, df -h и df -i, кто съел место, что удалить безопасно и как узнать при 80 %.

Когда на Linux-сервере заканчивается место, сайт почти никогда не падает «честно» — с понятной ошибкой и записью в лог. Он продолжает отвечать: главная отдаётся из кэша, статика и PHP-код читаются. Ломается только то, что пишет: сессии, загрузка файлов, запись в базу, логи, cron с бэкапами. Поэтому первые симптомы выглядят как ошибки в коде, и на диск смотрят в последнюю очередь. Первые две команды при любом «непонятно что сломалось» — df -h и df -i; они занимают пять секунд и закрывают самую частую причину.

Ниже — как распознать переполненный диск, куда уходит место, что удалять безопасно, а что нет, и как узнавать о проблеме при 80 %, а не при 100 %.

Симптомы, по которым не сразу понятно, что дело в диске

Общий принцип: чтение работает, запись нет. Отсюда набор ошибок, каждая из которых по отдельности выглядит как что-то другое.

Что видноЧто происходит на самом деле
HTTP 500, а в логе приложения и php-fpm пустоPHP не смог записать ошибку в лог — лог тоже на полном диске. В error.log nginx при этом может появиться [crit] open() "/var/lib/nginx/proxy/..." failed (28: No space left on device), если ему самому ещё есть куда писать
Пользователей разлогинивает, корзина «забывается»session_start(): open(/var/lib/php/sessions/sess_..., O_RDWR) failed: No space left on device (28) (если кончились иноды) или session_write_close(): Write of 512 bytes failed with errno=28 No space left on device (если кончились блоки) — сессия не сохраняется, каждый запрос начинается как гостевой
Загрузка файлов падает с непонятной ошибкойЛибо ещё при приёме: в $_FILES[...]['error'] код UPLOAD_ERR_CANT_WRITE (7), потому что PHP не смог записать временный файл; либо позже — move_uploaded_file() возвращает false, не сумев скопировать файл в каталог сайта. Фреймворк может показать «ошибка валидации»
MySQL: ERROR 1114 (HY000): The table 'orders' is full, ERROR 3 (HY000): Error writing file '/tmp/MYfd=42' (Errcode: 28 - No space left on device)InnoDB не может расширить tablespace или записать redo-лог. В логе: [ERROR] InnoDB: Write to file ./shop/orders.ibd failed at offset ..., Error number 28 means 'No space left on device' — запрос получает 1114. А при записи в binlog или временный файл — Disk is full writing './binlog.000042' (OS errno 28 - No space left on device). Waiting for someone to free space... Retry in 60 secs. — сервер останавливается на записи и раз в минуту пробует снова
Формы отправляются, но данные не сохраняютсяПриложение ловит исключение базы и показывает «спасибо», а INSERT не прошёл
Бэкапы «есть», но размером 0 байтmysqldump: Got errno 28 on write, tar: write error — cron отработал, код выхода ненулевой, письма никто не читает
Деплой сломался на полпутиfatal: unable to write new index file, composer: write error, npm ERR! ENOSPC — половина файлов старой версии, половина новой

Главный признак, что дело в диске, — несколько несвязанных вещей сломались одновременно, а перезапуск php-fpm ничего не меняет. В логах либо тишина, либо последняя запись датирована моментом, когда место закончилось.

Диагностика за две минуты

Начните с двух команд — место и иноды. Два реальных случая на разных серверах:

# первый сервер: кончились блоки
df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   40G     0 100% /
/dev/vdb1       200G   61G  139G  31% /data

# второй сервер: место есть, инодов нет
df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   16G   24G  40% /

df -i
Filesystem      Inodes   IUsed  IFree IUse% Mounted on
/dev/vda1      2621440 2621440      0  100% /

Три вещи, которые здесь легко пропустить:

  • Иноды. Файловая система может быть занята на 40 %, а инодов — 0. Тогда df -h показывает свободное место, а touch /var/tmp/x отвечает No space left on device. Типичные виновники — миллионы мелких файлов: PHP-сессии, кэш фреймворка, очередь почты, миниатюры изображений.
  • Не тот раздел. База лежит в /var/lib/mysql, а полный диск — /tmp на tmpfs или отдельный /var. Смотрите на строку того mount point, куда пишет упавшая служба.
  • Зарезервированные 5 %. На ext4 5 % блоков по умолчанию отданы root: www-data и mysql видят 0 Avail, а root ещё пишет — поэтому SSH и apt работают, а сайт нет.

Дальше — кто занял место. du по корню с ограничением глубины и без перехода на другие файловые системы (-x):

du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -15
du -xh --max-depth=1 /var 2>/dev/null | sort -h | tail -10

В интерактивной сессии удобнее ncdu -x /: дерево с размерами и удаление прямо из интерфейса. При нехватке инодов ищите каталоги с максимальным числом файлов — одним проходом по диску:

find / -xdev -printf '%h\n' | sort | uniq -c | sort -n | tail -10

Кто обычно съедает диск

Список из раза в раз один и тот же:

ГдеКак проверитьТипичный размер
Логи без ротации: nginx, php-fpm, приложение`du -sh /var/log/* \sort -h, ls -la /var/log/nginx/`access.log на 20–50 ГБ за год при среднем трафике
journaldjournalctl --disk-usageпо умолчанию до 10 % раздела, но не больше 4 ГБ
Бэкапы «на всякий случай»ls -la /var/backups /root/backup /home//backup*ежедневные дампы за два года без удаления
Docker: образы, слои, логи контейнеровdocker system df -v, du -sh /var/lib/docker/containers//-json.logлог одного контейнера легко доходит до 10 ГБ
Старые ядра`dpkg -l 'linux-image*' \grep ^ii, df -h /boot`забивают маленький /boot за несколько апдейтов
MySQL binlogSHOW BINARY LOGS; в клиенте, ls -la /var/lib/mysql/binlog.*десятки ГБ, если репликации нет, а expire не задан
Временные файлыdu -sh /tmp /var/tmp, upload_tmp_dir в php.iniнедокачанные загрузки, дампы после падения

Отдельный случай — удалённые, но открытые файлы. Кто-то сделал rm access.log, место не вернулось: nginx держит дескриптор, и блоки не освобождаются, пока процесс не закроет файл. du такой файл не видит, df — видит. Ищется так:

lsof +L1
COMMAND   PID   USER   FD   TYPE DEVICE  SIZE/OFF NLINK  NODE NAME
nginx    1234   root    5w   REG  252,1 18732441600     0 131077 /var/log/nginx/access.log (deleted)

Освободить без перезапуска процесса можно, обнулив файл через /proc:

: > /proc/1234/fd/5

Второй случай, когда df показывает 100 %, а du ничего не находит, — файлы записали в каталог (например, /data или /var/lib/mysql) до того, как поверх него смонтировали отдельный раздел. Типично после сбоя монтирования: том не подключился, служба писала в пустой каталог на корне, потом том вернули. Файлы скрыты точкой монтирования, и никакой du их не видит. Проверка — примонтировать корень второй раз в другое место:

mkdir /mnt/root && mount --bind / /mnt/root && du -xsh /mnt/root/data
umount /mnt/root

Что освободить сразу и без риска

Порядок — от безопасного к требующему внимания. Цель — получить несколько гигабайт, чтобы служба снова могла писать, а разбираться уже спокойно.

  1. Обнулить, а не удалять большие логи. : > /var/log/nginx/access.log освобождает место мгновенно и не оставляет открытый дескриптор. Удалять файл, который держит процесс, бесполезно (см. выше).
  2. Ужать journald. journalctl --vacuum-size=200M — удаляет старые архивные журналы, текущий не трогает.
  3. Очистить кэш пакетов. apt clean или dnf clean all. Ничего не ломает.
  4. Удалить старые ядра. apt autoremove --purge — оставит текущее и одно предыдущее. Перед этим проверьте uname -r, чтобы не удалить загруженное.
  5. Docker. docker image prune удаляет только висячие слои. docker system prune -a удаляет все остановленные контейнеры (вместе с их записываемым слоем — данные вне volume пропадут), неиспользуемые образы, сети и кэш сборки. Перед запуском посмотрите docker ps -a и убедитесь, что среди остановленных нет нужных.
  6. Бэкапы старше N дней. find /var/backups -name '*.sql.gz' -mtime +14 -ls — сначала посмотреть, потом заменить -ls на -delete. Убедитесь, что копии есть где-то ещё.
  7. MySQL binlog. Только из клиента, не rm: PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY;. Если есть реплика, сначала проверьте её позицию.
  8. Резерв ext4 как экстренная мера. tune2fs -m 1 /dev/vda1 уменьшает резерв root с 5 до 1 % и отдаёт 4 % диска пользователям — на 40 ГБ это 1,6 ГБ, чтобы оживить php-fpm и MySQL, пока вы чистите остальное.

Что делать нельзя: удалять ibdata1, ib_logfile*, binlog.index руками, чистить /var/lib/mysql и /var/lib/docker/overlay2 через rm, удалять активный журнал journald. После освобождения места проверьте MySQL: journalctl -u mysql -n 50, SHOW ENGINE INNODB STATUS\G. Если сервер ушёл в состояние «Waiting for someone to free space», он оживает сам при следующей попытке записи, в пределах минуты. Если же ошибка была при записи в redo-лог или файл таблицы, потребуется перезапуск: при старте InnoDB доводит зафиксированные транзакции по redo-логу и откатывает незавершённые по undo-логу; после этого прогоните mysqlcheck --all-databases. PHP-сессии восстанавливаются сами, разлогиненным пользователям придётся войти заново.

Настройки, чтобы не повторилось

Разовое удаление лечит симптом. Причина — четыре-пять источников без ограничения роста, и каждому нужен свой предел.

logrotate для nginx и приложения

На Debian/Ubuntu конфиг для nginx уже есть в /etc/logrotate.d/nginx (daily, rotate 14), в пакетах с nginx.org — daily, rotate 52. Проблема обычно не в нём, а в логах, которые лежат вне /var/log/nginx (access_log /var/www/site/logs/... в конфиге виртуального хоста) и не попадают ни под один блок logrotate, либо в отсутствии самого пакета logrotate на сервере. Для таких логов нужен отдельный блок:

/var/www/site/logs/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

postrotate с USR1 обязателен: без него nginx продолжит писать в переименованный файл, и вы получите тот же «удалённый, но открытый» лог. Логи приложения (storage/logs/laravel.log, var/log/prod.log) добавьте ещё одним блоком с copytruncate, если приложение не умеет переоткрывать файл. Проверить, что конфиг работает: logrotate -d /etc/logrotate.d/nginx (сухой прогон) и logrotate -f /etc/logrotate.d/nginx для принудительной ротации.

journald

В /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=300M
MaxRetentionSec=1month

и systemctl restart systemd-journald. Полный список параметров — в документации journald.conf.

Docker

В /etc/docker/daemon.json — предел на логи контейнеров, иначе json-file растёт бесконечно:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "20m", "max-file": "5" }
}

Применяется к контейнерам, созданным после перезапуска демона; существующие нужно пересоздать.

MySQL binlog и бэкапы

В my.cnf: binlog_expire_logs_seconds = 259200 (3 суток; в MySQL 5.7 и MariaDB — expire_logs_days = 3). Если реплики нет и point-in-time recovery не используется, binlog можно выключить совсем: skip-log-bin.

Скрипт бэкапа должен сам удалять старое, и только после того, как новая копия успешно записана и проверена размером:

mysqldump --single-transaction shop | gzip > "/var/backups/shop-$(date +%F).sql.gz" \
  && [ "$(stat -c %s "/var/backups/shop-$(date +%F).sql.gz")" -gt 1000000 ] \
  && find /var/backups -name 'shop-*.sql.gz' -mtime +7 -delete

Бэкапы на том же диске, что и база, защищают только от ошибочного DROP TABLE, но не от смерти диска — копируйте наружу.

Отдельный раздел под данные

Когда логи, бэкапы и база живут на одном корневом разделе, переполнение чего угодно валит всё. Вынесите /var/lib/mysql или /data на отдельный диск (на VDS — дополнительный том), а /var/log и /tmp ограничьте. Тогда разросшийся лог остановит только логирование, а база продолжит работать; сессии PHP переживут переполнение корня, только если session.save_path тоже вынесен на отдельный раздел (или в Redis). Для /tmp достаточно строки в /etc/fstab:

tmpfs /tmp tmpfs defaults,size=1G,nosuid,nodev 0 0

Как узнать при 80 %, а не при 100 %

Проверка «сайт отвечает 200» здесь почти бесполезна: главная страница отдаётся из кэша nginx или статикой и после того, как диск заполнился, а падают в это время оформление заказа, вход и загрузка файлов. Проверка доступности с нескольких точек покажет, что сайт снаружи открывается, но код 200 на главной ничего не говорит о том, может ли сервер писать. Внешний мониторинг доступности сайта поймает момент, когда упадёт уже всё, но не даст запаса времени. Нужна метрика с самого сервера и порог.

Самый простой вариант — cron-скрипт, который раз в 10 минут смотрит df и шлёт сообщение в Telegram при превышении. Псевдо-ФС и snap-образы исключаем, иначе на Ubuntu со snapd каждый /snap/core20/... будет «занят на 100 %»:

#!/bin/bash
LIMIT=80
TOKEN=123456:ABC...   # токен бота
CHAT=-1001234567890   # id чата
df -P -x tmpfs -x devtmpfs -x squashfs -x overlay | awk 'NR>1 {gsub("%","",$5); if ($5+0 > '"$LIMIT"') print $6, $5"%"}' | while read -r mnt pct; do
  curl -fsS -m 10 "https://api.telegram.org/bot$TOKEN/sendMessage" \
    -d chat_id="$CHAT" -d text="$(hostname): $mnt занят на $pct" >/dev/null
done

Добавьте второй проход с df -iP для инодов. Слабое место: скрипт живёт на том же сервере (сервер выключился — молчит и скрипт), и о нём нужно помнить при переезде. Если серверов несколько или хочется, чтобы показатели ждали снаружи, удобнее обратная схема: на сервере лежит скрипт, который отдаёт JSON с CPU, памятью, диском и load average, а внешняя служба сама его опрашивает и сравнивает с порогами. В Нотифарио так работает мониторинг ресурсов сервера: в JSON есть массив disks с процентом по каждой точке монтирования, так что отдельный раздел /data не потеряется, порог задаётся в мониторе, а при возврате в норму приходит второе сообщение.

Порог 80 % — не догма: на диске в 40 ГБ это 8 ГБ запаса, обычно на несколько дней, на терабайтном томе разумнее 90 %. Важно, чтобы между уведомлением и остановкой служб оставалось время почистить логи без ночного аврала. И отдельно контролируйте сами бэкапы: диск может быть в норме, а дамп — пустым; как узнать, что cron-задача или бэкап не выполнились, разобрано в отдельной статье.

Коротко

  • Полный диск ломает запись, а не чтение: 500 без строк в логе, разлогинивание, пустые бэкапы и сломанный деплой — это одна причина, а не четыре.
  • df -h и df -i — первые команды при любом непонятном сбое; смотрите на раздел, куда пишет упавшая служба, и на иноды.
  • Место чаще всего едят логи без ротации, journald, docker, бэкапы на том же диске, binlog MySQL и удалённые, но открытые файлы (lsof +L1).
  • Освобождать — обнулением логов, journalctl --vacuum-size, apt clean, PURGE BINARY LOGS; не трогать руками /var/lib/mysql и overlay2.
  • Настроить пределы: logrotate с USR1, SystemMaxUse для journald, max-size для docker, binlog_expire_logs_seconds, ротацию в скрипте бэкапа.
  • Узнавать при 80 %: метрика с сервера плюс порог, снаружи, а не только проверка кода ответа.

Вопросы и ответы

Почему df показывает 100 %, а du находит гораздо меньше?
Чаще всего место держит удалённый, но ещё открытый файл: процесс продолжает писать в него, а du его уже не видит. Найдите такие файлы командой lsof +L1 и обнулите через /proc/PID/fd/N или перезапустите процесс. Второй вариант — файлы записали в каталог (например, /data или /var/lib/mysql) до того, как поверх него смонтировали отдельный раздел; они скрыты точкой монтирования, и du их не видит. Проверить: mkdir /mnt/root && mount --bind / /mnt/root && du -xsh /mnt/root/data.
Как удалить старые логи journald и ограничить его размер?
Разово — journalctl --vacuum-size=200M или --vacuum-time=14d; удаляются только архивные журналы. Постоянно — в /etc/systemd/journald.conf задайте SystemMaxUse=300M и MaxRetentionSec=1month, затем перезапустите systemd-journald.
MySQL не запускается после того, как закончилось место. Что делать?
Сначала освободите несколько гигабайт на разделе с /var/lib/mysql, затем посмотрите journalctl -u mysql и лог ошибок MySQL. Обычно сервер стартует сам: доводит зафиксированные транзакции по redo-логу и откатывает незавершённые по undo-логу. Если жалуется на повреждение таблиц, выполните mysqlcheck --all-databases. Файлы ibdata1, ib_logfile и binlog руками не удаляйте.
Почему пользователей сайта разлогинивает, хотя сайт открывается?
PHP хранит сессии файлами в /var/lib/php/sessions, и при полном диске новый файл сессии не создаётся. Каждый запрос начинается как гостевой, корзина и авторизация теряются. После освобождения места сессии снова создаются, но пользователям придётся войти заново.
Сколько свободного места нужно держать на сервере?
Практический ориентир — не меньше 15–20 % на корневом разделе и на разделе с базой, а порог уведомления ставить на 80 %. Так между сообщением и остановкой служб остаётся время спокойно почистить логи. На томах в терабайт и больше порог можно поднять до 90 %.
Как проверить иноды и что делать, если они закончились?
df -i показывает занятые и свободные иноды по разделам. Если IUse% близок к 100 при свободном месте, ищите каталоги с миллионами мелких файлов: сессии PHP, кэш, очередь почты, миниатюры. Удалите старые файлы через find с -mtime и настройте их автоматическую очистку.

Не проверяйте вручную — следите автоматически

Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.

Подключить бесплатно Проверить сайт сейчас