jobsphp

Fuite de mémoire PHP : autopsie d'un crash de worker en prod

Ingénierie Web. Fuite de mémoire PHP : autopsie d'un crash de worker en prod

À 3 h 17, le service ne tombe pas forcément d’un coup. La mémoire grimpe lentement, les workers deviennent plus lourds les uns après les autres, le serveur commence à swapper, puis le système finit par tuer le processus qui consomme trop.

Fuite de mémoire PHP: autopsie d’un crash de worker en production

Au matin, le diagnostic tient parfois en une ligne de supervision: php memory limit dépassé cli. Et cette ligne donne souvent une fausse impression de simplicité.

Un worker PHP qui reste actif pendant des heures ou des jours ne se comporte pas comme un script lancé, exécuté, puis terminé. Il conserve son contexte, ses objets, ses références et, dans certains cas, des allocations effectuées par des bibliothèques externes. Une fuite de mémoire PHP dans un worker daemon n’est donc pas seulement un problème de nettoyage oublié. C’est un problème de cycle de vie.

La question utile n’est pas uniquement de savoir quelle ligne consomme trop de mémoire. Il faut comprendre pourquoi cette mémoire reste attachée au processus, ce que PHP peut réellement libérer, ce que le système d’exploitation observe — et à quel moment il devient plus professionnel de redémarrer un worker plutôt que de prétendre qu’un nettoyage manuel résoudra tout.

Le paradoxe du Zend Memory Manager: la mémoire libérée ne disparaît pas forcément

Le premier malentendu vient du fonctionnement interne de PHP. Le Zend Memory Manager, ou ZMM, ne demande pas une nouvelle zone au système d’exploitation pour chaque petite allocation, puis ne la restitue pas immédiatement dès qu’une variable disparaît. Il travaille avec de grands blocs de mémoire, souvent appelés chunks, généralement de 2 à 4 Mo.

Lorsqu’un script crée des tableaux, des chaînes ou des objets, PHP réserve de la mémoire dans ces blocs. Quand ces éléments sont détruits, l’espace peut redevenir disponible pour le processus PHP lui-même. Mais cela ne signifie pas que la RAM est rendue au système d’exploitation.

C’est la distinction qui manque dans beaucoup d’analyses de fuite:

  • la mémoire peut être libérée du point de vue du moteur PHP;
  • elle peut rester réservée dans l’espace du processus;
  • elle peut apparaître comme toujours consommée dans top, ps aux ou la supervision système;
  • elle pourra éventuellement être réutilisée par une allocation ultérieure, sans que la taille du processus diminue.

Autrement dit, voir la mémoire du processus rester élevée ne suffit pas à prouver que chaque octet est encore retenu par une variable applicative. Mais l’inverse est également vrai: constater que memory_get_usage() se stabilise ne prouve pas que le worker est sain.

Dans un processus PHP longue durée, la mémoire “libérée” et la mémoire “rendue au système” sont deux événements différents.

Cette nuance change la manière de déboguer. Sur une requête PHP-FPM classique, le processus enfant finit généralement par disparaître ou être remplacé selon sa configuration. La mémoire accumulée ne devient pas un héritage éternel. Dans un daemon, elle reste là, tâche après tâche, tant que le processus reste vivant.

Pourquoi le problème est moins visible sur une requête web courte

Le modèle traditionnel de PHP masque une partie des défauts de gestion mémoire. Une requête arrive, le code s’exécute, la réponse est envoyée, puis le contexte de la requête est nettoyé. Même si une structure de données a été inutilement conservée jusqu’à la fin, elle disparaît avec le processus ou avec le cycle de requête.

Le mode CLI longue durée retire cette frontière naturelle. Un consumer de file, un traitement d’import, un daemon de synchronisation ou un serveur applicatif persistant enchaîne les opérations dans le même processus. Une petite accumulation devient alors un problème d’exploitation.

