Acum două săptămâni, pe un worker PHP 8.2 CLI care procesa cozi de ~120k mesaje/zi, RSS-ul creștea de la 60 MB la 900 MB în șase ore, apoi OOM killer. Nu era PHP-FPM, deci nu se aplicau sfaturile obișnuite cu pm.max_requests. Workerul rula cu opcache.enable_cli=1, memory_limit=-1 și un loop simplu peste un Pheanstalk cuplat la un handler de mesaje.
Am început cu ce era ieftin: am numărat obiectele pe tip la fiecare 1000 de mesaje, fără xdebug, doar cu get_declared_classes() și gc_status(). La mesajul 4000, gc_status()['roots'] era constant, deci nu era un ciclu clasic. Am bănuit un buffer intern și am tăiat handlerul bucată cu bucată. Leak-ul a apărut la un PDOStatement refolosit într-o buclă, dar nu din cauza statement-ului: handlerul ținea o referință la un closure care captura $this într-un array static de „ultimele 50 de erori”. Fiecare mesaj adăuga câte un closure nou; array-ul era plafonat la 50, dar closure-urile capturau conexiunea PDO și statement-ul, deci 50 de conexiuni rămâneau deschise și fiecare avea buffer-ul ei.
// înainte: closure-ul ținea PDO în viață
static $recent = [];
$recent[] = function () use ($pdo, $msg) { return $pdo->lastInsertId(); };
$recent = array_slice($recent, -50);
Am schimbat contractul: în loc de closure, un array simplu cu id și errno, iar conexiunea PDO o singură instanță partajată, creată în afara loop-ului.
static $recent = [];
$recent[] = ['id' => $msg->getId(), 'errno' => $pdo->errorCode()];
$recent = array_slice($recent, -50);
RSS-ul s-a stabilizat la 75 MB după 8 ore. Prețul: am pierdut lastInsertId() în log-ul de erori, dar oricum nu avea sens acolo. Am adăugat și gc_collect_cycles() la fiecare 10k mesaje, deși nu a fost cauza; a rămas ca plasă de siguranță.
Unde se rupe abordarea: dacă leak-ul ar fi fost în extensia Pheanstalk sau în PDO la nivel de driver, tăierea handlerului nu l-ar fi găsit. Și get_declared_classes() nu ajută când leak-ul e în buffere C, nu în obiecte PHP. Mi s-a întâmplat și invers: am petrecut o zi pe un „leak” care era de fapt memory_get_usage(true) raportând pagini rezervate, nu ocupate.
Ce faceți voi când leak-ul nu apare în gc_status() și nu puteți rula xdebug în producție?


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