Шаг 29: Оптимизация производительности

5.0 version
Maintained

Оптимизация производительности

Преждевременная оптимизация — корень всех зол в программировании.

Возможно вы встречали эту цитату и ранее, однако я люблю цитировать её полностью:

Нам следует забывать о небольшой эффективности, например, в 97% случаев: преждевременная оптимизация — корень всех зол. И напротив, мы должны уделить всё внимание оставшимся 3%.

—Дональд Кнут

Даже незначительное увеличение производительности имеет важное значение, особенно для интернет-магазинов. Теперь, когда приложение гостевой книги готово, давайте проверим его производительность.

Лучший способ определить потенциальные области для оптимизации — использовать профилировщик. Одним из наиболее популярных на сегодняшний день является Blackfire (важное примечание: я также основатель проекта Blackfire).

Знакомство с Blackfire

Blackfire состоит из нескольких частей:

  • Клиент, запускающий профили (CLI-инструмент Blackfire или расширение для браузера Google Chrome или Firefox);
  • Агент, который подготавливает и собирает данные перед их отправкой на сайт blackfire.io для отображения;
  • PHP-модуль (зонд), который инструментирует PHP-код.

Зарегистрируйтесь, чтобы начать работу с Blackfire.

Установите Blackfire на вашу локальную машину, запустив скрипт быстрой установки:

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

Установщик загрузит CLI-инструмент Blackfire, а затем установит PHP-модуль зонда (не активируя его) для всех доступных версий PHP.

Включите PHP-зонд для нашего проекта:

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

Перезапустите веб-сервер, чтобы PHP cмог загрузить Blackfire:

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

Для хранения профилей приложений в вашей учётной записи, необходимо настроить инструмент Blackfire CLI, используя ваши персональные клиентские учётные данные. Найдите их в верху страницы Settings/Credentials и выполните следующую команду, предварительно подставив свои данные в соответствующие места:

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

Примечание

Полную инструкцию по установке вы сможете найти в официальном подробном руководстве по установке. Она также полезна при установке Blackfire на сервер.

Установка Blackfire-агента в Docker

Последним шагом будет добавление сервиса Blackfire-агента в стек 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]

Для соединения с сервером вам необходимо получить ваши персональные серверные учётные данные. Эти учётные данные указывают, где вы хотите хранить профили, которые вы можете создать для каждого проекта. Их можно найти внизу страницы Settings/Credentials. Сохраните данные локально в файле .env.local:

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

Теперь запустите новый контейнер:

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

Исправление неработающей установки Blackfire

Если во время профилирования появляется ошибка — увеличьте уровень логирования ошибок Blackfire, чтобы собирать больше информации в отчётах об ошибках:

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

Перезапустите веб-сервер:

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

И следите за логами:

1
$ symfony server:log

Запустите профилирование снова и проверьте записи лога.

Настройка Blackfire в продакшене

По умолчанию Blackfire добавлен во все проекты на SymfonyCloud.

Сохраните серверные учётные данные в переменных окружения:

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

И активируйте PHP-модуль зонда по аналогии с любым другим 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

Настройка Varnish для работы с Blackfire

Перед развёртыванием, чтобы приступить к профилированию, вам необходимо обойти HTTP-кеш Varnish. Если этого не сделать — Blackfire никогда не сможет получить доступ к вашему PHP-приложению. Достаточно разрешить HTTP-запросы профилирования, приходящие только с вашего локального компьютера.

Узнайте ваш текущий IP-адрес:

1
$ curl https://ifconfig.me/

И используйте его для настройки 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 {

Теперь можно разворачивать.

Профилирование веб-страниц

Для профилирования обычных веб-страниц в Firefox или Google Chrome используйте соответствующие расширения.

Во время профилирования не забудьте отключить HTTP-кеш на вашей локальной машине в файле public/index.php. Если этого не выполнить, то вместо своего кода, вы будете профилировать слой HTTP-кеша Symfony.

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

Для получения наиболее полного представления о производительности приложения в продакшене, вам также необходимо профилировать окружение продакшена («production»). По умолчанию ваше локальное окружение использует среду разработки («development»), что влечёт за собой существенные накладные расходы (это происходит в основном из-за сбора данных для отладочной панели и Symfony-профилировщика).

Переключите окружение вашей локальной машины на работу в продакшене путём изменения значения переменной окружения APP_ENV в файле .env.local:

1
APP_ENV=prod

Также вы можете использовать команду server:prod:

1
$ symfony server:prod

Не забудьте переключиться обратно на окружение разработки после завершения сеанса профилирования:

1
$ symfony server:prod --off

Профилирование API-ресурсов

Профилирование API или SPA лучше выполнять в командной строке с помощью установленного ранее инструмента Blackfire CLI:

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

Команда blackfire curl принимает те же аргументы и опции, что и cURL.

Сравнение производительности

В шаге про кеширование для повышения производительности мы добавили слой кеширования, однако мы не проверили, как внесённые изменения повлияли на производительность. Так как довольно сложно угадать, что будет работать быстрее, а что медленнее, то можно оказаться в ситуации, когда из-за оптимизации чего-либо ваше приложение на самом деле станет только медленнее.

С помощью профилировщика всегда необходимо измерять влияние каждой сделанной оптимизации. Blackfire позволяет упростить визуальную оценку производительности благодаря возможности сравнения.

Написание функциональных тестов по принципу чёрного ящика

Мы уже знакомы с тем, как писать функциональные тесты с помощью Symfony. Blackfire, в свою очередь, может быть использован для написания браузерных сценариев, которые можно запускать по желанию с помощью приложения Blackfire player. Давайте напишем сценарий, который отправит новый комментарий и проверит его по ссылке в электронной почте в окружении для разработки, а также в административной панели в продакшене.

Создайте файл .blackfire.yaml со следующим содержимым:

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

Загрузите Blackfire player для выполнения сценария на локальной машине:

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

Запустите этот сценарий в окружении разработки:

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"

Или в продакшене:

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

Сценарии Blackfire, также могут использовать профили для каждого запроса и запускать тесты производительности, если добавить соответствующий флаг --blackfire.

Автоматизация проверок производительности

Отслеживание производительности позволяет понять не только как улучшить производительность существующего кода, но также и контролировать её падение, вызванное новыми изменениями.

Написанный в предыдущем разделе сценарий может запускаться автоматически через непрерывную интеграцию или на регулярной основе в продакшене.

На SymfonyCloud также возможно запускать сценарии <https://blackfire.io/docs/integrations/paas/symfonycloud#builds-level-enterprise> при создании новой ветки или развёртывании в продакшене, чтобы автоматически проверять производительность нового кода.


  • « Previous Шаг 28: Локализация приложения
  • Next » Шаг 30: Изучение внутренностей Symfony

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