Суперскоростной Symfony с помощью nginx

Вкратце: поместите обратный прокси-сервер перед вашим приложением (для HTTP-кэширования или с целью балансировки нагрузки), чтобы увеличить его производительность.

Фреймворки, такие как Symfony, потенциально позволяют создавать суперскоростные приложения. Мы уже видели один способ как добиться этого (путём превращения приложения в HTTP сервер), другой способ заключается в установке обратного прокси перед ним.

В данной статье мы возьмём Symfony приложение и увидим, как увеличить его производительность в 140 раз с помощью nginx.

Примечание: указанные два способа могут использоваться как вместе, так и независимо.

Nginx с PHP-FPM

Устанавливаем nginx и PHP-FPM:


sudo apt-get install nginx php7.0-fpm

PHP-FPM собирается запустить наше PHP приложение по архитектуре без разделения ресурсов. Но нам бы хотелось, чтобы приложение выполнялось от того же пользователя, что и CLI, дабы избежать проблем с правами:


; /etc/php/7.0/fpm/pool.d/www.conf

; ...

user = foobar
group = foobar

; ...

listen.owner = foobar
listen.group = foobar

; ...

Вероятно, мы должны сделать тоже самое и для nginx:


# /etc/nginx/nginx.conf
user foobar foobar;

# ...

Теперь мы готовы создать виртуальный хост для нашего приложения:


