Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

    View Slide

  2. Telle perte de performance
    que l’eXpérience de
    Développement (DX) est
    devenue très désagréable

    View Slide

  3. Normal ! (?)
    - Docker sur mac
    - API Platform
    - beaucoup d’entités Doctrine
    - autowire / autoconfigure

    View Slide

  4. Arrêter de rejeter la faute et
    VRAIMENT analyser ce qui se
    passe…

    View Slide

  5. 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 )
    .

    View Slide

  6. View Slide

  7. Méthodologie

    View Slide

  8. Fabien Potencier, https://symfony.com/book

    View Slide

  9. 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

    View Slide

  10. D’autres outils
    - echo
    - var_dump
    - dump
    - dd
    - xdebug
    - … outils de tracing

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. Comment se décompose une
    requête en environnement de
    PROD ?

    View Slide

  14. View Slide

  15. View Slide

  16. 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

    View Slide

  17. Les futures analyses
    BLACKFIRE_EXTERNAL_ID= \
    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.

    View Slide

  18. View Slide

  19. View Slide

  20. Rendre les normalizers “cachable”
    https://symfony.com/blog/new-in-symfony-4-1-faster-serializer

    View Slide

  21. View Slide

  22. 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.

    View Slide

  23. 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.

    View Slide

  24. ApiPlatform#3317
    Encore une cardinalité qui semble off limit. Ça
    révèle que quelque chose doit-être caché.
    //

    View Slide

  25. 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.

    View Slide

  26. 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%

    View Slide

  27. View Slide

  28. /!\ RAPPEL /!\
    en prod, on n’oublie pas
    le PRELOAD
    (Symfony vous fournit nativement le script à inclure)
    *compatible PHP 7.4 seulement

    View Slide

  29. Comment se décompose une
    requête en environnement de
    DEV ?

    View Slide

  30. View Slide

  31. 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.)

    View Slide

  32. 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 :(

    View Slide

  33. View Slide

  34. View Slide

  35. View Slide

  36. View Slide

  37. 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.

    View Slide

  38. 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

    View Slide

  39. 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.

    View Slide

  40. Symfony#35009

    View Slide

  41. https://github.com/symfony/symfony/commit/d7a06790119b04e4f3e96f36dbf68e70ec5964ae

    View Slide

  42. https://github.com/symfony/symfony/commit/d7a06790119b04e4f3e96f36dbf68e70ec5964ae
    ¯\_(ツ)_/¯

    View Slide

  43. View Slide

  44. 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.

    View Slide

  45. 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

    View Slide

  46. 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%

    View Slide

  47. Les watchers coûtent chers
    Mais ils permettent les autowire /
    autoconfigure et la config
    simplifiée que je ne suis pas prêt
    à sacrifier !

    View Slide

  48. 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é)

    View Slide

  49. 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.

    View Slide

  50. On peut donc avoir des performances de production en dev
    tout en ayant une Dx au top !
    (optimisé)
    (désactivable si besoin)
    (optimisé)

    View Slide

  51. Enseignements

    View Slide

  52. On n’hésite plus à lire du code
    dans vendor/
    (la trace blackfire peut vous aider à savoir où regarder)

    View Slide

  53. Des canards

    View Slide

  54. View Slide

  55. 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)

    View Slide

  56. On maintient à jour sa stack !

    View Slide

  57. 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

    View Slide

  58. Merci.
    Symfony Live Paris 2020 – Bastien Jaillot – @bastnic

    View Slide