Prenons un exemple courant: chaque tâche charge un client, ses commandes, plusieurs relations, puis ajoute le résultat dans un tableau de statistiques. Si ce tableau reste attaché à un objet de service conservé par le conteneur, les données des tâches précédentes ne sont pas réellement oubliées. Le code a l’air correct à l’échelle d’une tâche. Il devient dangereux à l’échelle de dix mille tâches.

La directive memory_limit peut arrêter le script avant que le serveur ne soit totalement saturé, mais elle ne corrige pas l’accumulation. Elle ne distingue pas toujours non plus la mémoire suivie par le moteur PHP de celle consommée par une extension ou une bibliothèque native. Un worker qui atteint sa limite n’est donc pas nécessairement victime d’une seule variable démesurée; il peut être le résultat d’une dérive lente et répétitive.

Références circulaires: le nettoyage automatique a ses limites

PHP utilise principalement le comptage de références pour déterminer si une valeur peut être libérée. Lorsqu’aucune référence ne pointe plus vers une variable, sa mémoire peut être récupérée. Le mécanisme devient moins direct lorsqu’un ensemble d’objets se référence en boucle.

Un objet A contient une référence vers B, et B conserve une référence vers A. Si le reste de l’application ne pointe plus vers cet ensemble, les objets semblent inutiles. Pourtant, leur compteur de références n’atteint pas forcément zéro, puisque chacun maintient l’autre en vie.

PHP dispose d’un garbage collector capable d’identifier et de collecter certains cycles. Il peut aussi être déclenché manuellement avec gc_collect_cycles(). C’est utile, mais ce n’est pas une formule magique.

D’abord, le collecteur ne peut agir que sur les cycles qu’il sait identifier dans les structures gérées par PHP. Ensuite, déclencher une collecte après chaque élément traité peut dégrader les performances sans régler la cause. Enfin, la mémoire récupérée par le moteur peut rester dans les blocs du Zend Memory Manager, comme on l’a vu plus haut.

La bonne question est donc moins “faut-il appeler gc_collect_cycles()?” que “pourquoi ce cycle existe-t-il encore après le traitement de cette tâche?”.

Les endroits où les cycles apparaissent souvent

Dans les applications modernes, les références circulaires ne se limitent pas à deux classes écrites à la hâte. Elles peuvent se former dans plusieurs couches:

  • des objets métier qui conservent leur agrégat parent et ses enfants;
  • des événements ou écouteurs qui gardent une référence vers le service qui les a enregistrés;
  • des closures capturant implicitement un objet volumineux;
  • des caches locaux alimentés à chaque message;
  • des graphes d’entités construits par un ORM;
  • des objets de log ou de contexte conservés au-delà de l’opération initiale.

La difficulté tient au fait que le code responsable de la rétention n’est pas toujours celui qui effectue le traitement. Un handler peut appeler un service qui alimente un cache, lequel conserve une collection d’entités, laquelle contient elle-même une référence vers le contexte de traitement.

En entretien, lorsqu’un candidat me dit qu’il a “ajouté unset() partout”, je lui demande toujours ce qu’il cherchait à libérer et comment il l’a vérifié. unset() est un outil de gestion de portée, pas une preuve de correction. Supprimer une variable locale ne sert à rien si une autre référence conserve l’objet, si un tableau global l’a enregistré ou si l’ORM le maintient dans son unité de travail.

Un test simple, mais pas une conclusion

Pour observer une dérive, on peut mesurer la mémoire à intervalles réguliers, par exemple après chaque lot de tâches:

  • mémoire suivie par PHP avec memory_get_usage();
  • mémoire réellement occupée par le processus au niveau système;
  • nombre d’objets ou d’éléments traités;
  • durée d’exécution d’une tâche;
  • fréquence des collections de cycles;
  • taille des files ou des lots en entrée.

Ce suivi ne doit pas se limiter à une valeur affichée dans les logs. Une mesure isolée ne raconte rien. Ce qui compte, c’est la forme de la courbe: montée continue, paliers, hausse suivie d’une stabilisation, ou croissance uniquement lors de certaines familles de tâches.

