Paso 29: Gestionando el rendimiento

5.0 version
Maintained

Gestionando el rendimiento

Optimizar antes de tiempo es la raíz de todos los males.

Tal vez ya hayas leído esta cita antes. Pero me gusta citarla en su totalidad:

Deberíamos olvidar las pequeñas optimizaciones el 97 % de las veces; optimizar antes de tiempo es el origen de todos los males. Sin embargo, no deberíamos dejar pasar la oportunidad de optimizar ese crucial 3 %.

—Donald Knuth

Incluso pequeñas mejoras en el rendimiento pueden marcar la diferencia, especialmente para sitios de comercio electrónico. Ahora que la aplicación del libro de visitas está lista para el gran público, veamos cómo podemos comprobar su rendimiento.

La mejor manera de encontrar optimizaciones de rendimiento es usar un analizador de rendimiento o perfilador (profiler). La opción más popular hoy en día es Blackfire (aviso a navegantes: también soy el fundador del proyecto Blackfire).

Introducción a Blackfire

Blackfire se compone de varias partes:

  • Un cliente que activa perfiles (la herramienta CLI de Blackfire o una extensión de navegador para Google Chrome o Firefox);
  • Un agente que prepara y agrega datos antes de enviarlos a blackfire.io para su visualización;
  • Una extensión PHP (que actúa a modo de sonda) y que analiza el código PHP.

Para trabajar con Blackfire, primero tienes que registrarte .

Instala Blackfire en tu equipo local, ejecutando el siguiente script de instalación rápida:

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

Este instalador descarga la herramienta de línea de comandos de Blackfire, y luego instala la sonda PHP (sin habilitarla) en todas las versiones disponibles de PHP.

Habilita la sonda PHP para nuestro proyecto:

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

Reinicia el servidor web para que PHP pueda cargar Blackfire:

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

La herramienta de línea de comandos de Blackfire necesita estar configurada con tus credenciales personales de cliente (para almacenar los perfiles de tus proyectos en tu cuenta personal). Encuéntralas en la parte superior de la página ``Settings/Credentials` (configuración/credenciales) <https://blackfire.io/my/settings/credentials>`_ y ejecuta el siguiente comando reemplazando los marcadores de posición:

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

Nota

Para obtener instrucciones completas de instalación, sigue la guía oficial de instalación detallada. Son muy útiles cuando se instala Blackfire en un servidor.

Configuración del agente Blackfire en Docker

El último paso es añadir el servicio del agente Blackfire en el stack (pila) de servicios de 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]

Para comunicarse con el servidor, necesitas obtener tus credenciales personales del servidor (estas credenciales identifican dónde deseas almacenar los perfiles; puedes crear una por proyecto); se pueden encontrar en la parte inferior de la página Settings/Credentials https://blackfire.io/my/settings/credentials. Almacénalas en un archivo local .env.local:

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

Ahora puedes lanzar el nuevo contenedor:

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

Arreglando una instalación de Blackfire que no funciona

Si obtienes un error durante el análisis de rendimiento, aumenta el nivel de registro de Blackfire para obtener más información:

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

Reinicia el servidor web:

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

Y revisa los logs en vivo (en tiempo real):

1
$ symfony server:log

Analiza la aplicación de nuevo y comprueba la salida del log.

Configurando Blackfire en producción

Blackfire está incluido por defecto en todos los proyectos de SymfonyCloud.

Configura las credenciales del servidor como variables de entorno:

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

Y habilita la sonda PHP como cualquier otra extensión 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

Configurando Varnish para Blackfire

Antes de que puedas desplegar para empezar a realizar las tareas de análisis, necesitas una forma de evitar la caché HTTP de Varnish. Si no, Blackfire nunca llegará a la aplicación PHP. Vas a autorizar solo las peticiones de profiling que provengan de tu máquina local.

Encuentra tu dirección IP actual:

1
$ curl https://ifconfig.me/

y úsala para configurar 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 {

Ahora puedes realizar el despliegue.

Analizando el rendimiento de páginas web

Puedes hacer análisis de páginas web tradicionales desde Firefox o Google Chrome a través de sus extensiones dedicadas.

No olvides desactivar la caché HTTP en public/index.php cuando realices el análisis en tu máquina local: si no lo haces, analizarás la capa de caché HTTP de Symfony en lugar de tu propio código:

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

Para tener una mejor idea del rendimiento de tu aplicación en producción, deberías también hacer un análisis del entorno de «producción». Por defecto, tu entorno local utiliza el entorno de «desarrollo», que añade una sobrecarga significativa (principalmente para recopilar datos para la barra de herramientas de depuración web y el analizador de rendimiento de Symfony).

El cambio de tu máquina local al entorno de producción se puede hacer cambiando la variable de entorno APP_ENV en el archivo .env.local:

1
APP_ENV=prod

O puedes usar el comando server:prod:

1
$ symfony server:prod

No olvides volver a cambiar al entorno de desarrollo cuando finalice tu sesión de profiling (análisis de rendimiento):

1
$ symfony server:prod --off

Analizando el rendimiento de los recursos de la API

El análisis de rendimiento de la API o la SPA se realiza mejor en la línea de comandos usando la herramienta Blackfire que se ha instalado previamente:

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

El comando blackfire curl acepta exactamente los mismos argumentos y opciones que cURL.

Comparación del rendimiento

En el paso acerca de la «Caché» añadimos una capa de caché para mejorar el rendimiento de nuestro código, pero no hemos comprobado ni medido el impacto en el rendimiento que ha tenido el cambio. Como las personas somos poco efectivas adivinando qué será rápido y qué será lento, podrías terminar en una situación en la que añadir un poco de optimización hiciera que tu aplicación fuera, realmente, más lenta.

Deberías medir siempre el impacto de cualquier optimización que hagas con un analizador de rendimiento. Blackfire lo hace visualmente más fácil gracias a su función de comparación.

Escribiendo pruebas funcionales de caja negra

Hemos visto cómo escribir pruebas funcionales con Symfony. Blackfire se puede utilizar para escribir escenarios de navegación que se pueden ejecutar bajo demanda a través del Blackfire player. Escribamos un escenario que envíe un nuevo comentario y lo valide en desarrollo a través del enlace de correo electrónico y a través del admin en producción.

Crea un archivo de nombre .blackfire.yaml con el siguiente contenido:

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

Descarga el reproductor Blackfire para poder ejecutar el escenario localmente:

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

Ejecuta este escenario en desarrollo:

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"

O en producción:

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

Los escenarios de Blackfire también pueden desencadenar perfiles para cada solicitud y ejecutar pruebas de rendimiento añadiendo la opción --blackfire.

Automatizando los análisis de rendimiento

La gestión del rendimiento no sólo consiste en mejorar el rendimiento del código existente, sino también en comprobar que no se introducen regresiones de rendimiento.

El escenario escrito en la sección anterior puede ejecutarse automáticamente en un workflow de integración continua (CI), o en producción, de forma regular.

En SymfonyCloud, también se permite ejecutar los escenarios cada vez que se crea una nueva rama o se despliega en producción para analizar automáticamente el rendimiento del nuevo código.


  • « Previous Paso 28: Internacionalizando una aplicación
  • Next » Paso 30: Descubriendo el corazón de Symfony

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