Étape 29: Gérer les performances

5.0 version
Maintained

Gérer les performances

L’optimisation prématurée est la racine de tous les maux.

Peut-être avez-vous déjà lu cette citation auparavant, mais j’aimerais la citer en entier :

Nous devrions éviter les économies de bout de chandelles, disons dans environ 97 % des cas : l’optimisation prématurée est la racine de tous les maux. Pour autant, nous ne devrions pas ignorer ces occasions dans ces 3% cruciaux.

—Donald Knuth

Même de petites améliorations de performance peuvent faire la différence, en particulier pour les sites e-commerce. Maintenant que l’application du livre d’or est prête pour les heures de pointe, voyons comment nous pouvons analyser ses performances.

La meilleure façon de trouver des optimisations de performance est d’utiliser un profileur. L’option la plus populaire de nos jours est Blackfire (Avertissement : je suis aussi le fondateur du projet Blackfire).

Découvrir Blackfire

Blackfire est composé de plusieurs parties :

  • Un client qui déclenche des profils (l’outil Blackfire CLI ou une extension de navigateur web pour Google Chrome ou Firefox) ;
  • Un agent qui prépare et agrège les données avant de les envoyer à blackfire.io pour affichage ;
  • Une extension PHP (la sonde) qui analyse le code PHP.

Pour travailler avec Blackfire, vous devez d’abord vous inscrire.

Installez Blackfire sur votre machine locale en exécutant le script d’installation suivant :

1
$ curl https://installer.blackfire.io/ | bash

Cet installateur télécharge l’outil Blackfire CLI et installe ensuite la sonde PHP (sans l’activer) sur toutes les versions PHP disponibles.

Activez la sonde PHP pour notre projet :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/php.ini
+++ b/php.ini
@@ -6,3 +6,7 @@ max_execution_time=30
 session.use_strict_mode=On
 realpath_cache_ttl=3600
 zend.detect_unicode=Off
+
+[blackfire]
+# use php_blackfire.dll on Windows
+extension=blackfire.so

Redémarrez le serveur web pour que PHP puisse charger Blackfire :

1
2
$ symfony server:stop
$ symfony server:start -d

L’outil Blackfire CLI doit être configuré avec vos identifiants client personnels (pour stocker vos profils de projet dans votre compte personnel). Vous les trouverez en haut de la page Settings/Credentials. Exécutez la commande suivante en remplaçant les espaces réservés :

1
$ blackfire config --client-id=xxx --client-token=xxx

Note

Pour des instructions d’installation complètes, suivez le guide d’installation officiel détaillé. Elles sont utiles lors de l’installation de Blackfire sur un serveur.

Configurer l’agent Blackfire sur Docker

La dernière étape consiste à ajouter le service d’agent Blackfire dans Docker Compose :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -20,3 +20,8 @@ services:
     mailer:
         image: schickling/mailcatcher
         ports: [1025, 1080]
+
+    blackfire:
+        image: blackfire/blackfire
+        env_file: .env.local
+        ports: [8707]

Pour communiquer avec le serveur, vous devez récupérer vos identifiants de serveur personnels (ces identifiants spécifient l’endroit où vous voulez stocker les profils - vous pouvez en créer un par projet) ; ils se trouvent au bas de la page Settings/Credentials. Stockez-les dans un fichier local .env.local :

1
2
BLACKFIRE_SERVER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
BLACKFIRE_SERVER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Vous pouvez maintenant lancer le nouveau conteneur :

1
2
$ docker-compose stop
$ docker-compose up -d

Réparer une installation Blackfire en panne

Si vous obtenez une erreur pendant le profilage, augmentez le niveau de log Blackfire pour obtenir plus d’informations :

patch_file
1
2
3
4
5
6
7
--- a/php.ini
+++ b/php.ini
@@ -10,3 +10,4 @@ zend.detect_unicode=Off
 [blackfire]
 # use php_blackfire.dll on Windows
 extension=blackfire.so
+blackfire.log_level=4

Redémarrez le serveur web :

