De ce îmi moare PHP-FPM la 200 req/s și cum am rezolvat

这篇内容暂时只有 Română 版本,以下是原文。

Rulez un API pe PHP 8.2.14, Nginx 1.24, PHP-FPM cu pool-ul www pe 4 procese copii, pe un VPS cu 8 GB RAM și 4 vCPU. Sub 200 req/s constante, cam la 20 de minute, pm.max_children se umple, coada din Nginx crește, iar 502 apare intermitent. slowlog-ul PHP-FPM era gol; status-ul arăta listen queue: 511 și max children reached: 4. Problema nu era CPU, ci timpul de răspuns al bazei: o interogare de 1,8 s ținea un copil ocupat, iar ceilalți 3 se saturau imediat.

Primul pas a fost să măsor, nu să ghicesc. Am activat status-ul pool-ului:

; /etc/php/8.2/fpm/pool.d/www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 2s
request_terminate_timeout = 30s

În Nginx, accesul la status doar din localhost:

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}

Apoi am crescut copiii treptat, nu la întâmplare: pm = dynamic, pm.max_children = 24, pm.start_servers = 8, pm.min_spare_servers = 8, pm.max_spare_servers = 16. Am adăugat și pm.max_requests = 500, ca să nu acumuleze memorie. Am verificat cu ab -n 5000 -c 50 și am urmărit curl -s localhost/fpm-status?full | grep -E 'active|queue|max children'. Coada a scăzut sub 20, iar 502-urile au dispărut.

De ce funcționează: PHP-FPM nu are un model async; fiecare cerere blocantă consumă un proces copil. Dacă o interogare durează 1,8 s, ai nevoie de cel puțin req/s * timp copii ca să nu faci coadă. La 200 req/s și 1,8 s, matematica cere 360 de copii — imposibil pe 8 GB. Deci soluția reală a fost să reduc timpul interogării, nu să umflu pool-ul. Am adăugat un index compus pe (tenant_id, created_at) și am mutat un JOIN într-o interogare separată. Timpul a scăzut la 40 ms, iar 24 de copii au ajuns suficienți.

Unde se strică: dacă ai pm = static, pm.max_children trebuie dimensionat după memorie, nu după cereri; fiecare copil consumă 60–120 MB în funcție de extensii. Pe Windows, pm = dynamic nu are pm.max_spare_servers cu același comportament. Iar request_terminate_timeout nu oprește un curl blocat în kernel — procesul rămâne până la timeout-ul de socket. Am pățit-o cu un serviciu extern care ținea conexiuni deschise 60 s.

Voi cum dimensionați pm.max_children când aveți interogări de peste 1 s și trafic în rafale? Vă uitați la listen queue sau la max children reached?

0
赞同度 100% · 0 条评论 · 5 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。