pm.max_children w PHP-FPM: obliczanie zamiast zgadywania

Opublikowano 18 min czytania

Komunikat server reached pm.max_children nie oznacza, że masz podwoić wartość. Zmierz, policz, wybierz tryb pracy, a potem udowodnij, że wartość naprawdę pasuje.

W logu PHP-FPM prędzej czy później pojawia się taki wiersz, a razem z nim gotowa porada:

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

Ta porada nie jest błędna, tylko niepełna. pm.max_children to jedyne pokrętło w PHP-FPM, przy którym zbyt wysoka wartość jest groźniejsza od zbyt niskiej. Za nisko kosztuje czas oczekiwania, a w skrajnym przypadku błąd 502. Za wysoko kosztuje pamięć RAM całego serwera, a wtedy to kernel sam wybiera, który proces zakończy. Z doświadczenia wynika, że nie jest to PHP, tylko baza danych.

Ten poradnik pokazuje drogę od zgadywania do liczenia: zmierz zapotrzebowanie pojedynczego procesu roboczego na pamięć, wyznacz budżet, wybierz tryb pracy, a na koniec udowodnij, że wartość jest właściwa.

Wszystkie informacje dotyczą Debiana 13 (trixie), Debiana 12 (bookworm), Ubuntu 24.04 LTS oraz Ubuntu 22.04 LTS. Polecenia napisano z myślą o pracy jako root, jako zwykły użytkownik dopisz z przodu sudo. Wszędzie zastąp numer wersji PHP tym z twojego systemu.

SystemPHPUsługaPlik puliLog FPM
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/pool.d/www.conf/var/log/php8.4-fpm.log
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/pool.d/www.conf/var/log/php8.2-fpm.log
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/pool.d/www.conf/var/log/php8.3-fpm.log
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/pool.d/www.conf/var/log/php8.1-fpm.log

Które wersje są zainstalowane, zdradzi rzut oka na katalog. Na serwerach po aktualizacji dystrybucji są to często dwie:

ls /etc/php/
ls /etc/php/*/fpm/pool.d/

Co tak naprawdę ogranicza pm.max_children

Ta wartość nie ogranicza ani odwiedzających, ani połączeń, tylko liczbę żądań PHP wykonywanych w tej samej chwili. Żądanie trwające 80 milisekund zajmuje proces roboczy przez 80 milisekund i dopiero potem go zwalnia. Z tego wynika niemal wszystko inne:

  • Duży ruch potrzebuje niewielu procesów, dopóki skrypty są szybkie. 40 żądań na sekundę po 80 milisekund każde daje nieco ponad trzy równoczesne żądania, a nie 40.
  • Jedno wolne miejsce wywraca cały rachunek. Wywołanie obcego API bez własnego timeoutu trzyma proces zajęty przez pięć sekund, mimo że ten nic nie robi. Pięć takich wywołań na sekundę wiąże na stałe 25 procesów.
  • Oczekujące połączenia nie zajmują procesu. Połączenie keep-alive do nginx kosztuje jeden deskryptor pliku, ale żadnego procesu roboczego.

Gdy wszystkie procesy są zajęte, nowe żądania czekają w kolejce przyjęć socketu. Jej rozmiar określa w pliku puli parametr listen.backlog, fabrycznie 511, a dodatkowo kernel ogranicza ją przez net.core.somaxconn, które na wszystkich czterech systemach wynosi 4096:

sysctl net.core.somaxconn

Dopóki kolejka wystarcza, odwiedzający zauważa jedynie dłuższe czasy ładowania. Gdy i ona się zapełni, kernel odrzuca połączenie, nginx zapisuje w logu 11: Resource temporarily unavailable, a do odwiedzającego trafia błąd 502. Odróżnienie od pozostałych przyczyn znajdziesz tutaj: nginx 502 Bad Gateway: przyczyny i rozwiązania.

Szczegół, który powoduje sporo zamieszania: ostrzeżenie pojawia się raz na fazę nasycenia, a nie przy każdym dotkniętym żądaniu. FPM ustawia wewnętrznie znacznik i kasuje go dopiero wtedy, gdy znów zwolni się jakiś proces. Pojedynczy wiersz może więc odpowiadać całej godzinie szczytu. Kto liczy wiersze, ten nie docenia skali problemu. Uczciwą miarą jest licznik max children reached na stronie statusu, którą wywołamy w dalszej części.

Droga powrotna, zanim cokolwiek zmienisz

Dwie rzeczy mogą tu pójść źle i obie uderzają w działającą produkcję.

Po pierwsze: FPM przestaje startować. Polecenie systemctl reload wysyła procesowi głównemu sygnał USR2, po czym ten uruchamia się ponownie z nową konfiguracją. Jeśli jest ona niepoprawna, inicjalizacja przerywa się, a proces główny kończy pracę. Z działającej instancji robi się więc martwa. Dlatego bez wyjątku najpierw sprawdzaj, potem przeładowuj:

php-fpm8.2 -t

Przy powodzeniu wyjście kończy się słowami test is successful.

Po drugie: wartość jest za wysoka. Serwer nie psuje się wtedy od razu, tylko przy następnym szczycie obciążenia, i to tak dokładnie, że nawet logowanie po SSH zawiesza się albo w ogóle nie dochodzi do skutku. Na taką sytuację potrzebujesz drugiej drogi na serwer.

W przypadku serwerów root KVM i serwerów dedykowanych KernelHost jest to konsola VNC w panelu klienta. Jest podpięta do warstwy wirtualizacji lub bezpośrednio do samego przyłącza i pozostaje dostępna także wtedy, gdy usługa SSH z braku pamięci przestaje odpowiadać. Zaloguj się przez nią zawczasu, choćby jeden raz. Droga ratunkowa, którą testujesz po raz pierwszy dopiero w sytuacji awaryjnej, nie jest żadną drogą ratunkową.

Zrób poza tym kopię pliku puli, ze znacznikiem czasu, żeby druga próba nie nadpisała pierwszej kopii:

mkdir -p /root/backups
cp -a /etc/php/8.2/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)
ls -l /root/backups/

Droga powrotna zajmuje wtedy trzy wiersze:

cp -a /root/backups/www.conf.2026-09-03-1015 /etc/php/8.2/fpm/pool.d/www.conf
php-fpm8.2 -t
systemctl reload php8.2-fpm

Barierka na wypadek pomyłki w obliczeniach

Limit pamięci na jednostce systemd sprawia, że błędna ocena uderzy w proces roboczy PHP, a nie w bazę danych: kernel kończy wtedy proces wewnątrz control group należącej do FPM, zamiast szukać największego procesu w całym systemie.

mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/pamiec.conf <<'EOF'
[Service]
MemoryHigh=1500M
MemoryMax=2G
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm

Sprawdź, czy ustawienie dotarło:

systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax

MemoryHigh hamuje i sprząta, MemoryMax to twarda granica. Wszystkie cztery systemy korzystają z ujednoliconej hierarchii control groups, więc wartości działają natychmiast. To barierka, a nie zamiennik obliczeń: po osiągnięciu granicy dotknięty odwiedzający nadal zobaczy błąd, tyle że nie położy się cały serwer. O tym, jak porządnie odkładać takie pliki uzupełniające, przeczytasz tutaj: tworzenie usługi systemd.

I trzecia zasada, która nie wymaga żadnego polecenia: jedna zmiana naraz. Kto rusza jednocześnie tryb pracy, pm.max_children oraz wartości spare, ten potem nie wie, co właściwie zadziałało.

Inwentaryzacja: która pula obowiązuje

Każda pula ma własne pm.max_children, a dla pamięci liczy się suma ze wszystkich pul:

grep -n "^pm" /etc/php/*/fpm/pool.d/*.conf

W stanie fabrycznym na wszystkich czterech systemach jest to samo:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

To nie są zalecenia, tylko wartości zastępcze, dzięki którym PHP wystartuje nawet na bardzo małej maszynie testowej. Do tego dochodzi globalna granica obejmująca wszystkie pule:

grep -n "^process.max" /etc/php/*/fpm/php-fpm.conf

Jeśli wyjście pozostanie puste, wiersz jest zakomentowany i obowiązuje wartość domyślna 0, czyli brak globalnego ograniczenia. Na serwerach z wieloma pulami to właśnie ten wiersz zapobiega temu, żeby suma rozsadziła pamięć, gdy tylko kilka pul jednocześnie znajdzie się pod obciążeniem.

Ostatecznie miarodajny nie jest plik, tylko to, co FPM z niego zrobi. Przełącznik -tt wypisuje konfigurację w pełni rozwiniętą, razem ze wszystkimi wartościami domyślnymi, których nie ma w żadnym pliku:

php-fpm8.2 -tt 2>&1 | grep -E "^\[|pm |pm\.|listen ="

To wyjście posłuży później także jako dowód, że zmiana faktycznie doszła do skutku.

Pomiar zapotrzebowania procesu roboczego na pamięć