Une mémoire qui monte pendant un lot puis revient à un plateau peut correspondre à une réserve interne réutilisable. Une mémoire qui augmente à chaque lot sans jamais se stabiliser mérite une enquête plus sérieuse. Une divergence entre memory_get_usage() et la mémoire résidente du processus oriente vers une extension, une bibliothèque native ou un comportement situé en dehors de ce que PHP mesure directement.

Doctrine et les grands volumes: l’unité de travail peut devenir le véritable réservoir

Les scripts CLI qui parcourent une base avec Doctrine illustrent très bien le problème. Le développeur pense traiter une entité, puis passer à la suivante. L’EntityManager, lui, peut conserver un ensemble beaucoup plus large d’informations: entités chargées, identités suivies, relations hydratées, changements en attente.

Sur quelques centaines de lignes, la différence passe inaperçue. Sur un import massif ou un traitement nocturne, elle devient la courbe de mémoire que l’on découvre trop tard dans la supervision.

Dans ce contexte, appeler clear() sur l’EntityManager après un lot peut libérer les références que Doctrine conserve dans son unité de travail. Il faut souvent compléter cette opération par la destruction explicite des variables locales volumineuses avec unset(), notamment lorsqu’elles contiennent des résultats, des tableaux intermédiaires ou des objets qui ne seront plus utilisés.

L’ordre et la granularité comptent. Nettoyer après chaque ligne peut coûter cher en temps et multiplier les opérations inutiles. Ne nettoyer qu’à la fin de plusieurs centaines de milliers d’enregistrements laisse au processus le temps de devenir instable. Le bon découpage dépend de la taille des entités, des relations chargées et du coût de reconstruction du contexte.

Ce qui mérite d’être observé dans un traitement ORM

Dans un script de longue durée, je regarde en priorité:

  • si les relations sont chargées par défaut alors que le traitement n’en a pas besoin;
  • si les résultats sont transformés en tableaux complets au lieu d’être consommés progressivement;
  • si l’EntityManager conserve toutes les entités depuis le début du traitement;
  • si les transactions sont refermées proprement entre les lots;
  • si les objets de log gardent des références vers les entités traitées;
  • si un cache applicatif grandit sans politique d’expiration;
  • si les exceptions interrompent le nettoyage prévu.

La dernière situation est particulièrement trompeuse. Le chemin nominal libère correctement les variables et nettoie l’EntityManager. Mais une exception, une validation impossible ou un retour anticipé contourne cette logique. Dans un worker, le processus continue ensuite avec un contexte partiellement pollué.

Un nettoyage fiable doit donc être attaché au cycle de traitement, y compris lorsque la tâche échoue. Cela ne signifie pas qu’il faut engloutir toutes les exceptions. Cela signifie qu’une erreur métier ne doit pas laisser derrière elle une transaction ouverte, une collection géante ou un contexte ORM incohérent qui contaminera les tâches suivantes.

Dans un traitement massif, le lot n’est pas seulement une unité fonctionnelle: c’est aussi une unité de récupération mémoire et de reprise sur incident.

Cette manière de penser améliore à la fois la performance, la stabilité et la négociabilité du système. Un traitement découpé en lots raisonnables est plus facile à relancer, à superviser et à expliquer à l’équipe produit qu’un daemon qui absorbe tout jusqu’au crash.

memory_get_usage() ne voit pas toute la vérité

C’est l’un des pièges les plus coûteux dans une enquête. Un développeur ajoute memory_get_usage() dans la boucle, observe une valeur presque stable et conclut que la fuite n’est pas dans son processus PHP. Pourtant, la mémoire résidente du worker continue d’augmenter dans top.

Les deux mesures ne répondent pas à la même question.

memory_get_usage() concerne la mémoire suivie par le moteur PHP. La mémoire résidente observée par le système inclut également les allocations réalisées par des extensions ou des bibliothèques natives utilisées sous le capot. Une extension liée à cURL, par exemple, peut mobiliser de la mémoire qui ne se reflète pas intégralement dans la mesure PHP.

