$30 off During Our Annual Pro Sale. View Details »

Retour d'expérience sur l'optimisation de la st...

Bastien Jaillot
September 24, 2020

Retour d'expérience sur l'optimisation de la stack Symfony

Sur un projet Symfony avec une stack assez commune (API avec APIP, beaucoup d'entités doctrine, plein de fichiers de config YAML, environnement de développement sur docker), le projet grossissant, nous avons commencé à subir des ralentissements, aussi bien en environnement de développement qu'en production.

Souhaitant proposer une meilleure expérience de développement à mes équipes, équipé d'une superbe frontale (coucou Blackfire), j'ai étudié en profondeur le fonctionnement interne de Symfony à la recherche d'améliorations possibles.

Cette présentation retrace ce parcours initiatique dans le fonctionnement interne de Symfony, l'outillage utilisé et les leçons apprises. Elle est à la fois pratique (quoi / comment / pourquoi chercher) mais aussi théorique (fonctionnement interne de Symfony sur quelques chemins critiques).

Bastien Jaillot

September 24, 2020
Tweet

More Decks by Bastien Jaillot

Other Decks in Programming

Transcript

  1. Retour d’expérience Optimisation performance de la stack Symfony SymfonyLive Paris

    2020 – Bastien Jaillot – @bastnic Architecte @JoliCode
  2. Normal ! (?) - Docker sur mac - API Platform

    - beaucoup d’entités Doctrine - autowire / autoconfigure
  3. Performances retrouvées ! - Utilisation du binaire Symfony pour les

    environnements mac - Mock des APIs - Reste plus que notre application Symfony, elle même bien optimisée sur le papier On va donc chercher à optimiser ce qu’il y a sous le capot : Symfony, API Platform, Doctrine, les bundles... 15 Pull Requests mergées et 2 billets publiés … (et une conférence ) .
  4. Donne une vue complète et très détaillée de l’application lorsqu’elle

    est en train de tourner - vue "graphe d’appels" et "timeline" - intégration parfaite avec docker compose - permet de profiler des appels CLI comme FPM - un "player" pour les fainéants l’automatisation de tout ça (ils ont des bouquins sur leur stand, allez le faire signer par Fabien) Disclaimer : pour le besoin de cette conférence uniquement, Blackfire m’a attribué une licence entreprise
  5. D’autres outils - echo - var_dump - dump - dd

    - xdebug - … outils de tracing
  6. Mais concrètement ? - on vérifie que l’on utilise les

    dernières versions publiées - on utilise blackfire pour regarder par où l'exécution du code passe - on lit le code dans vendors/ - si on a une idée de correctif, on modifie les fichiers dans vendors - sinon, on écrit un ticket pour parler de son souci - on relance blackfire - on constate une optimisation ou pas ? - on pull le projet, on répercute le fix, on crée une PR, - on ping @bastnic sur github et/ou Slack
  7. Performance ? Ce qui coûte cher 1. les exécutions inutiles,

    le code le plus rapide c'est celui qui n'est pas écrit, ou à minima qui ne tourne pas. Indices : a. cardinalités anormales b. des appels à des services non utilisés 2. les IOs, même si on optimise son code php au maximum, un appel réseau sera toujours lent 3. l'exécution elle-même
  8. La baseline prod BLACKFIRE_EXTERNAL_ID=init-prod \ php blackfire-player.phar run .blackfire.bkf \

    --blackfire-env="bjaillot-laptop" Une seule commande lancée : - env prod, warmup effectué - plusieurs routes testées - accès aux profils blackfire /!\ projet moins critique que celui qui a déclenché toute cette conférence
  9. Les futures analyses BLACKFIRE_EXTERNAL_ID=<nom du test> \ BLACKFIRE_EXTERNAL_PARENT_ID= init-prod \

    php blackfire-player.phar run .blackfire.bkf \ --blackfire-env="bjaillot-laptop" Fournit automatiquement le comparatif de toutes mes routes par rapport à la baseline.
  10. Symfony#35252 Utilisation de isset sur une valeur qui peut valoir

    null. isset retourne toujours false dans ce cas, forçant le recalcul. Remplacement par array_key_exists et voilà! Ce qui met la puce à l'oreille ? La cardinalité du code “lourd” appelé, je n’avais pas des milliers de noms à convertir, donc le cache n’était pas efficace.
  11. Symfony#35079 Le cache optimisé généré par le warmup n’était pas

    mémoizé, et nécessitait donc d’aller chercher dans le cache backend à chaque appel. Sur un volume conséquent de données, c’est 10% de gagnés gratuitement… Ce qui met la puce à l’oreille ? La cardinalité + le nombre d’appels à apcu_fetch.
  12. ApiPlatform#3317 Encore une cardinalité qui semble off limit. Ça révèle

    que quelque chose doit-être caché. // <du code coûteux qui appelle avec plein de is_a>
  13. Les dépendances du main path : LAZY Certaines classes sur

    le chemin critique méritent votre attention, à minima : - EventListener / Subscriber - Normalizers Car ils dépendent du contenu de la requête, et pas d’un build : ils ont besoin d’être bootés pour savoir s’ils sont nécessaire pour la req courante. S’ils ont des dépendances, elles seront chargées. Ca ralentit TOUTES les requêtes.
  14. Prod : bilan des courses * sur une application existante

    choisie exprès pour son manque total d’intérêt majeur (application lambda par excellence). API liste d’entités API une entité Baseline 161ms 121ms Final 119ms 98ms Différences -26% -19%
  15. /!\ RAPPEL /!\ en prod, on n’oublie pas le PRELOAD

    (Symfony vous fournit nativement le script à inclure) *compatible PHP 7.4 seulement
  16. Conditions qui déclenchent un rebuild - quelconque modification sur Kernel.php,

    dépendances composer, fichier de config, ou classes de CompilerPass - modification de signature de classe, phpdoc, ou signature de méthode & propriétés public ou protected - ajout / suppression de fichier dans un dossier surveillé par autoconfig / autowire - et plein d’autres spécificités (routes, translations, templates, etc.)
  17. Dev : on en est où ? * sur une

    application existante choisie exprès pour son manque total d’intérêt majeur (application lambda par excellence). API liste d’entité API une entité Baseline 838ms 839ms Final optim prod 822ms 826ms Différences que dale :( snif :(
  18. doctrine/annotations#301 Doctrine a son propre watcher de fichiers, basé sur

    filemtime. Mais pour chaque fichier, il va chercher toutes ses interfaces, classes parentes, traits, et récupérer leur date de dernière modification. J’utilise beaucoup de traits et d’interfaces, ce qui fait que c’est ce qui me coûte le plus cher. On ajoute un cache statique.
  19. symfony/recipes#405 Le Kernel généré par défaut à l’époque était archi

    suboptimal. Il indiquait beaucoup beaucoup de dossiers / fichiers possibles, multipliant les chemins à vérifier à chaque requête. Vérifiez votre Kernel.php
  20. Le Kernel par défaut parcourait trop de dossier, et en

    GLOB et sur plusieurs extensions (php,xml,yaml,yml), maintenant il ne cherche que quelques fichiers, et uniquement en YAML. Du coup : utilisation de code performant, et sur moins de fichiers / dossiers.
  21. Symfony#35109 En dev, les fichiers Yaml/XML validator et serialization sont

    lus au runtime. Alors que s’ils sont modifiés, une nouvelle phase de build est lancée. On ajoute une phase de cache warmer. Récupération d’un code vu sur Api Platform dans Symfony. Merci Teoh.
  22. Doctrine-bundle#1115 Doctrine Bundle valide le schéma de vos entités à

    chaque requête. Mon schéma est figé depuis des mois et il existe une commande en CI pour ça. J’ai ajouté une option pour le désactiver, évitant ainsi le chargement de metadata inutiles
  23. Dev : bilan des courses SANS la modif Alpine, qui

    représente 60% à elle toute seule, qui aurait fait que la baseline serait à 3s (temps sans rebuild, car blackfire joue un warmup puis plusieurs samples) API liste d’entités API une entité Baseline 838ms 839ms Post-prod 822ms 822ms Final optim dev 482ms 427ms Différences -42% -49%
  24. Les watchers coûtent chers Mais ils permettent les autowire /

    autoconfigure et la config simplifiée que je ne suis pas prêt à sacrifier !
  25. On peut donc avoir des performances bien plus agréable en

    dev tout en ayant une Dx au top ! (optimisé) (désactivable si besoin) (optimisé) (optimisé)
  26. Strangebuzz/cache-warmer https://www.strangebuzz.com/en/blog/introducing-cw-a-cache-watcher-for-symfony Le principe : watcher en go qui relance

    le rebuild du kernel dans votre dos. Double gains : - ce n’est pas vous dans votre client d’api / browser qui vous vous tapez le rebuild du cache - on pourrait enlever les watcher vu qu’ils ne servent plus à rien PS : également réalisable depuis PHPStorm, gulp and co.
  27. On peut donc avoir des performances de production en dev

    tout en ayant une Dx au top ! (optimisé) (désactivable si besoin) (optimisé)
  28. On n’hésite plus à lire du code dans vendor/ (la

    trace blackfire peut vous aider à savoir où regarder)
  29. Ticket / Pull request / Patience - avoir une PR

    mergée dans les gros projets est compliqué et demande un investissement conséquent - il faut être patient : votre cas n’est pas le seul, et il faut convenir à tous, donc beaucoup auxquels vous n’avez même pas idée - qualité++ / test++ / documentation : il ne faudra rien laisser au hasard - performance c’est de la feature, donc release dans longtemps (sauf si considéré comme un bug)
  30. La performance c’est bon mangez-en • le temps gagné par

    mon équipe sur ce projet grâce aux gains de performance en environnement de développement équivaut à plusieurs centaines d’années de licence blackfire • sur un projet open source les gains de chacun profite à tous • ces conseils peuvent s’appliquer à TOUS les projets (je ferais bien une passe sur Sylius, Drupal…) • je peux vous aider, pingez moi par Mail / Github / Slack / Twitter / Carte postale / Pigeon voyageur