W tym miejscu większość poradników robi się nieprecyzyjna. Trzy liczby regularnie bywają mylone:

  • memory_limit, fabrycznie 128M w trybie FPM: to górna granica na żądanie, a nie zużycie. Skrypt, który potrzebuje 12 MB, przy 512M nadal potrzebuje tylko 12 MB.
  • RSS: wszystko, co akurat leży w pamięci RAM, łącznie ze współdzielonym opcache i współdzielonymi bibliotekami. Kto sumuje RSS z 20 procesów, ten liczy ten sam opcache dwadzieścia razy.
  • PSS: współdzielone strony pamięci dzielone są przez liczbę korzystających z nich procesów. Jedyna z tych trzech liczb, którą da się sensownie zsumować.

Do obliczeń potrzebujesz PSS. Kernel podaje go gotowego, już podsumowanego, w /proc/<pid>/smaps_rollup, do odczytu jako root:

pgrep -f "php-fpm: pool www" | while read -r p; do
  awk '/^Pss:/ {print $2}' "/proc/$p/smaps_rollup"
done | awk '{s+=$1; n++} END {
  if (!n) { print "nie znaleziono procesów roboczych"; exit }
  printf "Procesy: %d   Suma: %.0f MiB   Średnia: %.1f MiB\n", n, s/1024, s/1024/n
}'

Dla porównania ta sama pula liczona przez RSS:

ps -eo pid,rss,args --sort=-rss | grep '[p]hp-fpm' | head

Suma RSS wypada, zależnie od rozmiaru opcache, wyraźnie wyżej. Kto na niej opiera obliczenia, dostaje za małe pm.max_children i kupuje sobie czas oczekiwania, którego wcale nie musiałoby być.

O tym, czy pomiar jest coś wart, decydują dwa warunki. Mierz pod prawdziwym obciążeniem, a nie zaraz po restarcie: świeżo utworzony proces jest tani, bo na początku tylko współużytkuje strony pamięci procesu głównego, a drogi robi się dopiero z pierwszymi żądaniami. Poza tym nie bierz samej średniej: jeśli średnia wynosi 60 MiB, a największy proces 190 MiB, nie licz z wartością 60.

Drugie źródło: log dostępu FPM

FPM potrafi na życzenie zapisywać dla każdego żądania szczytowe zużycie pamięci oraz czas działania. Oba wiersze stoją zakomentowane w pliku puli:

access.log = /var/log/php8.2-fpm.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

Zapis %{mili}d jest w tej formie poprawny, pochodzi bez zmian z dostarczonego pliku i podaje czas działania w milisekundach, a %{kilo}M szczytowe zużycie w kilobajtach. Potem sprawdź, przeładuj i zobacz, czy plik powstaje:

php-fpm8.2 -t
systemctl reload php8.2-fpm
ls -l /var/log/php8.2-fpm.access.log

Zostaw log włączony przez pełną dobę, żeby objął godziny szczytu. Potem przeanalizuj szczytowe zużycie w postaci kwantyli:

awk '{print $(NF-1)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Żądania: %d   Mediana: %d kB   p95: %d kB   Maksimum: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

Ta sama analiza dla czasu działania, potrzebnego zaraz do drugiego rachunku, sięga po kolumnę wcześniejszą:

awk '{print $(NF-2)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Mediana: %.0f ms   p95: %.0f ms   Maksimum: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'

Dwa zastrzeżenia. Po pierwsze, pozycje pól zależą od pokazanego wyżej wiersza formatu, bo kończy się on czasem działania, pamięcią i udziałem CPU. Kto zmieni access.format, musi je dostosować. Po drugie, %{kilo}M to własna księgowość pamięci PHP, a więc dolna granica: brakuje w niej kodu programu, rozszerzeń oraz pamięci, której biblioteka C nie oddaje od razu po zakończeniu żądania. Do wzoru bierz zatem PSS. Log dostępu służy do znalezienia skryptów odstających, tych, które trwale rozdymają proces.

Potem wyłącz ten log albo skonfiguruj dla niego rotację. Dostarczona reguła obejmuje wyłącznie log błędów, a pełny dysk generuje objawy, które nie mają już nic wspólnego z PHP-FPM: pełny dysk: jak znaleźć i zwolnić miejsce na serwerze Linux.

Rachunek

Liczby są dwie: górna granica wynikająca z pamięci RAM oraz zapotrzebowanie wynikające z obciążenia. Właściwa wartość to ta mniejsza z nich.

Górna granica z pamięci RAM

pm.max_children = budżet dla PHP  /  PSS na proces roboczy

Budżet to nie cała pamięć RAM:

free -m

