Retour d’expérience sur l’optimisation des performances sur Symfony

Renaud Jacquot
4 min readFeb 12, 2018

--

Tour d’horizon des différentes façons d’améliorer les performances sur une application développée en Symfony à travers les problématiques rencontrées sur le site checknews.liberation.fr

Nous verrons dans cet article, à travers le retour d’expérience des optimisations du site liberation.checknews.fr, les différentes actions à mettre en place pour améliorer les performances et réduire l’utilisation de ressources serveur.

Celles-ci se regroupent au sein de cinq étapes clés :

  • Optimisation de l’utilisation de la base de données
  • Gestion d’Algolia
  • Mise en cache Doctrine
  • Cache Symfony
  • Cache HTTP

Problématique :

Dans le cadre du projet Checknews, développé en Symfony 3.4 et hébergé sur Heroku, nous avons utilisé ClearDB Mysql pour la base de données et Memcachier pour le serveur Memcached. Suite à cette configuration, nous avons été confrontés à des problèmes de performances. De nombreuses erreurs 500 “Mysql server has gone away“ étaient visibles dans les logs (addons LogEntry d’Heroku).

ClearDB, le prestataire de base de données choisi, autorise un nombre de requêtes maximum par heure, en fonction de l’abonnement souscrit. Dans un premier temps, nous avons donc augmenté l’offre d’abonnement, mais celle-ci n’était pas toujours suffisante lors des pics de fréquentations liés au trafic issu des réseaux sociaux lors du partage des articles du site. Une fois arrivés au maximum de l’offre disponible, il est apparu évident qu’il fallait s’attaquer de front aux problèmes de requêtes effectuées en trop grand nombre.

Optimisation de l’utilisation de la base de données

Afin de ne pas atteindre le quota horaires de ClearDB, il fallait limiter le nombre de requêtes effectuées.

Le bundle LexikMaintenanceBundle a été mis en place pour la mise en maintenance du site. Celui-ci permet de créer un verrouillage (lock file).

Néanmoins, il s’est avéré que le FileDriver, chargé de créer le lock dans le filesystem du serveur, ne convenait pas avec un hébergement sur Heroku. En effet, Heroku ne conserve pas les fichiers à chaque build.

Afin d’assurer la persistance du mode maintenance entre deux déploiements, nous avions opté pour le driver MySQL, mais celui-ci effectue 2 requêtes à chaque page affichée :

  • une première pour vérifier que la table existe
  • une seconde pour la présence du lock

Pour assurer la persistance du lock tout en supprimant ces requêtes, nous avons utilisé notre serveur Memcached. Le bundle ne propose pas de driver pour Memcached, mais il est simple d’en créer un en utilisant le driver Memcache existant.

Deux options “username” et “password” ont été ajoutées pour permettre d’utiliser les crédentials fournis par le Memcachier.

Driver memcached pour LexikMaintenaceBundle

Algolia

La plateforme Checknews dispose d’une recherche textuelle gérée par Algolia. Nous avons donc mis en place le bundle algolia/search-bundle. Il permet d’indexer les entités Doctrine et d’effectuer des recherches en utilisant l’API du moteur de recherche.

Au départ, nous étions partis initialement sur l’utilisation de la native search du bundle algolia/search-bundle. Cette méthode a l’avantage d’être simple à mettre en place, mais il s’est avéré qu’elle multipliait les requêtes SQL effectuées. En effet, pour chaque résultat, le bundle convertit chaque résultat en entité doctrine à l’aide la méthode “find” du repository. Il n’est pas possible de personnaliser la méthode du repository utilisée, ce qui empêche de faire des jointures SQL, et peut engendrer des requêtes supplémentaires pour accéder aux données des entités liées.

Pour diminuer le nombre de requêtes, nous avons utilisé la raw search qui permet de récupérer les données brutes d’Algolia et d’effectuer la requête manuellement.

Service exécutant une raw query Algolia

Ce changement a permis de réduire drastiquement le nombre de requête. En parallèle, pour améliorer les performances, nous avons utilisé l’hydratation array de doctrine.

Mise en cache doctrine

Nous avons mis à contribution le serveur Memcached en configurant Doctrine pour utiliser Memcached comme système de cache.

Pour cela, il a fallu :

  • D’abord créer un service “app.memcached”, qui permet d’utiliser la classe Memcached et de configurer les paramètres de connection au serveur Memcached
  • Puis un second service “doctrine.cache.memcached”, en utilisant la classe fournie par Doctrine, où nous avons injecté notre premier service

Les résultats des recherches paginées sont ainsi mis en cache pour une durée de 10min. Les paramètres de la recherche sont concaténés avec le numéro de la page courante pour servir d’identifiant lors de l’enregistrement dans le cache.

Cache Symfony

Nous avons mis en cache les éléments communs aux utilisateurs en utilisant le service ”‘app.memcached” configuré précédemment, en déclarant un nouveau cache Symfony (tout comme la liste des dernières questions publiées).

Configuration cache symfony

Cache HTTP

A ce stade, il était encore possible d’économiser les ressources serveurs, mais aussi d’améliorer le temps de réponse côté utilisateur, en configurant les entêtes de réponses en suivant la spécification HTTP (Documentation Symfony cache HTTP).

Parallèlement aux entêtes, nous avons activé le mode ESI de Symfony. Ceci permet de gérer les entêtes des sous-requêtes individuellement.

Les contrôleurs ont été modifiés afin de retourner un code 304 en se basant sur la date de dernière modification du contenu en utilisant l’entête HTTP “Last-Modified”. Afin de connaître cette date, nous avons pris l’habitude de créer une colonne permettant de stocker la date de dernière modification sur nos entités.

Dans le cas de listing où la date de dernière de modification n’est pas identifiable, nous avons utilisé l’entête “ETag”. L’entête est générée en faisant une empreinte md5 du contenu rendu par Twig.

Conclusion

Aujourd’hui, suite à la mise en place de ces différentes étapes, travaillées de façon empirique, la plateforme Checknews ne rencontre plus de problèmes de performance ni d’erreurs lors des pics de fréquentation sur le site.

Bien sur, il est toujours possible d’aller encore plus loin, nous nous interrogeons notamment sur l’utilisation de APCu pour la mise en cache au niveau de Doctrine ou de remplacer le serveur Memcached par Redis ou encore sur la durée optimale de mise en cache des différents éléments. A ce sujet, je vous invite à lire cet article qui se penche sur les différentes solutions de cache possibles.

--

--