Использование обработки в фоновом режиме для ускорения загрузки страницы

Чтобы ускорить загрузку страниц сайта, нужно перенести выполнение ресурсоемких задач в фоновый режим.

Задачи в фоновом режиме

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

Обработка и генерация файлов, отправка электронных писем и любые другие некритические задачи должны выполняться в фоновом режиме.

Как работает обработка в фоновом режиме?

В реализации данного подхода используются два ключевых компонента: очередь заданий и workers. Приложение создает задания, которые должны обрабатываться, а workers ожидают их и берут из очереди по одному заданию за раз.

Как работает обработка в фоновом режиме?

Вы можете создать несколько экземпляров workers для ускорения обработки. А также разбить большую задачу на более мелкие фрагменты и обрабатывать их одновременно.

Технический стек

Мы используем:

  • Beanstalkd - для хранения задач.
  • Компонент Symfony Console - для реализации workers в качестве команд консоли.
  • Supervisor -для обслуживания процессов workers .

Установка Beanstalkd

Beanstalkd – это менеджер задач с универсальным интерфейсом, изначально разработанный для ускорения загрузки страниц в веб-приложениях большого объема за счет асинхронного выполнения ресурсоемких процессов.

Чтобы установить Beanstalkd на сервер, работающий на базе Ubuntu или Debian, запустите sudo apt-get install beanstalkd. На официальной странице загрузки описано, как установить Beanstalkd на другие операционные системы.

После установки Beanstalkd запускается как фоновая программа, ожидающая подключения клиентов и создания (или обработки) задач:

/etc/init.d/beanstalkd
Usage: /etc/init.d/beanstalkd {start|stop|force-stop|restart|force-reload|status}

Установите Pheanstalk в качестве зависимости, запустив: composer require pda/pheanstalk.

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

<?php

namespace AppService;

use PheanstalkPheanstalk;

class JobQueueFactory
{
    private $host = 'localhost';
    private $port = '11300';

    const QUEUE_IMAGE_RESIZE = 'resize';

    public function createQueue(): Pheanstalk
    {
        return new Pheanstalk($this->host, $this->port);
    }
}

Теперь мы можем внедрить фабрику методов везде, где нужно взаимодействовать с очередями задач Beanstalkd. Мы определяем имя очереди как константу и ссылаемся на нее при помещении задачи в очередь или при просмотре очереди в workers.

Установка Supervisor

Supervisor - это клиент-серверная система, позволяющая пользователям отслеживать и контролировать процессы в UNIX-подобных операционных системах. Мы будем использовать ее для запуска, перезапуска, масштабирования и мониторинга процессов workers.

Установите Supervisor на сервер, работающий на базе Ubuntu / Debian. Для этого выполните команду sudo apt-get install supervisor. После установки Supervisor будет работать в фоновом режиме. Используйте supervisorctl для управления процессами:

$ sudo supervisorctl help

default commands (type help <topic>):
=====================================
add    exit      open  reload  restart   start   tail
avail  fg        pid   remove  shutdown  status  update
clear  maintail  quit  reread  signal    stop    version

Для этого нужно создать файл конфигурации. Параметры инструмента хранятся в /etc/supervisor/conf.d/. Простая конфигурация Supervisor для workers будет выглядеть следующим образом:

[program:resize-worker]
process_name=%(program_name)s_%(process_num)02d
command=php PATH-TO-YOUR-APP/bin/console app:resize-image-worker
autostart=true
autorestart=true
numprocs=5
stderr_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stderr.log
stdout_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stdout.log

Мы указываем Supervisor, как именовать генерируемые процессы. А также путь к команде, которая должна быть выполнена для автоматического запуска и перезапуска процессов. Количество процессов и где осуществлять вывод. Более подробно о конфигурации Supervisor можно узнать здесь.

Изменение размера изображений в фоновом режиме

Чтобы реализовать изменение размера изображений после создания галереи, происходящее в фоновом режиме, нужно:

  • обновить логику обслуживания изображений в ImageController;
  • реализовать workers как команды консоли;
  • создать конфигурацию Supervisor для workers;
  • обновить и изменить размеры изображений.

Обновление логики обслуживания изображений

Нужно изменить ImageController, чтобы он возвращал изображения с измененными размерами. Если такого нет, приложение вернет ответ, в котором сообщается, что в данный момент изменяется размер изображения.

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

Мы создадим простое событие GalleryCreatedEvent с идентификатором галереи в качестве полезной нагрузки. Это событие будет отправлено в UploadController после успешного создания галереи:

...

$this->em->persist($gallery);
$this->em->flush();

$this->eventDispatcher->dispatch(
    GalleryCreatedEvent::class,
    new GalleryCreatedEvent($gallery->getId())
);

$this->flashBag->add('success', 'Gallery created! Images are now being processed.');
...

Также мы обновим сообщение: “Images are now being processed.”, чтобы пользователь знал, что нужно некоторое время для обработки изображений.

Мы создадим подписчика событий GalleryEventSubscriber, который будет реагировать на запрос GalleryCreatedEvent и запрашивать задачу изменения размера для каждого изображения в только что созданной галерее:

