Три наиболее частых виновника на продакшн-серверах: разросшийся journald которого забыли ограничить, Docker который накапливает слои образов в overlay2, и Nginx который пишет в ротированный лог потому что logrotate забыли настроить правильно. В каждом случае du молчит, df кричит.
Диагностика за 30 секунд
Сначала — убедиться что проблема именно в открытых удалённых файлах а не в чём-то другом:
lsof +L1 2>/dev/null | awk 'NR>1 {sum+=$7} END {printf "%.1f GB held by deleted open files\n", sum/1024/1024/1024}'
+L1 — показать только файлы с link count меньше 1, то есть удалённые но открытые. Если число больше нуля — вот куда делось место.
Посмотреть топ виновников:
lsof +L1 2>/dev/null | awk 'NR>1 {print $7/1024/1024 " MB\t" $1 "\t" $NF}' | sort -rn | head -15
Сценарий 1: journald съел всё
journald по умолчанию не имеет жёсткого лимита и спокойно занимает 4–10 ГБ на активном сервере. При этом старые файлы могут быть «удалены» logrotate но journald держит их открытыми.
Проверить сколько занимает journald прямо сейчас:
journalctl --disk-usage
Archived and active journals take up 8.3G in the file system.
Установить лимит и немедленно очистить:
sudo journalctl --vacuum-size=500M
Закрепить лимит навсегда в конфиге:
sudo nano /etc/systemd/journald.conf
[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
MaxRetentionSec=2weeks
Перезапустить journald:
sudo systemctl restart systemd-journald
Сценарий 2: Docker overlay2
Docker хранит слои образов, build-кеш и тома в /var/lib/docker/overlay2. du /var/lib/docker/ покажет реальный размер, но после удаления контейнеров и образов место не освобождается — слои остаются как кеш.
Проверить что занимает место в Docker:
docker system df -v
Удалить всё неиспользуемое — остановленные контейнеры, образы без тегов, build-кеш:
docker system prune -a --volumes
Если Docker не установлен но /var/lib/docker осталась от предыдущей установки:
sudo du -sh /var/lib/docker/
sudo rm -rf /var/lib/docker/
Ограничить размер Docker log-файлов — по умолчанию они не ротируются:
sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
sudo systemctl restart docker
Сценарий 3: Nginx пишет в удалённый лог
logrotate переименовал access.log в access.log.1 и удалил старый. Nginx не знает об этом — продолжает писать в тот же файловый дескриптор. Файл физически существует под именем дескриптора в /proc, но в директории его нет. du не видит, df видит.
Найти конкретно эту ситуацию:
lsof +L1 | grep nginx
Послать Nginx сигнал переоткрыть логи (без перезапуска, без потери соединений):
sudo nginx -s reopen
Или через systemd:
sudo kill -USR1 $(cat /var/run/nginx.pid)
Предотвратить повторение — в /etc/logrotate.d/nginx должна быть секция postrotate:
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
/usr/sbin/nginx -s reopen
endscript
}
Без postrotate — Nginx будет вечно писать в удалённый файл после каждой ротации.
Освободить место мгновенно без перезапуска процесса
Когда перезапуск невозможен — усечь файл через его дескриптор в /proc. Содержимое обнуляется, место освобождается, процесс продолжает писать в тот же дескриптор.
Найти PID и номер дескриптора:
lsof +L1 | grep -v "^COMMAND" | awk '{print $2, $4, $NF}' | head -10
14821 5w /var/log/nginx/access.log (deleted)
PID = 14821, дескриптор = 5.
Обнулить через truncate:
truncate -s 0 /proc/14821/fd/5
Место освобождается немедленно. Работает для любого процесса — MySQL, PHP-FPM, rsyslog.
Дополнительные причины расхождения df и du
Зарезервированные блоки ext4. По умолчанию 5% диска зарезервировано для root. На диске 200 ГБ это 10 ГБ которые du никогда не покажет.
Проверить:
tune2fs -l /dev/sda1 | grep -E "Reserved|Block count|Block size"
Посчитать сколько реально зарезервировано:
python3 -c "
import subprocess
out = subprocess.check_output(['tune2fs','-l','/dev/sda1']).decode()
blocks = int([l.split(':')[1] for l in out.split('\n') if 'Reserved block count' in l][0])
size = int([l.split(':')[1] for l in out.split('\n') if 'Block size' in l][0])
print(f'Reserved: {blocks * size / 1024**3:.1f} GB')
"
Уменьшить резерв до 1% на дата-дисках:
sudo tune2fs -m 1 /dev/sda1
tmpfs невидим через du /.
df -h | grep -E "tmpfs|/run|/dev/shm"
/run может занимать несколько гигабайт если в нём накопились сокеты или временные файлы. Очистить:
sudo find /run -name "*.tmp" -delete
Скрипт ежедневного контроля
Добавить в cron — будет ловить ситуацию до того как диск забьётся:
#!/bin/bash
DELETED_GB=$(lsof +L1 2>/dev/null | awk 'NR>1 {sum+=$7} END {printf "%.1f", sum/1024/1024/1024}')
DISK_PCT=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$DISK_PCT" -gt 80 ] || [ $(echo "$DELETED_GB > 1" | bc) -eq 1 ]; then
echo "Host: $(hostname)
Disk used: ${DISK_PCT}%
Held by deleted files: ${DELETED_GB} GB
$(lsof +L1 2>/dev/null | awk 'NR>1 {print $7/1024/1024 " MB " $1}' | sort -rn | head -5)" | \
mail -s "Disk alert: $(hostname)" admin@example.com
fi
Шпаргалка
| Задача | Команда |
|---|---|
| Сколько держат удалённые файлы | lsof +L1 2>/dev/null | awk 'NR>1 {sum+=$7} END {print sum/1024/1024/1024 " GB"}' |
| Топ виновников | lsof +L1 2>/dev/null | awk 'NR>1 {print $7/1024/1024 " MB\t" $1}' | sort -rn | head -10 |
| Обнулить файл без перезапуска | truncate -s 0 /proc/PID/fd/N |
| Очистить journald | sudo journalctl --vacuum-size=500M |
| Состояние Docker | docker system df -v |
| Очистить Docker | docker system prune -a --volumes |
| Переоткрыть логи Nginx | sudo nginx -s reopen |
| Зарезервировано в ext4 | tune2fs -l /dev/sda1 | grep Reserved |
| Уменьшить резерв до 1% | sudo tune2fs -m 1 /dev/sda1 |
| Проверить tmpfs | df -h | grep tmpfs |