Konfiguracja swapa i zapobieganie awariom z braku pamięci
Usługa zniknęła, żadnego raportu o awarii, plik logu urywa się w połowie zdania. Tak udowodnisz zabicie procesu przez OOM, poprawnie założysz plik swap i rozpoznasz, kiedy swap tylko odsuwa problem.
Usługa zniknęła. Żadnego raportu o awarii, żadnego stosu wywołań, plik logu urywa się w połowie wiersza. MariaDB przestaje odpowiadać, nginx zwraca 502, serwer Minecraft jest offline, a w samej aplikacji nie widać niczego podejrzanego. Prawie zawsze stoi za tym ten sam schemat: jądru zabrakło wolnej pamięci i zakończyło jakiś proces, żeby utrzymać system przy życiu. W tym poradniku pokazujemy, jak udowodnić to ponad wszelką wątpliwość, jak poprawnie założyć plik swap i wpisać go na stałe, oraz w jakich sytuacjach swap odsuwa problem tylko o kilka minut.
Zbierz dowody: dmesg i journalctl
Zanim cokolwiek zaczniesz konfigurować, potrzebujesz dowodu. Przy każdym zabiciu procesu z powodu braku pamięci (OOM, Out of Memory) jądro zapisuje w buforze pierścieniowym obszerny blok informacji:
dmesg -T | grep -iE 'out of memory|oom-kill'
Jeśli nie wróci żaden wiersz, a polecenie zakończy się kodem wyjścia 1, od ostatniego startu systemu nie doszło do OOM na poziomie jądra. Ten kod wyjścia nie oznacza błędu, to zwykła odpowiedź polecenia grep, które niczego nie znalazło. Jeśli coś wróci, na wszystkich czterech omawianych tu systemach wygląda to tak:
mariadbd invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/mariadb.service,task=mariadbd,pid=1043,uid=107
Out of memory: Killed process 1043 (mariadbd) total-vm:2894760kB, anon-rss:1583204kB, file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:3820kB oom_score_adj:0
Ważne są w tym trzy rzeczy. Po pierwsze: invoked oom-killer wskazuje proces, który zażądał pamięci, niekoniecznie winowajcę. Po drugie: zabity proces stoi w wierszu Killed process. Po trzecie: liczy się anon-rss, czyli faktycznie zajęta pamięć anonimowa. total-vm to zarezerwowana przestrzeń adresowa, przy Javie albo Go regularnie jej wielokrotność, i niczego nie dowodzi.
Uruchomiony bez roota dmesg odpowiada na Debianie 12, Debianie 13, Ubuntu 22.04 i Ubuntu 24.04 komunikatem dmesg: read kernel buffer failed: Operation not permitted, bo kernel.dmesg_restrict ma wszędzie wartość 1. Pracuj więc przez sudo albo jako root. Jeśli komunikat zostaje także na koncie root, siedzisz w kontenerze LXC albo OpenVZ, który nie ma dostępu do bufora pierścieniowego hosta. Wtedy prowadzi dalej wyłącznie druga droga, przez journalctl -k.
Po restarcie bufor pierścieniowy jest pusty. Stąd druga droga, przez journal, i tu właśnie czai się pułapka, którą większość poradników pomija:
journalctl -k --grep "Out of memory"
-k włącza w domyśle -b, pokazuje więc wyłącznie bieżące uruchomienie systemu. Jeśli maszyna została po zdarzeniu zrestartowana, to polecenie nie zwróci nic, choć zdarzenie zostało zapisane. Dla poprzedniego startu albo dla wybranego okresu:
journalctl -k -b -1 --grep "Out of memory"
journalctl --since "7 days ago" --grep "Out of memory|oom-kill"
Jeśli pierwsze z tych dwóch poleceń odpowie No journal boot entry found for the specified boot (-1), w journalu po prostu nie ma zapisanego wcześniejszego uruchomienia. To również nie jest błąd, tylko normalna sytuacja na systemie, którego journal zapisuje dane dopiero od bieżącego startu.
Działa to jednak tylko wtedy, gdy journal w ogóle jest trwały. Sprawdź to:
ls -d /var/log/journal
Na Debianie i Ubuntu ten katalog istnieje fabrycznie, więc test prawie zawsze coś zwróci, a późniejsze mkdir zadziała wtedy na pusto i niczego nie zepsuje. Jeśli wyjątkowo katalogu jednak nie ma, journal leży wyłącznie w /run i po każdym restarcie przepada. Nadrabia się to dwoma poleceniami:
mkdir -p /var/log/journal
systemctl restart systemd-journald
OOM jądra czy limit systemd? Dwie różne przyczyny
To rozróżnienie decyduje o tym, czy swap w ogóle coś da. Zwróć uwagę na pole constraint w komunikacie jądra.
CONSTRAINT_NONE oznacza: pamięci zabrakło całemu systemowi. Tu swap pomoże.
CONSTRAINT_MEMCG oznacza: limit przekroczyła tylko pojedyncza control group, a reszta systemu miała mnóstwo zapasu. Tu swap nie pomoże, tu trzeba podnieść limit albo zmusić aplikację do oszczędności. Takie zabicia rozpoznasz również po samej usłudze:
systemctl status mariadb
Jeśli stoi tam Main process exited, code=killed, status=9/KILL, a niżej Failed with result 'oom-kill', było to zabicie z powodu braku pamięci. Czy w grę wchodził limit cgroup, zdradzi plik z licznikami. Wymaga on cgroup v2, czyli standardu na wszystkich czterech dystrybucjach; przy starszym cgroup v1 ta ścieżka nie istnieje:
cat /sys/fs/cgroup/system.slice/mariadb.service/memory.events
systemctl show mariadb -p MemoryMax -p MemoryHigh
Wartość większa od zera przy oom_kill razem z ustawionym MemoryMax to dowód na lokalny limit.
Jest jeszcze trzeci kandydat, często pomijany, bo nie zapisuje w dmesg zupełnie niczego: systemd-oomd. Ta usługa działa w przestrzeni użytkownika, analizuje wskaźnik presji PSI i kończy całe control groups, zanim jądro w ogóle zdąży zareagować. Jej komunikat w journalu brzmi mniej więcej Killed /system.slice/... due to memory pressure for /system.slice being 60.00% > 50.00% for > 20s with reclaim activity. Sprawdź, czy usługa działa:
systemctl is-active systemd-oomd
Na Ubuntu systemd-oomd jest od wersji 22.04 fabrycznie zainstalowany i włączony, na Debianie nie należy do standardowego wyposażenia. Odpowiedź inactive na serwerze z Debianem jest więc spodziewana i nie świadczy o żadnym błędzie.
Ważne na później: systemd-oomd potrafi zadziałać również dlatego, że zapełnia się swap. Przy włączonym oomd większy swap może więc wywoływać przerwania wcześniej, a nie później.
Zanim dodasz swap: ile pamięci naprawdę brakuje
Dwie minuty pomiarów oszczędzą ci błędnej decyzji.
free -h
Interesuje cię wyłącznie kolumna available, a nie free. Cache liczy się jako dostępny i w razie potrzeby zostaje zwolniony. Kto patrzy na kolumnę free, uzna każdy zdrowy system za przeciążony.
ps -eo pid,comm,rss,%mem --sort=-rss | head -n 11
systemd-cgtop --order=memory -b -n 1
Pierwsze polecenie pokazuje największe pojedyncze procesy, drugie grupuje je według usług. W praktyce stoją tam prawie zawsze ci sami trzej podejrzani: MariaDB ze zbyt dużym innodb_buffer_pool_size, PHP-FPM ze zbyt wysokim pm.max_children oraz JVM ze zbyt hojnym -Xmx.
Jako punkt wyjścia dla rozmiaru pliku swap wystarczy ta tabela. Na serwerze większy nie znaczy lepszy, bo swap, który faktycznie zapełnisz w całości, sprawia, że maszyna przestaje reagować.
| Pamięć RAM | Rozsądny plik swap |
|---|---|
| 1 GB | 1-2 GB |
| 2 GB | 2 GB |
| 4-8 GB | 2-4 GB |
| 16 GB i więcej | 4 GB, rzadko więcej |
Zakładanie pliku swap
Najpierw sprawdź, czy swap już istnieje. Serwery Ubuntu z instalatora ISO często mają już /swap.img, Debian z instalatora zwykle prawdziwą partycję swap. Obrazy chmurowe obu dystrybucji z reguły nie mają nic.
swapon --show
Jeśli wynik pozostanie pusty, swapa nie ma. Następnie sprawdź system plików, bo od niego zależy dalsze postępowanie:
findmnt -no FSTYPE -T /
Przy ext4 albo xfs idziesz od razu dalej. Plik zakładaj poleceniem dd, nie fallocate. fallocate jest szybszy, ale zależnie od systemu plików i jądra tworzy plik z niezapisanymi obszarami, a wtedy swapon go odrzuci. dd zapisuje prawdziwe zera i działa wszędzie:
dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
chmod 600 /swapfile
Przed następnym krokiem warto wykonać kontrolę, którą wielu pomija. Jeśli pod ścieżką /swapfile jest już włączony swap, choćby z wcześniejszej próby albo z domyślnej konfiguracji obrazu, mkswap odmówi pracy komunikatem mkswap: error: /swapfile is mounted; will not make swapspace. Liczy się przy tym wyłącznie to, czy ścieżka figuruje jako aktywna w /proc/swaps. Sprawdź więc i w razie potrzeby wyłącz:
swapon --show
swapoff /swapfile
Jeśli w swapon --show nie pojawi się wiersz z /swapfile, możesz pominąć swapoff. Potem zapisz obszar wymiany:
mkswap /swapfile
mkswap odpowiada komunikatem Setting up swapspace version 1, size = 2 GiB (2147479552 bytes) i nowym UUID. Dopiero potem włącz:
swapon /swapfile
Kolejność nie podlega negocjacji. chmod przed mkswap, mkswap przed swapon, a ewentualne swapoff przed wszystkim innym.
Po czym poznasz, że naprawdę się udało
To, że swapon przejdzie bez komunikatu o błędzie, jeszcze niczego nie dowodzi. Dowodem są te dwa wyniki:
swapon --show
free -h
swapon --show musi wypisać wiersz z /swapfile, typem file, rozmiarem i priorytetem. W free -h wiersz Swap: musi przeskoczyć z 0B na nowy rozmiar. Jeśli jeden z tych dwóch wyników pozostanie bez zmian, swap nie jest aktywny, cokolwiek wcześniej zgłosiło samo polecenie.
Wpis na stałe, bez ryzykowania startu systemu
Samo swapon nie przeżyje restartu. Wpis należy do /etc/fstab, a właśnie tam najłatwiej rozłożyć serwer. Najpierw kopia zapasowa:
cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Zwróć uwagę na dwa znaki większości. Pojedynczy > nadpisze cały plik, a wtedy maszyna nie wystartuje już poprawnie. Potem sprawdź składnię, zanim zrestartujesz:
findmnt --verify
Jedno ostrzeżenie jest przy tym zupełnie normalne i nie stanowi powodu, żeby usuwać wiersz: [W] non-bind mount source /swapfile is a directory or regular file. Przy pliku swap inaczej być nie może, a polecenie i tak kończy się kodem powrotu 0. Jeśli plikowi brakuje dodatkowo poprawnego nagłówka swap, dochodzi [W] cannot detect on-disk filesystem type, i wtedy faktycznie zapomniałeś o mkswap.
Jeśli swap włączyłeś już wcześniej ręcznie przez swapon /swapfile, jest teraz aktywny. Jeśli nie, swapon -a uruchomi wszystkie wpisy z fstab bez konieczności restartu. Właściwy test jest jednak inny. systemd tworzy z każdego wiersza fstab osobną jednostkę, dla /swapfile nazywa się ona swapfile.swap. Jeśli ta jednostka się pojawi i będzie aktywna, przy następnym starcie swap zostanie podłączony na pewno:
systemctl daemon-reload
systemctl list-units --type swap
Spodziewany jest wiersz swapfile.swap loaded active active Swap. Jeśli go brakuje, wiersz w fstab jest błędny, a restart skończyłby się bez swapa. Dopiero gdy wszystko się zgadza, warto zrestartować i sprawdzić wynik przez swapon --show.
Prawidłowe ustawienie swappiness
Parametr jądra vm.swappiness steruje tym, jak chętnie jądro wypycha strony anonimowe do wymiany, zamiast odrzucać cache plików. Wartość domyślna jest na Debianie 12, Debianie 13, Ubuntu 22.04 i Ubuntu 24.04 identyczna i wynosi 60:
cat /proc/sys/vm/swappiness
Zakres wartości sięga od jądra 5.8 od 0 do 200, a wszystkie cztery dystrybucje pracują na nowszych jądrach. Dwa rozpowszechnione nieporozumienia: vm.swappiness=0 nie wyłącza swapa, blokuje jedynie wypychanie stron na zapas, a jądro i tak sięgnie po wymianę, zanim kogokolwiek zabije. Poza tym niska wartość nie przyspieszy systemu, w którym pamięci po prostu brakuje.
Rozsądne wartości: 10-20 na serwerach bazodanowych, 60 na mieszanych serwerach WWW, 100 i więcej, jeśli używasz zram. Na stałe wpisuje się to do własnego pliku w /etc/sysctl.d/, a nie do /etc/sysctl.conf, który przeszkadza przy aktualizacjach pakietów:
echo 'vm.swappiness = 10' > /etc/sysctl.d/99-swappiness.conf
sysctl --system
cat /proc/sys/vm/swappiness
Trzecie polecenie to kontrola skuteczności. sysctl --system czyta wszystkie katalogi w stałej kolejności, a istniejący już plik o wyższym numerze może nadpisać twoją wartość.
Gdy coś pójdzie nie tak: komunikaty błędów dosłownie
swapon: /swapfile: insecure permissions 0644, 0600 suggested. To tylko ostrzeżenie, swap mimo wszystko działa. Napraw to jednak, bo inaczej każdy użytkownik może odczytać zawartość wypchniętych procesów: chmod 600 /swapfile.
swapon: /swapfile: swapon failed: Invalid argument Najczęstszy błąd. Albo zapomniano o mkswap, albo plik zawiera dziury. W logu jądra pojawia się wtedy dodatkowo swapon: swapfile has holes. Rozwiązanie: usuń plik i załóż go od nowa poleceniem dd zamiast fallocate.
Na btrfs obowiązuje ten sam błąd plus BTRFS warning: swapfile must not be copy-on-write. Tutaj kolejność jest inna: plik trzeba utworzyć pusty i przed wypełnieniem oznaczyć jako nieobjęty mechanizmem copy-on-write. Jedna uwaga na wstępie, która potrafi zadecydować o losie działającego serwera: truncate -s 0 oraz rm wykonają się także wtedy, gdy pod tą ścieżką podłączony jest jeszcze aktywny swap. Jądro wskazuje potem na bloki, których już nie ma. Jeśli swapon --show pokazuje ścieżkę jako aktywną, przed tymi poleceniami musi więc koniecznie paść swapoff /swapfile.
truncate -s 0 /swapfile
chattr +C /swapfile
Potem wypełnij plik jak zwykle poleceniem dd, dalej chmod 600, mkswap, swapon. Na skompresowanych subwolumenach i w snapshotach swap nadal nie działa.
swapon: /swapfile: swapon failed: Operation not permitted Siedzisz w kontenerze. LXC, OpenVZ i Docker dzielą jądro hosta i nie mogą włączyć własnego swapa. Sprawdzisz to poleceniem:
systemd-detect-virt
Jeśli polecenie zgłosi kvm, qemu albo none, działa własne jądro i swap jest możliwy. Jeśli zgłosi lxc, openvz albo docker, pomoże tylko więcej pamięci RAM albo produkt z pełną wirtualizacją. Serwery root KVM i serwery dedykowane od KernelHost mają własne jądro, swap można tam skonfigurować bez ograniczeń.
dd: error writing '/swapfile': No space left on device Dysk jest zbyt zapełniony. Najpierw df -h, potem usuń niedokończony plik przez rm /swapfile, bo inaczej dalej zajmuje miejsce. Także tutaj obowiązuje zasada: jeśli swapon --show pokazuje ścieżkę jako aktywną, przed usunięciem musi paść swapoff /swapfile.
System nie startuje, pojawia się konsola awaryjna. Prawie zawsze chodzi o literówkę w /etc/fstab. Zaloguj się przez konsolę w panelu klienta, potem mount -o remount,rw /, usuń błędny wiersz albo skopiuj z powrotem /etc/fstab.bak, zrestartuj. Dokładnie po to powstała wcześniej kopia zapasowa.
Różnice między Debianem 13, Debianem 12, Ubuntu 24.04 i 22.04
Polecenia są na wszystkich czterech systemach identyczne, stan wyjściowy już nie.
- Istniejący swap: Ubuntu Server z instalatora ISO często zakłada
/swap.img, Debian z instalatora partycję swap. Obrazy chmurowe obu dystrybucji przychodzą bez swapa. Zawsze najpierwswapon --show. - systemd-oomd: na Ubuntu od 22.04 fabrycznie zainstalowany i włączony, w obu wersjach Debiana nie należy do standardowego wyposażenia. To tłumaczy, dlaczego identyczne oprogramowanie potrafi umierać inaczej na dwóch pozornie takich samych systemach.
- Baza danych: Debian 12 i Debian 13 nie dostarczają pakietu
mysql-server, tam działa zawsze MariaDB (10.11 na Debianie 12, 11.8 na Debianie 13). Ubuntu 22.04 i 24.04 mają jedno i drugie. Domyślne wartościinnodb_buffer_pool_sizeodpowiednio się różnią, a właśnie ta wartość bywa na małych serwerach najczęstszą przyczyną OOM. - Java: Debian 12 zna wyłącznie OpenJDK 17, Debian 13 wyłącznie OpenJDK 21, Ubuntu 22.04 i 24.04 obejmują wersje od 8 do 21. JVM bez ustawionego
-Xmxbierze sobie domyślnie ćwierć pamięci RAM, a przy kilku instancjach zabicie przez OOM jest wtedy zaprogramowane z góry. - Komunikaty jądra: format i treść komunikatu OOM są na wszystkich czterech systemach takie same, powyższe przykłady pasują wszędzie.
- swappiness: wszędzie domyślnie 60.
Kiedy swap pomaga, a kiedy tylko odsuwa problem
Swap pomaga niezawodnie przy krótkich szczytach, na przykład podczas tworzenia kopii zapasowej, aktualizacji pakietów albo nocnego importu. Pomaga przy usługach, które zajmują dużo pamięci, a potem całymi dniami nic nie robią, bo te strony spokojnie mogą leżeć na dysku. I daje ci w razie kłopotów kilka minut, w których sesja SSH jeszcze odpowiada i możesz zareagować, zamiast stać przed martwym serwerem.
Swap nie pomoże, jeśli stałe zapotrzebowanie po prostu przewyższa dostępny RAM. System zaczyna wtedy thrashing: bez przerwy wypycha strony i wciąga je z powrotem, load rośnie do wartości dwucyfrowych, obciążenie CPU pozostaje niskie, a wszystko czeka na I/O. W praktyce jest to gorsze niż czyste zabicie przez OOM, bo nie przechodzi nawet logowanie. Dwa polecenia pokażą, czy jesteś w tym stanie:
vmstat 1 5
test -e /proc/pressure/memory && cat /proc/pressure/memory
Przy vmstat liczą się kolumny si i so. Trwale trzycyfrowe wartości oznaczają aktywne wypychanie i wciąganie stron. W /proc/pressure/memory decyduje full avg10: wartości powyżej 10 znaczą, że cały system dziesięć procent czasu czeka na pamięć. Powyżej 40 maszyna jest praktycznie martwa. Plik istnieje jednak tylko wtedy, gdy w jądrze włączone jest Pressure Stall Information. Standardowe jądra Debiana i Ubuntu to mają, inne jądra niekoniecznie. Stąd zabezpieczenie przez test -e: jeśli pliku brakuje, wynik zostaje pusty, zamiast żebyś wziął No such file or directory za usterkę. PSI da się dołożyć przez parametr startowy jądra psi=1.
Które procesy faktycznie leżą w swapie, pokaże ten wiersz:
grep VmSwap /proc/*/status | sort -k2 -rn | head
Swap nic nie da również przeciwko limitowi cgroup (CONSTRAINT_MEMCG), przeciwko pamięci przypiętej przez mlock ani przeciwko JVM, której sterta jest skonfigurowana większa niż dostępny RAM. Garbage collector regularnie przechodzi przez całą stertę i natychmiast ściąga z powrotem każdą wypchniętą stronę.
Trzy narzędzia na wypadek, gdy swap nie wystarczy
zram zakłada skompresowany obszar wymiany bezpośrednio w pamięci RAM. Jest o rzędy wielkości szybszy niż plik na dysku i zależnie od charakteru danych daje efektywnie 20-40 procent więcej pamięci:
apt update
apt install -y zram-tools
Konfiguracja odbywa się w /etc/default/zramswap przez ALGO=zstd i PERCENT=50, potem systemctl restart zramswap. Kontrola przez zramctl i ponownie swapon --show, gdzie pojawi się wtedy /dev/zram0. Przy zram vm.swappiness należy podnieść do 100-180, bo wypychanie stron jest tu tanie.
earlyoom wkracza, zanim jądro zamrozi system, i zabija celowo zamiast według heurystyki:
apt install -y earlyoom
W /etc/default/earlyoom ustawiasz przez EARLYOOM_ARGS progi i chronisz ważne procesy, na przykład przez -m 5 -s 5 --avoid '(^|/)(sshd|systemd)$'. Dzięki temu logowanie po SSH pozostaje dostępne, podczas gdy pada proces pożerający pamięć.
Sztywne limity dla poszczególnych usług to najczystsze rozwiązanie, gdy konkretna usługa regularnie się wyrywa. Przez systemctl edit mariadb wpisujesz MemoryHigh=1200M i MemoryMax=1500M. Usługa zostaje wtedy przyhamowana, a w razie potrzeby zakończona sama, zamiast pociągnąć za sobą cały serwer. Kontrola przez systemctl show mariadb -p MemoryMax.
Właściwą naprawą pozostaje jednak w większości przypadków konfiguracja aplikacji: innodb_buffer_pool_size na realistycznym poziomie, pm.max_children wyliczone z rzeczywistego zapotrzebowania pamięci na jeden proces roboczy, ustawione -Xmx dla każdej JVM. Swap to siatka bezpieczeństwa, a nie rozwiązanie.
Wycofanie zmian, jeśli rozwiązanie się nie sprawdzi
Droga powrotna jest krótka i powinna przebiegać w tej kolejności:
swapoff /swapfile
Przy mocno zapełnionym swapie polecenie może pracować kilka minut, bo wszystkie strony muszą wrócić do RAM. Jeśli zakończy się komunikatem swapoff: /swapfile: swapoff failed: Cannot allocate memory, w RAM nie ma miejsca na powrót danych i trzeba najpierw zatrzymać jakieś usługi. Dopiero po udanym swapoff usuń wiersz z fstab i skasuj plik:
rm /swapfile
swapon --show
Skasowanie pliku, dopóki jest on jeszcze podłączony, prowadzi do systemu, którego zarządzanie pamięcią wskazuje na nieistniejący inode. Kończy się to nieprzyjemnie. Na koniec jeszcze raz free -h i restart jako próba kontrolna.
Najczęstsze pytania
Ile swapa potrzebuję na serwerze?
Dlaczego journalctl -k nie pokazuje zabicia przez OOM, choć do niego doszło?
Po czym poznam, czy proces zakończyło jądro, czy limit systemd?
Dlaczego swapon kończy się błędem „Invalid argument”?
Czy mogę skonfigurować swap w kontenerze?
Czy vm.swappiness=0 oznacza, że nic już nie trafia do wymiany?
2026 KernelHost GmbH. Wszelkie prawa zastrzeżone. Ten poradnik jest chroniony prawem autorskim. Publikowanie go w innych serwisach, w całości, we fragmentach lub w zmienionej formie, wymaga naszej pisemnej zgody. Cytaty z podaniem źródła i z linkiem są jak najbardziej mile widziane.

