Использование обработки в фоновом режиме для ускорения загрузки страницы
Чтобы ускорить загрузку страниц сайта, нужно перенести выполнение ресурсоемких задач в фоновый режим.
- Задачи в фоновом режиме
- Как работает обработка в фоновом режиме?
- Технический стек
- Установка Beanstalkd
- Установка Supervisor
- Изменение размера изображений в фоновом режиме
- Обновление логики обслуживания изображений
- Реализация workers в качестве команд консоли
- Создание конфигурации для Supervisor
- Обновление Fixtures
- Секреты и тонкости
Задачи в фоновом режиме
То же самое делает 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 завершат выполнение своих задач или истечет время ожидания. Помните об этом при создании процедур развертывания!