# /etc/nginx/sites-available/super-speed-nginx
server {
    listen 80;
    server_name super-speed-nginx.example.com;
    root /home/foobar/super-speed-nginx/web;

    location / {
        # try to serve file directly, fallback to app.php
        try_files $uri /app.php$is_args$args;
    }

    location ~ ^/app\.php(/|$) {
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/app.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    # Keep your nginx logs with the symfony ones
    error_log /home/foobar/super-speed-nginx/var/logs/nginx_error.log;
    access_log /home/foobar/super-speed-nginx/var/logs/nginx_access.log;
}

Примечания:

  • fastcgi_pass: адрес сервера FastCGI, может быть как IP и порт (например, 127.0.0.1:9000 или сокет)
  • fastcgi_split_path_info: регулярное выражение, выделяющее значение для переменной $fastcgi_path_info
    • имя скрипта (здесь (.+\.php) – файл с php расширением), используется для установки значения переменной $fastcgi_script_name
    • путь (здесь (/.*) – URL как строка), используется для установки значения переменной $fastcgi_path_info
  • include: подключает файл (здесь /etc/nginx/fastcgi_params)
  • fastcgi_param: задаёт параметр, который будет передаваться FastCGI (проверьте значения по умолчанию в /etc/nginx/fastcgi_params)

Убедимся, что хост включён:


sudo ln -s /etc/nginx/sites-available/super-speed-nginx /etc/nginx/sites-enabled/super-speed-nginx

Единственное, чего не хватает, так это Symfony приложения! Давайте создадим его, используя Standard Edition:


composer create-project symfony/framework-standard-edition super-speed-nginx
cd super-speed-nginx
SYMFONY_ENV=prod SYMFONY_DEBUG=0 composer install -o --no-dev

Наконец, настроем домен и перезапустим nginx:


echo '127.0.0.1 super-speed-nginx.example.com' | sudo tee --append /etc/hosts
sudo service nginx restart

Проверим, работает ли он: http://super-speed-nginx.example.com/. Если отображается приветствие «Добро пожаловать», то всё в порядке.

Примечание: если не работает, то проверьте логи:

  • логи приложения расположены в /home/foobar/super-speed-nginx/var/logs
  • nginx в /var/log/nginx
  • PHP-FPM в /var/log/php7.0-fpm.log

Давайте запустим быстрый сравнительный тест:


curl 'http://super-speed-nginx.example.com/'
ab -t 10 -c 10 'http://super-speed-nginx.example.com/'

Результат:

  • Запросов в секунду: 146.86 (в среднем)
  • Время на запрос: 68.091 мс (в среднем)
  • Время на запрос: 6.809 мс (среднее значение из всех параллельных запросов)

HTTP-кэширование

По сравнению с Apache2, nginx работает лучше при обслуживании статичных файлов и под интенсивным трафиком (понятно почему).

Но наш главный интерес здесь заключается в возможностях HTTP-кэширования nginx.

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

  • Cache-Control: max-age: 60 попросит кэш сохранять копию в течении 60 секунд после получения ответа
  • Expires: Fri, 30 Oct 1991 14:19:41 GMT попросит, чтобы кэш сохранил копию до указанной даты
  • Last-Modified: Sat, 30 Apr 2016 14:29:35 GMT позволяет кэшу сохранять копию и проверять позже в фоновом режиме существует ли более свежая дата в «Last-Modified»
  • Etag: a3e455afd позволяет кэшу сохранять копию и проверять позже в фоновом режиме существует ли другая метка объекта «Etag»

Примечание: для получения дополнительной информации о заголовках, посмотрите статью о HTTP-кэше.

Так как nginx находится между клиентами (например, браузерами) и приложением, он может действовать как кэш:

  • если запрос не соответствует ни одной копии, попросить приложение создать ответ и сделать копию из него (сценарий MISS)
  • если запрос соответствует свежей копии, то она возвращается, приложение ничего не делает (сценарий HIT)
  • если запрос соответствует устаревшей копии, то она возвращается и в фоновом режиме производится запрос приложения, чтобы тот создал новый ответ, заменяющий устаревшую копию на свежую (сценарий UPDATING, при условии, что так сконфигурировано)

Таким способом можно возвращать устаревшие данные даже тогда, когда приложение сломалось (например, 500 ошибка)!

Для использования этой функции мы сначала должны настроить nginx:


# /etc/nginx/nginx.conf

# ...

http {
    proxy_cache_path /home/foobar/super-speed-nginx/var/nginx levels=1:2 keys_zone=super-speed-nginx:10m max_size=10g inactive=60m use_temp_path=off;

    # ...
}

Примечания:

  • levels: устанавливает глубину каталогов в папке кэша, рекомендуется 2, поскольку размещение всех файлов в одном каталоге может привести к замедлению работы.
  • keys_zone: устанавливает в памяти хранилище для ключей кэша, чтобы избежать их извлечения из диска.
  • max-size: устанавливает максимальный размер дискового кэша. Когда этот предел достигнут, наименее используемые копии будут удалены.
  • inactive: устанавливает время, после которого может быть удалена неиспользуемая копия
  • use_temp_path: разрешает или запрещает писать кэшируемые копии в каталог временных файлов, прежде чем они будут перемещены в постоянное место. Рекомендуется отключать (off), чтобы избежать ненужных операций с файловой системой.

Далее нам нужно отредактировать виртуальный хост, изменив 80 порт на какой-нибудь другой (например, 8042) и добавив «кэширующий сервер» перед ним (кэш-сервер будет слушать 80 порт, предоставляемый клиентам):

# /etc/nginx/sites-available/super-speed-nginx
server {
    listen 80;
    server_name super-speed-nginx.example.com;

    location / {
        proxy_pass http://super-speed-nginx.example.com:8042;

        proxy_cache super-speed-nginx;
        proxy_cache_key "$scheme://$host$request_uri";
        proxy_cache_lock on;
        proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
        add_header X-Cache $upstream_cache_status;
    }

    # Keep your nginx logs with the symfony ones
    error_log /home/foobar/super-speed-nginx/var/logs/nginx_cache_error.log;
    access_log /home/foobar/super-speed-nginx/var/logs/nginx_cache_access.log;
}

server {
    listen 8042;
    server_name super-speed-nginx.example.com;
    root /home/foobar/super-speed-nginx/web;

    location / {
        # try to serve file directly, fallback to app.php
        try_files $uri /app.php$is_args$args;
    }

    location ~ ^/app\.php(/|$) {
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/app.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    # Keep your nginx logs with the symfony ones
    error_log /home/foobar/super-speed-nginx/var/logs/nginx_error.log;
    access_log /home/foobar/super-speed-nginx/var/logs/nginx_access.log;
}

Примечания:

  • proxy_pass: адрес сервера, к которому мы хотим направить запросы
  • proxy_cache: устанавливает имя кэша, то же, что используется в keys_zone
  • proxy_cache_key: ключ, используемый для хранения копии (результат преобразовывается в md5)
  • proxy_cache_lock: включает/отключает параллельный кэш, пишущий в данный ключ
  • proxy_cache_use_stale: устанавливает использование несвежей копии в следующих ситуациях:
    • во время обновления копии
    • во время ошибки, тайм-аута, http_5** при сбое приложения
  • add_header: добавляет заголовок в HTTP ответ (например, значение $upstream_cache_status, которое могло бы быть MISS, HIT, EXPIRED, и т.д.)

Теперь настала очередь нашего приложения. По умолчанию Symfony устанавливает заголовок Cache-Control: no-cache для всех ответов. Давайте изменим его:

<?php
// src/AppBundle/Controller/DefaultController.php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     * @Cache(maxage="20", public=true)
     */
    public function indexAction(Request $request)
    {
        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
        ]);
    }
}

