Otázka nezní, jestli váš web spadne, ale kdy. Proces zadusí memory leak, deploy nechá viset starou verzi, přijde nárazová návštěvnost a PHP-FPM dojdou workeři. To potká každého. Rozdíl mezi amatérským a profesionálním provozem není v tom, že profesionálovi to nikdy nepadá — je v tom, že se jeho server zvedne sám dřív, než si toho kdokoli všimne.

V tomhle článku vám ukážu ověřené mechanismy, kterými to na běžném serveru zařídíte. Všechny příkazy platí pro Debian 12 (Bookworm) a Ubuntu 22.04+. Verze balíčků si podle své distribuce upravte — u PHP-FPM na to konkrétně upozorním.

Proč weby vlastně padají

Než začneme cokoli automatizovat, je dobré vědět, proti čemu bojujeme. V praxi jde skoro vždy o jednu z těchhle věcí:

  • Proces spadne — segfault, neošetřená výjimka nebo memory leak, který proces nafoukne, až ho zabije jádro.
  • Proces běží, ale neodpovídá — deadlock, zaseknuté připojení k databázi, vyčerpaná fronta workerů. Nejzákeřnější varianta, protože „zvenčí" to vypadá živě.
  • Došla paměť — a OOM killer sestřelí něco důležitého, typicky databázi nebo právě váš aplikační proces.
  • Špatně nastavené limity — Nginx nebo PHP-FPM narazí na strop, který jste nastavili (nebo nechali v defaultu) vy sami.

Ke každé příčině existuje obrana:

Typ selhání Vrstva obrany
Proces spadne systemd Restart=on-failure
Proces neodpovídá health-check přes curl
Došla paměť swap, vm.swappiness, OOMScoreAdjust, MemoryMax
Špatné limity pm.max_children, rate limiting v Nginxu

Pojďme popořadě.

systemd: první záchranná síť

Základní pravidlo: aplikaci nikdy nespouštějte „ručně na pozadí" ani přes nohup. Ať ji drží systemd — ten umí proces automaticky restartovat, když spadne.

Klíčová je direktiva Restart=. Hodnota on-failure znovu nastartuje proces ve čtyřech případech: skončí s nenulovým návratovým kódem, je ukončen „nečistým" signálem (typicky SIGKILL, SIGSEGV, SIGABRT), vyprší mu timeout operace, nebo se spustí watchdog. Pozor na detail: mezi „nečisté" signály nepatří SIGTERM, SIGINT, SIGHUP ani SIGPIPE. Právě proto vás čisté systemctl stop (které posílá SIGTERM) nerozhodí — systemd to bere jako řádné ukončení a restartovat nebude. Nebude s vámi bojovat.

Ukázková unita /etc/systemd/system/muj-web.service:

[Unit]
Description=Muj web
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
ExecStart=/usr/bin/node /var/www/app/server.js
Restart=on-failure
RestartSec=5s
User=www-data

[Install]
WantedBy=multi-user.target

Dvě věci, které lidé pletou:

RestartSec=5s říká, jak dlouho systemd počká před dalším pokusem. Když ho necháte v defaultu (100 ms) a aplikace padá hned po startu, dostanete se do smyčky.

A tady přichází past: StartLimitIntervalSec a StartLimitBurst (pozor, patří do sekce [Unit], ne [Service]) omezují, kolikrát se smí unita nastartovat za daný interval. Default je 5 startů za 10 sekund. Když to překročíte, systemd to vzdá a napíše „start request repeated too quickly". Proto ten RestartSec=5s — s pěti sekundami mezi pokusy se do defaultního limitu nevejdete a musíte si ho vědomě rozvolnit, jak je výše.

Po každé editaci souboru:

sudo systemctl daemon-reload
sudo systemctl enable --now muj-web.service

Když už unita jednou „vzdala" restarty, vynulujete čítač takhle:

sudo systemctl reset-failed muj-web.service

Co se děje, sledujete v žurnálu:

journalctl -u muj-web.service -f

Health-check: když proces běží, ale web neodpovídá

systemd hlídá, jestli proces žije, ne jestli odpovídá. Zaseknutou aplikaci musíte odhalit zvenčí — zavoláním na health endpoint.

Jednoduchý skript /usr/local/bin/healthcheck.sh:

#!/usr/bin/env bash
if ! curl -fsS --max-time 5 http://127.0.0.1/health >/dev/null; then
    systemctl restart muj-web.service
fi

Přepínač -f u curl vrátí nenulový kód i při HTTP chybě (5xx/4xx), --max-time 5 utne zaseknuté spojení. Pusťte to jednou za minutu — nejjednodušeji přes cron:

* * * * * /usr/local/bin/healthcheck.sh

Pokud nechcete cron, sáhněte po healthcheck.timer. Jenže samotný OnUnitActiveSec=60 první spuštění nikdy neodpálí — nemá se od čeho odrazit, dokud služba aspoň jednou neproběhla. Zakotvěte ho proto přes OnBootSec=:

[Timer]
OnBootSec=60
OnUnitActiveSec=60

Kvůli výchozí AccuracySec=1min navíc timer nefiruje přesně po minutě; pokud potřebujete přesný interval, přidejte AccuracySec=1s.

Monitoring a alerting bez cirkusu

Automatický restart je skvělý, ale musíte se dozvědět, že k němu došlo — jinak vám server tiše restartuje umírající aplikaci třikrát za hodinu a vy o problému nevíte.