Kolumna available uwzględnia już cache, który da się zwolnić, ale nie zawiera pamięci trzymanej w tej chwili przez twoje procesy robocze. Budżet to zatem available plus zmierzona suma PSS, minus rezerwa. Jako rezerwa sprawdza się mniej więcej 20% pamięci całkowitej, ale nie mniej niż 512 MB. Wyłapuje ona to, czego nie widać w żadnym pomiarze: rosnącą pulę buforów bazy danych, backup, aktualizację pakietów, import uruchomiony w najgorszym momencie.

RAM łącznieStale zajęteRezerwaBudżet dla PHPPSS na procespm.max_children
4 GB1,5 GB0,8 GB1,7 GB60 MB29
8 GB3,0 GB1,6 GB3,4 GB80 MB43
16 GB6,0 GB3,2 GB6,8 GB110 MB63

Zawsze zaokrąglaj w dół. Jeden proces więcej nic nie daje, jeden proces za dużo może kosztować wszystko.

Zapotrzebowanie wynikające z obciążenia

potrzebne procesy = żądania na sekundę  ×  średni czas działania w sekundach

Liczbę żądań na sekundę podaje strona statusu FPM: accepted conn podzielone przez start since daje średnią od ostatniego startu. Czas działania bierzesz z analizy powyżej, i to wartość p95, a nie medianę, inaczej zaplanujesz serwer pod spokojne popołudnie.

Przykład: 40 żądań na sekundę w szczycie, czas p95 równy 300 milisekundom, daje 12 równoczesnych żądań, a z narzutem na krótkie skoki około 20 do 25. Jeśli górna granica wynosi 43, wpisz wartość zapotrzebowania i zostaw resztę pamięci tam, gdzie przynosi więcej pożytku, czyli w cache bazy danych.

Jeśli zapotrzebowanie leży powyżej górnej granicy, i tak go nie wpisuj. Wtedy albo pamięci RAM jest za mało, albo skrypty są za wolne, albo przez PHP przechodzą żądania, które mogłyby zostać obsłużone jako plik statyczny lub z cache. Wszystkie trzy przyczyny da się usunąć, zbyt wysokiej wartości nie: ona jedynie przesuwa moment awarii.

dynamic, ondemand czy static

Tryb pracy określa, kiedy procesy powstają i znikają. Górna granica pm.max_children obowiązuje we wszystkich trzech przypadkach.

Tryb pracyProcesy przy starcieZachowaniePamięćPasuje do
dynamicpm.start_serverstrzyma w gotowości od min_spare do max_spare bezczynnych procesów, w razie potrzeby tworzy kolejne aż do max_childrenwaha się wraz z obciążeniemsytuacji typowej: jedna lub kilka pul, zmienne obciążenie
ondemandbrakuruchamia proces dopiero przy żądaniu i kończy go po pm.process_idle_timeoutw bezczynności najniższawielu pulom na jednym serwerze, witrynom o niewielkim ruchu
staticpm.max_childrendokładnie tyle procesów, na stałe, bez tworzenia i kończenia ich w trakcie pracystale na maksimumjednej puli na przeznaczonym do tego sprzęcie, równomiernemu obciążeniu

dynamic to ustawienie domyślne i dla typowego przypadku właściwe. Kosztuje trochę czasu procesora na tworzenie procesów i wymaga, żeby cztery wartości do siebie pasowały.

ondemand oszczędza w bezczynności zauważalnie pamięć, gdy na jednej maszynie leży kilkanaście pul dla rzadko odwiedzanych witryn. Ceną jest pierwsze żądanie po okresie ciszy, które musi poczekać na utworzenie procesu. pm.process_idle_timeout steruje tym, jak długo przeżywa bezczynny proces (fabrycznie dziesięć sekund), i działa wyłącznie w tym trybie pracy. Zwróć poza tym uwagę: ostrzeżenie o nasyceniu podaje tutaj max_children bez przedrostka pm.. Kto szuka znanego sobie brzmienia, ten nic nie znajdzie.

static jest uczciwy: to, co wpiszesz, zostaje zajęte od razu i na stałe, za to pod obciążeniem nie ma już żadnej niespodzianki. Ma to sens, gdy PHP jest głównym konsumentem zasobów maszyny. Jeśli PHP dzieli pamięć RAM z bazą danych, static odbiera jej możliwość okresowego trzymania większego cache.

Niezależnie od trybu pracy warto spojrzeć na pm.max_requests, fabrycznie 0, czyli bez ograniczenia. Wartość rzędu 500 wymienia każdy proces roboczy po 500 żądaniach. Przeciwko powoli rosnącemu zużyciu pamięci przez nieszczelne rozszerzenie jest to skuteczne i tanie, bo opcache pozostaje współdzielony i nie jest budowany od nowa. Nie ustawiaj jednak wartości 20, bo wtedy FPM spędzi czas na tworzeniu nowych procesów.