1
2
$ symfony server:stop
$ symfony server:start -d

Et suivez les logs :

1
$ symfony server:log

Profilez à nouveau et vérifiez les logs.

Configurer Blackfire en production

Blackfire est inclus par défaut dans tous les projets SymfonyCloud.

Configurez les identifiants du serveur comme variables d’environnement :

1
2
$ symfony var:set BLACKFIRE_SERVER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
$ symfony var:set BLACKFIRE_SERVER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Et activez la sonde PHP comme n’importe quelle autre extension PHP :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/.symfony.cloud.yaml
+++ b/.symfony.cloud.yaml
@@ -4,6 +4,7 @@ type: php:7.3

 runtime:
     extensions:
+        - blackfire
         - xsl
         - amqp
         - redis

Configurer Varnish pour Blackfire

Avant de pouvoir déployer pour commencer le profilage, vous devez trouver un moyen de contourner le cache HTTP de Varnish. Sinon, Blackfire n’atteindra jamais l’application PHP. Vous n’autoriserez que les demandes de profil provenant de votre machine locale.

Récupérez votre adresse IP actuelle :

1
$ curl https://ifconfig.me/

Et utilisez-la pour configurer Varnish :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--- a/.symfony/config.vcl
+++ b/.symfony/config.vcl
@@ -1,3 +1,11 @@
+acl profile {
+   # Authorize the local IP address (replace with the IP found above)
+   "a.b.c.d";
+   # Authorize Blackfire servers
+   "46.51.168.2";
+   "54.75.240.245";
+}
+
 sub vcl_recv {
     set req.backend_hint = application.backend();
     set req.http.Surrogate-Capability = "abc=ESI/1.0";
@@ -8,6 +14,16 @@ sub vcl_recv {
         }
         return (purge);
     }
+
+    # Don't profile ESI requests
+    if (req.esi_level > 0) {
+        unset req.http.X-Blackfire-Query;
+    }
+
+    # Bypass Varnish when the profile request comes from a known IP
+    if (req.http.X-Blackfire-Query && client.ip ~ profile) {
+        return (pass);
+    }
 }

 sub vcl_backend_response {

Vous pouvez maintenant déployer.

Profiler les pages web

Vous pouvez profiler les pages web traditionnelles depuis Firefox ou Google Chrome grâce à leurs extensions dédiées.

Sur votre machine locale, n’oubliez pas de désactiver le cache HTTP dans public/index.php lors du profilage : sinon, vous profilerez la couche de cache HTTP Symfony au lieu de votre propre code :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- a/public/index.php
+++ b/public/index.php
@@ -24,7 +24,7 @@ if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false
 $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);

 if ('dev' === $kernel->getEnvironment()) {
-    $kernel = new HttpCache($kernel);
+//    $kernel = new HttpCache($kernel);
 }

 $request = Request::createFromGlobals();

Pour avoir une meilleure idée de la performance de votre application en production, vous devez également profiler l’environnement « production ». Par défaut, votre environnement local utilise l’environnement de « développement », ce qui ajoute un surcoût important (principalement pour collecter des données pour la web debug toolbar et le profileur Symfony).

Le passage de votre machine locale à l’environnement de production peut se faire en changeant la variable d’environnement APP_ENV dans le fichier .env.local :

1
APP_ENV=prod

Ou vous pouvez utiliser la commande server:prod :

1
$ symfony server:prod

N’oubliez pas de le remettre sur dev à la fin de votre session de profilage :

1
$ symfony server:prod --off

Profiler les ressources de l’API

Le profilage de l’API ou de la SPA est plus efficace en ligne de commande, en utilisant l’outil Blackfire CLI que vous avez installé précédemment :

1
$ blackfire curl `symfony var:export SYMFONY_DEFAULT_ROUTE_URL`api

La commande blackfire curl accepte exactement les mêmes arguments et options que cURL.

Comparer les performances

Dans l’étape traitant du « Cache », nous avons ajouté une couche cache pour améliorer les performances de notre code, mais nous n’avons pas vérifié ni mesuré l’impact du changement sur les performances. Comme nous sommes tous très mauvais pour deviner ce qui sera rapide et ce qui est lent, vous pourriez vous retrouver dans une situation où l’optimisation rend votre application plus lente.