Nejčistší je systemd direktiva OnFailure=, která při pádu spustí notifikační unitu:

[Unit]
OnFailure=alert@%n.service

K tomu šablona [email protected], která zavolá skript posílající e-mail nebo webhook (Slack, Telegram):

[Unit]
Description=Alert pro %i

[Service]
Type=oneshot
ExecStart=/usr/local/bin/alert.sh %i

Jedno důležité upozornění z praxe: monitoring, který běží na stejném serveru, vám neřekne, že celý server leží. Proto vždy doplňte kontrolu zvenčí — z druhého stroje nebo z externí uptime služby. Lokální hlídání řeší proces, externí řeší výpadek celé mašiny.

Kdo chce hotové řešení, apt install monit dá dohledový démon, který umí procesy hlídat, restartovat i alertovat. Nedělá nic, co byste nesvedli výše popsaným způsobem, ale ušetří psaní.

Nginx a PHP-FPM: správné limity

Většina „záhadných" 502 a 504 na PHP webech má jednu příčinu — došli workeři PHP-FPM.

PHP-FPM: strop workerů

Konfigurace poolu je v /etc/php/8.2/fpm/pool.d/www.conf (na Ubuntu 22.04 je to 8.1, cestu si upravte podle verze).

Nejdůležitější je pm.max_children — tvrdý strop souběžně zpracovávaných PHP požadavků. Když se vyčerpá, PHP-FPM zapíše do logu jasnou hlášku:

WARNING: [pool www] server reached pm.max_children setting (10), consider raising it

Číslo ale nenastavujte od oka. Změřte si, kolik paměti průměrně žere jeden worker — master proces přitom vynechte, jinak si průměr zkreslíte:

ps -o rss=,args= -C php-fpm8.2 | awk '/pool/{s+=$1; n++} END {if(n) printf "%.0f MB prumer, %d workeru\n", s/n/1024, n}'

Filtr /pool/ vybere jen workery (php-fpm: pool www) a vynechá master proces. A pak počítejte: pm.max_children = (paměť pro PHP) / (velikost jednoho workeru). Když serveru zbude po odečtení systému, Nginx a databáze třeba 2 GB a jeden worker bere 60 MB, vejde se vám zhruba 30 workerů. Nastavit strop výš, než unese RAM, je nejrychlejší cesta k OOM killeru.

Přidejte ještě pm.max_requests = 500 — worker se po 500 požadavcích recykluje, což omezí dopad drobných memory leaků. Po změně:

sudo systemctl reload php8.2-fpm

Nginx: rate limiting

Na straně Nginxu drží web ve zdraví hlavně rozumné timeouty a ochrana proti náporu. Rate limiting nastavíte takto:

limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;

server {
    location / {
        limit_req zone=perip burst=20 nodelay;
    }
}

Tím server pod náporem odmítne přebytek požadavků slušným 503 místo toho, aby se celý zahltil a spadl.

Swap a OOM killer

Když serveru dojde fyzická paměť i swap, jádro spustí OOM killer a sestřelí proces s nejvyšším oom skóre (tzv. největší badness) — bohužel to bývá ten největší žrout paměti, tedy databáze nebo vaše aplikace. Že k tomu došlo, poznáte takto:

journalctl -k | grep -i "out of memory"

Swap je pojistka proti krátkým špičkám, ne náhrada za chybějící RAM. Pár giga swapu často stačí, aby nárazový výkyv neshodil celý stroj. Na ext4 ho na Debianu/Ubuntu založíte jednoduše:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Aby přežil restart, přidejte řádek do /etc/fstab:

/swapfile none swap sw 0 0

Poznámka: tenhle postup platí pro ext4. Na Btrfs by takový swap soubor swapon odmítl — soubor musí být NOCOW. Nejjednodušeji ho na Debianu 12 / Ubuntu 22.04+ založíte jediným příkazem:

sudo btrfs filesystem mkswapfile --size 2G /swapfile
sudo swapon /swapfile

Jak agresivně jádro swapuje, řídí vm.swappiness (default 60). Na serveru s databází se osvědčuje snížit ho — data zůstanou v RAM a swap se sáhne až pod tlakem:

echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

A konečně — pokud máte proces, kterého se OOM killer nemá dotknout jako prvního, řekněte to systemd přímo v unitě:

[Service]
OOMScoreAdjust=-500

Nižší skóre = menší šance, že padne jako první. Naopak MemoryMax=512M v unitě nastaví tvrdý strop paměti pro jeden proces — když ho překročí, jádro sestřelí jen jeho (jeho cgroup), a ne zbytek serveru.


Shrnutí

Odolný server není o jednom kouzelném nastavení, ale o vrstvách, které se zastupují:

  1. systemd s Restart=on-failure a rozumným RestartSec zvedne spadlý proces.
  2. Health-check přes curl odhalí proces, který běží, ale neodpovídá.
  3. OnFailure= a externí monitoring vám dají vědět, že se něco děje.
  4. Změřený pm.max_children a rate limiting v Nginxu drží web ve zdravých mezích pod zátěží.
  5. Swap, vm.swappiness a OOMScoreAdjust stojí mezi vámi a OOM killerem.

Web, který se sám zvedne do minuty, je pro uživatele lepší než web, který nespadl nikdy — protože ten první existuje, kdežto ten druhý ne.

Nastavte tyhle vrstvy jednou pořádně a příští pád zvládne server sám. Vy se o něm dozvíte z alertu, ne z telefonu naštvaného klienta.