start_servers i wartości spare

Te trzy wartości działają tylko przy pm = dynamic i decydują o tym, jak szybko FPM reaguje na szczyt obciążenia:

  • pm.min_spare_servers: tyle bezczynnych procesów FPM trzyma w gotowości co najmniej, to bufor na skoki. Za nisko oznacza, że każdy szczyt czeka najpierw na utworzenie procesów.
  • pm.max_spare_servers: tyle bezczynnych procesów FPM toleruje najwyżej. Bez tej granicy FPM zachowywałby po szczycie wszystkie procesy, a razem z nimi ich pamięć.
  • pm.start_servers: tyle procesów istnieje bezpośrednio po starcie.

Przy starcie FPM sprawdza cztery warunki i odmawia uruchomienia usługi, jeśli któryś jest naruszony: obie wartości spare muszą być większe od zera, żadna z nich nie może być większa niż pm.max_children, max_spare nie może być mniejsze niż min_spare, a start_servers musi leżeć pomiędzy nimi. Jeśli pm.start_servers brakuje w ogóle, FPM liczy sam i zapisuje w logu:

NOTICE: [pool www] pm.start_servers is not set. It's been set to 3.

Wzór na to brzmi min_spare + (max_spare - min_spare) / 2, czyli środek między obiema wartościami spare. Jako punkt wyjścia w praktyce sprawdza się:

pm.max_children       = 40
pm.start_servers      = 10
pm.min_spare_servers  = 6
pm.max_spare_servers  = 16

Łatwo przeoczyć: bezczynne procesy też zajmują pamięć. Budżet musi udźwignąć pm.max_children, natomiast codzienne zużycie odpowiada mniej więcej wartości max_spare powiększonej o procesy aktywne. Wysoki max_spare trzyma pamięć zajętą także o trzeciej w nocy.

Związek ze swapem

Kuszące jest, żeby wciągnąć pamięć wymiany do obliczeń. Nie rób tego. Proces roboczy, którego dane leżą na dysku, odpowiada o rzędy wielkości wolniej i przez to pozostaje dłużej zajęty. Liczba równoczesnych żądań rośnie, FPM tworzy kolejne procesy, a te wypychają na dysk jeszcze więcej pamięci. To sprzężenie zwrotne jest powodem, dla którego przeciążony serwer nie pogarsza się stopniowo, tylko wywraca się w ciągu kilku minut.

To, co swap mimo wszystko daje, to bufor na wypadek awarii. Bez niego przeciążenie kończy się gwałtownie tym, że kernel zabija proces. Z nim dostajesz najpierw wolny serwer, a więc okno czasowe na reakcję. Zasada brzmi: swap tak, ale pm.max_children licz wyłącznie względem prawdziwej pamięci RAM, nigdy względem sumy pamięci RAM i swapa.

Czy już wypychasz strony na dysk, sprawdzisz w dwóch miejscach. free -m pokazuje zajętość, ale nie mówi nic o aktywności, bo raz wypchnięta i nigdy więcej niepotrzebna strona jest nieszkodliwa. Miarodajne są kolumny si oraz so, czyli wczytywanie i wypychanie na sekundę:

free -m
vmstat 1 5

Trwałe wartości powyżej zera oznaczają, że serwer pracuje przeciwko dyskowi. Jeśli twój kernel udostępnia wskaźnik presji, jest on jeszcze bardziej bezpośredni, bo nie mówi, ile stron wypchnięto, tylko jak długo procesy musiały z tego powodu czekać:

cat /proc/pressure/memory

Jeśli plik nie istnieje, funkcja jest w kernelu wyłączona i zostajesz przy vmstat. Jak założyć swap, wpisać go na stałe i odpowiednio ustawić vm.swappiness, opisuje wpis konfiguracja swapa i zapobieganie awariom z braku pamięci.

Ustawione za wysoko: OOM killer zamiast kolejki

Załóżmy, że wpisujesz 200, żeby ostrzeżenie wreszcie zniknęło. W bezczynności nic się nie dzieje, strona się ładuje, wszystko wygląda na rozwiązane. Przy następnym napływie ruchu FPM rzeczywiście tworzy do 200 procesów, każdy z pierwszymi żądaniami urasta do swojego prawdziwego rozmiaru, wolna pamięć spada, kernel wyrzuca najpierw cache plików (przez co baza danych zwalnia, a jej zapytania trwają dłużej), potem wypycha strony na dysk, a potem wkracza OOM killer.

