Am oprit un memory leak în PHP 8.2 pornind de la un var_dump uitat

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

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
赞同度 100% · 0 条评论 · 4 阅读

0 条评论

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

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