Schritt 29: Performance-Management

5.0 version
Maintained

Performance-Management

Vorzeitige Optimierung ist die Wurzel allen Übels.

Vielleicht hast Du dieses Zitat schon einmal gelesen. Aber ich würde es gern vollständig zitieren:

Wir sollten die kleinen Effizienzgewinne vergessen, sagen wir in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels. Dennoch sollten wir unsere Chancen bei diesen kritischen 3 % nicht verpassen.

—Donald Knuth

Selbst kleine Performance-Steigerungen können einen Unterschied machen, insbesondere bei E-Commerce-Websites. Nachdem die Gästebuchanwendung nun für die Prime Time bereit ist, lass uns sehen, wie wir ihre Performance überprüfen können.

Der beste Weg, Performance-Optimierungen zu finden, ist die Verwendung eines Profilers. Die beliebteste Option ist heutzutage Blackfire (voller Haftungsausschluss: Ich bin auch der Gründer des Blackfire-Projekts).

Blackfire

Blackfire besteht aus mehreren Teilen:

  • Ein Client, der die Analyse auslöst (das Blackfire CLI-Tool oder eine Browsererweiterung für Google Chrome oder Firefox);
  • Ein Agent, der Daten aufbereitet und aggregiert, bevor er sie zur Anzeige an blackfire.io sendet;
  • Eine PHP-Erweiterung (die * Probe*), die den PHP-Code instrumentiert.

Um mit Blackfire arbeiten zu können, musst Du Dich zuerst anmelden.

Installiere Blackfire auf Deinem lokalen Computer, indem Du das folgende Schnellinstallationsskript ausführst:

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

Dieser Installer lädt das Blackfire CLI Tool herunter und installiert dann die PHP-Probe (ohne sie zu aktivieren) auf allen verfügbaren PHP-Versionen.

Aktiviere die PHP-Probe für unser Projekt:

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

Starte den Webserver neu, damit PHP Blackfire laden kann:

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

Das Blackfire CLI Tool muss mit Deinen persönlichen Client-Credentials konfiguriert werden (um Deine Projektanalyse unter Deinem persönlichen Konto zu speichern). Du findest sie oben auf der Settings/Credentials Seite. Führe den folgenden Befehl aus, indem Du die Platzhalter ersetzt:

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

Bemerkung

Eine vollständige Installationsanleitung findest Du in der offiziellen ausführlichen Installationsanleitung. Sie ist nützlich, wenn Du Blackfire auf einem Server installierst.

Den Blackfire-Agenten in Docker einrichten

Der letzte Schritt besteht darin, den Blackfire Agent-Service in den Docker Compose-Stack aufzunehmen:

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]

Um mit dem Server zu kommunizieren, benötigst Du Deine persönlichen Server-Credentials (diese identifizieren, wo Du Deine Analysen speichern möchtest – Du kannst pro Projekt eines erstellen); Du findest sie am Ende der Settings/Credentials-Seite. Speichere sie in einer lokalen .env.local-Datei:

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

Du kannst nun den neuen Container starten:

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

Eine nicht funktionierende Blackfire-Installation reparieren

Wenn Du beim Analysieren einen Fehler erhälst, erhöhe das Blackfire-Log-Level, um weitere Informationen in den Logs zu erhalten:

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

Starte den Webserver neu:

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

Und lass dir die Logs ausgeben:

1
$ symfony server:log

Analysiere erneut und überprüfe die Log-Ausgabe.

Blackfire auf dem Produktivsystem konfigurieren

Blackfire ist standardmäßig in allen SymfonyCloud-Projekten enthalten.

Richte die Server-Credentials als Environment-Variable ein:

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

Und aktiviere die PHP-Probe wie jede andere PHP-Erweiterung:

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 für Blackfire konfigurieren

Bevor Du deployst um Blackfire nutzen zu können, benötigst Du eine Möglichkeit, den Varnish HTTP-Cache zu umgehen. Wenn nicht, wird Blackfire nie direkt auf die PHP-Anwendung zugreifen. Du wirst nur autorisierte Anfragen analysieren, die von deinem lokalen Rechner kommen.