Ten wybiera ofiarę według zużycia pamięci, a największym pojedynczym procesem na serwerze WWW nie jest proces roboczy PHP z 80 MB, tylko baza danych ze swoją pulą buforów:

dmesg -T | grep -iE "out of memory|oom-kill"
journalctl -k --since "24 hours ago" | grep -i "out of memory"

Dwa wiersze, o które chodzi, wyglądają tak:

php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Out of memory: Killed process 1234 (mariadbd) total-vm:2891234kB, anon-rss:1783456kB,
file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:4096kB oom_score_adj:0

Sprawca i ofiara to tutaj dwa różne procesy i właśnie to czyni diagnostykę nieprzyjemną: w skrzynce leży powiadomienie o padniętej bazie danych, a nikt nie myśli o konfiguracji PHP sprzed dwóch dni. Zależnie od bazy danych w nawiasie stoi mariadbd albo mysqld.

Jeśli jednak trafi na proces roboczy, FPM zgłosi to sam, a przy ustawionym limicie pamięci pojawi się to dodatkowo w journalu:

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start
php8.2-fpm.service: A process of this unit has been killed by the OOM killer.

Różnica między tymi dwoma obrazami awarii to właściwa myśl tego wpisu. Zbyt niskie pm.max_children tworzy kolejkę: mierzalną, udokumentowaną w logu, dającą się sprowadzić do jednej wartości, a serwer pozostaje osiągalny. Zbyt wysokie tworzy zabity proces w miejscu, którego sobie nie wybrałeś, w usłudze, która w pewnych okolicznościach sama nie wróci. Czas oczekiwania to stan pracy, zdarzenie OOM to incydent.

Typowe błędy i ich rozwiązania

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: w pewnym momencie wszystkie procesy robocze były zajęte. Wiersz pojawia się raz na fazę nasycenia, pojedynczy wiersz może więc oznaczać godzinę pełnego obciążenia. Podnieś wartość dopiero po zmierzeniu PSS i wyznaczeniu budżetu.

WARNING: [pool www] server reached max_children setting (5), consider raising it: ta sama sytuacja przy pm = ondemand, w treści bez przedrostka pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, a po nim ERROR: failed to post process the configuration oraz ERROR: FPM initialization failed: najczęstszy przypadek przy obniżaniu wartości pm.max_children. Kto schodzi z 50 na 8 i zostawia pm.max_spare_servers = 20, ma potem usługę, która przestaje startować. Wszystkie cztery wartości należy zmieniać razem.

ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: pm.start_servers leży poza dopuszczalnym zakresem. Popraw wartość albo usuń ten wiersz, wtedy FPM policzy sam.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): to jest memory_limit i nie ma nic wspólnego z pm.max_children. Pojedynczy skrypt zażądał więcej, niż PHP dopuszcza na jedno żądanie. Wyższe pm.max_children nic tu nie zmieni, a niższe memory_limit odwrotnie nie zmniejszy zapotrzebowania procesu roboczego na pamięć, tylko wcześniej przerwie skrypty. Przy planowaniu obowiązuje mimo to zasada: memory_limit jest górną granicą tego, o co proces może poprosić.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream w logu błędów nginx: wszystkie procesy zajęte, a do tego pełna kolejka przyjęć. Wyższe listen.backlog tylko to odsuwa, przyczyna leży w liczbie procesów albo w czasie działania skryptów.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: proces został twardo zabity z zewnątrz, z reguły przez OOM killer. Tutaj wartość jest za wysoka, a nie za niska. Sprawdź kontrolnie log kernela.

„Podniosłem wartość i nic się nie zmienia.” Niemal zawsze edytowany był niewłaściwy plik: druga wersja PHP w /etc/php/, druga pula albo kopia w pool.d/, która w ogóle nie jest wczytywana. Miarodajne jest to, do którego socketu odzywa się nginx i która pula na nim nasłuchuje:

grep -Rn "fastcgi_pass" /etc/nginx/
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

„Po przeładowaniu strona zniknęła.” Przeładowanie uruchamia FPM na nowo z nową konfiguracją, a jeśli jest ona niepoprawna, proces główny kończy pracę. Wgraj z powrotem kopię z /root/backups i przyzwyczaj się do php-fpm8.2 -t przed każdym przeładowaniem.

Po czym poznasz, że wartość jest właściwa

„Ostrzeżenie zniknęło” to żaden dowód. Przy wartości 500 też zniknie, aż do pierwszego zdarzenia OOM. Wiarygodnych sprawdzeń jest pięć.

