Статьи

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

Эта статья является частью серии по созданию примера приложения — блога галереи с несколькими изображениями — для оценки производительности и оптимизации. (Посмотреть репо здесь.)


В предыдущей статье мы добавили изменение размера изображения по требованию . Размер изображений изменяется по первому запросу и кэшируется для последующего использования. Сделав это, мы добавили некоторые накладные расходы к первой загрузке; система должна визуализировать миниатюры на лету и «блокирует» рендеринг страницы первого пользователя до тех пор, пока не будет завершено рендеринг изображения.

Оптимизированный подход заключается в визуализации миниатюр после создания галереи. Вы можете подумать: «Хорошо, но тогда мы заблокируем пользователя, который создает галерею?». Это не только плохо для пользователя, но и не является масштабируемым решением. Пользователь может запутаться из-за длительного времени загрузки или, что еще хуже, из-за превышения времени ожидания и / или ошибок, если изображения слишком тяжелы для обработки. Лучшее решение — перенести эти тяжелые задачи на задний план.

Фоновые задания

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

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

Как работает фоновая обработка?

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

Фоновые задания

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

Наш технический стек

Мы используем очередь заданий Beanstalkd для хранения заданий, компонент Symfony Console для реализации рабочих в качестве команд консоли, а Supervisor — для обслуживания рабочих процессов.

Если вы используете Homestead Improved , Beanstalkd и Supervisor уже установлены, так что вы можете пропустить инструкции по установке ниже.

Установка Beanstalkd

Бобовый стебель

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

Есть много доступных клиентских библиотек, которые вы можете использовать. В нашем проекте мы используем Pheanstalk .

Чтобы установить 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 App\Service;

use Pheanstalk\Pheanstalk;

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. Мы определяем имя очереди как константу и ссылаемся на нее при помещении задания в очередь или при просмотре очереди у рабочих.

Установка Supervisor

Согласно официальной странице , Supervisor является

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

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

Установите 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

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

 [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, как назвать порожденные процессы, путь к команде, которая должна быть запущена, для автоматического запуска и перезапуска процессов, сколько процессов мы хотим иметь и где регистрировать результаты. Узнайте больше о конфигурации супервизора здесь .

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

После того, как мы настроили нашу инфраструктуру (т.е. установили Beanstalkd и Supervisor), мы можем изменить наше приложение для изменения размера изображений в фоновом режиме после создания галереи. Для этого нам необходимо:

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

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

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

Теперь мы изменим 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.');
...

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

Мы создадим подписчика на событие 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());
    }
}

Теперь, когда пользователь успешно создает галерею, приложение отобразит страницу галереи, но некоторые изображения не будут отображаться в виде миниатюр, пока они еще не готовы:

Страница галереи

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

Реализовать изменения размера рабочих как консольные команды

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

Только один работник может взять и обработать работу. Задание обычно содержит полезную нагрузку — например, строку или сериализованный массив / объект. В нашем случае это будет UUID созданной галереи.

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

 // Construct a Pheanstalk queue and define which queue to watch.
$queue = $this->getContainer()
    ->get(JobQueueFactory::class)
    ->createQueue()
    ->watch(JobQueueFactory::QUEUE_IMAGE_RESIZE);

// Block execution of this code until job is added to the queue
// Optional argument is timeout in seconds
$job = $queue->reserve(60 * 5);

// On timeout
if (false === $job) {
    $this->output->writeln('Timed out');

    return;
}

try {
    // Do the actual work here, but make sure you're catching exceptions
    // and bury job so it doesn't get back to the queue
    $this->resizeImage($job->getData());

    // Deleting a job from the queue will mark it as processed
    $queue->delete($job);
} catch (\Exception $e) {
    $queue->bury($job);
    throw $e;
}

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

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

Создать конфигурацию супервизора

Мы хотим, чтобы наши работники запускались автоматически, поэтому в конфигурации мы установим директиву autostart=true
Поскольку работник должен быть перезапущен после истечения времени ожидания или успешного выполнения задачи обработки, мы также установим директиву autorestart=true

Лучшая часть фоновой обработки — это простота параллельной обработки. Мы можем установить директиву numprocs=5 Они будут ждать работы и обрабатывать их самостоятельно, что позволяет нам легко масштабировать нашу систему. По мере роста вашей системы вам, вероятно, придется увеличивать количество процессов. Поскольку у нас будет запущено несколько процессов, нам нужно определить структуру имени процесса, поэтому мы устанавливаем директиву process_name=%(program_name)s_%(process_num)02d

И последнее, но не менее важное: мы хотим сохранить результаты работы рабочих, чтобы мы могли анализировать и отлаживать их, если что-то пойдет не так. Мы определим пути stderr_logfilestdout_logfile

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

 [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/

 supervisorctl reread
supervisorctl update

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

Обновить Светильники

Миниатюры изображений больше не будут отображаться при первом запросе, поэтому нам нужно явно запрашивать рендеринг для каждого изображения, когда мы загружаем наши приборы в классе приспособлений 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);
    }
}

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

Советы и приемы

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

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