SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

df показує full, а du ні: де подівся простір

VPS показує помилку «disk full», але du не знаходить причину. Знайдіть через lsof видалений файл, який процес досі тримає відкритим, без reboot.

Чому df показує, що диск заповнений, а du — ні

df показує, що диск заповнений, тоді як du не знаходить цієї зайнятої ємності, оскільки процес усе ще утримує відкритий файл, який уже видалено. Видалення файлу прибирає його ім’я з каталогу. Блоки даних звільняються лише після закриття останнього відкритого файлового дескриптора, що посилається на цей inode. du обходить імена файлів, тому нічого не підраховує. df запитує у файлової системи кількість виділених блоків, тому продовжує враховувати файл, у якого більше немає імені.

У цьому посібнику відтворено таку ситуацію на звичайному Ubuntu VPS за допомогою вже встановлених інструментів. Ви знайдете процес, який утримує файл, через /proc і звільните місце без перезавантаження. Нижче також описано інші причини такого самого симптому: таблиця inode без вільних записів, файли, приховані під точкою монтування, і блоки, зарезервовані для root.

Виконуйте кожну команду та переглядайте власний результат. Значення залежать від вашого диска, тому порівнюйте стан до і після на власній машині, а не з цифрою, наведеною в посібнику.

Що підраховують df і du

df (disk free) отримує дані безпосередньо від кожної змонтованої файлової системи: скільки блоків у ній існує, скільки виділено та скільки вільно. Він не відкриває каталоги. Результат охоплює всі виділені блоки, зокрема блоки файлу, на який не вказує жоден запис каталогу.

du (disk usage) працює навпаки. Він починає з указаного шляху, читає каталоги, отримує атрибути кожного знайденого запису та підсумовує блоки. Файл без імені для нього невидимий. Так само він не бачить каталоги, які не має права читати. Тому звичайний користувач отримує менший підсумок, ніж root. Виконайте du від імені sudo, перш ніж робити висновки з порівняння.

Під час кожного порівняння важливі два параметри.

  • -x обмежує роботу du однією файловою системою. Без нього du / переходить у кожну файлову систему, змонтовану нижче /, і формує підсумок, який df / взагалі не вимірював.
  • -s виводить один підсумковий рядок для кожного аргументу, а не окремий рядок для кожного каталогу.

Отже, для потрібної файлової системи слід запускати цю пару команд паралельно.

df -h /
sudo du -xhs / 2>/dev/null

df відповідає одразу. du може виконуватися кілька хвилин у великій файловій системі, оскільки отримує атрибути кожного файлу на шляху. Якщо два підсумки суттєво відрізняються, а du запущено від імені root із -x, зайнятий простір належить об’єкту, який не має імені.

Навмисно відтворіть розбіжність

Виконайте ці дії на тестовому VPS. Усе нижче використовує bash і coreutils, тому нічого встановлювати не потрібно.

Зафіксуйте початковий стан файлової системи, у якій міститься /var/tmp.

cd /var/tmp
df -h .
df --output=used -B1 .

Друга команда виводить використані байти без округлення. Завдяки цьому підсумкова перевірка буде точною.

Тепер створіть файл. Його розмір визначається за вільним простором, який повідомляє сама машина, тому приклад підходить для диска будь-якого розміру.

free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .

$(...) — це підстановка команди: оболонка виконує команду всередині неї, а її вивід стає значенням free. Якщо цей синтаксис вам незнайомий, у матеріалі підстановка команд у bash він належно пояснюється. fallocate резервує реальні блоки без записування даних, тому команда завершується миттєво. У файловій системі, яка цього не підтримує, команда завершується помилкою, а head -c $((free / 10)) /dev/zero > ghost.bin виконує те саме завдання, записуючи байти.

Порівняйте цей df -h . із тим, який ви зафіксували. Значення у стовпці використаного простору збільшилося, а значення у стовпці доступного простору зменшилося.

Тепер відкрийте файл в іншому процесі, а потім видаліть його.

sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null

Уся суть полягає в перенаправленні. sleep infinity < ghost.bin & запускає фоновий процес, стандартним введенням якого є цей файл, тому оболонка відкриває файл і передає дескриптор процесу sleep, який утримує його відкритим. $! зберігає ідентифікатор процесу цього фонового завдання. Потім rm видаляє ім’я файлу, доки дескриптор залишається відкритим.

Перегляньте вивід. ls не може знайти файл, оскільки його ім’я видалено. du повернувся приблизно до початкового значення, оскільки він обходить імена. df не змінився, оскільки блоки все ще виділені. Тепер файлова система й дерево каталогів не узгоджуються, а розбіжність між ними — це файл, який ви щойно видалили.

Знайдіть процес, який утримує видалений файл

Кожен відкритий файловий дескриптор з’являється в /proc/<pid>/fd/ як символічне посилання на файл, якого він стосується. Коли файл від’єднано, ядро позначає ціль цього посилання як deleted. Отже, щоб знайти процес, який утримує файл, потрібно знайти посилання, ціль якого має це позначення.

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

-lname зіставляє ціль символічного посилання, а не його ім’я, %p виводить шлях до дескриптора, а %l — об’єкт, на який він вказує. Ідентифікатор процесу є другим елементом виведеного шляху. Запускайте команду з sudo, оскільки без цього можна читати лише /proc/<pid>/fd власних процесів. Перенаправлення stderr відкидає повідомлення про процеси, які завершилися під час обходу find.

На зайнятому сервері в будь-який момент можуть утримуватися кілька видалених файлів, і більшість із них невеликі та безпечні. Відсортуйте їх за розміром, щоб найбільші опинилися на початку списку.

sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
  target=$(readlink "$fd" 2>/dev/null) || continue
  case "$target" in
    *"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
  esac
done' | sort -rn | head

stat -L переходить за посиланням безпосередньо до inode, тому %s показує розмір файлу, який більше не має імені. Сортування за цим числом розміщує найбільший файл першим.

Потім визначте процес, якому належить потрібний дескриптор. У верхньому рядку цього списку містяться обидва потрібні числа, тому спочатку запишіть їх у змінні, замінивши PID і N значеннями з виведеного вами списку.

pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"

ps називає програму та показує, як довго вона працює. stat -L виводить розмір і кількість виділених блоків видаленого inode. Разом ці дані відповідають на головне запитання: який сервіс утримує цей файл відкритим.

Якщо на машині вже встановлено lsof, sudo lsof +L1 виводить відкриті файли, кількість посилань на які зменшилася до нуля, і показує їхні розміри в одній таблиці. На мінімальному образі Ubuntu цього пакета немає, а встановлення пакета у файлову систему без вільного місця саме може завершитися помилкою, тому обхід через /proc — це варіант, який працює завжди.

Звільнення місця без перезавантаження

Перезавантаження справді усуває проблему, але це неправильна перша дія: воно зупиняє сервіс і знищує докази. Є чотири м’якші варіанти. Використовуйте їх у наведеному порядку.

Спочатку скопіюйте дані, якщо вони вам ще потрібні. Читання шляху до дескриптора читає активний inode.

sudo cp /proc/<pid>/fd/<n> /root/recovered.log

Це єдиний випадок, коли видалений файл легко повернути. Саме тому відновлення файлів, видалених за допомогою rm -rf починається з перевірки, чи утримує процес файл відкритим. Після закриття останнього дескриптора цей спосіб більше недоступний.

По-друге, очистьте файл через дескриптор. Шлях /proc веде до того самого inode, тому його усічення звільняє блоки, а процес продовжує працювати.

sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /

Це коректно працює, коли процес записує у файл, відкритий у режимі append, оскільки кожен запис тоді виконується в поточному кінці файлу. Якщо файл відкрито не в цьому режимі, процес зберігає стару позицію запису. Тому наступний запис потрапляє далеко за межі файлу та відтворює його з розрідженою ділянкою на початку. Розріджена ділянка не виділяється, тому блоки залишаються вільними, а df зберігає звільнений простір. Змінюється лише розмір: повторно виконайте sudo stat -L "/proc/$pid/fd/$n" після запису процесу. Команда покаже старий розмір поруч із кількістю блоків, яка йому більше не відповідає. Перезапустіть процес, якщо потрібно, щоб розмір також починався з нуля.

По-третє, попросіть сервіс повторно відкрити журнали. Типовий практичний варіант цієї проблеми — daemon, у якого файл журналу видалили під час роботи. Багато daemon повторно відкривають файли журналів після отримання сигналу: nginx використовує SIGUSR1, а rsyslog — SIGHUP. Перевірте документацію саме для потрібного daemon, а не вгадуйте, оскільки неправильний сигнал може зупинити daemon.

sudo systemctl kill -s USR1 nginx

По-четверте, перезапустіть unit. sudo systemctl restart <unit> закриває всі дескриптори, які утримував старий процес, тому блоки гарантовано повертаються. У наведеному прикладі дескриптор утримує sleep, який ви запустили самостійно, тому достатньо завершити цей процес.

kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

Порівняйте використані байти зі значенням, яке ви записали до створення файлу. Значення знову збігаються, а find більше не показує ваш дескриптор. Варто перевіряти результат тією самою командою, яка виявила проблему.

Стежити за зміною цього значення простіше, ніж знову й знову запускати df вручну. watch повторює команду через заданий інтервал і виводить результат на тому самому місці, тому watch df -h / показує, як змінюється стовпець використаного простору в міру його повернення.

Коли підсумки збігаються, але диск усе ще заповнений

Якщо df і root du -x узгоджуються між собою, видаленого файла немає. Причини, що залишилися, мають іншу природу, і для кожної є окрема перевірка.

Немає inode, хоча блоки ще доступні

Inode зберігає метадані одного файлу. ext4 створює фіксовану кількість inode під час створення файлової системи, тому файловій системі можуть знадобитися всі inode, навіть якщо в ній ще є вільні блоки. Нові файли тоді не створюються, хоча df -h показує вільне місце.

df -h /
df -i /

Перша команда підраховує блоки, а друга — inode. Порівняйте стовпець use в обох виводах. Низьке використання блоків і граничне використання inode означають, що проблема полягає у великій кількості дуже малих файлів.

df не приймає -i і --output під час одного запуску. Тому, якщо потрібно отримати необроблені значення для читання або передавання іншій команді, виберіть поля inode за іменем і не вказуйте -i.

df --output=itotal,iused,iavail,ipcent /

Ці стовпці містять ті самі облікові дані, які виводить df -i, у форматі, який можна розібрати.

Знайдіть файли, підраховуючи записи, а не байти.

sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head

Повторюйте ту саму команду на один рівень нижче для каталогу, який опинився на першому місці, доки не знайдете дерево каталогів, у якому створюються ці файли. Якщо ваш du не підтримує --inodes, тоді sudo find /var -xdev -type f | wc -l повільно підраховує вміст піддерева.

Рішення — видалити або перемістити ці файли. Додати inode до наявної файлової системи ext4 неможливо, оскільки їхню кількість фіксують під час mkfs. Тому для збільшення цієї кількості потрібно заново створити файлову систему й відновити дані з резервної копії. XFS виділяє inode за потреби, тому не має такого фіксованого обмеження. На машині, що запускає контейнери, обидва ліміти досягаються швидше, ніж у більшості випадків, оскільки шари образів містять багато малих файлів. На такій машині очищення дискового використання Docker на VPS є цільовим рішенням і звільняє значно більше місця, ніж загальне очищення файлової системи.

Простір, прихований під точкою монтування

Каталог може містити файли ще до того, як на нього щось змонтують. Змонтуйте файлову систему поверх цього каталогу — файли під нею залишаться саме там, де були: вони й надалі займатимуть місце, враховуватимуться в df і більше не будуть доступні за іменем. du не може їх бачити, оскільки монтування закриває доступ до них.

Покажемо це за допомогою tmpfs, якій не потрібне вільне місце на диску. Для цього потрібна машина, на якій вам дозволено монтувати файлові системи, тому приклад працює на KVM VPS.

sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered

Середній ls показує порожній каталог. Копія нікуди не зникла: вона й досі міститься в кореневій файловій системі та знову з’явиться одразу після демонтування. Тепер уявіть сервіс, який протягом місяця записував журнали до цього шляху, перш ніж хтось змонтував поверх нього том.

Щоб знайти такі файли на запущеному сервері, змонтуйте кореневу файлову систему вдруге в іншому місці. Bind-монтування показує одну файлову систему без файлових систем, змонтованих усередині неї.

sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck

Усе, що є в цьому списку, але відсутнє за звичайним шляхом, приховане під точкою монтування. Після завершення роботи демонтуйте bind-монтування, інакше наступний du без -x порахує ті самі файли двічі.

Блоки, зарезервовані для root

ext4 резервує частину блоків для користувача root, щоб заповнений диск не завадив root увійти в систему та відновити її. Процес, запущений від імені звичайного користувача, першим досягає цього обмеження, тоді як df ще показує певний вільний простір. Перевірте це налаштування у власній файловій системі, а не покладайтеся на значення за замовчуванням.

dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'

Команда виводить загальну кількість блоків і кількість зарезервованих блоків в однакових одиницях, тому їхнє співвідношення можна обчислити безпосередньо. df показує в колонці available простір, який ще може використовувати звичайний користувач. Тому сума used і available менша за загальний розмір. Різниця — це резерв.

Змініть його за допомогою sudo tune2fs -m <percent> "$dev". Зміна застосовується негайно, повторне монтування не потрібне. Зменшити резерв на окремій файловій системі для даних може бути доцільно. Для кореневої файлової системи залиште достатньо резерву, щоб root і надалі міг записувати дані, оскільки кореневу файлову систему, у якій зовсім не залишилося вільного місця, значно складніше відновити. tune2fs працює з ext2, ext3 і ext4. В XFS еквівалентного налаштування немає.

Де du без додаткових параметрів вводить в оману