Po pierwsze strona statusu FPM. Wpisz pm.status_path = /status do pliku puli, sprawdź konfigurację i przeładuj, a potem odpytaj socket bezpośrednio, całkowicie z pominięciem nginx. Dzięki temu strona nie jest osiągalna z sieci:

apt-get install -y libfcgi-bin
SCRIPT_NAME=/status SCRIPT_FILENAME=/status REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock

Odpowiedź niosą cztery wiersze wyjścia:

PoleZnaczenieWartość docelowa
max children reachedile razy od startu osiągnięto górną granicę0
max listen queuenajdłuższa zaobserwowana kolejka na sockecie0
max active processesnajwiększa liczba jednocześnie aktywnych procesówwyraźnie poniżej pm.max_children
slow requestsżądania powyżej request_slowlog_timeoutnajlepiej 0

Miarodajne staje się to dopiero po pełnym tygodniu obejmującym wszystkie godziny szczytu. Jeśli max active processes stoi potem na 12, podczas gdy pm.max_children wynosi 43, masz zapas i możesz wykorzystać rezerwę gdzie indziej.

Po drugie pamięć pod obciążeniem, nie w nocy, tylko w szczycie. available musi pozostawać wyraźnie powyżej zera, a kolumny si oraz so powinny stać na zerze:

free -m
vmstat 1 5

Po trzecie: żadnego zdarzenia OOM. To sprawdzenie jest najważniejsze, bo wyklucza najgorszy obraz awarii. Puste wyjście jest pożądanym wynikiem:

journalctl -k --since "7 days ago" | grep -i "out of memory"

Po czwarte suma ze wszystkich pul. Na serwerze z kilkoma witrynami liczy się nie pojedyncza wartość, tylko suma pomnożona przez PSS na proces. Musi mieścić się w budżecie także wtedy, gdy wszystkie pule jednocześnie znajdą się pod obciążeniem:

php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Po piąte: przetrwa restart. Najczęściej pomijany punkt. Wartość w pliku, który w ogóle nie jest wczytywany, rzuci się w oczy dopiero przy następnym restarcie, a ten rzadko zdarza się akurat wtedy, gdy patrzysz:

systemctl is-enabled php8.2-fpm
systemctl restart php8.2-fpm
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Zrestartuj potem serwer raz w kontrolowany sposób, dopóki jeszcze patrzysz. W przypadku serwerów root KVM i serwerów dedykowanych KernelHost taki restart wywołujesz w panelu klienta, a podnoszenie się systemu śledzisz przez konsolę VNC, nawet jeśli usługa WWW jeszcze nie odpowiada. Serwery stoją w centrum danych maincubes we Frankfurcie nad Menem.

Krótka lista kontrolna

  1. Kopia pliku puli w /root/backups, dostęp przez konsolę VNC raz wypróbowany.
  2. Zmierz PSS na proces roboczy pod prawdziwym obciążeniem, nie po restarcie, i nie licz z RSS.
  3. Wyznacz budżet: available plus bieżąca suma PSS, minus świadomie ustawiona rezerwa.
  4. Policz górną granicę, zestaw z nią zapotrzebowanie z żądań na sekundę razy czas p95, weź mniejszą wartość, zaokrąglij w dół.
  5. Wybierz tryb pracy i dostosuj wartości spare razem z pm.max_children.
  6. php-fpm8.2 -t, potem systemctl reload, potem php-fpm8.2 -tt jako dowód, że wartość została wczytana.
  7. Tydzień później sprawdź max children reached, max listen queue oraz log kernela.

Najczęstsze pytania

