Ostrzeżenie php-fpm max_children: ustal przyczynę w logach, zanim dodasz workery

Informacja o linkach partnerskich v3.1
Informacja o linkach partnerskich v3.2 (wersja mobilna)
Środowisko testowe Rodzina RHELAlmaLinux 9.8konto rootSprawdzono 12.10.2026

Ostrzeżenie php-fpm max_children oznacza, że wszystkie procesy workerów PHP obsługują żądania, więc nowe trafiły do kolejki. Na serwerze WordPress z Nginx, Apache i php-fpm, gdzie to ostrzeżenie narastało od tygodni, wskazałem w logach żądania zapełniające workery i rozwiązałem problem limitem szybkości w nginx zamiast podnosić ustawienie.

W cotygodniowej kontroli ostrzeżenie zapisywało się 27–187 razy dziennie, a mimo to przez te same cztery dni odwiedzający nie dostali ani jednego błędu 502 czy 504. Workery zapełniali nie odwiedzający, lecz bot zasypujący adresy administracyjne i skaner żądający nieistniejących ścieżek.

1️⃣ php-fpm max_children & seems busy

Gdy ostrzeżeń jest 66 dziennie, łatwo odczytać to jako 66 braków workerów. Log php-fpm miesza dwa różne ostrzeżenia, a większość to wczesne ostrzeżenia zapisywane, zanim limit zostanie osiągnięty.