Vous devriez toujours mesurer l’impact de toute optimisation que vous faites avec un profileur. Blackfire facilite l’analyse grâce à sa fonction de comparaison.

Écrire les tests fonctionnels de boîte noire

Nous avons vu comment écrire des tests fonctionnels avec Symfony. Blackfire peut être utilisé pour écrire des scénarios de navigation qui peuvent être exécutés à la demande via le lecteur Blackfire. Rédigeons un scénario qui soumet un nouveau commentaire et le valide via le lien email en développement, et via l’interface d’admin en production.

Créez un fichier .blackfire.yaml avec le contenu suivant :

.blackfire.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
scenarios: |
    #!blackfire-player

    group login
        visit url('/login')
        submit button("Sign in")
            param username "admin"
            param password "admin"
            expect status_code() == 302

    scenario
        name "Submit a comment on the Amsterdam conference page"
        include login
        visit url('/fr/conference/amsterdam-2019')
            expect status_code() == 200
        submit button("Submit")
            param comment_form[author] 'Fabien'
            param comment_form[email] '[email protected]'
            param comment_form[text] 'Such a good conference!'
            param comment_form[photo] file(fake('image', '/tmp', 400, 300, 'cats'), 'awesome-cat.jpg')
            expect status_code() == 302
        follow
            expect status_code() == 200
            expect not(body() matches "/Such a good conference/")
            # Wait for the workflow to validate the submissions
            wait 5000
        when env != "prod"
            visit url(webmail_url ~ '/messages')
                expect status_code() == 200
                set message_ids json("[*].id")
            with message_id in message_ids
                visit url(webmail_url ~ '/messages/' ~ message_id ~ '.html')
                    expect status_code() == 200
                    set accept_url css("table a").first().attr("href")
                visit url(accept_url)
                    # we don't check the status code as we can deal
                    # with "old" messages which do not exist anymore
                    # in the DB (would be a 404 then)
        when env == "prod"
            visit url('/admin/?entity=Comment&action=list')
                expect status_code() == 200
                set comment_ids css('table.table tbody tr').extract('data-id')
            with id in comment_ids
                visit url('/admin/comment/review/' ~ id)
                    # we don't check the status code as we scan all comments,
                    # including the ones already reviewed
        visit url('/fr/')
            wait 5000
        visit url('/fr/conference/amsterdam-2019')
            expect body() matches "/Such a good conference/"

Téléchargez le lecteur Blackfire pour pouvoir exécuter le scénario localement :

1
2
$ curl -OLsS https://get.blackfire.io/blackfire-player.phar
$ chmod +x blackfire-player.phar

Exécutez ce scénario en développement :

1
$ ./blackfire-player.phar run --endpoint=`symfony var:export SYMFONY_DEFAULT_ROUTE_URL` .blackfire.yaml --variable "webmail_url=`symfony var:export MAILER_WEB_URL 2>/dev/null`" --variable="env=dev"

Ou en production :

1
$ ./blackfire-player.phar run --endpoint=`symfony env:urls --first` .blackfire.yaml --variable "webmail_url=NONE" --variable="env=prod"

Les scénarios Blackfire peuvent également déclencher des profils pour chaque requête et exécuter des tests de performance en ajoutant l’option --blackfire.

Automatiser les contrôles de performance

La gestion de la performance ne consiste pas seulement à améliorer la performance du code existant, mais aussi à vérifier qu’aucune régression de performance n’est introduite.

Le scénario décrit dans la section précédente peut être exécuté automatiquement dans un workflow d’intégration continue ou régulièrement en production.

SymfonyCloud permet également d”exécuter les scénarios à chaque fois que vous créez une nouvelle branche ou déployez en production pour vérifier automatiquement les performances du nouveau code.


  • « Previous Étape 28: Localiser une application
  • Next » Étape 30: Voyager au cœur de Symfony

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.