Для применения изменений перезапустите nginx и очистите кэш Symfony:


sudo service nginx restart
bin/console cache:clear -e=prod --no-debug

Теперь мы можем проверить заголовки Response:


curl -I 'http://super-speed-nginx.example.com/'
curl -I 'http://super-speed-nginx.example.com/'

Первый из этого набора должен содержать X-Cache, установленный в MISS, в то время как второй должен быть установлен в HIT.

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


curl 'http://super-speed-nginx.example.com/'
ab -t 10 -c 10 'http://super-speed-nginx.example.com/'

  • Запросов в секунду: 21994.33 (в среднем)
  • Время на запрос: 0,455 мс (в среднем)
  • Время на запрос: 0,045 мс (среднее значение, для всех параллельных запросов)

Приблизительно в 140 раз быстрее, чем без кэша.

Балансировка нагрузки

В вышеупомянутых примерах мы видели пример использования proxy_pass в nginx. Это позволяет прокси-серверу передавать запрос к «восходящему» серверу (например, PHP-FPM).

Путём обеспечения многих вышестоящих серверов для одного proxy_pass мы включаем балансировку нагрузки nginx, которая может быть полезна вместе с ReactPHP, например:


# /etc/nginx/sites-available/super-speed-nginx
upstream backend  {
    server 127.0.0.1:5500 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:5501 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:5502 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:5503 max_fails=1 fail_timeout=5s;
}

server {
    root /home/foobar/bench-sf-standard/web/;
    server_name localhost;

    location / {
        try_files $uri @backend;
    }

    location @backend {
        proxy_pass http://backend;
        proxy_next_upstream http_502 timeout error;
        proxy_connect_timeout 1;
        proxy_send_timeout 5;
        proxy_read_timeout 5;
    }
}

Примечания:

  • proxy_next_upstream: условия для передачи запроса на другой сервер (здесь ошибки и таймауты)
  • proxy_connect_timeout: максимальное время при попытке соединиться с вышестоящим сервером
  • proxy_send_timeout: максимальное время при попытке отправить данные в вышестоящий сервер
  • proxy_read_timeout: максимальное время при попытке чтения данных из вышестоящего сервера

Заключение

С обратным прокси, таким как nginx, мы можем сократить число вызовов к нашим приложениям:

  • при включении HTTP-кэширования (добавления соответствующих HTTP-заголовков к ответам, используя ~50 строк конфигурации),
  • и при включении балансировки нагрузки (используя ~30 строк конфигурации)

Это приведёт к резкому сокращению времени отклика с точки зрения клиента.

Примечание

Это авторский перевод статьи «Super Speed Symfony — nginx» на русский язык.

Комментарии

  1. Алексей пишет:

    Спасибо за перевод!

    Нужно упомянуть что данное ускорение достигается только в случае Public Cache — т.е. для страниц, на которых не присутствует никаких персональных данных и контент страниц одинаков для всех пользователей. Такие страницы symfony сохраняет в каталоге cache в виде php-файлов, которые отдаются напрямую — отсюда и такое сильное ускорение.

    К сожалению, в большинстве случаев на страницах могут присутствовать персональные данные — данные об авторизации, корзина с товарами и т.д. Такие страницы нельзя ускорить указанным выше способом.

  2. Александр пишет:

    В статье хорошо было бы рассмотреть вариант с fastcgi_cache, без использования proxy_cache. Его плюсы и минусы, либо упомянуть что есть альтернатива поднятию второго сервера nginx.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *