Pełny dysk: jak znaleźć i zwolnić miejsce na serwerze Linux

Opublikowano 12 min czytania

Gdy df melduje 100%, a du niczego nie znajduje: cała droga od pomiaru do zwolnienia miejsca, dla Debiana 12 i 13 oraz Ubuntu 22.04 i 24.04.

Zapełniony nośnik rzadko zgłasza się grzecznie. Zwykle najpierw pada jakaś usługa: MariaDB wpisuje do logu OS error 28, nginx odpowiada kodem 500, backup przerywa się z komunikatem write error: No space left on device, a przy naprawdę pechowym zbiegu okoliczności nikt nie wejdzie już na maszynę przez SSH, bo sshd nie potrafi utworzyć plików sesji. Ten artykuł pokazuje całą drogę: zmierz, znajdź, zwolnij, sprawdź. Razem z dwoma przypadkami, na których większość poradników się kończy, czyli gdy df melduje zapełnienie, a du niczego nie znajduje.

Najpierw pomiar: która partycja jest właściwie pełna?

Zanim cokolwiek skasujesz, musi być jasne, którego systemu plików dotyczy problem. Pełny /boot ma zupełnie inne przyczyny niż pełny /var.

df -h
df -hT -x tmpfs -x devtmpfs -x squashfs

Drugi wiersz ukrywa pseudosystemy plików. W Ubuntu jest to szczególnie przydatne, bo tam każda zainstalowana aplikacja Snap pojawia się jako osobny loop mount squashfs i zaśmieca wynik. Te loop mounty są zresztą zawsze zapełnione w 100%, to normalne i nie stanowi problemu.

Ważna jest kolumna Mounted on. Typowe układy na serwerach:

Punkt montowaniaTypowa przyczyna zapełnienia
/logi, Docker, cache apt, dane aplikacji
/bootstare kernele i ich obrazy initramfs
/varjournal, rsyslog, kolejka pocztowa, bazy danych, Docker
/tmpprzerwane uploady, sesje, pozostałości po budowaniu

Jeszcze jeden szczegół, który często wprowadza zamieszanie: ext4 domyślnie rezerwuje pięć procent pojemności dla użytkownika root. Usługa działająca jako www-data albo mysql dostanie więc No space left on device już w chwili, gdy root w tej samej sekundzie zapisuje jeszcze bez najmniejszego problemu. Na czystych partycjach z danymi (czyli nie na głównym systemie plików) możesz tę rezerwę bezpiecznie obniżyć:

tune2fs -m 1 /dev/sdb1

Na / rezerwa powinna zostać. To właśnie ten margines, dzięki któremu w ogóle da się jeszcze naprawić zapchany system.

Jak używać du, żeby się nie zgubić

Klasyczne wejście to schodzenie poziom po poziomie, zawsze z -x:

du -xh --max-depth=1 / | sort -h
du -xh --max-depth=1 /var | sort -h

-x to najważniejszy przełącznik w całym artykule. Trzyma du wewnątrz jednego systemu plików i nie pozwala poleceniu zanurkować w /proc, /sys, zamontowane udziały sieciowe czy dyski z backupami. Bez -x wyszukiwanie ciągnie się minutami i zwraca liczby, które z pełną partycją nie mają nic wspólnego. sort -h poprawnie sortuje rozmiary czytelne dla człowieka, więc największy kawałek ląduje na dole.

Jeśli wolisz klikać się interaktywnie, zainstaluj ncdu i uruchom go również z -x:

apt-get install -y ncdu
ncdu -x /

Pojedyncze duże pliki znajdziesz szybciej wprost:

find /var -xdev -type f -size +100M -exec ls -lh {} +

Dwie pułapki regularnie prowadzą tu do błędnych wniosków. Po pierwsze: du liczy zajęte bloki, a nie logiczny rozmiar pliku. Przy plikach rzadkich (tablespace'y baz danych, obrazy dysków wirtualnych) obie wartości rozjeżdżają się bardzo mocno. Porównanie to uwidacznia:

du -sh /var/log
du --apparent-size -sh /var/log

Po drugie: du liczy twarde dowiązania tylko raz. Pracując bez roota, dostaniesz dodatkowo wiersze w rodzaju du: cannot read directory '/var/lib/private': Permission denied, a wraz z nimi systematycznie zaniżone sumy. Wszystkie analizy prowadź więc jako root albo przez sudo.

Jak ograniczyć journal systemd

Na serwerach journal to najczęstszy cichy pożeracz miejsca. Domyślnie wolno mu zająć dziesięć procent systemu plików, z górnym limitem czterech gigabajtów, a dodatkowo journald zostawia wolne 15% systemu plików. Na dysku o pojemności 500 gigabajtów daje to nawet cztery gigabajty samego logu.

journalctl --disk-usage

Tu pojawia się prawdziwa różnica między dystrybucjami, którą wiele poradników przemilcza. To, czy journal w ogóle ląduje na dysku, zależy przy domyślnym ustawieniu Storage=auto wyłącznie od tego, czy istnieje katalog /var/log/journal:

ls -d /var/log/journal
ls -d /run/log/journal

Na minimalnych instalacjach Debiana oraz na wielu obrazach chmurowych Debiana 12 i Debiana 13 katalog /var/log/journal nie istnieje. Journal leży wtedy w /run/log/journal, czyli w RAM, znika przy każdym restarcie i w ogóle nie obciąża dysku. Obciąża za to pamięć RAM. Ubuntu Server 22.04 i 24.04 zakładają ten katalog zazwyczaj od razu i logują trwale na dysk. Sprawdzaj, zamiast zgadywać.

Natychmiastowe zwolnienie miejsca:

journalctl --rotate
journalctl --vacuum-size=200M
journalctl --vacuum-time=7d

Poprzedzające wywołanie --rotate nie jest ozdobnikiem: opcje vacuum kasują wyłącznie już zarchiwizowane pliki journala, nigdy tego aktualnie aktywnego. Jeśli aktywny plik stanowi większość, to bez wcześniejszej rotacji pozornie nic się nie dzieje, a właśnie na tym potykają się czytelnicy kopiujący polecenie z posta na forum.

Trwałe ograniczenie zapisz we własnym pliku, żeby późniejsze aktualizacje pakietów niczego nie nadpisały:

mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSystemMaxUse=200M\nRuntimeMaxUse=50M\n' > /etc/systemd/journald.conf.d/00-size.conf
systemctl restart systemd-journald

Kontrola, czy naprawdę zadziałało: journalctl --disk-usage musi teraz meldować mniejszą wartość, a df -h musi pokazywać więcej wolnego miejsca. Jeśli rozmiar journala maleje, a df pozostaje bez zmian, to jakiś proces trzyma jeszcze otwarte skasowane pliki. Więcej o tym niżej.

Druga różnica między dystrybucjami: na klasycznych instalacjach serwerowych Debiana i Ubuntu często działa równolegle rsyslog i zapisuje te same komunikaty drugi raz do /var/log/syslog. Na minimalnych obrazach Ubuntu (chmura, kontener) rsyslog z kolei nie występuje. Sprawdź to poleceniem ls -l /var/log/syslog. Jeśli plik istnieje i jest ogromny, to problemem nie jest journal, tylko brakująca albo zepsuta reguła logrotate w /etc/logrotate.d/.

Cache apt i stare kernele

Pobrane pakiety zostają na dysku po zakończonej instalacji. Na długo działającym serwerze szybko robi się z tego kilka gigabajtów.

du -sh /var/cache/apt
apt-get clean
du -sh /var/lib/apt/lists

apt-get clean opróżnia /var/cache/apt/archives całkowicie, apt-get autoclean tylko te pakiety, których nie ma już w źródłach. Czego clean nie rusza, to listy pakietów w /var/lib/apt/lists. Przy wielu podpiętych źródłach mogą one urosnąć do kilkuset megabajtów, a odbudować da się je bez ryzyka:

rm -rf /var/lib/apt/lists/*
apt-get update

Drugi wiersz nie jest przy tym dodatkiem, tylko obowiązkiem, i to natychmiastowym. Między skasowaniem list a kolejnym apt-get update apt nie zna ani jednego pakietu: każde apt-get install w tym stanie kończy się komunikatem E: Unable to locate package ... i kodem wyjścia 100, mimo że pakiet oczywiście istnieje w źródłach.

Drugi klasyk to stare kernele. Ubuntu przez unattended-upgrades instaluje na bieżąco nowe kernele, ale stare sprząta tylko wtedy, gdy wyraźnie na to pozwolisz. Każdy kernel razem z initramfs zajmuje około 100 do 150 megabajtów w /boot, które często ma tylko od 512 megabajtów do jednego gigabajta.

uname -r
dpkg -l 'linux-image-*'
apt-get autoremove --purge

Działający kernel z uname -r nigdy nie zostanie przy tym usunięty, tak samo jak ten najnowszy. W Ubuntu zabezpieczysz się na przyszłość, wpisując w /etc/apt/apt.conf.d/50unattended-upgrades wiersz Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";. Na Debianie 12 i 13 unattended-upgrades nie jest domyślnie aktywne, tam /boot rośnie tylko wtedy, gdy ktoś regularnie aktualizuje ręcznie i nigdy nie sprząta.

Gdy /boot jest już pełny, a apt nie przechodzi do końca

To przypadek, który naprawdę boli. Typowe komunikaty w oryginalnym brzmieniu:

update-initramfs: failed for /boot/initrd.img-6.8.0-60-generic with 1.
dpkg: error processing package linux-image-6.8.0-60-generic (--configure):
 installed linux-image-6.8.0-60-generic package post-installation script subprocess returned error exit status 1
E: Sub-process /usr/bin/dpkg returned an error code (1)

Baza pakietów jest teraz w stanie niedokończonym, a każde kolejne wywołanie apt wykłada się w tym samym miejscu. Droga wyjścia, w tej kolejności:

  1. Zanotuj uname -r. Tej wersji nie ruszasz pod żadnym pozorem.
  2. Wypisz ls -lh /boot i wyszukaj najstarszą, niedziałającą wersję.
  3. Skasuj wyłącznie jej initrd.img-*, nie vmlinuz-*. Plik initramfs jest zdecydowanie największy i da się go w każdej chwili wygenerować od nowa.
  4. Wykonaj apt-get -f install, żeby dpkg mógł dokończyć przerwaną konfigurację.
  5. Dopiero potem apt-get autoremove --purge, żeby stare pakiety zniknęły czysto, razem z wpisem w GRUB-ie.
  6. Uruchom update-grub i przeczytaj wynik.

Czego robić nie należy: kasować na oślep poleceniem rm plików kernela z /boot i ignorować całą resztę. dpkg nadal będzie wtedy uważał, że pakiety są zainstalowane, GRUB zaoferuje wpisy prowadzące donikąd, a kolejny restart skończy się w awaryjnym wierszu poleceń GRUB-a. Jeśli już skasowałeś, spójność przywrócisz przez apt-get install --reinstall danego pakietu albo przez dpkg --purge, a potem koniecznie update-grub.

Kontrola: df -h /boot pokazuje znowu wolne miejsce, dpkg -l 'linux-image-*' wypisuje już tylko dwa albo trzy wpisy ze statusem ii, a wynik update-grub wymienia dokładnie te kernele, które faktycznie leżą w /boot.

Docker, Snap i logi kontenerów

Na hostach z Dockerem odpowiedź leży prawie zawsze w /var/lib/docker. Nie zgaduj, tylko zapytaj:

docker system df
docker system df -v

Wariant szczegółowy wyraźnie rozdziela obrazy, kontenery, wolumeny i cache budowania. Potem sprzątaj celowo:

docker image prune -a
docker builder prune
docker system prune -a

Słowo ostrożności przy --volumes: ten przełącznik kasuje także wolumeny, do których nie jest podpięty żaden działający kontener. Jeśli trzymasz bazę danych w nazwanym wolumenie, a kontener właśnie zatrzymałeś, tracisz w ten sposób dane. Bez aktualnego backupu docker system prune -a --volumes nie ma czego szukać na serwerze produkcyjnym.

Niedoceniana pozycja to logi kontenerów w /var/lib/docker/containers/*/*-json.log. Domyślny sterownik json-file bez wyraźnego ustawienia nie rotuje w ogóle. Gadatliwy kontener zapisuje w ten sposób przez kilka miesięcy dwucyfrowe liczby gigabajtów do jednego pliku. Lekarstwo w /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}

Potem systemctl restart docker. Uwaga, o czym pisze mało który poradnik: ustawienie obowiązuje tylko dla nowo utworzonych kontenerów. Istniejące zachowują starą konfigurację, dopóki nie zostaną założone od nowa, przy Compose więc przez docker compose up -d --force-recreate.

Wersje Dockera wyraźnie się tu różnią. Debian 12 dostarcza docker.io 20.10, Debian 13 przynosi 26.1, Ubuntu 22.04 i 24.04 są już przy 29.1. Im nowsza wersja, tym więcej miejsca zajmuje cache BuildKit i tym ważniejsze staje się docker builder prune, którego samo docker system prune nie zawsze opróżnia do końca.

W Ubuntu dochodzi jeszcze Snap. Stare rewizje zostają na dysku jako wyłączone i dalej zajmują miejsce:

snap list --all
snap set system refresh.retain=2

Wartość 2 to minimum akceptowane przez snapd, mniejsze wartości zostaną odrzucone komunikatem o błędzie. Debian nie ma Snapa na pokładzie, ten fragment po prostu tam odpada.

df melduje zapełnienie, du niczego nie znajduje

Teraz ciekawy przypadek. df -h pokazuje 100%, a suma z du daje tylko połowę. Wiarygodnych przyczyn jest dokładnie cztery.

Pliki skasowane, ale wciąż otwarte

To zdecydowanie najczęstszy powód. Ktoś wykonał rm /var/log/riesig.log, podczas gdy usługa trzymała ten plik nadal otwarty. Wpis w katalogu zniknął, dlatego du niczego już nie widzi. Bloki pozostają zajęte, dopóki nie zamknie się ostatni deskryptor pliku, dlatego df widzi je dalej.

apt-get install -y lsof
lsof +L1

W kolumnie NLINK pojawia się wtedy 0, a za ścieżką widnieje (deleted). Jeśli lsof niczego nie znajdzie, nie ma żadnego wyniku, a kod powrotu wynosi 1, co nie jest błędem. Bez lsof da się też wprost przez system plików procesów:

ls -l /proc/*/fd 2>/dev/null | grep deleted

Ważne: wykonaj to koniecznie jako root. Jako zwykły użytkownik widzisz tylko własne deskryptory i przeoczysz dokładnie te usługi systemowe, które w dziewięciu przypadkach na dziesięć są sprawcą.

Czystą drogą do zwolnienia miejsca jest restart usługi, na przykład systemctl restart rsyslog. Jeśli restart nie wchodzi w grę, plik da się skrócić do zerowej długości przez jego deskryptor. Identyfikator procesu i numer deskryptora pochodzą z wyniku lsof:

truncate -s 0 /proc/1234/fd/7

To natychmiast zwalnia bloki, a proces pisze dalej. Metoda jest przeznaczona wyłącznie dla zwykłych plików logów otwartych w trybie dopisywania. Na plikach baz danych, obrazach maszyn wirtualnych i wszystkim innym z dostępem swobodnym nie wolno jej nigdy stosować, tam prowadzi do utraty danych.

Po czym poznasz, że się udało: df -h od razu pokazuje więcej wolnego miejsca, a lsof +L1 nie wypisuje już tego wpisu. Drugi przebieg du nie zmieni się natomiast wcale, bo tam plik i tak był wcześniej niewidoczny. To także dokładnie ten powód, dla którego restart serwera pozornie rozwiązuje problem w magiczny sposób.

Pliki przykryte punktem montowania

Klasyk: ktoś zapisał dane do /mnt/backup, zanim właściwy dysk został tam zamontowany. Dane leżą dalej na głównym systemie plików, ale przykrywa je montowanie założone na wierzchu. Widać to dopiero przy drugim spojrzeniu na ten sam system plików:

mkdir -p /mnt/rootview
mount --bind / /mnt/rootview
du -xh --max-depth=2 /mnt/rootview | sort -h
umount /mnt/rootview

Bind mount jest bezpieczny, niczego nie przemontowuje, a usuwa się go przez umount.

Rezerwa roota i błąd uprawnień

Wspomniane już pięć procent rezerwy ext4 wyjaśnia lukę między „jeszcze nie całkiem pełny” a „usługi już się wywracają”. I wreszcie: kto uruchamia du bez roota, ten nie widzi całych drzew katalogów. Rzekoma różnica jest wtedy po prostu problemem z uprawnieniami. Na btrfs i ZFS w grę wchodzą dodatkowo snapshoty, których du również nigdy nie pokazuje.

Gdy kończy się nie miejsce, tylko inody

Istnieje drugi rodzaj „pełnego dysku”, który daje dokładnie ten sam komunikat błędu. Każdy plik i każdy katalog zajmuje jeden inode, a ich liczba jest w ext4 ustalona od momentu formatowania.

df -i
stat -f /

Jeśli w df -i kolumna IUse% pokazuje 100, podczas gdy df -h melduje mnóstwo wolnego miejsca, diagnoza jest jednoznaczna: serwer nie ma problemu z miejscem, tylko z liczbą plików. Miliony maleńkich plików zużyły wszystkie inody. Komunikat błędu i tak brzmi No space left on device, dlatego właśnie większość szuka w złym miejscu.

Sprawców znajdziesz, licząc pliki zamiast bajtów:

find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20

Częstymi kandydatami są kolejka pocztowa w /var/spool/postfix albo /var/spool/exim4, sesje PHP w /var/lib/php/sessions, katalogi cache aplikacji webowych, drzewa zależności Node oraz nigdy nieopróżniany /tmp.

Przy samym kasowaniu czeka kolejna przeszkoda. rm /var/lib/php/sessions/* przy bardzo dużej liczbie plików kończy się komunikatem bash: /usr/bin/rm: Argument list too long, bo wiersz poleceń przekracza limit rozmiaru. Droga, która działa zawsze:

find /var/lib/php/sessions -type f -mtime +7 -delete

Co trzeba wiedzieć: liczby inodów w istniejącym systemie plików ext4 nie da się później zwiększyć. Pomaga tylko powiększenie systemu plików (inody rosną wtedy proporcjonalnie) albo założenie go od nowa z gęstszym rozmieszczeniem, na przykład przez mkfs.ext4 -i 8192 /dev/sdb1. Jeśli z góry wiadomo, że będziesz pracować z bardzo dużą liczbą małych plików, na przykład przy serwerach pocztowych albo archiwach zdjęć, lepiej wyjdziesz na XFS, bo XFS zakłada inody dynamicznie i praktycznie kończą się one dopiero wtedy, gdy skończy się miejsce. Debian i Ubuntu formatują domyślnie na ext4, XFS trzeba wybrać świadomie.

Kontrola i zabezpieczenie na przyszłość

Sprzątanie uznasz za udane dopiero wtedy, gdy zgodzą się trzy rzeczy: df -h pokazuje więcej wolnego miejsca, df -i pokazuje malejące zajęcie inodów, a usługa, która pierwotnie padła, znowu działa. Polecenie, które przeszło bez komunikatu o błędzie, samo w sobie nie jest jeszcze dowodem. Zwłaszcza journalctl --vacuum-sizedocker system prune chętnie robią bez słowa dokładnie nic.

W ruchu ciągłym sprawdza się krótka lista: journal twardo ogranicz przez SystemMaxUse, rotację logów Dockera ustaw w daemon.json, w Ubuntu włącz Remove-Unused-Kernel-Packages, własne logi aplikacji obejmij regułami w /etc/logrotate.d/ i dołóż prosty cron, który po przekroczeniu progu wyśle mail. Na serwerach KVM i dedykowanych opłaca się poza tym wynieść /var albo przynajmniej /var/log na osobną partycję. Wtedy szał w logach unieruchomi wprawdzie jakąś usługę, ale nie system operacyjny, a na maszynę w każdym razie wejdziesz jeszcze przez SSH.

Najczęstsze pytania

Dlaczego df pokazuje 100%, chociaż du znajduje wyraźnie mniej?
W zdecydowanej większości przypadków procesy trzymają jeszcze otwarte skasowane pliki. Wpis w katalogu zniknął, dlatego du niczego już nie widzi, ale bloki pozostają zajęte, dopóki nie zamknie się ostatni deskryptor pliku. Wytropisz to poleceniem 'lsof +L1' jako root, a zwolnisz przez restart usługi albo przez 'truncate -s 0 /proc/PID/fd/N'. Pozostałe wyjaśnienia to pliki przykryte punktem montowania, pięcioprocentowa rezerwa roota w ext4 oraz przebieg du bez uprawnień roota.
Ile miejsca może zająć journal systemd i jak ograniczyć go na stałe?
Domyślnie dziesięć procent systemu plików, z górnym limitem czterech gigabajtów, a dodatkowo journald utrzymuje wolne 15% systemu plików. Na stałe ograniczysz go plikiem w /etc/systemd/journald.conf.d/ z wpisem SystemMaxUse, a potem zrestartuj systemd-journald. Dla natychmiastowego efektu najpierw 'journalctl --rotate', a dopiero potem 'journalctl --vacuum-size=200M', bo vacuum usuwa wyłącznie zarchiwizowane pliki journala.
Czy journal leży w Debianie i Ubuntu w tym samym miejscu?
Nie. Przy domyślnym ustawieniu Storage=auto decyduje wyłącznie to, czy istnieje /var/log/journal. Na wielu minimalnych instalacjach Debiana 12 i Debiana 13 tego katalogu brakuje, journal leży wtedy w RAM pod /run/log/journal i znika po restarcie. Ubuntu Server 22.04 i 24.04 zakładają ten katalog zazwyczaj od razu i logują na dysk. Jedno 'ls -d /var/log/journal' wyjaśnia to w sekundę.
/boot jest pełny, a apt przerywa z błędem dpkg. Co teraz?
Najpierw zanotuj 'uname -r', potem wyszukaj w /boot najstarszą, niedziałającą wersję i skasuj wyłącznie jej initrd.img, nie plik vmlinuz. Następnie 'apt-get -f install', żeby dpkg dokończył przerwaną konfigurację, dalej 'apt-get autoremove --purge', a na koniec 'update-grub'. Kasowanie plików kernela na oślep prowadzi do wpisów GRUB-a, które wskazują donikąd.
Czy 'docker system prune -a --volumes' jest bezpieczne?
Na serwerze produkcyjnym bez aktualnego backupu nie. Przełącznik --volumes usuwa także wolumeny, do których nie jest właśnie podpięty żaden działający kontener, a więc w pewnych okolicznościach bazę danych zatrzymanego kontenera. Bezpieczniejsza jest droga przez 'docker system df -v' do diagnozy, a potem celowo 'docker image prune -a' względnie 'docker builder prune'.
Co zrobić, gdy df -i pokazuje inody zajęte w 100%?
Wtedy brakuje nie miejsca, tylko możliwej liczby plików. Sprawców znajdziesz poleceniem "find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20", typowo są to kolejka pocztowa, sesje PHP, katalogi cache albo /tmp. Kasuj przez find z -delete, bo rm z gwiazdką wykłada się na "Argument list too long". Liczby inodów w systemie plików ext4 nie da się później zwiększyć, pomaga tylko powiększenie albo założenie systemu plików od nowa, alternatywnie XFS.

Linux Zarządzanie serwerem Debian Ubuntu Miejsce na dysku systemd Docker Rozwiązywanie problemów