Etap 29: Zarządzanie wydajnością

5.0 version
Maintained

Zarządzanie wydajnością

Przedwczesna optymalizacja jest źródłem wszelkiego zła.

Ten cytat może już być Ci znany, ale lubię go w całości:

Powinniśmy zapomnieć o małych usprawnieniach, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła. Nie powinniśmy jednak rezygnować z możliwości zwiększenia wydajności w tych krytycznych trzech procentach.

—Donald Knuth

Nawet niewielka poprawa wydajności może mieć znaczenie, zwłaszcza w przypadku sklepów internetowych. Teraz, gdy aplikacja księgi gości jest gotowa, zobaczmy, jak możemy sprawdzić jej wydajność.

Najlepszym sposobem na znalezienie optymalizacji wydajności jest użycie profilera. Najbardziej popularną obecnie opcją jest Blackfire (Ważna informacja: jestem również założycielem projektu Blackfire).

Przedstawienie Blackfire

Blackfire składa się z kilku części:

  • Klient, który uruchamia profilowanie (narzędzie Blackfire CLI lub rozszerzenie przeglądarki dla Google Chrome lub Firefox);
  • Agent, który przygotowuje i zbiera dane przed wysłaniem ich do serwisu blackfire.io w celu ich wyświetlenia;
  • Rozszerzenie PHP (sonda), które analizuje wykonanie kodu PHP.

Aby pracować z Blackfire, najpierw musisz się zarejestrować.

Zainstaluj Blackfire na swoim komputerze, uruchamiając następujący skrypt instalacyjny:

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

Instalator pobiera narzędzie Blackfire CLI, a następnie instaluje sondę PHP (bez włączania jej) na wszystkich dostępnych wersjach PHP.

Włącz sondę PHP dla naszego projektu:

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

Uruchom ponownie serwer WWW, aby PHP mógł załadować Blackfire:

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

Aby powiązać profile z Twoim kontem, narzędzie Blackfire CLI po zainstalowaniu musi być skonfigurowane Twoimi danymi uwierzytelniającymi, które znajdziesz na górze strony Settings/Credentials. Wykonaj następujące polecenie podmieniając symbole zastępcze:

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

Informacja

Aby uzyskać pełną instrukcję instalacji, postępuj zgodnie z oficjalną szczegółową instrukcją instalacji. Jest przydatna podczas instalacji Blackfire na serwerze.

Instalowanie agenta Blackfire na Dockerze

Ostatnim krokiem jest dodanie usługi agenta Blackfire w stosie 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]

Aby komunikować się z serwerem, musisz uzyskać swoje dane uwierzytelniające do serwera (te dane wskazują, gdzie chcesz przechowywać profile – możesz utworzyć jeden na projekt); można je znaleźć na dole strony Settings/Credentials. Przechowuj je w lokalnym pliku .env.local:

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

Możesz już uruchomić nowy kontener:

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

Naprawianie niedziałającej instalacji Blackfire

Jeśli podczas profilowania pojawi się błąd, zwiększ poziom logowania Blackfire, aby uzyskać więcej informacji w logach:

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

Uruchom ponownie serwer WWW:

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

A następnie śledź logi:

1
$ symfony server:log

Wykonaj profilowanie ponownie i sprawdź wyjście logów.

Konfigurowanie Blackfire w środowisku produkcyjnym

Blackfire jest domyślnie włączony dla wszystkich projektów SymfonyCloud.

Ustaw dane uwierzytelniające serwera jako zmienne środowiskowe:

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

I włącz sondę PHP jak każde inne rozszerzenie 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

Konfigurowanie serwera Varnish dla Blackfire

Zanim będziesz w stanie wykonać wdrożenie potrzebne do rozpoczęcia profilowania, potrzebujesz sposobu na ominięcie pamięci podręcznej Varnish HTTP. W innym przypadku, Blackfire nigdy nie odpyta aplikacji PHP. Będziesz autoryzować tylko prośby o profilowanie pochodzące z twojej lokalnej maszyny.

Znajdź swój bieżący adres IP:

1
$ curl https://ifconfig.me/