Jak poprawnie obliczyć pm.max_children?
Przez dwie liczby, a wygrywa ta mniejsza. Górna granica to budżet dla PHP podzielony przez proporcjonalną pamięć (PSS) jednego procesu roboczego. Budżet to kolumna available z free -m, powiększona o pamięć trzymaną w tej chwili przez działające procesy robocze i pomniejszona o rezerwę rzędu 20% pamięci całkowitej. Druga liczba to zapotrzebowanie: żądania na sekundę w szczycie pomnożone przez czas p95 w sekundach. Jeśli górna granica wynosi 43, a zapotrzebowanie 22, wpisz 22 i zostaw resztę pamięci bazie danych. Zawsze zaokrąglaj w dół.
Dlaczego zbyt wysoka wartość jest groźniejsza niż zbyt niska?
Zbyt niska wartość tworzy kolejkę. Żądania czekają w kolejce przyjęć socketu, strona zwalnia, serwer pozostaje osiągalny, a log mówi ci dokładnie, co się dzieje. Zbyt wysoka wartość powoduje pod obciążeniem brak pamięci, a wtedy kernel sam szuka sobie ofiary, i to według zużycia pamięci. Największym procesem na serwerze WWW jest zwykle baza danych ze swoją pulą buforów, a nie proces roboczy PHP. Zamieniasz więc mierzalny czas oczekiwania na niezapowiedzianą awarię zupełnie innej usługi.
Dlaczego mam liczyć z PSS, a nie z RSS?
RSS zawiera także obszary pamięci współdzielonej, przede wszystkim opcache i współdzielone biblioteki. Kto zsumuje RSS z 20 procesów roboczych, ten liczy ten sam opcache dwadzieścia razy i wychodzi mu o wiele za duże zapotrzebowanie na pamięć, czyli niepotrzebnie małe pm.max_children. PSS dzieli współdzielone strony przez liczbę korzystających z nich procesów i dlatego daje się poprawnie sumować. Wartość znajdziesz jako root w /proc/PID/smaps_rollup w wierszu Pss.
Kiedy wybrać dynamic, kiedy ondemand, a kiedy static?
dynamic to ustawienie domyślne i dla typowego przypadku właściwe: jedna lub kilka pul ze zmiennym obciążeniem. ondemand opłaca się wtedy, gdy na jednej maszynie leży wiele pul dla rzadko odwiedzanych witryn, bo w bezczynności nie istnieją tam żadne procesy. Ceną jest pierwsze żądanie po okresie ciszy, które czeka na utworzenie procesu. static pasuje, gdy PHP jest głównym konsumentem zasobów maszyny, a obciążenie jest równomierne: pamięć zostaje zajęta od razu i na stałe, za to pod obciążeniem nie ma już niespodzianek. Jeśli PHP dzieli serwer z bazą danych, static odbiera jej możliwość okresowego trzymania większego cache.
Czy mogę wliczyć swap do obliczeń?
Nie. Proces roboczy, którego dane leżą na dysku, odpowiada o rzędy wielkości wolniej i przez to pozostaje dłużej zajęty. Liczba równoczesnych żądań rośnie, FPM tworzy kolejne procesy, a te wypychają na dysk jeszcze więcej pamięci. Dlatego przeciążony serwer wywraca się w ciągu kilku minut, zamiast stopniowo zwalniać. Swap i tak ma sens, ale jako bufor na wypadek awarii: daje ci okno czasowe na reakcję, zanim kernel zabije proces. Liczy się wyłącznie względem prawdziwej pamięci RAM.
Podniosłem pm.max_children i nic się nie zmienia. Z czego to wynika?
Niemal zawsze edytowany był niewłaściwy plik. Na serwerach z kilkoma wersjami PHP w /etc/php/ albo z kilkoma pulami liczy się tylko ta pula, do której socketu naprawdę odzywa się nginx. Porównaj fastcgi_pass z konfiguracji nginx z wierszami listen w plikach pul. Co FPM faktycznie wczytał, pokaże php-fpm8.2 -tt, którego wyjście zawiera w pełni rozwiniętą konfigurację razem ze wszystkimi wartościami domyślnymi.
Po obniżeniu pm.max_children PHP-FPM przestał startować. Co się stało?
Prawdopodobnie wartości spare stoją jeszcze na starych, wyższych liczbach. FPM odmawia startu komunikatem, że pm.min_spare_servers i pm.max_spare_servers nie mogą być większe niż pm.max_children, a po nim pojawia się FPM initialization failed. Dostosuj wszystkie cztery wartości razem: obie wartości spare muszą być większe od zera i najwyżej równe pm.max_children, max_spare nie może być mniejsze niż min_spare, a pm.start_servers musi leżeć pomiędzy nimi.
Czy memory_limit ma coś wspólnego z pm.max_children?
Tylko pośrednio. memory_limit to górna granica, którą PHP wymusza na każde żądanie, fabrycznie 128M w trybie FPM. Komunikat o wyczerpanej pamięci (Allowed memory size exhausted) dotyczy pojedynczego skryptu i wyższe pm.max_children niczego tu nie poprawi. Odwrotnie, niższe memory_limit nie zmniejszy zapotrzebowania procesu roboczego na pamięć, tylko wcześniej przerwie skrypty. Związek pojawia się przy planowaniu: kto ustawia memory_limit na 1024M, musi w najgorszym razie liczyć się z takim rozmiarem na proces.

PHP-FPM pm.max_children Pamięć RAM Debian Ubuntu nginx OOM killer Optymalizacja serwera