De ce îmi moare PHP-FPM sub load fără erori în log

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

Rulez PHP 8.2.13 cu FPM pe o mașină cu 8 vCPU și 16 GB RAM, Nginx în față. La vreo 400 req/s, cu un endpoint care face două interogări MySQL și un apel către un serviciu intern, apare un comportament care m-a ținut ocupat două zile: procesele FPM dispar, Nginx începe să dea 502, dar în php-fpm.log nu scrie nimic, iar error_log al PHP-ului e curat. dmesg însă arată altceva:

[12345.678] Out of memory: Killed process 44211 (php-fpm) total-vm:4123456kB, anon-rss:3890104kB

Deci nu era o eroare PHP, era OOM killer-ul. Problema e că un request care alocă mult nu moare singur, moare cel mai mare proces de sub pm.max_children, iar asta poate fi un worker care servea un request sănătos.

Primul pas a fost să nu mai ghicesc. Am pornit FPM cu status page activ, pe un socket intern, nu pe port public:

; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 20
pm.max_requests = 500
pm.status_path = /fpm-status
request_terminate_timeout = 30s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log

Apoi, în Nginx, doar din rețeaua internă:

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

pm.max_children = 40 nu e o valoare aleasă după simț. Am măsurat RSS-ul unui worker sub load real, nu pe request-ul de health check, și am împărțit bugetul de memorie la valoarea aceea. La mine un worker ajungea la ~180 MB pe endpoint-ul greu, deci 40 × 180 MB ≈ 7,2 GB, plus MySQL și sistemul, încape în 16 GB. Înainte aveam 100, iar asta garanta că OOM killer-ul intră în scenă înainte să apuce FPM să refuze.

Al doilea lucru: pm.max_requests = 500 nu e magie, dar limitează acumularea de memorie din extensii care nu eliberează perfect. Al treilea: request_terminate_timeout = 30s omoară request-ul blocat pe un apel intern care nu respectă timeout-ul, în loc să țină un worker ocupat la nesfârșit.

Mecanismul din spate e simplu și merită spus: FPM nu știe câtă memorie are voie să folosească, pm.max_children e singura frână reală. Dacă îl lași mai mare decât încape în RAM, kernelul decide pentru tine și o face prost — omoară procese după RSS, nu după vină. Costul e că limitezi concurența: sub vârfuri scurte o să vezi coadă în Nginx, nu 502. Asta e un compromis acceptabil, dar e un compromis.

Unde se strică: dacă ai un endpoint care alocă 1 GB pentru un export, media de RSS nu te mai salvează, iar soluția e separarea pool-ului, nu mărirea lui max_children. Dacă rulezi în container cu limită de memorie, OOM killer-ul din container are alt comportament decât cel al hostului și dmesg de pe host nu o să te ajute. Și dacă pm = ondemand, măsurătoarea de RSS de dinainte devine inutilă, pentru că workerii se recreează și nu prin ciclul normal.

Voi cum dimensionați pm.max_children — după RSS mediu, după percentila 95, sau pur și simplu după memorie împărțită la un număr rotund?

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

0 条评论

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

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