Чотири особливості du дають підсумки, які здаються неправильними.

  • Жорсткі посилання: du враховує inode лише один раз, навіть якщо на нього вказує кілька імен, тому дерево, заповнене жорсткими посиланнями, показує менший обсяг, ніж сума його файлів.
  • Розріджені файли: du показує фактично виділені блоки, а ls -l — видимий розмір. Додайте --apparent-size, щоб побачити інше значення.
  • Права доступу: під час запуску від імені звичайного користувача du пропускає те, що не може прочитати, і занижує результат. Повідомлення про помилки, які він виводить, зазвичай перенаправляють до /dev/null і більше не переглядають.
  • Межі файлових систем: без -x du / враховує кожну файлову систему, змонтовану нижче /, тому його підсумок може перевищувати значення, яке показує df /.

df також має одну особливість, яку варто знати. Він показує кожну файлову систему окремо, тому запускайте його для точного шляху, до якого намагається записати процес. Окремий /boot заповнюється за власним графіком у міру накопичення пакетів ядра, а видалення старих ядер в Ubuntu — це інше завдання, ніж звільнення місця в /.

Робочий порядок дій під час реального інциденту

  1. Виконайте df -h <path> і df -i <path> у файловій системі, до якої мав записуватися процес, що завершився помилкою, а не автоматично в /.
  2. Виконайте sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h, а потім переходьте до найбільшого каталогу.
  3. Якщо du не може пояснити обсяг, який df показує як використаний, перевірте /proc на видалені файли, які ще відкриті.
  4. Якщо результати збігаються, підключіть файлову систему через bind mount в інше місце та перевірте, чи немає файлів під точкою монтування.
  5. Якщо ліміт досягнуто через використання inode, підраховуйте файли, а не байти.

На кожному кроці використовується команда, результат якої можна прочитати. Саме це відрізняє усунення проблеми від припущень.

FAQ

Чому df показує, що диск заповнений, хоча du знаходить набагато менше даних?

Зазвичай причина полягає у файлі, який видалили, поки процес усе ще мав його відкритим. Видалення файлу прибирає його запис із каталогу, тому du більше не має імені, за яким можна перейти, і припиняє враховувати цей файл. Inode та його блоки залишаються виділеними, доки не закриється останній дескриптор, а df враховує виділені блоки. Виконайте пошук у /proc/<pid>/fd символічних посилань, ціль яких позначена як видалена. Так ви знайдете і файл, і процес, який його утримує відкритим. Перш ніж покладатися на це порівняння, переконайтеся, що запустили du від імені root і з параметром -x, оскільки звичайний користувач без повідомлення пропускає каталоги, які не може читати.

Як знайти видалений файл, який усе ще відкритий, без lsof?

Використайте власний облік відкритих дескрипторів ядра. sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null перелічує всі дескриптори, що вказують на файл без імені, а ідентифікатор процесу міститься в надрукованому ним шляху. Виконайте sudo stat -Lc %s для одного з таких шляхів дескриптора, щоб отримати його розмір. Потім можна відсортувати результати й вибрати потрібний файл. Для цього не потрібен жоден пакет. Це важливо, оскільки встановлення пакета у файлову систему без вільного місця може завершитися помилкою.

Чи можна звільнити місце, не завершуючи процес?

Іноді. sudo truncate -s 0 /proc/<pid>/fd/<n> звертається до того самого inode через дескриптор і звільняє його блоки, поки процес продовжує працювати. Це найкращий варіант, якщо процес відкрив файл у режимі append, оскільки записи завжди додаються в поточний кінець файлу. Якщо це не так, позиція запису залишається на попередньому місці, а наступний запис відтворює файл із розрідженою ділянкою на початку. Тому повідомлений розмір знову збільшується, хоча блоки під цією розрідженою ділянкою залишаються вільними. Перезапуск unit або надсилання йому сигналу для повторного відкриття журналів, названого в документації, усуває проблему без створення розрідженого файлу.

df показує вільне місце, але запис усе одно не вдається. У чому ще може бути причина?

Перевірте inode за допомогою df -i для того самого шляху, оскільки файлова система з вільними блоками, але без вільних inode, не може створювати нові файли. Перевірте, чи виконується запис від імені користувача без прав root у файлову систему ext4, де залишилися лише зарезервовані блоки. Це покаже sudo tune2fs -l для відповідного пристрою. Переконайтеся, що перевіряєте файлову систему, до якої фактично спрямовано запис, оскільки окремий /boot або /var заповнюється незалежно від /.

Чому du повідомляє більший загальний обсяг, ніж df?

du без -x переходить у кожну файлову систему, змонтовану всередині вказаного шляху. У результаті він підсумовує кілька файлових систем, тоді як df описує лише одну. Bind mounts погіршують ситуацію, оскільки ті самі файли враховуються один раз для кожного шляху, за яким вони доступні. Додайте -x, щоб обмежити du однією файловою системою, і передайте df той самий шлях, щоб обидві команди описували один і той самий об’єкт.