Skip to content

Управління продуктивністю

Передчасна оптимізація — корінь усього зла.

Можливо ви вже читали цю цитату раніше. Але я хотів би навести її повністю:

Нам слід забути про невелику ефективність, скажімо, у 97% випадків: передчасна оптимізація — корінь усього зла. Однак ми не маємо упускати наші можливості в цих критичних 3%.

-- Donald Knuth

Навіть невеликі поліпшення продуктивності можуть щось змінити, особливо для веб-сайтів електронної комерції. Тепер, коли застосунок гостьової книги готовий до прайм-тайму, подивімося, як ми можемо перевірити його продуктивність.

Найкращим способом пошуку потенційно слабких місць для оптимізації продуктивності є використання профілювальника. Найпопулярнішим варіантом сьогодні є Blackfire (повна відмова від відповідальності: я також є засновником проекту Blackfire).

Знайомство з Blackfire

Blackfire складається з кількох частин:

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

Щоб працювати з Blackfire, вам спочатку потрібно зареєструватися.

Встановіть Blackfire на ваш локальний комп'ютер, запустивши наступний сценарій швидкого встановлення:

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

Цей інсталятор завантажує і встановлює інструмент Blackfire CLI.

Після завершення встановлює зонд PHP на всі доступні версії PHP:

1
$ sudo blackfire php:install

І увімкніть зонд PHP для нашого проекту:

1
2
3
4
5
6
7
8
9
10
--- a/php.ini
+++ b/php.ini
@@ -7,3 +7,7 @@ session.use_strict_mode=On
 realpath_cache_ttl=3600
 zend.detect_unicode=Off
 xdebug.file_link_format=vscode://file/%f:%l
+
+[blackfire]
+# use php_blackfire.dll on Windows
+extension=blackfire.so

Перезавантажте веб-сервер, щоб PHP міг завантажити Blackfire:

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

Інструмент Blackfire CLI потрібно налаштувати за допомогою ваших клієнтських облікових даних (щоб зберігати профілі проектів у вашому особистому кабінеті). Знайдіть їх у верхній частині сторінки Settings/Credentials і виконайте наступну команду, замінивши заповнювачі:

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

Налаштування агента Blackfire у Docker

Сервіс агента Blackfire вже налаштований у стеку Docker Compose:

docker-compose.override.yml
1
2
3
4
5
6
7
8
9
###> blackfireio/blackfire-symfony-meta ###
blackfire:
    image: blackfire/blackfire:2
    # uncomment to store Blackfire credentials in a local .env.local file
    #env_file: .env.local
    environment:
    BLACKFIRE_LOG_LEVEL: 4
    ports: [8307]
###< blackfireio/blackfire-symfony-meta ###

Щоб організувати обмін інформацією з сервером вам потрібно отримати ваші особисті серверні облікові дані (ці облікові дані визначають, де ви хочете зберігати профілі — ви можете створити один для кожного проекту); їх можна знайти в нижній частині сторінки Settings/Credentials`. Збережіть їх у локальному файлі .env.local``:

1
2
3
4
$ symfony console secrets:set BLACKFIRE_SERVER_ID
# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
$ symfony console secrets:set BLACKFIRE_SERVER_TOKEN
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

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

Виправлення помилок Blackfire

Якщо ви отримуєте помилку під час профілювання, збільште рівень журналювання Blackfire, щоб отримувати більше інформації в журналах:

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 включено у всі проекти Platform.sh за замовчуванням.

Налаштуйте серверні облікові дані як конфіденційні дані продакшн:

1
2
3
4
$ symfony console secrets:set BLACKFIRE_SERVER_ID --env=prod
# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
$ symfony console secrets:set BLACKFIRE_SERVER_TOKEN --env=prod
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Зонд PHP уже ввімкнено, як і будь-яке інше необхідне розширення PHP:

.platform.app.yaml
1
2
3
4
5
6
7
8
9
10
runtime:
    extensions:
        - apcu
        - blackfire
        - ctype
        - iconv
        - mbstring
        - pdo_pgsql
        - sodium
        - xsl

Налаштування Varnish для Blackfire

Перш ніж ви зможете розгорнути, щоб почати профілювання, вам потрібен спосіб, щоб обійти HTTP-кеш Varnish. Якщо ні, то Blackfire ніколи не потрапить у застосунок PHP. Ви збираєтеся дозволити обхід Varnish тільки для запитів профілювання, що надходять з вашого локального комп'ютера.

Знайдіть свою поточну IP-адресу:

1
$ curl https://ifconfig.me/

І використовуйте її для налаштування Varnish:

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/.platform/config.vcl
+++ b/.platform/config.vcl
@@ -1,3 +1,11 @@
+acl profile {
+   # Authorize the local IP address (replace with the IP found above)
+   "192.168.0.1";
+   # 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 +16,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-кеш на вашому локальному комп'ютері, у файлі config/packages/framework.yaml: якщо ні, ви профілюватимите шар HTTP-кешу Symfony, замість власного коду.

Щоб отримати краще уявлення про продуктивність вашого застосунку в продакшн, вам також слід профілювати "production" середовище. За замовчуванням ваше локальне середовище використовує середовище "development", яке додає значні накладні витрати (в основному для збору даних для панелі інструментів веб-наладження і профілювальника Symfony).

Note

Оскільки ми будемо профільувати «продакшн» середовище, у конфігурації нічого змінювати не потрібно, оскільки ми ввімкнули рівень HTTP-кешу Symfony лише для середовища «розробки» в попередньому розділі.

Перемкнути ваш локальний комп'ютер у продакшн середовище можна змінивши змінну середовища APP_ENV у файлі .env.local:

1
APP_ENV=prod

Або ви можете використовувати команду server:prod:

1
$ symfony server:prod

Не забудьте перемкнути її назад у середовище розробки, коли ваш сеанс профілювання завершиться:

1
$ symfony server:prod --off

Профілювання ресурсів API

Профілювання API чи ОЗ краще виконувати за допомогою Blackfire CLI, інструменту, який ви встановили раніше:

1
$ blackfire curl `symfony var:export SYMFONY_PROJECT_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
51
52
53
54
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] 'me@example.com'
            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")
                include login
                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')
                expect status_code() == 302
            follow
            click link("Comments")
                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_PROJECT_DEFAULT_ROUTE_URL` .blackfire.yaml --variable "webmail_url=`symfony var:export MAILER_WEB_URL 2>/dev/null`" --variable="env=dev" -vv
1
$ rm blackfire-player.phar

Або в продакшн:

1
$ ./blackfire-player.phar run --endpoint=`symfony cloud:env:url --pipe --primary` .blackfire.yaml --variable "webmail_url=NONE" --variable="env=prod" -vv

Сценарії Blackfire також можуть профілювати кожен запит і виконувати тести продуктивності, додавши прапорець --blackfire.

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

Управління продуктивністю полягає не тільки в поліпшенні продуктивності наявного коду, але і в перевірці того, що внесені зміни не призводять до її регресії.

Сценарій, що написаний у попередньому розділі, може виконуватися автоматично в робочому процесі безперервної інтеграції або у продакшн, на регулярній основі.

Platform.sh також дозволяє виконувати сценарії щоразу, коли ви створюєте нову гілку чи розгортаєте у продакшн, щоб автоматично перевірити продуктивність нового коду.

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