public function onGalleryCreated(GalleryCreatedEvent $event)
{
    $queue = $this->jobQueueFactory
        ->createQueue()
        ->useTube(JobQueueFactory::QUEUE_IMAGE_RESIZE);

    $gallery = $this->entityManager
        ->getRepository(Gallery::class)
        ->find($event->getGalleryId());

    if (empty($gallery)) {
        return;
    }

    /** @var Image $image */
    foreach ($gallery->getImages() as $image) {
        $queue->put($image->getId());
    }
}

После этого приложение выведет страницу галереи. Но некоторые изображения не отобразятся в виде миниатюр, пока они не будут готовы:

Обновление логики обслуживания изображений

Как только workers закончат изменение размеров изображение, следующее обновление должно будет отобразить всю страницу галереи.

Реализация workers в качестве команд консоли

Worker - это простой процесс, выполняющий одну и ту же работу для каждой задачи, которую он получает из очереди. Выполнение worker блокируется при вызове $queue->reserve() до тех пор, пока не останется зарезервированных или не истечет время ожидания.

Один worker обрабатывает одно задание. Задача обычно содержит полезную нагрузку. В нашем случае это будет UUID созданной галереи.

Простой worker выглядит так:

// Создаем очередь Pheanstalk и определяем, какую очередь отслеживать
$queue = $this->getContainer()
    ->get(JobQueueFactory::class)
    ->createQueue()
    ->watch(JobQueueFactory::QUEUE_IMAGE_RESIZE);

// Блокируем исполнение этого кода, пока задача добавлена в очередь
// Необязательный аргумент - это задержка в секундах
$job = $queue->reserve(60 * 5);

// При задержке
if (false === $job) {
    $this->output->writeln('Timed out');

    return;
}

try {
    // Выполняем задание
    // и удаляем задачи, чтобы они не возвращались в очередь
    $this->resizeImage($job->getData());

    // Удаление задачи из очереди помечает ее, как обработанную
    $queue->delete($job);
} catch (Exception $e) {
    $queue->bury($job);
    throw $e;
}

workers завершаются после окончания времени ожидания или после обработки задания. Можно обернуть workers в бесконечный цикл и заставить его повторяться. Но это может вызвать задержки соединения с базой данных после долгого простоя и усложнить развертывание. Чтобы предотвратить это, жизненный цикл workers будет завершен после выполнения одной задачи. Затем Supervisor перезапустит worker как новый процесс.

Рассмотрите ResizeImageWorkerCommand, чтобы получить четкое представление о том, как структурирована команда. Worker, реализованный таким образом, быть запущен вручную как команда консоли Symfony: ./bin/console app:resize-image-worker.

Создание конфигурации для Supervisor

Мы хотим, чтобы workers запускались автоматически. Для этого установим в конфигурации директиву autostart=true.

Но worker должен быть перезапущен после определенного промежутка времени или успешного выполнения обработки. Поэтому также установим директиву autorestart=true.

Мы можем установить директиву numprocs=5, и Supervisor  создаст пять экземпляров workers. Они будут ожидать задачи, и обрабатывать их самостоятельно. Это позволяет легко масштабировать систему.

Поскольку будет запущено несколько процессов, то нужно определить структуру имен процессов. Для этого устанавливаем директиву process_name=%(program_name)s_%(process_num)02d.

Также нужно сохранить результаты работы workers для анализа и отладки. Мы определим пути stderr_logfile и stdout_logfile.

Полная конфигурация Supervisor наших workers выглядит следующим образом:

[program:resize-worker]
process_name=%(program_name)s_%(process_num)02d
command=php PATH-TO-YOUR-APP/bin/console app:resize-image-worker
autostart=true
autorestart=true
numprocs=5
stderr_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stderr.log
stdout_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stdout.log

После создания файла конфигурации, располагающегося в каталоге /etc/supervisor/conf.d/, нужно  указать Supervisor заново обновить конфигурацию:

supervisorctl reread
supervisorctl update

Если вы используете Homestead Improved, то можно воспользоваться scripts/setup-supervisor.sh для создания конфигурации Supervisor: sudo./scripts/setup-supervisor.sh

Обновление Fixtures

Теперь миниатюры изображений больше не будут отображаться при первом запросе. Из-за этого необходимо явно запрашивать рендеринг для каждого изображения, когда мы загружаем данные в классе fixtures LoadGalleriesData:

$imageResizer = $this->container->get(ImageResizer::class);
$fileManager = $this->container->get(FileManager::class);

...

$gallery->addImage($image);
$manager->persist($image);

$fullPath = $fileManager->getFilePath($image->getFilename());
if (false === empty($fullPath)) {
    foreach ($imageResizer->getSupportedWidths() as $width) {
        $imageResizer->getResizedPath($fullPath, $width, true);
    }
}

Чтобы ускорить загрузку данных, мы переместили процесс в фоновый режим.

Секреты и тонкости

Workers запускаются в фоновом режиме. Поэтому даже после развертывания новой версии приложения у вас будут работать устаревшие workers, пока они не будут перезапущены.

Для этого нужно подождать, пока workers завершат выполнение своих задач или истечет время ожидания. Помните об этом при создании процедур развертывания!

Вадим Дворниковавтор-переводчик статьи «Using Background Processing to Speed Up Page Load Times»