Symfony Live 2016 : PHP – La chasse aux memory leaks

PHP – La chasse aux memory leaks
Dans le cadre du dernier Symfony Live qui s’est tenu à Paris, un talk était dédié à la gestion de la mémoire en PHP. Les objectifs du talk était d’expliquer les origines des surconsommations mémoires, de savoir les identifier et d’éviter d’en produire.
Nous sommes tous d’accord, la première réponse serait de ne pas utiliser PHP pour des processus susceptibles d’avoir une longue durée de traitement. Malheureusement dans de nombreux cas, le choix est historique ou imposé. Et par conséquent, c’est finalement aux développeurs de gérer les éventuels problèmes.
Qu’est-ce qu’une fuite mémoire ?
Benoit introduit son talk en nous rappelant ce qu’est une fuite mémoire ; d’après Wikipédia :
A memory leak [...] occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
En gros, un programme crée des fuites mémoires lorsqu’il ne libère pas les allocations mémoires qui ne lui sont plus nécessaires.
Un tel programme consomme donc évidemment plus de mémoire qu’il ne devrait. Ce qui a pour conséquences de ralentir son exécution et également de laisser moins de ressources pour d’autres processus.
Ce cas ne devient évidemment gênant que lorsque le programme a une durée d’exécution longue, comme un démon, ou l’exécution d’une suite de tests unitaires particulièrement chargée, ralentissant alors fortement le déroulement des tests. Dans le cas de traitement de requêtes HTTP, le processus a une durée de vie trop courte pour que cela ait a priori un impact. La mémoire consommée étant libérée à la fin du traitement.
Les mécanismes internes à PHP de gestion de la mémoire
Comme dans la plupart des langages, on trouve chez PHP des mécanismes natifs de gestion de la mémoire pour libérer ce qui n’est plus nécessaire et ainsi éviter autant que possible la création de fuite. Tout cela avec plus ou moins d’efficacité.
Benoit nous détaille les principes « RefCounter » et « Circular Reference Collector » présents dans le mécanisme de « Garbage Collector » à partir de PHP 5.3 :
A savoir : une variable est stockée en mémoire dans un conteneur appelé « zval » comprenant comme informations, entre autres, sa valeur, son type et le nombre de variables, ou symboles, pointant sur ce conteneur.
Concrètement, les fuites mémoires surviennent lorsque des conteneurs qui ne servent plus restent en mémoire. Tout l’enjeu consiste donc à déterminer si un conteneur est toujours utile ou non.
On fait l’appel avec le compteur de références
De manière grossière, le « RefCounter » consiste à compter, pour chaque variable, le nombre de références qui lui sont faites. C’est-à-dire, que pour une variable donnée :
- On incrémente ce nombre à chaque création de référence
- On décrémente à chaque suppression de référence
Ce compteur, le « refcount », est stocké dans le conteneur « zval » de chaque variable.
L’objectif de ce mécanisme est de supprimer de la mémoire une variable dès lors que celle-ci n’est plus référencée, ie : un refcount égale à 0.
Grâce à Xdebug, il est possible de voir le contenu d’un conteneur « zval » d’une variable (ou symbole) avec la fonction « xdebug_debug_zval ».
Exemple minimaliste, xdebug_debug_zval permet d’afficher l’état d’un conteneur « zval »
On pourrait s’imaginer que ce simple mécanisme pourrait suffire à contenir les fuites mémoire.
Mais voici un exemple de référence circulaire tiré de la présentation :
Lors de l’exécution de la fonction buildObjects, les variables « foo » et « bar » sont référencées deux fois en mémoire ; une fois pour leur déclaration et une seconde pour les affectations mutuelles. A la fin de l’exécution, le refCounter va décrémenter les références concernant les déclarations. Chaque variable reste donc référencée une fois. Ce qui empêche le refCounter de les supprimer de la mémoire. Par cet exemple, on comprend que les références circulaires créent des fuites mémoire.
Malheureusement on crée plus de références circulaires qu’on ne le pense :
- Dans une simple structure d’arbre : parent->children->parent
- Avec un ORM comme Doctrine et un mapping de ce type : product->category->product
Tu me vois, tu me vois plus, le Circular Reference Collector
Dans l’objectif de répondre à cette problématique, PHP 5.3 vient justement avec un second mécanisme, le collecteur de références circulaires. Son principe est de s’assurer que les conteneurs « zval » sont bien pointés par des symboles. Et dans le cas contraire, de les supprimer de la mémoire.
En réalité, pour optimiser le traitement, PHP ne vérifie pas tous les conteneurs. Il ne va s’intéresser qu’à ceux dont le « refcount » vient d’être décrémenté à une valeur différente de 0. Car ceux dont la valeur vient d’être incrémentée sont, par définition, utilisés.
Mais scanner l’ensemble des conteneurs pour effectuer cette vérification est consommateur de ressources. Et plutôt que d’effectuer cette vérification continuellement, PHP stocke ces conteneurs, susceptibles d’être nettoyés, en mémoire tampon. Il ne déclenchera de vérification que lorsque la mémoire sera remplie en atteignant son nombre maximum d’éléments. PHP fixe cette limite dans le code à 10.000 éléments (la modifier est possible mais nécessite de recompiler PHP).
On comprend alors rapidement les limites de ce mécanisme : lorsque la mémoire tampon est remplie, PHP va donc s’arrêter de référencer les conteneurs qui ne sont potentiellement plus référencés. Et dès lors qu’une nouvelle variable voit son « refcount » décrémenté, c’est l’intégralité de la mémoire tampon qui est scannée. L’exécution du programme, sans cesse interrompue, est fortement ralentie et consomme de plus en plus de mémoire.
Augmenter la taille du tampon a pour conséquence d’augmenter la durée du scan lorsque celui-ci est plein. La diminuer augmente le risque de ne plus pouvoir référencer les conteneurs potentiellement inutile.
Optimiser son code
Partant des constats précédents, on comprend que l’on ne peut pas simplement se reposer sur les mécanismes internes au langage et que la fuite mémoire se traite au niveau du code. La deuxième partie du talk de Benoit se concentre justement sur les moyens d’éviter les fuites mémoires à l’échelle du développeur.
C’est l’occasion pour lui de nous présenter son outil « phpmeminfo » qui permet de debugger plus en profondeur la mémoire utilisée par un programme PHP qu’un profiler classique. L’outil nous donne accès aux données en mémoire avec notamment l’ensemble des classes et le nombre d’instance pour chacune d’entre elles.
L’outil est un peu long à prendre en main avec beaucoup d’opérations manuelles.
Néanmoins, il lui permet de nous faire une démonstration sur la base d’un code Symfony générant une fuite mémoire importante due à une mauvaise utilisation de Doctrine dans un processus d’import de données.
La présentation de Benoit nous a permis de mieux comprendre la manière dont PHP gère sa mémoire et ainsi d’anticiper les fuites mémoires lors de l’écriture de nos programmes.
Toutefois, cette problématique ne pose de réel problème que dans des cas précis, pour des processus ayant une période d’exécution « longue », pour lesquels PHP n’est pas le langage le plus adapté. Il n’y a pas de bonne raison de rencontrer ce problème dans le traitement de requêtes web classique, si c’est le cas, il y a très probablement un souci d’architecture logicielle. Cela n’empêche pas d’optimiser le traitement de chacune de vos requêtes HTTP pour abaisser la consommation mémoire globale de l’ensemble des processus.
Le sujet reste utile car il arrive malheureusement trop souvent que les environnements techniques de nos clients nous imposent le « choix » de PHP pour la réalisation de worker de traitement de données par exemple.
On retiendra donc surtout de bonne pratique de code pour libérer nos objets en mémoire de manière explicite. Dans la pratique, l’outil de Benoit n’est pas encore très pratique.
Si vous souhaitez l’aider à améliorer ça, voici son GitHub : https://github.com/BitOne/php-meminfo
Références
Slides : https://speakerdeck.com/bitone/hunting-down-memory-leaks-with-php-meminfo
Wikipédia – Memory Leak : https://fr.wikipedia.org/wiki/Fuite_de_m%C3%A9moire
PHP.net – Garbage collector :
- http://php.net/manual/fr/features.gc.php
- http://php.net/manual/fr/features.gc.refcounting-basics.php
- http://php.net/manual/fr/features.gc.collecting-cycles.php
Benjamin V
Ajouter un commentaire