Finde Deine aktuelle IP-Adresse heraus:

1
$ curl https://ifconfig.me/

Und verwende sie, um Varnish zu konfigurieren:

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 {

Nun kannst Du deployen.

Websiten analysieren

Du kannst traditionelle Webseiten in Firefox oder Google Chrome über die entsprechenden Erweiterungen analysieren.

Vergiss nicht, den HTTP-Cache auf Deinem lokalen Rechner in public/index.php beim Analysieren zu deaktivieren: Wenn nicht, wirst Du den Symfony HTTP-Cache-Layer anstelle Deines eigenen Codes analysieren:

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

Um Dir ein besseres Bild von der Performance Deiner Anwendung auf dem Produktivsystem zu machen, solltest Du auch die „Production“-Environment analysieren. Standardmäßig verwendet Deine lokale Environment die „Development“-Environment, die eine Menge Extras mit sich bringt (hauptsächlich um Daten für die Web-Debug-Toolbar und den Symfony-Profiler zu sammeln).

Die Umstellung Deiner lokalen Maschine auf die Produktivumgebung kann durch Ändern der Environment-Variable APP_ENV in der .env.local-Datei erfolgen:

1
APP_ENV=prod

Oder du kannst den server:prod-Befehl verwenden:

1
$ symfony server:prod

Vergiss nicht, wieder auf dev umzustellen, wenn deine Analyse-Sitzung endet:

1
$ symfony server:prod --off

API-Ressourcen analysieren

Die Analyse der API oder der SPA erfolgt besser in der CLI über das Blackfire CLI Tool, das Du zuvor installiert hast:

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

Der blackfire curl-Befehl akzeptiert genau die gleichen Argumente und Optionen wie cURL.

Performancevergleich

Im Schritt über „Cache“ haben wir eine Cache-Ebene hinzugefügt, um die Performance unseres Codes zu verbessern, aber wir haben die Auswirkungen der Änderung auf die Performance weder überprüft noch gemessen. Da wir alle sehr schlecht darin sind, zu erraten, was schnell und was langsam ist, kannst Du in eine Situation geraten, in der eine Optimierung Deine Anwendung tatsächlich langsamer macht.

Du solltest immer die Auswirkungen jeder Optimierung, die Du durchführst, analysieren. Blackfire macht dies dank seiner Vergleichsfunktion optisch einfacher.

Funktionale Black-Box-Tests schreiben

Wir haben gesehen, wie man mit Symfony funktionale Tests schreibt. Mit Blackfire kann man Browser-Szenarien schreiben, die bei Bedarf über den Blackfire-Player ausgeführt werden können. Schreiben wir ein Szenario, das einen neuen Kommentar einreicht und ihn über den E-Mail-Link in DEV und übers Admin-Backend in PROD validiert.

Erstelle eine .blackfire.yaml Datei mit folgendem Inhalt:

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

Lade den Blackfire-Player herunter, um das Szenario lokal ausführen zu können:

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

Führe dieses Szenario in DEV aus:

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"

Oder PROD:

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

Blackfire-Szenarien können auch Analysen für jeden Request auslösen und Performance-Tests durchführen, indem Du das --blackfire Flag hinzufügst.

Performance Tests automatisieren

Beim Performance-Management geht es nicht nur darum, die Performance des vorhandenen Codes zu verbessern, sondern auch darum, sicherzustellen, dass keine Performanceregressionen eingeführt werden.

Das im vorherigen Abschnitt beschriebene Szenario kann automatisch in einem Continuous Integration-Workflow oder regelmäßig auf dem Produktivsystem ausgeführt werden.

In der SymfonyCloud können die Szenarien auch ausgeführt werden, wenn Du einen neuen Branch erstellst oder zum Produktivsystem deployst, um die Performance des neuen Codes automatisch zu überprüfen.


  • « Previous Schritt 28: Eine Anwendung lokalisieren
  • Next » Schritt 30: Symfony Internals entdecken

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