Закончилось место на диске: почему сайт падает молча и как узнать заранее
Закончилось место на диске 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 ГБ за год при среднем трафике |
| journald | journalctl --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 binlog | SHOW 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
Что освободить сразу и без риска
Порядок — от безопасного к требующему внимания. Цель — получить несколько гигабайт, чтобы служба снова могла писать, а разбираться уже спокойно.
- Обнулить, а не удалять большие логи.
: > /var/log/nginx/access.logосвобождает место мгновенно и не оставляет открытый дескриптор. Удалять файл, который держит процесс, бесполезно (см. выше). - Ужать journald.
journalctl --vacuum-size=200M— удаляет старые архивные журналы, текущий не трогает. - Очистить кэш пакетов.
apt cleanилиdnf clean all. Ничего не ломает. - Удалить старые ядра.
apt autoremove --purge— оставит текущее и одно предыдущее. Перед этим проверьтеuname -r, чтобы не удалить загруженное. - Docker.
docker image pruneудаляет только висячие слои.docker system prune -aудаляет все остановленные контейнеры (вместе с их записываемым слоем — данные вне volume пропадут), неиспользуемые образы, сети и кэш сборки. Перед запуском посмотритеdocker ps -aи убедитесь, что среди остановленных нет нужных. - Бэкапы старше N дней.
find /var/backups -name '*.sql.gz' -mtime +14 -ls— сначала посмотреть, потом заменить-lsна-delete. Убедитесь, что копии есть где-то ещё. - MySQL binlog. Только из клиента, не
rm:PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY;. Если есть реплика, сначала проверьте её позицию. - Резерв 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 находит гораздо меньше?
Как удалить старые логи journald и ограничить его размер?
MySQL не запускается после того, как закончилось место. Что делать?
Почему пользователей сайта разлогинивает, хотя сайт открывается?
Сколько свободного места нужно держать на сервере?
Как проверить иноды и что делать, если они закончились?
Не проверяйте вручную — следите автоматически
Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.
Подключить бесплатно Проверить сайт сейчас