Stap 29: Prestaties beheren

5.0 version
Maintained

Prestaties beheren

Vroegtijdige optimalisaties zijn de wortel van het kwaad.

Misschien heb je dit citaat al eerder gelezen. Maar ik citeer het graag volledig:

We moeten kleine efficiëntieverbeteringen vergeten, zeg maar 97% van de tijd: voortijdige optimalisatie is de wortel van alle kwaad. Toch mogen we onze kansen in die kritische 3 procent niet voorbij laten gaan.

—Donald Knuth

Zelfs kleine prestatieverbeteringen kunnen een verschil maken, vooral voor e-commerce websites. Nu de gastenboekapplicatie klaar is voor prime time, laten we eens kijken hoe we de prestaties ervan kunnen controleren.

De beste manier om de prestaties te optimaliseren is door het gebruik van een profiler. De meest populaire optie is tegenwoordig Blackfire (volledige disclaimer: ik ben ook de oprichter van het Blackfire project).

Introductie van Blackfire

Blackfire bestaat uit verschillende onderdelen:

  • Een client die profielen activeert (de Blackfire CLI-tool of een browserextensie voor Google Chrome of Firefox);
  • Een agent die gegevens voorbereidt en verzamelt voordat ze naar blackfire.io worden gestuurd voor weergave;
  • Een PHP-extensie (de probe) die de PHP-code instrumenteert.

Om met Blackfire te werken, moet je eerst een account maken.

Installeer Blackfire op jouw lokale machine door het volgende installatiescript uit te voeren:

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

Dit installatieprogramma downloadt de Blackfire CLI Tool en installeert vervolgens de PHP-sonde (zonder deze in te schakelen) voor alle beschikbare PHP-versies.

Activeer de PHP-probe voor ons project:

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

Herstart de webserver zodat PHP Blackfire kan laden:

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

De Blackfire CLI Tool moet worden geconfigureerd met jouw persoonlijke account gegevens (om je projectprofielen op te slaan onder je persoonlijke account). Je kan deze bovenaan de Settings/Credentials pagina vinden. Voer het volgende commando uit om de plaatsvervangers in te vullen:

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

Notitie

Voor volledige installatie-instructies, volg de officiële gedetailleerde installatiehandleiding . Deze zijn nuttig bij het installeren van Blackfire op een server.

Het opzetten van de Blackfire Agent op Docker

De laatste stap is het toevoegen van de Blackfire agent service aan de Docker Compose-stack:

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]

Om met de server te kunnen communiceren, moet je jouw persoonlijke server gegevens opvragen (deze gegevens geven aan waar je de profielen wilt opslaan – je kan er per project één aanmaken); deze kan je onderaan de Settings/Credentials pagina vinden. Bewaar ze in een lokaal .env.local bestand:

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

Je kan nu de nieuwe container lanceren:

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

Herstellen van een niet-werkende Blackfire installatie

Als je een fout krijgt tijdens het profileren, verhoog dan het Blackfire logniveau om meer informatie in de logs te krijgen:

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

Herstart de webserver:

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

En volg de logs:

1
$ symfony server:log

Profileer opnieuw en controleer de uitvoer van de log.

Blackfire in productie configureren

Blackfire is standaard opgenomen in alle SymfonyCloud projecten.

Stel de server-credentials in als omgevingsvariabelen:

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

En schakel de PHP-probe in zoals elke andere PHP extensie:

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

Varnish configureren voor Blackfire

Voordat je kan starten met profileren, moet je een manier vinden om de Varnish HTTP-cache te omzeilen. Zo niet, dan zal Blackfire nooit de PHP-applicatie raken. Je wil alleen toestemming geven voor profielaanvragen die van je lokale machine komen.

Vind jouw huidige IP-adres:

1
$ curl https://ifconfig.me/

En gebruik het om Varnish te configureren:

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 {

Je kan nu deployen.

Webpagina’s profileren

Je kan traditionele webpagina’s van Firefox of Google Chrome profileren via speciale extensies.

Vergeet op jouw lokale machine niet om de HTTP cache uit te schakelen in public/index.php. Zo niet, dan zal je de Symfony HTTP cache laag in plaats van je eigen code profileren:

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();

Om een beter beeld te krijgen van de prestaties van jouw applicatie in productie, moet je ook een profiel aanmaken van de “productieomgeving”. Standaard maakt jouw lokale omgeving gebruik van de “ontwikkelomgeving”, die een aanzienlijke overhead toevoegt (voornamelijk voor het verzamelen van gegevens voor de web debug toolbar en de Symfony profiler).

Het overschakelen van jouw lokale machine naar de productieomgeving kan worden gedaan door de APP_ENV omgevingsvariabele in het .env.local bestand te wijzigen:

1
APP_ENV=prod

Of je kunt het server:prod commando gebruiken:

1
$ symfony server:prod

Vergeet niet om aan het einde van jouw profileringssessie terug te schakelen naar dev:

1
$ symfony server:prod --off

API-resources profileren

Het profileren van de API of de SPA kan beter vanaf de CLI worden gedaan via de Blackfire CLI Tool die je eerder hebt geïnstalleerd:

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

Het blackfire curl commando accepteert exact dezelfde argumenten en opties als cURL.

Prestaties vergelijken

In de stap over “Cache” hebben we een cache-laag toegevoegd om de prestaties van onze code te verbeteren, maar we hebben de impact van de verandering op de prestaties niet gecontroleerd of gemeten. Omdat je slecht kan raden of een aanpassing alles sneller of net trager maakt, kan het zijn dat je fout raad en je jouw applicatie juist langzamer maakt in plaats van sneller.

Je zou altijd de impact van elke uitgevoerde optimalisatie moeten meten met een profiler. Blackfire maakt dit visueel gemakkelijker dankzij de vergelijkingsfunctie.

Het schrijven van functionele blackboxtesten

We hebben gezien hoe we functionele tests kunnen schrijven met Symfony. Blackfire kan gebruikt worden om surf-scenario’s te schrijven die op verzoek via de Blackfire-player kunnen worden uitgevoerd. Laten we een scenario schrijven dat een nieuw commentaar indient en deze valideert via de e-mail link in development en via de admin in productie.

Maak een .blackfire.yaml bestand aan met de volgende inhoud:

.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/"

Download de Blackfire-player om het scenario lokaal te kunnen uitvoeren:

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

Voer dit scenario uit in development:

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"

Of in productie:

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

Blackfire-scenario’s kunnen ook profielen triggeren voor elke request en prestatietests uitvoeren door het toevoegen van de --blackfire parameter.

Automatiseren van performancecontroles

Het managen van de prestaties is niet alleen het verbeteren van de prestaties van bestaande code, maar ook het controleren of er geen prestatie-regressies worden geïntroduceerd.

Het scenario dat in de vorige paragraaf is beschreven, kan automatisch worden uitgevoerd in een Continuous Integration-workflow of op regelmatige basis in productie.

Op SymfonyCloud kan je de scenario’s ook uitvoeren wanneer je een nieuwe branch aanmaakt of naar productie gaat deployen, om de prestaties van de nieuwe code automatisch te controleren.


  • « Previous Stap 28: Een applicatie internationaliseren
  • Next » Stap 30: Het ontdekken van Symfony Internals

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