[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it

[seems busy] oznacza, że wolnych workerów jest 0 i tworzony jest nowy proces; przy zarządzaniu procesami dynamic zapisuje się to nawet przy krótkim wzroście żądań. [server reached pm.max_children] to moment osiągnięcia limitu 26, gdy nie dało się utworzyć kolejnych, i od tej chwili nowe żądania czekają.

To różnica między zawołaniem jeszcze jednego kelnera a zostawieniem gości na zewnątrz, bo wszystkie miejsca są zajęte.

Zliczenie jednego dnia według typu dało 58 seems busy i 6 max_children. Rutyna kontrolna zsumowała oba i zgłosiła 66, ale limit został faktycznie osiągnięty sześć razy.

grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44

Sześć godzin wypisanych przez drugie polecenie staje się punktem odniesienia do śledzenia przyczyny; następny krok używa tylko tych godzin.

2️⃣ Dlaczego podniesienie max_children nie jest rozwiązaniem

Podniesienie limitu 26 może zmniejszyć liczbę ostrzeżeń, ale nie zmienia tego, jakie żądania zapełniły workery.

Typowe wyliczenie dzieli dostępną pamięć przez średnią pamięć jednego workera. Na mierzonym serwerze 26 workerów zużywało średnio 131MB, a maksymalnie 214MB, i licząc z pozostałej dostępnej pamięci, na papierze był jeszcze zapas na kolejne workery.

ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'
26 130.579 214.27 3395.05
free -m

Cztery wartości w pierwszym wyniku to liczba workerów, średnia pamięć (MB), maksymalna pamięć (MB) i suma (MB); kolumna available w wyniku free to dostępna pamięć.

Gdy workerów jest więcej niż rdzeni CPU, kolejne workery nie zwiększają liczby żądań obsługiwanych naraz; tylko wydłużają czas każdego. Dopóki odwiedzający nie dostają żadnych błędów 502 czy 504, podniesienie limitu nic nie daje, a jedynie przydziela więcej procesów botom i skanerom zajmującym workery.

Przed podniesieniem limitu sprawdź, czy żądania zajmujące workery w chwilach ostrzeżeń pochodziły od odwiedzających, czy nie; następna część pokazuje, jak to sprawdzić w access.log.

3️⃣ Śledzenie przyczyny w access.log: czas, IP, ścieżka, kod odpowiedzi

Wybierz jedną chwilę ostrzeżenia, a odpowiedź znajdziesz w access.log z tych 1–2 minut. Kolejność: wyciągnij wiersze po czasie, zlicz według IP, a potem sprawdź ścieżki i kody odpowiedzi czołowych IP.

grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
    115 140.245.x.x
     61 187.15.x.x
     17 3.36.x.x
      8 91.239.x.x
      4 79.127.x.x
      3 109.248.x.x
      3 109.248.x.x
      3 109.248.x.x
      2 79.127.x.x
      2 40.77.x.x
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head
     55 / 200
     13 /wp-login.php 404
     13 /wp-admin/plugins.php 302
     13 /wp-admin/index.php 302
     13 /wp-admin/edit.php 302
     12 /wp-admin/profile.php 302
      4 / 499
      3 /xmlrpc.php 302
      2 /xmlrpc.php 499
      2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404

9 października o 18:09 w ciągu minuty zapisano 286 żądań; jedno IP odpowiadało za 115 z nich, ścieżki były adresami administracyjnymi jak /wp-admin/profile.php, a wszystkie kody odpowiedzi to 302.

Kod odpowiedzi mówi, czy żądanie dotarło do PHP. 200 dla HTML z cache podanego bezpośrednio przez nginx oraz 444 czy 400 nigdy nie dotykają PHP, a 302, 404 i 200 bez cache zużywają po jednym workerze. Gdy ktoś bez logowania żąda adresu administracyjnego, WordPress odpowiada 302 do strony logowania, a to jedno 302 to jedno wykonanie PHP.

Jeśli na jednym serwerze są dwie lub więcej stron, globalny access.log zapisuje wiersze ich wszystkich. Najpierw sądziłem, że ten bot atakuje pierwszą stronę, ale porównanie z logami poszczególnych stron pokazało, że 22 390 z 22 391 żądań dotyczyło drugiej strony.

4️⃣ Trzy rodzaje żądań, które zapełniły workery: wp-admin bota, 404 skanera, preload cache

Zastosowanie tej samej kolejności do wszystkich chwil ostrzeżeń pokazało trzy rodzaje żądań zapełniających workery.

Pierwszym był bot, który w kółko żądał adresów administracyjnych. Jedno IP wysłało w trzy godziny 22 391 żądań; 9 944 z nich trafiło do czterech ścieżek, w tym /wp-admin/profile.php, a ponieważ każde 302 bez logowania to jedno wykonanie PHP, zajął workera 9 944 razy.

Drugim był skaner luk żądający nieistniejących ścieżek. Nawet z Googlebot w User-Agent to nie jest Googlebot: IP, które wysyłało 14 672 żądania dziennie, było wirtualnym serwerem Google Cloud z odwrotnym DNS kończącym się na googleusercontent.com, a Googlebot kończy się na googlebot.com. To samo IP na zmianę używało nazw bingbot, Applebot, GPTBot i ClaudeBot.

Żądania plików typu .env nginx ucina kodem 444 w 0,04 sekundy, ale zwykłe ścieżki spoza reguł blokady, jak /userfiles czy /secrets.yml, zmuszają WordPress do wygenerowania strony 404. Ta strona 404 kosztuje 266KB i 1,4–2,9 sekundy, tyle co wygenerowanie zwykłego wpisu, a jedno IP żądało jej 4 069 razy dziennie.

Trzecim był sam serwer. Zapisanie wpisu lub tłumaczenia opróżnia cache, a preload WP Rocket ponownie żąda wszystkich adresów, co dało 8 489 żądań dziennie i do 60 na minutę. Sprawdzanie stanu indeksowania przez Rank Math też zajmowało workera 754 razy dziennie przez Action Scheduler. Oba skupiały się w godzinach pracy i nie powodowały błędów u odwiedzających, więc pominąłem je w naprawie.

W symulatorze poniżej widać, na której warstwie kończy się każde żądanie i czy zużywa workera PHP.

🧭 Symulator drogi żądaniaStrona administracyjna wp-adminSymulator objaśniający
🗂️ Żądania, które zapełniły workery
Żądania wp-admin bota
Gdy ktoś bez logowania żąda adresu administracyjnego, WordPress odpowiada 302 do strony logowania. To jedno 302 to jedno wykonanie PHP.
Skaner: pliki typu .env
Żądania plików typu .env reguła blokady nginx ucina kodem 444. 0,04 sekundy, bez udziału PHP.
Skaner: zwykłe fałszywe ścieżki
Zwykłe ścieżki spoza reguł blokady, jak /userfiles czy /secrets.yml, zmuszają WordPress do wygenerowania strony 404. 266KB i 1,4–2,9 sekundy, tyle samo co zwykły wpis.
Preload cache
Zapisanie wpisu lub tłumaczenia opróżnia cache, a preload WP Rocket ponownie żąda wszystkich adresów. Do 60 na minutę, każde żądanie zużywa jednego workera.
Wizyta we wpisie z cache
Jeśli jest plik cache, nginx odpowiada bezpośrednio, bez udziału PHP.
Wizyta we wpisie bez cache
Jeśli nie ma pliku cache, PHP generuje stronę i zapisuje ten HTML jako plik cache.
Cały przepływ
Żądanie schodzi z góry na dół i zatrzymuje się na warstwie, gdzie kończy się odpowiedź. Workera PHP zużywają tylko żądania, które dotrą aż do WordPressa.
ŻądanieZatrzymuje się naOdpowiedźWorkery
wp-admin botaWordPress302 (do strony logowania)Zużywa 1
Skaner typu .envReguła blokady nginx444 (0,04 s)Nieużywane
Skaner zwykła fałszywa ścieżkaWordPressStrona 404 (266KB · 1,4–2,9 s)Zużywa 1
Preload cacheWordPress200 · zapis jako plik cacheZużywa 1
Wizyta we wpisie z cachePlik cache200 (nginx odpowiada bezpośrednio)Nieużywane
Wizyta we wpisie bez cacheWordPress200 · zapis jako plik cacheZużywa 1
Objaśniający symulator działający tylko w przeglądarce. Odpowiedzi i czasy pochodzą z logów i pomiarów serwera z Nginx + Apache z października 2026.

5️⃣ Limit szybkości w nginx: ograniczanie żądań bez logowania i przywracanie prawdziwego IP

Wybrałem limit szybkości zamiast blokady. Zablokujesz ścieżkę, a skaner zmieni ścieżkę; zablokujesz IP, a następnego dnia pojawi się inne. Ograniczane są tylko żądania bez logowania docierające do PHP, do 30 na minutę i 60 naraz na IP, a nadwyżka dostaje 429.

Dla serwera za CDN samo to ustawienie nie wystarczy. IP widziane przez nginx to adres edge'a CDN, więc wszyscy odwiedzający przechodzący przez ten edge zlewają się w jedno IP; najpierw trzeba zarejestrować listę IP edge'ów przez set_real_ip_from, aby przywrócić prawdziwe IP z X-Forwarded-For. Żądania z ciasteczkiem logowania są zwolnione, ale nic nie jest zwalniane na podstawie User-Agent.

# kontekst http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24;   # lista edge'ów CDN
map $http_cookie $php_anon_key {
    default                 $binary_remote_addr;
    ~*wordpress_logged_in   '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;

# location przekazująca do PHP
location @apache {
    limit_req zone=php_anon burst=60 nodelay;
    limit_req_status 429;
    proxy_pass http://127.0.0.1:8080;
}

Strony z cache obsługuje w nginx try_files i nigdy nie trafiają do tej location, a admin-ajax.php to osobna location, więc nie podlega limitowi.

Testuj równoczesnymi żądaniami. Żądania po kolei nigdy nie przekroczą 30 na minutę, bo jedno 404 trwa 1,5 sekundy, więc nie trafiają w limit; wysłanie 100 żądań po 20 naraz daje 67 odpowiedzi 404 i 33 odpowiedzi 429, co potwierdza działanie limitu.

seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -c

Jeśli wynik liczy 429, limit działa. Jeśli zwolniłeś własne IP serwera, na czas testu na chwilę zdejmij zwolnienie albo uruchom go z innej sieci.

6️⃣ Przed i po

Limit na adresy administracyjne bez logowania zaczął działać wieczorem 9 października, a limit na żądania docierające do PHP wieczorem 11. Liczba 302 dla adresów administracyjnych spadła z 9 996 dziewiątego do 11 dziesiątego i 1 jedenastego, a IP, które w trzy godziny wysłało 22 391 żądań, od następnego dnia nie wysłało żadnego.

W przypadku skanera limit dopiero wszedł w życie, więc nie ma jeszcze liczb z pełnego dnia; ocena przyjdzie z tego, jak bardzo w przyszłotygodniowej kontroli spadnie liczba stron 404 generowanych przez WordPress (2 563 dziewiątego, 2 612 dziesiątego, 1 630 jedenastego). Błędy 502 i 504 u odwiedzających wynosiły 0 przed i po.

Te liczby zliczono bezpośrednio z logów serwera WordPress z Nginx, Apache i php-fpm, a ocena opierała się nie na liczbie ostrzeżeń, lecz na błędach odwiedzających i żądaniach docierających do PHP.

📊Przed i po limicie szybkościDziennie · zliczenie logów
PrzedPo
Odpowiedzi 302 dla adresów administracyjnych
302 wysłane przez WordPress na żądania wp-admin bez logowania. Limit na adresy administracyjne bez logowania zaczął działać wieczorem 9 października.
02,4994,9987,4979,996
10/99,996Przed
10/1011Po
10/111Po
Żądania z IP bota
Dzienne żądania z jednego IP, które w trzy godziny wysłało 22 391.
05,59811,19616,79322,391
10/922,391Przed
10/100Brak żądań
10/110Brak żądań
Strony 404 wygenerowane przez WordPress
Żądania skanera do zwykłych fałszywych ścieżek. Limit na żądania docierające do PHP zaczął działać wieczorem 11 października, więc ocena nastąpi przy przyszłotygodniowej kontroli.
06531,3061,9592,612
10/92,563Przed
10/102,612Przed
10/111,630Przed · wdrożone wieczorem 11
Liczby to dzienne zliczenia access.log nginx z 9–11 października 2026.

🔢 FAQ & polecane treści

dynamic tworzy i zatrzymuje workery zależnie od liczby żądań i zapisuje ostrzeżenie seems busy, gdy wolnych workerów jest 0. static zawsze utrzymuje max_children workerów, więc nie ma opóźnienia przy tworzeniu ani ostrzeżeń, ale pamięć jest stale zajęta. Ten serwer został przy dynamic z 26; jeśli masz zapas pamięci, a ruch jest równy przez cały dzień, pasuje static.

Włącz go, gdy w chwilach ostrzeżeń pewne żądania wydają się trwać długo. Ustaw w konfiguracji puli ścieżkę [slowlog] i [request_slowlog_timeout], a dla każdego żądania przekraczającego ten czas zostanie zapisane miejsce wykonania. Tu przyczyną była liczba żądań, więc wystarczyło zliczyć access.log i slowlog nie był używany.

Limit dotyczy tylko żądań bez logowania, które omijają cache i docierają do PHP, a 429 dostaje tylko nadwyżka, gdy jedno IP przekroczy 30 na minutę lub 60 naraz. W zliczeniu z 11 października jedynym IP powyżej tej granicy był skaner, a stan przekroczenia mija w ciągu minuty, więc następne żądanie dostaje normalną odpowiedź.

Nie ograniczałem. Preload skupiał się w godzinach zapisywania wpisów i tłumaczeń i nigdy nie powodował błędów u odwiedzających. Miarą nie jest liczba ostrzeżeń, lecz to, czy odwiedzający dostali błędy 502 lub 504; jeśli ta liczba wynosi 0, preload zostaje bez zmian.

ℹ️ Informacja o linkach partnerskich
Treści tej witryny zawierają linki partnerskie. Jeśli odwiedzający kupi produkt lub usługę przez taki link, witryna otrzymuje od sprzedawcy prowizję. Kwota płacona przy tym przez kupującego (podczas promocji spada↓) nie wzrasta. Podane ceny, rabaty i dostępność odnoszą się do chwili pisania i mogą różnić się od stanu faktycznego, dlatego przed zakupem sprawdź je u sprzedawcy. Produkty wybieramy i oceniamy według własnych kryteriów, a prowizja nie wpływa ani na kolejność prezentacji, ani na treść ocen.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Prove your humanity: 4   +   5   =