Le décalage ne constitue pas automatiquement la preuve d’une fuite dans cURL. Il indique plutôt que l’enquête doit sortir du seul périmètre du code PHP. C’est précisément là que les diagnostics deviennent plus exigeants: il faut comparer plusieurs métriques au lieu de chercher une explication unique.

Une grille de lecture des symptômes

ObservationHypothèse raisonnableDirection d’enquête
memory_get_usage() augmente avec chaque lotObjets, tableaux, références ou cache conservés par le code PHPExaminer les collections, les propriétés persistantes et les cycles
La mémoire PHP se stabilise mais la mémoire résidente augmenteAllocation dans une extension ou une bibliothèque native, ou réserve conservée par le processusComparer les mesures PHP et système, isoler les appels externes
La mémoire monte puis se stabilise à un niveau élevéRéservation interne du Zend Memory Manager ou pic durable lié au volume d’un lotRéduire la taille des lots et observer la réutilisation
La croissance n’apparaît que sur certaines tâchesDonnées particulières, chemin d’exception, réponse externe ou relation ORM spécifiqueRejouer les familles de tâches séparément
Le worker meurt après un nombre variable de messagesAccumulation progressive ou pression mémoire dépendante du contenuAjouter une limite de tâches et corréler avec la télémétrie

Cette grille ne remplace pas un profilage. Elle évite simplement de partir dans la mauvaise direction avec une certitude injustifiée.

Le cas des appels réseau et des bibliothèques natives

Les clients HTTP, les parseurs, les extensions de compression ou les bibliothèques de traitement de fichiers peuvent modifier la consommation mémoire sans que le code applicatif semble retenir quoi que ce soit. Une connexion réutilisée trop longtemps, une réponse chargée entièrement en mémoire ou un objet de transfert conservé dans un contexte de service sont autant de pistes possibles.

Dans un environnement de type Swoole ou dans tout serveur PHP persistant, cette question devient encore plus sensible: les objets et ressources ne sont pas recréés à chaque requête comme dans le modèle PHP-FPM traditionnel. Un service conçu avec l’hypothèse implicite que “tout sera nettoyé à la fin de la requête” peut donc révéler des défauts dès qu’il passe en mode longue durée.

Il faut alors vérifier les bibliothèques utilisées, leur mode de réutilisation et leur comportement lorsqu’une tâche échoue. Le débogage d’une fuite de mémoire PHP ne consiste pas toujours à inspecter davantage de lignes PHP. Parfois, il faut isoler un appel réseau, remplacer temporairement un adaptateur, réduire le contenu de la réponse ou comparer le comportement avec un processus neuf.

Nettoyer à la main ou redémarrer: le choix qui révèle la maturité de l’architecture

Dans une équipe, le redémarrage périodique d’un worker est parfois présenté comme un aveu d’échec. C’est une mauvaise lecture. Un redémarrage planifié peut être une mesure de robustesse parfaitement légitime, surtout lorsque le processus dépend d’extensions natives, d’un ORM complexe ou de composants dont la libération mémoire n’est pas entièrement maîtrisée.

La question n’est pas de choisir entre “corriger le code” et “redémarrer”. Il faut faire les deux lorsque c’est nécessaire:

1. identifier et réduire les rétentions évitables;

2. limiter les volumes traités en une seule fois;

3. nettoyer les cycles et les contextes persistants;

4. mesurer la mémoire PHP et la mémoire du processus;

5. prévoir un remplacement contrôlé avant que la saturation ne devienne un incident.

Pour PHP-FPM, pm.max_requests joue précisément ce rôle. Il définit le nombre maximal de requêtes traitées par un processus enfant avant son arrêt et son remplacement. Cette directive ne répare pas une fuite. Elle empêche qu’un défaut modéré se transforme en accumulation infinie dans le même processus.

Le même principe s’applique aux workers CLI: un superviseur peut relancer le processus après un nombre déterminé de tâches, après une durée maximale ou lorsque certains seuils de mémoire sont atteints. Le remplacement doit être progressif et observable, pas déclenché par un meurtre brutal du système.

La différence entre un contournement propre et un pansement

Un redémarrage est un contournement sain lorsque:

  • la limite est documentée et assumée;
  • le worker termine les tâches en cours avant de sortir;
  • la file de messages garantit la reprise;
  • la supervision sait distinguer un redémarrage prévu d’un crash;
  • la fréquence de remplacement est suivie dans le temps;
  • une enquête continue si la mémoire dérive anormalement vite.

Il devient un pansement lorsque l’équipe ne sait pas combien de tâches le processus peut traiter, lorsque les messages en cours sont perdus ou lorsque les redémarrages masquent une régression qui s’aggrave à chaque livraison.

Un worker qui redémarre toutes les quelques minutes sans alerte n’est pas résilient. Il est simplement discret sur sa fragilité. La différence tient à la visibilité et à la capacité de décision.

Monitoring: surveiller la courbe, pas seulement l’alarme

Le monitoring mémoire d’un daemon PHP doit permettre de répondre à des questions opérationnelles, pas seulement de colorer un tableau de bord en rouge.

À quel rythme la mémoire augmente-t-elle? Est-ce lié au nombre de tâches, à leur taille, à un type de client, à une API particulière ou à une séquence d’erreurs? Le processus récupère-t-il une partie de la mémoire entre deux lots? Le temps de traitement s’allonge-t-il avant le crash? Les redémarrages sont-ils répartis ou concentrés sur une version précise du code?

Une supervision utile associe plusieurs dimensions:

  • mémoire suivie par PHP;
  • mémoire résidente du processus;
  • nombre de tâches traitées depuis le démarrage;
  • durée de vie du worker;
  • taille des lots;
  • nombre d’exceptions;
  • temps moyen et temps maximal par tâche;
  • nombre de redémarrages;
  • état de la file d’attente;
  • pression mémoire globale de la machine.

Le nombre de tâches depuis le démarrage est particulièrement précieux. Une hausse linéaire de la mémoire en fonction de ce compteur suggère une accumulation progressive. Une consommation qui dépend surtout de la taille de la tâche oriente plutôt vers un pic de travail ou un lot trop ambitieux.

Le seuil de mémoire n’est pas le seul seuil pertinent

Attendre que le processus atteigne la limite PHP ou que le noyau le tue revient à utiliser l’incident comme mécanisme de contrôle. Il vaut mieux sortir avant la zone dangereuse, avec une marge suffisante pour terminer proprement la tâche et laisser le superviseur relancer le worker.

Cette marge dépend de la charge de la machine, des autres services et de la variabilité des messages. Elle ne se déduit pas d’une valeur universelle. Une limite raisonnable sur une machine dédiée peut devenir risquée sur un serveur partagé avec la base de données, le cache et le frontal web.

Le monitoring doit aussi conserver l’historique des déploiements. Une fuite qui existait déjà mais restait lente n’a pas le même traitement qu’une hausse apparue immédiatement après l’introduction d’un nouveau client HTTP, d’un nouveau mapping ORM ou d’une nouvelle stratégie de mise en cache.

Méthode de diagnostic: partir du cycle de vie, puis réduire le périmètre

Lorsque le worker est déjà instable, l’urgence pousse à ajouter des nettoyages un peu partout. C’est rarement la meilleure première décision. Une démarche plus rigoureuse permet de préserver du temps et d’éviter les conclusions trop rapides.

1. Reproduire la durée de vie réelle

Un script exécuté une seule fois avec une petite quantité de données ne reproduira pas forcément le défaut. Il faut simuler l’enchaînement des tâches, les erreurs, les réponses externes et les mêmes volumes que ceux observés en production.

La reproduction doit conserver le rythme général du système. Certaines dérives n’apparaissent qu’après des milliers de cycles ou lorsqu’une tâche particulière intervient régulièrement dans la séquence.

2. Mesurer à plusieurs niveaux

Ajoutez des points de mesure à des étapes comparables: début de tâche, fin de traitement métier, après écriture en base, après nettoyage, puis avant passage à la tâche suivante.

En parallèle, observez la mémoire du processus avec les outils système. Cette comparaison permet de ne pas confondre une dérive PHP avec une consommation située dans une extension native ou avec une réserve interne du gestionnaire mémoire.

3. Réduire le scénario

Désactivez temporairement les composants un par un: ORM, appels HTTP, cache, génération de fichiers, journalisation détaillée, traitement d’images ou autre extension impliquée. L’objectif n’est pas de trouver immédiatement le coupable, mais de réduire le périmètre jusqu’à faire apparaître une différence nette.

Une réduction bien menée vaut mieux qu’une lecture héroïque de milliers de lignes. Elle donne une information exploitable sur l’adéquation entre le composant et le cycle de vie du worker.

4. Examiner les références persistantes

Cherchez les propriétés de services qui grandissent, les tableaux statiques, les caches sans expiration, les closures capturées, les gestionnaires d’événements et les objets ajoutés à des collections globales.

Demandez-vous toujours qui possède encore l’objet après la fin de la tâche. Cette question est plus efficace que la recherche mécanique de tous les appels à unset().

5. Tester le redémarrage contrôlé

Même pendant l’enquête, mettez en place une limite temporaire de durée de vie ou de nombre de tâches. Elle protège la production et fournit une mesure utile: si le worker reste stable lorsqu’il est remplacé régulièrement, cela confirme qu’une accumulation existe quelque part, sans identifier encore sa cause.

Il faut présenter ce résultat avec honnêteté. Il prouve un besoin de confinement, pas l’origine exacte de la fuite.

Ce que j’attends d’un développeur face à ce type d’incident

Sur le marché de l’emploi PHP, la différence entre un profil qui connaît les bons mots et un profil réellement opérationnel apparaît souvent dans ce genre de situation. Tout le monde peut citer le garbage collector, memory_get_usage() ou pm.max_requests. La valeur professionnelle se voit dans la manière de relier ces éléments au risque de production.

Un bon diagnostic ne promet pas que gc_collect_cycles() rendra toute la mémoire au système. Il ne présente pas unset() comme un bouton de remise à zéro. Il ne désigne pas le code PHP comme responsable avant d’avoir examiné les extensions C, l’ORM, les bibliothèques tierces et la mémoire résidente.

Il sait également expliquer les compromis à l’équipe:

  • corriger la rétention applicative lorsque c’est possible;
  • réduire les lots pour limiter les pics;
  • faire tourner les workers avec une durée de vie maîtrisée;
  • accepter un redémarrage périodique si le composant le justifie;
  • instrumenter le système afin que le prochain incident soit plus court à comprendre.

C’est une question de posture autant que de technique. En entretien, je préfère un candidat qui dit “je ne peux pas encore attribuer la fuite, mais je vais comparer ces deux métriques et isoler ces composants” à un candidat qui affirme avoir trouvé la cause après avoir vu une seule valeur de mémoire.

La lucidité est une compétence d’ingénierie. Elle évite les corrections superficielles et protège la relation avec les équipes qui dépendent du service.

Le plan d’action pour un worker PHP qui consomme trop de mémoire

Si votre daemon dérive déjà en production, avancez dans cet ordre:

1. Protégez le service. Limitez temporairement le nombre de tâches ou la durée de vie de chaque worker afin d’éviter le crash brutal et la saturation de la machine.

2. Mesurez deux réalités. Comparez memory_get_usage() avec la mémoire résidente observée au niveau système. Ne tirez aucune conclusion à partir d’une seule métrique.

3. Cartographiez le cycle d’une tâche. Notez ce qui est chargé, conservé, écrit, envoyé puis nettoyé entre deux messages.

4. Traitez les accumulations évidentes. Réduisez les collections, videz les caches temporaires, utilisez clear() avec Doctrine lorsque l’unité de travail devient trop volumineuse et libérez les variables qui retiennent effectivement de gros objets.

5. Cherchez les cycles. Utilisez le garbage collector avec discernement et inspectez surtout les propriétaires persistants: services, événements, closures, collections et contextes globaux.

6. Isolez les composants externes. Si la mémoire PHP reste stable alors que le processus grossit, examinez les appels réseau, cURL et les autres extensions ou bibliothèques natives.

7. Instrumentez le remplacement. Un redémarrage périodique doit être propre, traçable et compatible avec la reprise des tâches.

8. Revenez à la cause. Le redémarrage protège la production; il ne dispense pas d’améliorer le code et l’architecture lorsque la dérive est anormale.

Une fuite de mémoire PHP dans un worker daemon n’est pas toujours spectaculaire au départ. Elle se présente souvent comme une petite concession répétée: un tableau conservé “pour plus tard”, une entité que l’on ne détache pas, une réponse externe chargée entièrement, un service conçu pour une requête courte mais utilisé pendant plusieurs jours.

Le bon réflexe consiste à regarder le processus dans le temps. PHP nettoie beaucoup de choses, mais il ne peut pas deviner la fin logique d’un contexte que votre architecture conserve volontairement. Et lorsqu’une partie de la mémoire échappe au suivi du moteur, seule la supervision du système révèle la véritable pression exercée sur la machine.

Un worker robuste n’est pas celui qui ne redémarre jamais. C’est celui dont le cycle de vie est prévu, mesuré et compatible avec la réalité de ses dépendances. Si vous savez où la mémoire reste attachée, vous pouvez corriger. Si vous ne le savez pas encore, réduisez le périmètre, protégez la production et remplacez le processus avant le crash. C’est moins spectaculaire qu’une promesse de nettoyage parfait, mais c’est une posture d’ingénieur fiable.

Questions fréquentes

Pourquoi la mémoire libérée par PHP ne fait-elle pas baisser la mémoire du processus ?
Le Zend Memory Manager réserve de grands blocs de mémoire. Lorsqu’une variable est détruite, l’espace peut redevenir disponible pour PHP tout en restant réservé dans l’espace du processus et visible dans les outils système.
Pourquoi memory_get_usage() peut-il rester stable alors que la mémoire du worker augmente ?
memory_get_usage() mesure la mémoire suivie par le moteur PHP, tandis que la mémoire résidente inclut aussi des allocations effectuées par des extensions ou des bibliothèques natives. Il faut donc comparer les deux mesures.
Comment détecter une accumulation de mémoire dans un worker PHP ?
Mesurez régulièrement la mémoire PHP, la mémoire résidente, le nombre de tâches traitées, leur durée et la fréquence des collectes de cycles. La forme de la courbe permet de distinguer une stabilisation, des paliers ou une croissance continue.
Comment limiter la mémoire utilisée par Doctrine lors d’un traitement massif ?
Appelez clear() sur l’EntityManager après des lots adaptés et détruisez explicitement les variables locales qui retiennent de gros résultats, tableaux ou objets. La taille des lots doit tenir compte du volume des entités, des relations chargées et du coût de reconstruction du contexte.
À quoi sert gc_collect_cycles() dans un worker PHP ?
Cette fonction peut collecter certains cycles de références dans les structures gérées par PHP. Elle ne libère pas nécessairement toute la mémoire au niveau du système et son appel après chaque élément peut dégrader les performances.
Faut-il redémarrer régulièrement un worker PHP qui consomme trop de mémoire ?
Un redémarrage planifié peut être une mesure de robustesse lorsque la libération mémoire n’est pas entièrement maîtrisée. Il doit être contrôlé, observable, compatible avec la reprise des tâches et accompagné d’une enquête sur les rétentions anormales.