I użyj go, aby skonfigurować 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 {

Teraz możesz wdrożyć.

Profilowanie stron internetowych

Możesz profilować tradycyjne strony internetowe używając Firefoksa lub Google Chrome poprzez ich dedykowane rozszerzenia.

Na lokalnym komputerze, podczas profilowania, nie zapomnij wyłączyć pamięci podręcznej HTTP w public/index.php: jeśli tego nie zrobisz, będziesz profilować warstwę pamięci podręcznej HTTP Symfony zamiast własnego kodu:

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

Aby uzyskać lepszy obraz wydajności Twojej aplikacji w środowisku produkcyjnym, należy je również profilować. Domyślnie, twoje lokalne środowisko korzysta ze środowiska deweloperskiego, które dodaje znaczny narzut (głównie w celu zebrania danych dla paska narzędzi do debugowania sieci i profilera Symfony).

Przełączenie maszyny lokalnej do środowiska produkcyjnego można wykonać poprzez zmianę zmiennej środowiskowej APP_ENV w pliku .env.local:

1
APP_ENV=prod

Albo możesz użyć polecenia server:prod:

1
$ symfony server:prod

Nie zapomnij przełączyć go z powrotem na środowisko deweloperskie po zakończeniu sesji profilowania:

1
$ symfony server:prod --off

Profilowanie zasobów API

Profilowanie API lub SPA jest wygodniejsze z poziomu linii komend, za pomocą zainstalowanego wcześniej narzędzia Blackfire CLI:

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

Polecenie blackfire curl akceptuje dokładnie takie same argumenty i opcje jak cURL.

Porównanie wydajności

W etapie poświęconym pamięci podręcznej dodaliśmy warstwę pamięci podręcznej, aby poprawić wydajność naszego kodu, ale nie sprawdziliśmy ani nie zmierzyliśmy wpływu zmiany na wydajność. Ponieważ źle nam idzie zgadywanie, co będzie szybkie, a co powolne, możesz znaleźć się w sytuacji, w której niektóre usprawnienia spowalniają działanie Twojej aplikacji.

Za każdym razem należy zmierzyć wpływ wszelkich optymalizacji, które robisz za pomocą profilera. Blackfire ułatwia wizualne ich porównanie dzięki narzędziu porównywania.

Tworzenie czarnoskrzynkowych testów funkcjonalnych (ang. black box)

Dowiedzieliśmy się już, jak pisać testy funkcjonalne z Symfony. Blackfire może być używany do pisania scenariuszy przeglądania, które mogą być uruchamiane na żądanie przez odtwarzacz Blackfire. Napiszmy scenariusz, który przesyła nowy komentarz i zatwierdza go poprzez link e-mailowy w środowisku deweloperskim oraz przez konto administracyjne w środowisku produkcyjnym.

Utwórz plik .blackfire.yaml o następującej zawartości:

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

Pobierz odtwarzacz Blackfire, aby móc uruchomić scenariusz lokalnie:

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

Uruchomienie tego scenariusza w środowisku deweloperskim:

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"

Albo w środowisku produkcyjnym:

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

Scenariusze Blackfire mogą również wyzwalać profilowanie dla każdego żądania i uruchamiać testy wydajności poprzez dodanie flagi --blackfire.

Automatyzowanie kontroli wydajności

Zarządzanie wydajnością polega nie tylko na poprawie wydajności istniejącego kodu, ale również na sprawdzeniu, czy dotychczasowe działania wydajności nie zmniejszyły.

Scenariusz napisany w poprzedniej sekcji może być uruchamiany automatycznie w trybie ciągłej integracji (ang. continuous integration) lub w środowisku produkcyjnym w regularnych odstępach.

SymfonyCloud pozwala również na uruchamianie scenariuszy, gdy tworzysz nową gałąź lub wdrażasz w środowisku produkcyjnym, aby automatycznie sprawdzić wydajność nowego kodu.


  • « Previous Etap 28: Lokalizacja aplikacji
  • Next » Etap 30: Odkrywanie wewnętrznych mechanizmów Symfony

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